using CashLineERP.Application.Finance;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.Workflow;
using CashLineERP.SharedKernel.Finance;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class FinanceAccountingRuleRepository : IAccountingRuleRepository
{
    private readonly CashLineErpDbContext _dbContext;

    public FinanceAccountingRuleRepository(CashLineErpDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IReadOnlyCollection<FinanceAccountingRuleDetailView> GetAccountingRules(bool activeOnly = false) =>
        _dbContext.Set<FinanceAccountingRule>()
            .AsNoTracking()
            .ToArray()
            .Where(rule => !activeOnly || rule.IsActive)
            .OrderBy(rule => rule.DisplayOrder)
            .ThenBy(rule => rule.GeneralLedgerAccountCode)
            .Select(Map)
            .ToArray();

    public FinanceAccountingRuleDetailView? GetAccountingRule(long financeAccountingRuleId)
    {
        var rule = _dbContext.Set<FinanceAccountingRule>()
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == financeAccountingRuleId);

        return rule is null ? null : Map(rule);
    }

    public FinanceAccountingRuleDetailView Create(CreateFinanceAccountingRuleCommand command)
    {
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, command.EffectiveFromDate, "finance accounting rule");

        var normalizedAccountCategoryCode = FinanceAccountingRuleCatalog.NormalizeAccountCategoryCode(command.AccountCategoryCode);
        var normalizedStatementSectionCode = FinanceAccountingRuleCatalog.NormalizeStatementSectionCode(command.StatementSectionCode);
        var normalizedCashFlowCategoryCode = FinanceAccountingRuleCatalog.NormalizeCashFlowCategoryCode(command.CashFlowCategoryCode);
        var normalizedAccountCode = command.GeneralLedgerAccountCode.Trim().ToUpperInvariant();

        var duplicateExists = _dbContext.Set<FinanceAccountingRule>()
            .AsNoTracking()
            .Any(entry =>
                entry.GeneralLedgerAccountId == command.GeneralLedgerAccountId
                && entry.EffectiveFromDate == command.EffectiveFromDate);

        if (duplicateExists)
        {
            throw new InvalidOperationException("Accounting rule already exists for the same GL account and effective-from date.");
        }

        var nextSequence = (_dbContext.Set<FinanceAccountingRule>().Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var rule = new FinanceAccountingRule(
            ruleCode: $"ACR-{nextSequence:D6}",
            ruleName: command.RuleName,
            generalLedgerAccountId: command.GeneralLedgerAccountId,
            generalLedgerAccountCode: normalizedAccountCode,
            generalLedgerAccountName: command.GeneralLedgerAccountName,
            accountCategoryCode: normalizedAccountCategoryCode,
            statementSectionCode: normalizedStatementSectionCode,
            cashFlowCategoryCode: normalizedCashFlowCategoryCode,
            isCashAccount: command.IsCashAccount,
            displayOrder: command.DisplayOrder,
            effectiveFromDate: command.EffectiveFromDate,
            effectiveToDate: command.EffectiveToDate,
            notes: command.Notes);

        _dbContext.Add(rule);
        _dbContext.SaveChanges();
        return GetAccountingRule(rule.Id)!;
    }

    public FinanceAccountingRuleDetailView Submit(long financeAccountingRuleId, long actorUserId)
    {
        var rule = _dbContext.Set<FinanceAccountingRule>().Single(entry => entry.Id == financeAccountingRuleId);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, rule.EffectiveFromDate, "finance accounting rule");
        rule.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "FINANCE",
            documentType: "FIN_ACCOUNTING_RULE",
            documentId: rule.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Finance accounting rule submitted for checker approval.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetAccountingRule(rule.Id)!;
    }

    public FinanceAccountingRuleDetailView Approve(long financeAccountingRuleId, long actorUserId)
    {
        var rule = _dbContext.Set<FinanceAccountingRule>().Single(entry => entry.Id == financeAccountingRuleId);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, rule.EffectiveFromDate, "finance accounting rule");
        rule.Approve(actorUserId);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "FIN_ACCOUNTING_RULE" && entry.DocumentId == financeAccountingRuleId);
        workflowInstance?.Approve(actorUserId, "Finance accounting rule approved.");

        _dbContext.SaveChanges();
        return GetAccountingRule(rule.Id)!;
    }

    public FinanceAccountingRuleDetailView Activate(long financeAccountingRuleId, long actorUserId)
    {
        var rule = _dbContext.Set<FinanceAccountingRule>().Single(entry => entry.Id == financeAccountingRuleId);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, rule.EffectiveFromDate, "finance accounting rule");
        rule.Activate(actorUserId);
        _dbContext.SaveChanges();
        return GetAccountingRule(rule.Id)!;
    }

    private static FinanceAccountingRuleDetailView Map(FinanceAccountingRule rule) =>
        new(
            rule.Id,
            rule.RuleCode,
            rule.RuleName,
            rule.GeneralLedgerAccountId,
            rule.GeneralLedgerAccountCode,
            rule.GeneralLedgerAccountName,
            rule.AccountCategoryCode,
            rule.StatementSectionCode,
            rule.CashFlowCategoryCode,
            rule.IsCashAccount,
            rule.DisplayOrder,
            rule.EffectiveFromDate,
            rule.EffectiveToDate,
            WorkflowStatusSeedIds.ToCode(rule.StatusId),
            rule.IsActive,
            rule.Notes,
            rule.SubmittedAtUtc,
            rule.SubmittedByUserId,
            rule.ApprovedAtUtc,
            rule.ApprovedByUserId,
            rule.ActivatedAtUtc,
            rule.ActivatedByUserId);
}
