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 TreasuryTransferRepository : ITreasuryTransferRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<TreasuryTransferDetailView> GetTreasuryTransfers() =>
        _dbContext.Set<TreasuryTransfer>()
            .AsNoTracking()
            .OrderByDescending(transfer => transfer.TransferDate)
            .ThenByDescending(transfer => transfer.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public TreasuryTransferDetailView? GetTreasuryTransfer(long treasuryTransferId)
    {
        var transfer = _dbContext.Set<TreasuryTransfer>()
            .AsNoTracking()
            .SingleOrDefault(existingTransfer => existingTransfer.Id == treasuryTransferId);

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

    public TreasuryTransferDetailView Create(CreateTreasuryTransferCommand command)
    {
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, command.TransferDate, "Treasury transfer");
        var accounts = LoadAccounts(command.FromCashAccountId, command.ToCashAccountId);
        var fromAccount = accounts[command.FromCashAccountId];
        var toAccount = accounts[command.ToCashAccountId];

        ValidateAccounts(fromAccount, toAccount, command.CurrencyCode, command.AmountAmount);

        var nextSequence = (_dbContext.Set<TreasuryTransfer>().Max(transfer => (long?)transfer.Id) ?? 0L) + 1L;
        var transfer = new TreasuryTransfer(
            transferNumber: $"TRF-{nextSequence:D6}",
            fromCashAccountId: fromAccount.Id,
            toCashAccountId: toAccount.Id,
            transferDate: command.TransferDate,
            currencyCode: command.CurrencyCode.Trim().ToUpperInvariant(),
            amountAmount: command.AmountAmount,
            referenceNumber: NormalizeOptional(command.ReferenceNumber),
            notes: NormalizeOptional(command.Notes));

        _dbContext.Add(transfer);
        _dbContext.SaveChanges();
        return GetTreasuryTransfer(transfer.Id)!;
    }

    public TreasuryTransferDetailView Submit(long treasuryTransferId, long actorUserId)
    {
        var transfer = _dbContext.Set<TreasuryTransfer>().Single(existingTransfer => existingTransfer.Id == treasuryTransferId);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, transfer.TransferDate, "Treasury transfer");
        transfer.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "FINANCE",
            documentType: "TREASURY_TRANSFER",
            documentId: transfer.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Treasury transfer submitted for checker approval.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetTreasuryTransfer(transfer.Id)!;
    }

    public TreasuryTransferDetailView Approve(long treasuryTransferId, long actorUserId)
    {
        var transfer = _dbContext.Set<TreasuryTransfer>().Single(existingTransfer => existingTransfer.Id == treasuryTransferId);
        transfer.Approve(actorUserId);

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

        workflowInstance?.Approve(actorUserId, "Treasury transfer approved by checker.");
        _dbContext.SaveChanges();
        return GetTreasuryTransfer(transfer.Id)!;
    }

    public TreasuryTransferDetailView Post(long treasuryTransferId, long actorUserId)
    {
        var transfer = _dbContext.Set<TreasuryTransfer>().Single(existingTransfer => existingTransfer.Id == treasuryTransferId);

        if (transfer.StatusId == WorkflowStatusSeedIds.Posted)
        {
            return GetTreasuryTransfer(transfer.Id)!;
        }

        var postingPeriodId = AccountingPeriodLockGuard.RequireOpenPeriod(_dbContext, transfer.TransferDate, "Treasury transfer");
        using var transaction = _dbContext.Database.IsRelational()
            ? _dbContext.Database.BeginTransaction()
            : null;

        var accounts = LoadAccounts(transfer.FromCashAccountId, transfer.ToCashAccountId);
        var fromAccount = accounts[transfer.FromCashAccountId];
        var toAccount = accounts[transfer.ToCashAccountId];
        ValidateAccounts(fromAccount, toAccount, transfer.CurrencyCode, transfer.AmountAmount);
        var conversion = CurrencyConversionContext.Create(_dbContext);
        var fxRateToBase = conversion.ResolveRateToBase(transfer.CurrencyCode, transfer.TransferDate);

        fromAccount.ApplyOutgoingTransfer(transfer.AmountAmount, actorUserId);
        toAccount.ApplyIncomingTransfer(transfer.AmountAmount, actorUserId);

        var nextBatchSequence = (_dbContext.GeneralLedgerBatches.Max(batch => (long?)batch.Id) ?? 0L) + 1L;
        var batch = new GeneralLedgerBatch(
            batchNumber: $"GLB-{nextBatchSequence:D6}",
            batchType: "TREASURY_TRANSFER",
            branchId: fromAccount.BranchId ?? toAccount.BranchId,
            periodId: postingPeriodId,
            descriptionText: $"Treasury transfer {transfer.TransferNumber}",
            sourceModule: "FINANCE",
            sourceDocumentId: transfer.Id);

        var journal = new GeneralLedgerJournal(
            generalLedgerBatchId: batch.Id,
            journalNumber: $"JRNL-{nextBatchSequence:D6}",
            journalType: "TREASURY_TRANSFER",
            postingDate: transfer.TransferDate,
            taxDate: transfer.TransferDate,
            documentDate: transfer.TransferDate,
            descriptionText: $"Treasury transfer {transfer.TransferNumber}");

        journal.AddLine(
            lineNumber: 1,
            generalLedgerAccountId: 1001L,
            branchId: toAccount.BranchId,
            departmentId: null,
            partyId: null,
            itemId: null,
            debitAmount: transfer.AmountAmount,
            creditAmount: 0m,
            currencyCode: transfer.CurrencyCode,
            fxRate: fxRateToBase,
            lineDescription: $"Incoming treasury transfer to {toAccount.AccountCode}");

        journal.AddLine(
            lineNumber: 2,
            generalLedgerAccountId: 1001L,
            branchId: fromAccount.BranchId,
            departmentId: null,
            partyId: null,
            itemId: null,
            debitAmount: 0m,
            creditAmount: transfer.AmountAmount,
            currencyCode: transfer.CurrencyCode,
            fxRate: fxRateToBase,
            lineDescription: $"Outgoing treasury transfer from {fromAccount.AccountCode}");

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

    private Dictionary<long, CashAccount> LoadAccounts(long fromCashAccountId, long toCashAccountId)
    {
        return _dbContext.Set<CashAccount>()
            .Where(account => account.Id == fromCashAccountId || account.Id == toCashAccountId)
            .ToDictionary(account => account.Id);
    }

    private static void ValidateAccounts(CashAccount fromAccount, CashAccount toAccount, string currencyCode, decimal amountAmount)
    {
        if (!fromAccount.IsActive || !toAccount.IsActive)
        {
            throw new InvalidOperationException("Treasury transfer requires active source and destination cash accounts.");
        }

        if (!string.Equals(fromAccount.CurrencyCode, currencyCode, StringComparison.OrdinalIgnoreCase)
            || !string.Equals(toAccount.CurrencyCode, currencyCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Treasury transfer requires matching cash-account currency and transfer currency.");
        }

        if (fromAccount.Id == toAccount.Id)
        {
            throw new InvalidOperationException("Treasury transfer requires different source and destination cash accounts.");
        }

        if (amountAmount <= 0m)
        {
            throw new InvalidOperationException("Treasury transfer amount must be greater than zero.");
        }
    }

    private TreasuryTransferDetailView Map(TreasuryTransfer transfer)
    {
        var accounts = _dbContext.Set<CashAccount>()
            .AsNoTracking()
            .Where(account => account.Id == transfer.FromCashAccountId || account.Id == transfer.ToCashAccountId)
            .ToDictionary(account => account.Id);
        var conversion = CurrencyConversionContext.Create(_dbContext);
        var fxRateToBase = conversion.ResolveRateToBase(transfer.CurrencyCode, transfer.TransferDate);
        var baseAmount = conversion.ConvertToBase(transfer.CurrencyCode, transfer.AmountAmount, transfer.TransferDate);

        return new TreasuryTransferDetailView(
            transfer.Id,
            transfer.TransferNumber,
            transfer.FromCashAccountId,
            accounts[transfer.FromCashAccountId].AccountCode,
            transfer.ToCashAccountId,
            accounts[transfer.ToCashAccountId].AccountCode,
            transfer.TransferDate,
            transfer.CurrencyCode,
            transfer.AmountAmount,
            conversion.BaseCurrencyCode,
            fxRateToBase,
            baseAmount,
            transfer.ReferenceNumber,
            transfer.Notes,
            WorkflowStatusSeedIds.ToCode(transfer.StatusId),
            transfer.SubmittedAtUtc,
            transfer.SubmittedByUserId,
            transfer.ApprovedAtUtc,
            transfer.ApprovedByUserId,
            transfer.PostedAtUtc,
            transfer.PostedByUserId,
            transfer.GeneralLedgerBatchId);
    }

    private static string? NormalizeOptional(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        return value.Trim();
    }
}
