using CashLineERP.Application.Finance;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.Inventory;
using CashLineERP.Domain.Procurement;
using CashLineERP.Application.Tax;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class AccountingPeriodRepository : IAccountingPeriodRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<AccountingPeriodView> GetPeriods() =>
        _dbContext.AccountingPeriods
            .AsNoTracking()
            .OrderBy(period => period.StartDate)
            .ToArray()
            .Select(Map)
            .ToArray();

    public AccountingPeriodCloseReadinessView GetCloseReadiness(long accountingPeriodId)
    {
        var period = _dbContext.AccountingPeriods
            .AsNoTracking()
            .Single(period => period.Id == accountingPeriodId);

        var statementIds = _dbContext.BankStatements
            .AsNoTracking()
            .Where(statement => statement.StatementDate >= period.StartDate && statement.StatementDate <= period.EndDate)
            .Select(statement => statement.Id)
            .ToArray();

        var bankStatementCount = statementIds.Length;
        var postedGeneralLedgerBatchCount = _dbContext.GeneralLedgerBatches
            .AsNoTracking()
            .Count(batch => batch.PeriodId == accountingPeriodId);

        var statementLines = _dbContext.BankStatementLines
            .AsNoTracking()
            .ToArray()
            .Where(line => statementIds.Contains(line.BankStatementId))
            .ToArray();
        var statementLineIds = statementLines.Select(line => line.Id).ToArray();

        var reconciledStatementLineCount = statementLines.Count(line => line.IsReconciled);
        var unreconciledStatementLineCount = statementLines.Count(line => !line.IsReconciled);
        var exceptionReconciliationItemCount = _dbContext.ReceiptReconciliationItems
            .AsNoTracking()
            .ToArray()
            .Count(item => statementLineIds.Contains(item.BankStatementLineId) && item.MatchStatusCode == "EXCEPTION");

        var pendingEtaInvoiceCount = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .ToArray()
            .Count(invoice =>
                invoice.InvoiceDate >= period.StartDate &&
                invoice.InvoiceDate <= period.EndDate &&
                invoice.StatusId == SharedKernel.Workflow.WorkflowStatusSeedIds.Posted &&
                EtaPayloadBuilder.SupportsInvoiceType(invoice.InvoiceType) &&
                !string.Equals(invoice.EtaStatus, "ACCEPTED", StringComparison.OrdinalIgnoreCase));

        var openArInvoiceCount = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Count(invoice =>
                invoice.InvoiceDate >= period.StartDate &&
                invoice.InvoiceDate <= period.EndDate &&
                invoice.StatusId != WorkflowStatusSeedIds.Posted);

        var openArReceiptCount = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Count(receipt =>
                receipt.ReceiptDate >= period.StartDate &&
                receipt.ReceiptDate <= period.EndDate &&
                receipt.StatusId != WorkflowStatusSeedIds.Posted);

        var openApBillCount = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Count(bill =>
                bill.BillDate >= period.StartDate &&
                bill.BillDate <= period.EndDate &&
                bill.StatusId != SharedKernel.Workflow.WorkflowStatusSeedIds.Posted);

        var openApPaymentCount = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .Count(payment =>
                payment.PaymentDate >= period.StartDate &&
                payment.PaymentDate <= period.EndDate &&
                payment.StatusId != SharedKernel.Workflow.WorkflowStatusSeedIds.Posted);

        var openInventoryAdjustmentCount = _dbContext.Set<InventoryAdjustment>()
            .AsNoTracking()
            .Count(adjustment =>
                adjustment.AdjustmentDate >= period.StartDate &&
                adjustment.AdjustmentDate <= period.EndDate &&
                adjustment.StatusId != WorkflowStatusSeedIds.Posted);

        var openStockCountCount = _dbContext.Set<StockCount>()
            .AsNoTracking()
            .Count(stockCount =>
                stockCount.CountDate >= period.StartDate &&
                stockCount.CountDate <= period.EndDate &&
                stockCount.StatusId != WorkflowStatusSeedIds.Posted);

        var openWarehouseTransferCount = _dbContext.Set<WarehouseTransfer>()
            .AsNoTracking()
            .Count(transfer =>
                transfer.TransferDate >= period.StartDate &&
                transfer.TransferDate <= period.EndDate &&
                transfer.StatusId != WorkflowStatusSeedIds.Posted);

        var openInventoryStatusControlCount = _dbContext.Set<InventoryStatusControl>()
            .AsNoTracking()
            .Count(control =>
                control.ControlDate >= period.StartDate &&
                control.ControlDate <= period.EndDate &&
                control.StatusId != WorkflowStatusSeedIds.Posted);

        var openCloseJournalCount = _dbContext.Set<Domain.Finance.FinanceCloseJournal>()
            .AsNoTracking()
            .Count(journal =>
                journal.AccountingPeriodId == accountingPeriodId &&
                journal.StatusId != SharedKernel.Workflow.WorkflowStatusSeedIds.Posted);

        var postedAccrualCloseJournalCount = _dbContext.Set<FinanceCloseJournal>()
            .AsNoTracking()
            .Count(journal =>
                journal.AccountingPeriodId == accountingPeriodId &&
                journal.StatusId == WorkflowStatusSeedIds.Posted &&
                journal.JournalClassCode == "ACCRUAL");

        var postedAdjustmentCloseJournalCount = _dbContext.Set<FinanceCloseJournal>()
            .AsNoTracking()
            .Count(journal =>
                journal.AccountingPeriodId == accountingPeriodId &&
                journal.StatusId == WorkflowStatusSeedIds.Posted &&
                journal.JournalClassCode == "ADJUSTMENT");

        var openFxRevaluationCount = _dbContext.Set<Domain.Finance.ForeignExchangeRevaluation>()
            .AsNoTracking()
            .Count(entry =>
                entry.AccountingPeriodId == accountingPeriodId &&
                entry.StatusId != SharedKernel.Workflow.WorkflowStatusSeedIds.Posted);

        var blockingReasons = new List<string>();
        if (postedGeneralLedgerBatchCount == 0)
        {
            blockingReasons.Add("No posted general ledger batches exist for the accounting period.");
        }

        if (bankStatementCount == 0)
        {
            blockingReasons.Add("No bank statements have been loaded for the accounting period.");
        }

        if (unreconciledStatementLineCount > 0)
        {
            blockingReasons.Add("Unreconciled bank statement lines remain open for the accounting period.");
        }

        if (exceptionReconciliationItemCount > 0)
        {
            blockingReasons.Add("Reconciliation exception items remain unresolved for the accounting period.");
        }

        if (pendingEtaInvoiceCount > 0)
        {
            blockingReasons.Add("Posted tax invoices remain pending or rejected in ETA for the accounting period.");
        }

        if (openArInvoiceCount > 0)
        {
            blockingReasons.Add("Accounts receivable invoices remain unposted within the accounting period.");
        }

        if (openArReceiptCount > 0)
        {
            blockingReasons.Add("Accounts receivable receipts remain unposted within the accounting period.");
        }

        if (openApBillCount > 0)
        {
            blockingReasons.Add("Accounts payable bills remain unposted within the accounting period.");
        }

        if (openApPaymentCount > 0)
        {
            blockingReasons.Add("Accounts payable payments remain unposted within the accounting period.");
        }

        if (openInventoryAdjustmentCount > 0)
        {
            blockingReasons.Add("Inventory adjustments remain unposted within the accounting period.");
        }

        if (openStockCountCount > 0)
        {
            blockingReasons.Add("Stock counts remain unposted within the accounting period.");
        }

        if (openWarehouseTransferCount > 0)
        {
            blockingReasons.Add("Warehouse transfers remain unposted within the accounting period.");
        }

        if (openInventoryStatusControlCount > 0)
        {
            blockingReasons.Add("Inventory stock-status controls remain unposted within the accounting period.");
        }

        if (openCloseJournalCount > 0)
        {
            blockingReasons.Add("Finance close journals remain unposted within the accounting period.");
        }

        if (openFxRevaluationCount > 0)
        {
            blockingReasons.Add("FX revaluations remain unposted within the accounting period.");
        }

        return new AccountingPeriodCloseReadinessView(
            period.Id,
            period.PeriodCode,
            period.CloseStatusCode,
            postedGeneralLedgerBatchCount,
            bankStatementCount,
            reconciledStatementLineCount,
            unreconciledStatementLineCount,
            exceptionReconciliationItemCount,
            pendingEtaInvoiceCount,
            openArInvoiceCount,
            openArReceiptCount,
            openApBillCount,
            openApPaymentCount,
            openInventoryAdjustmentCount,
            openStockCountCount,
            openWarehouseTransferCount,
            openInventoryStatusControlCount,
            openCloseJournalCount,
            openFxRevaluationCount,
            postedAccrualCloseJournalCount,
            postedAdjustmentCloseJournalCount,
            blockingReasons.Count == 0,
            blockingReasons);
    }

    public AccountingPeriodMonthEndReviewView GetMonthEndReview(long accountingPeriodId)
    {
        var period = _dbContext.AccountingPeriods
            .AsNoTracking()
            .Single(existingPeriod => existingPeriod.Id == accountingPeriodId);

        var readiness = GetCloseReadiness(accountingPeriodId);

        var closeJournalClassSummaries = _dbContext.Set<FinanceCloseJournal>()
            .AsNoTracking()
            .Where(journal => journal.AccountingPeriodId == accountingPeriodId)
            .ToArray()
            .GroupBy(journal => journal.JournalClassCode)
            .Select(group => new AccountingPeriodMonthEndJournalClassSummaryView(
                group.Key,
                group.Count(journal => journal.StatusId == WorkflowStatusSeedIds.Draft),
                group.Count(journal => journal.StatusId == WorkflowStatusSeedIds.Submitted),
                group.Count(journal => journal.StatusId == WorkflowStatusSeedIds.Approved),
                group.Count(journal => journal.StatusId == WorkflowStatusSeedIds.Posted),
                group.Where(journal => journal.StatusId == WorkflowStatusSeedIds.Posted).Sum(journal => journal.TotalDebitAmount)))
            .OrderBy(summary => summary.JournalClassCode)
            .ToArray();

        var openItems = GetMonthEndOpenItems(period);

        return new AccountingPeriodMonthEndReviewView(
            period.Id,
            period.PeriodCode,
            readiness,
            closeJournalClassSummaries,
            openItems);
    }

    public AccountingPeriodView MarkReadyToClose(long accountingPeriodId, long actorUserId)
    {
        var readiness = GetCloseReadiness(accountingPeriodId);
        if (!readiness.IsReadyToClose)
        {
            throw new InvalidOperationException(string.Join(" ", readiness.BlockingReasons));
        }

        var period = _dbContext.AccountingPeriods.Single(existingPeriod => existingPeriod.Id == accountingPeriodId);
        period.MarkReadyForClose(actorUserId);
        _dbContext.SaveChanges();
        return Map(period);
    }

    public AccountingPeriodView Close(long accountingPeriodId, long actorUserId)
    {
        var readiness = GetCloseReadiness(accountingPeriodId);
        if (!readiness.IsReadyToClose)
        {
            throw new InvalidOperationException(string.Join(" ", readiness.BlockingReasons));
        }

        var period = _dbContext.AccountingPeriods.Single(existingPeriod => existingPeriod.Id == accountingPeriodId);
        period.Close(actorUserId);
        _dbContext.SaveChanges();
        return Map(period);
    }

    private static AccountingPeriodView Map(Domain.Finance.AccountingPeriod period) =>
        new(
            period.Id,
            period.PeriodCode,
            period.StartDate,
            period.EndDate,
            period.CloseStatusCode,
            period.IsYearEnd,
            period.ReadyForCloseAtUtc,
            period.ClosedAtUtc);

    private IReadOnlyCollection<AccountingPeriodMonthEndOpenItemView> GetMonthEndOpenItems(Domain.Finance.AccountingPeriod period)
    {
        var openItems = new List<AccountingPeriodMonthEndOpenItemView>();

        openItems.AddRange(_dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(invoice =>
                invoice.InvoiceDate >= period.StartDate &&
                invoice.InvoiceDate <= period.EndDate &&
                invoice.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(invoice => new AccountingPeriodMonthEndOpenItemView(
                "FINANCE",
                "AR_INVOICE",
                invoice.Id,
                invoice.InvoiceNumber,
                invoice.InvoiceDate,
                WorkflowStatusSeedIds.ToCode(invoice.StatusId),
                invoice.TotalAmount,
                null))
            .ToArray());

        openItems.AddRange(_dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(receipt =>
                receipt.ReceiptDate >= period.StartDate &&
                receipt.ReceiptDate <= period.EndDate &&
                receipt.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(receipt => new AccountingPeriodMonthEndOpenItemView(
                "FINANCE",
                "AR_RECEIPT",
                receipt.Id,
                receipt.ReceiptNumber,
                receipt.ReceiptDate,
                WorkflowStatusSeedIds.ToCode(receipt.StatusId),
                receipt.AmountAmount,
                receipt.Notes))
            .ToArray());

        openItems.AddRange(_dbContext.AccountsPayableBills
            .AsNoTracking()
            .Where(bill =>
                bill.BillDate >= period.StartDate &&
                bill.BillDate <= period.EndDate &&
                bill.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(bill => new AccountingPeriodMonthEndOpenItemView(
                "PROCUREMENT",
                "AP_BILL",
                bill.Id,
                bill.BillNumber,
                bill.BillDate,
                WorkflowStatusSeedIds.ToCode(bill.StatusId),
                bill.TotalAmount,
                bill.MatchNotes))
            .ToArray());

        openItems.AddRange(_dbContext.AccountsPayablePayments
            .AsNoTracking()
            .Where(payment =>
                payment.PaymentDate >= period.StartDate &&
                payment.PaymentDate <= period.EndDate &&
                payment.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(payment => new AccountingPeriodMonthEndOpenItemView(
                "FINANCE",
                "AP_PAYMENT",
                payment.Id,
                payment.PaymentNumber,
                payment.PaymentDate,
                WorkflowStatusSeedIds.ToCode(payment.StatusId),
                payment.AmountAmount,
                payment.Notes))
            .ToArray());

        openItems.AddRange(_dbContext.Set<InventoryAdjustment>()
            .AsNoTracking()
            .Where(adjustment =>
                adjustment.AdjustmentDate >= period.StartDate &&
                adjustment.AdjustmentDate <= period.EndDate &&
                adjustment.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(adjustment => new AccountingPeriodMonthEndOpenItemView(
                "INVENTORY",
                "INV_ADJUSTMENT",
                adjustment.Id,
                adjustment.AdjustmentNumber,
                adjustment.AdjustmentDate,
                WorkflowStatusSeedIds.ToCode(adjustment.StatusId),
                adjustment.TotalAdjustmentAmount,
                adjustment.Notes))
            .ToArray());

        openItems.AddRange(_dbContext.Set<StockCount>()
            .AsNoTracking()
            .Where(stockCount =>
                stockCount.CountDate >= period.StartDate &&
                stockCount.CountDate <= period.EndDate &&
                stockCount.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(stockCount => new AccountingPeriodMonthEndOpenItemView(
                "INVENTORY",
                "INV_STOCK_COUNT",
                stockCount.Id,
                stockCount.CountNumber,
                stockCount.CountDate,
                WorkflowStatusSeedIds.ToCode(stockCount.StatusId),
                stockCount.TotalVarianceAmount,
                stockCount.Notes))
            .ToArray());

        openItems.AddRange(_dbContext.Set<WarehouseTransfer>()
            .AsNoTracking()
            .Where(transfer =>
                transfer.TransferDate >= period.StartDate &&
                transfer.TransferDate <= period.EndDate &&
                transfer.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(transfer => new AccountingPeriodMonthEndOpenItemView(
                "INVENTORY",
                "INV_TRANSFER",
                transfer.Id,
                transfer.TransferNumber,
                transfer.TransferDate,
                WorkflowStatusSeedIds.ToCode(transfer.StatusId),
                transfer.TotalTransferQuantity,
                transfer.Notes))
            .ToArray());

        openItems.AddRange(_dbContext.Set<InventoryStatusControl>()
            .AsNoTracking()
            .Where(control =>
                control.ControlDate >= period.StartDate &&
                control.ControlDate <= period.EndDate &&
                control.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(control => new AccountingPeriodMonthEndOpenItemView(
                "INVENTORY",
                "INV_STATUS_CONTROL",
                control.Id,
                control.ControlNumber,
                control.ControlDate,
                WorkflowStatusSeedIds.ToCode(control.StatusId),
                control.TotalQuantityAffected,
                control.Notes))
            .ToArray());

        openItems.AddRange(_dbContext.Set<FinanceCloseJournal>()
            .AsNoTracking()
            .Where(journal =>
                journal.AccountingPeriodId == period.Id &&
                journal.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(journal => new AccountingPeriodMonthEndOpenItemView(
                "FINANCE",
                "FIN_CLOSE_JOURNAL",
                journal.Id,
                journal.CloseJournalNumber,
                journal.PostingDate,
                WorkflowStatusSeedIds.ToCode(journal.StatusId),
                journal.TotalDebitAmount,
                journal.Notes))
            .ToArray());

        openItems.AddRange(_dbContext.Set<ForeignExchangeRevaluation>()
            .AsNoTracking()
            .Where(entry =>
                entry.AccountingPeriodId == period.Id &&
                entry.StatusId != WorkflowStatusSeedIds.Posted)
            .Select(entry => new AccountingPeriodMonthEndOpenItemView(
                "FINANCE",
                "FX_REVALUATION",
                entry.Id,
                entry.RevaluationNumber,
                entry.RevaluationDate,
                WorkflowStatusSeedIds.ToCode(entry.StatusId),
                entry.NetVarianceBaseAmount,
                entry.Notes))
            .ToArray());

        openItems.AddRange(_dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(invoice =>
                invoice.InvoiceDate >= period.StartDate &&
                invoice.InvoiceDate <= period.EndDate &&
                invoice.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Where(invoice =>
                EtaPayloadBuilder.SupportsInvoiceType(invoice.InvoiceType) &&
                !string.Equals(invoice.EtaStatus, "ACCEPTED", StringComparison.OrdinalIgnoreCase))
            .Select(invoice => new AccountingPeriodMonthEndOpenItemView(
                "TAX",
                "ETA_PENDING_INVOICE",
                invoice.Id,
                invoice.InvoiceNumber,
                invoice.InvoiceDate,
                invoice.EtaStatus ?? "PENDING",
                invoice.TotalAmount,
                "ETA acceptance remains pending for period close."))
            .ToArray());

        return openItems
            .OrderBy(item => item.BusinessDate)
            .ThenBy(item => item.ModuleCode)
            .ThenBy(item => item.DocumentType)
            .ThenBy(item => item.DocumentNumber)
            .ToArray();
    }
}
