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 AccountsReceivableInvoiceRepository : IAccountsReceivableInvoiceRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<AccountsReceivableInvoiceDetailView> GetInvoices() =>
        _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Include(invoice => invoice.Lines)
            .OrderByDescending(invoice => invoice.InvoiceDate)
            .ThenByDescending(invoice => invoice.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public AccountsReceivableInvoiceDetailView? GetInvoice(long accountsReceivableInvoiceId)
    {
        var invoice = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Include(existingInvoice => existingInvoice.Lines)
            .SingleOrDefault(existingInvoice => existingInvoice.Id == accountsReceivableInvoiceId);

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

    public AccountsReceivableInvoiceDetailView Create(CreateAccountsReceivableInvoiceCommand command)
    {
        EnsureInvoicePeriodsOpen(command.InvoiceDate, command.TaxDate);

        var nextSequence = (_dbContext.AccountsReceivableInvoices.Max(invoice => (long?)invoice.Id) ?? 0L) + 1L;
        var invoice = new AccountsReceivableInvoice(
            invoiceNumber: $"ARINV-{nextSequence:D6}",
            customerId: command.CustomerId,
            salesOrderId: command.SalesOrderId,
            dispatchOrderId: command.DispatchOrderId,
            invoiceType: command.InvoiceType,
            invoiceDate: command.InvoiceDate,
            taxDate: command.TaxDate,
            dueDate: command.DueDate,
            currencyCode: command.CurrencyCode);

        var lineNumber = 1;
        foreach (var line in command.Lines)
        {
            invoice.AddLine(
                lineNumber++,
                line.SalesOrderLineId,
                line.ItemId,
                line.QuantityInvoiced,
                line.UnitOfMeasureCode,
                line.UnitPrice,
                line.TaxCode);
        }

        _dbContext.AccountsReceivableInvoices.Add(invoice);
        _dbContext.SaveChanges();
        return GetInvoice(invoice.Id)!;
    }

    public AccountsReceivableInvoiceDetailView Submit(long accountsReceivableInvoiceId, long actorUserId)
    {
        var invoice = _dbContext.AccountsReceivableInvoices
            .Include(existingInvoice => existingInvoice.Lines)
            .Single(existingInvoice => existingInvoice.Id == accountsReceivableInvoiceId);

        EnsureInvoicePeriodsOpen(invoice.InvoiceDate, invoice.TaxDate);
        invoice.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "FINANCE",
            documentType: "AR_INVOICE",
            documentId: invoice.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "AR invoice submitted for controlled issuance.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetInvoice(invoice.Id)!;
    }

    public AccountsReceivableInvoiceDetailView Approve(long accountsReceivableInvoiceId, long actorUserId)
    {
        var invoice = _dbContext.AccountsReceivableInvoices
            .Include(existingInvoice => existingInvoice.Lines)
            .Single(existingInvoice => existingInvoice.Id == accountsReceivableInvoiceId);

        invoice.Approve(actorUserId);

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

        workflowInstance?.Approve(actorUserId, "AR invoice approved by checker for controlled issuance.");

        _dbContext.SaveChanges();
        return GetInvoice(invoice.Id)!;
    }

    public AccountsReceivableInvoiceDetailView Post(long accountsReceivableInvoiceId, long actorUserId)
    {
        var invoice = _dbContext.AccountsReceivableInvoices
            .Include(existingInvoice => existingInvoice.Lines)
            .Single(existingInvoice => existingInvoice.Id == accountsReceivableInvoiceId);

        if (invoice.StatusId == WorkflowStatusSeedIds.Posted)
        {
            return GetInvoice(invoice.Id)!;
        }

        var postingPeriodId = AccountingPeriodLockGuard.RequireOpenPeriod(_dbContext, invoice.InvoiceDate, "AR invoice");
        if (invoice.TaxDate is { } taxDate)
        {
            AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, taxDate, "AR invoice tax date");
        }

        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: "AR_INVOICE",
            branchId: null,
            periodId: postingPeriodId,
            descriptionText: $"AR invoice posting for {invoice.InvoiceNumber}",
            sourceModule: "FINANCE",
            sourceDocumentId: invoice.Id);

        var journal = new GeneralLedgerJournal(
            generalLedgerBatchId: batch.Id,
            journalNumber: $"JRNL-{nextBatchSequence:D6}",
            journalType: "AR_INVOICE",
            postingDate: invoice.InvoiceDate,
            taxDate: invoice.TaxDate,
            documentDate: invoice.InvoiceDate,
            descriptionText: $"Customer invoice {invoice.InvoiceNumber}");

        var isCreditNote = string.Equals(invoice.InvoiceType, "CREDIT_NOTE", StringComparison.OrdinalIgnoreCase)
            || string.Equals(invoice.InvoiceType, "CREDIT", StringComparison.OrdinalIgnoreCase)
            || string.Equals(invoice.InvoiceType, "CREDIT_MEMO", StringComparison.OrdinalIgnoreCase);

        if (isCreditNote)
        {
            journal.AddLine(
                lineNumber: 1,
                generalLedgerAccountId: 4001L,
                branchId: null,
                departmentId: null,
                partyId: invoice.CustomerId,
                itemId: null,
                debitAmount: invoice.TotalAmount,
                creditAmount: 0m,
                currencyCode: invoice.CurrencyCode,
                fxRate: 1m,
                lineDescription: "Revenue reversal for customer credit note");

            journal.AddLine(
                lineNumber: 2,
                generalLedgerAccountId: 1101L,
                branchId: null,
                departmentId: null,
                partyId: invoice.CustomerId,
                itemId: null,
                debitAmount: 0m,
                creditAmount: invoice.TotalAmount,
                currencyCode: invoice.CurrencyCode,
                fxRate: 1m,
                lineDescription: "Accounts receivable reversal for customer credit note");
        }
        else
        {
            journal.AddLine(
                lineNumber: 1,
                generalLedgerAccountId: 1101L,
                branchId: null,
                departmentId: null,
                partyId: invoice.CustomerId,
                itemId: null,
                debitAmount: invoice.TotalAmount,
                creditAmount: 0m,
                currencyCode: invoice.CurrencyCode,
                fxRate: 1m,
                lineDescription: "Accounts receivable recognition");

            journal.AddLine(
                lineNumber: 2,
                generalLedgerAccountId: 4001L,
                branchId: null,
                departmentId: null,
                partyId: invoice.CustomerId,
                itemId: null,
                debitAmount: 0m,
                creditAmount: invoice.TotalAmount,
                currencyCode: invoice.CurrencyCode,
                fxRate: 1m,
                lineDescription: "Revenue recognition");
        }

        batch.AddJournal(journal);
        _dbContext.GeneralLedgerBatches.Add(batch);
        invoice.Post(actorUserId, batch.Id);
        _dbContext.SaveChanges();
        transaction?.Commit();
        return GetInvoice(invoice.Id)!;
    }

    private void EnsureInvoicePeriodsOpen(DateOnly invoiceDate, DateOnly? taxDate)
    {
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, invoiceDate, "AR invoice");
        if (taxDate is { } existingTaxDate)
        {
            AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, existingTaxDate, "AR invoice tax date");
        }
    }

    private static AccountsReceivableInvoiceDetailView Map(AccountsReceivableInvoice invoice) =>
        new(
            invoice.Id,
            invoice.InvoiceNumber,
            invoice.CustomerId,
            invoice.SalesOrderId,
            invoice.DispatchOrderId,
            invoice.InvoiceType,
            invoice.InvoiceDate,
            invoice.TaxDate,
            invoice.DueDate,
            invoice.CurrencyCode,
            WorkflowStatusSeedIds.ToCode(invoice.StatusId),
            invoice.SubtotalAmount,
            invoice.TaxAmount,
            invoice.TotalAmount,
            invoice.EtaUuid,
            invoice.EtaStatus,
            invoice.GeneralLedgerBatchId,
            invoice.Lines.OrderBy(line => line.LineNumber).Select(line => new AccountsReceivableInvoiceLineView(
                line.Id,
                line.LineNumber,
                line.SalesOrderLineId,
                line.ItemId,
                line.QuantityInvoiced,
                line.UnitOfMeasureCode,
                line.UnitPrice,
                line.TaxCode,
                line.LineNetAmount,
                line.LineTaxAmount,
                line.LineTotalAmount)).ToArray());
}
