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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class FinanceCloseJournalRepository : IFinanceCloseJournalRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<FinanceCloseJournalDetailView> GetCloseJournals() =>
        _dbContext.Set<FinanceCloseJournal>()
            .AsNoTracking()
            .Include(journal => journal.Lines)
            .OrderByDescending(journal => journal.PostingDate)
            .ThenByDescending(journal => journal.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public FinanceCloseJournalDetailView? GetCloseJournal(long financeCloseJournalId)
    {
        var journal = _dbContext.Set<FinanceCloseJournal>()
            .AsNoTracking()
            .Include(existingJournal => existingJournal.Lines)
            .SingleOrDefault(existingJournal => existingJournal.Id == financeCloseJournalId);

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

    public FinanceCloseJournalDetailView Create(CreateFinanceCloseJournalCommand command)
    {
        ValidateCreateCommand(command);

        var period = _dbContext.AccountingPeriods.Single(existingPeriod => existingPeriod.Id == command.AccountingPeriodId);
        EnsurePostingDateWithinPeriod(period, command.PostingDate, command.DocumentDate);

        var nextSequence = (_dbContext.Set<FinanceCloseJournal>().Max(journal => (long?)journal.Id) ?? 0L) + 1L;
        var journal = new FinanceCloseJournal(
            closeJournalNumber: $"CLJ-{nextSequence:D6}",
            accountingPeriodId: command.AccountingPeriodId,
            branchId: command.BranchId,
            journalClassCode: NormalizeJournalClassCode(command.JournalClassCode),
            postingDate: command.PostingDate,
            documentDate: command.DocumentDate,
            notes: command.Notes);

        var lineNumber = 1;
        foreach (var line in command.Lines)
        {
            journal.AddLine(
                lineNumber++,
                line.GeneralLedgerAccountId,
                line.BranchId,
                line.DepartmentId,
                line.PartyId,
                line.ItemId,
                line.DebitAmount,
                line.CreditAmount,
                line.CurrencyCode,
                line.FxRate,
                line.LineDescription);
        }

        _dbContext.Add(journal);
        _dbContext.SaveChanges();
        return GetCloseJournal(journal.Id)!;
    }

    public FinanceCloseJournalDetailView Submit(long financeCloseJournalId, long actorUserId)
    {
        var journal = _dbContext.Set<FinanceCloseJournal>()
            .Include(existingJournal => existingJournal.Lines)
            .Single(existingJournal => existingJournal.Id == financeCloseJournalId);

        journal.Submit(actorUserId);

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

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetCloseJournal(journal.Id)!;
    }

    public FinanceCloseJournalDetailView Approve(long financeCloseJournalId, long actorUserId)
    {
        var journal = _dbContext.Set<FinanceCloseJournal>()
            .Include(existingJournal => existingJournal.Lines)
            .Single(existingJournal => existingJournal.Id == financeCloseJournalId);

        journal.Approve(actorUserId);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(instance => instance.Actions)
            .SingleOrDefault(instance =>
                instance.DocumentType == "FIN_CLOSE_JOURNAL" &&
                instance.DocumentId == journal.Id);

        workflowInstance?.Approve(actorUserId, "Finance close journal approved by checker.");

        _dbContext.SaveChanges();
        return GetCloseJournal(journal.Id)!;
    }

    public FinanceCloseJournalDetailView Post(long financeCloseJournalId, long actorUserId)
    {
        var closeJournal = _dbContext.Set<FinanceCloseJournal>()
            .Include(existingJournal => existingJournal.Lines)
            .Single(existingJournal => existingJournal.Id == financeCloseJournalId);

        if (closeJournal.StatusId == WorkflowStatusSeedIds.Posted)
        {
            return GetCloseJournal(closeJournal.Id)!;
        }

        var period = _dbContext.AccountingPeriods.Single(existingPeriod => existingPeriod.Id == closeJournal.AccountingPeriodId);
        if (string.Equals(period.CloseStatusCode, "CLOSED", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Cannot post close journal into a closed accounting period.");
        }

        EnsurePostingDateWithinPeriod(period, closeJournal.PostingDate, closeJournal.DocumentDate);

        using var transaction = _dbContext.Database.IsRelational()
            ? _dbContext.Database.BeginTransaction()
            : null;

        var nextBatchSequence = (_dbContext.GeneralLedgerBatches.Max(batch => (long?)batch.Id) ?? 0L) + 1L;
        var batch = new GeneralLedgerBatch(
            batchNumber: $"GLB-{nextBatchSequence:D6}",
            batchType: "FIN_CLOSE_JOURNAL",
            branchId: closeJournal.BranchId,
            periodId: closeJournal.AccountingPeriodId,
            descriptionText: $"Finance close journal posting for {closeJournal.CloseJournalNumber}",
            sourceModule: "FINANCE",
            sourceDocumentId: closeJournal.Id);

        var journalType = closeJournal.JournalClassCode == "ACCRUAL" ? "FIN_ACCRUAL" : "FIN_ADJUSTMENT";
        var glJournal = new GeneralLedgerJournal(
            generalLedgerBatchId: batch.Id,
            journalNumber: $"JRNL-{nextBatchSequence:D6}",
            journalType: journalType,
            postingDate: closeJournal.PostingDate,
            taxDate: closeJournal.DocumentDate,
            documentDate: closeJournal.DocumentDate,
            descriptionText: closeJournal.Notes ?? $"{closeJournal.JournalClassCode} close journal {closeJournal.CloseJournalNumber}");

        foreach (var line in closeJournal.Lines.OrderBy(line => line.LineNumber))
        {
            glJournal.AddLine(
                lineNumber: line.LineNumber,
                generalLedgerAccountId: line.GeneralLedgerAccountId,
                branchId: line.BranchId,
                departmentId: line.DepartmentId,
                partyId: line.PartyId,
                itemId: line.ItemId,
                debitAmount: line.DebitAmount,
                creditAmount: line.CreditAmount,
                currencyCode: line.CurrencyCode,
                fxRate: line.FxRate,
                lineDescription: line.LineDescription);
        }

        batch.AddJournal(glJournal);
        _dbContext.GeneralLedgerBatches.Add(batch);
        closeJournal.Post(actorUserId, batch.Id);
        _dbContext.SaveChanges();
        transaction?.Commit();
        return GetCloseJournal(closeJournal.Id)!;
    }

    private static FinanceCloseJournalDetailView Map(FinanceCloseJournal journal) =>
        new(
            journal.Id,
            journal.CloseJournalNumber,
            journal.AccountingPeriodId,
            journal.BranchId,
            journal.JournalClassCode,
            journal.PostingDate,
            journal.DocumentDate,
            journal.Notes,
            WorkflowStatusSeedIds.ToCode(journal.StatusId),
            journal.TotalDebitAmount,
            journal.TotalCreditAmount,
            journal.SubmittedAtUtc,
            journal.SubmittedByUserId,
            journal.ApprovedAtUtc,
            journal.ApprovedByUserId,
            journal.PostedAtUtc,
            journal.PostedByUserId,
            journal.GeneralLedgerBatchId,
            journal.Lines.OrderBy(line => line.LineNumber)
                .Select(line => new FinanceCloseJournalLineView(
                    line.Id,
                    line.LineNumber,
                    line.GeneralLedgerAccountId,
                    line.BranchId,
                    line.DepartmentId,
                    line.PartyId,
                    line.ItemId,
                    line.DebitAmount,
                    line.CreditAmount,
                    line.CurrencyCode,
                    line.FxRate,
                    line.LineDescription))
                .ToArray());

    private static void ValidateCreateCommand(CreateFinanceCloseJournalCommand command)
    {
        if (command.AccountingPeriodId <= 0)
        {
            throw new InvalidOperationException("Finance close journal requires a valid accounting period.");
        }

        NormalizeJournalClassCode(command.JournalClassCode);

        if (command.Lines.Count < 2)
        {
            throw new InvalidOperationException("Finance close journal requires at least two lines.");
        }
    }

    private static string NormalizeJournalClassCode(string journalClassCode) =>
        journalClassCode.Trim().ToUpperInvariant() switch
        {
            "ACCRUAL" => "ACCRUAL",
            "ADJUSTMENT" => "ADJUSTMENT",
            _ => throw new InvalidOperationException("Finance close journal class must be either ACCRUAL or ADJUSTMENT.")
        };

    private static void EnsurePostingDateWithinPeriod(AccountingPeriod period, DateOnly postingDate, DateOnly documentDate)
    {
        if (postingDate < period.StartDate || postingDate > period.EndDate)
        {
            throw new InvalidOperationException("Finance close journal posting date must fall within the accounting period.");
        }

        if (documentDate < period.StartDate || documentDate > period.EndDate)
        {
            throw new InvalidOperationException("Finance close journal document date must fall within the accounting period.");
        }
    }
}
