using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Sales;
using ERPCore.Infra.Auth;
using ERPCore.Infra.UoW;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Gl;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services;

/// <summary>
/// Customer payments against a Posted sales invoice. Multiple installments are allowed
/// until <see cref="SalesInvoice.BalanceAmount"/> reaches zero. Each payment posts its
/// own real GL journal entry (Debit the selected bank-or-cash account / Credit Accounts
/// Receivable) before being recorded, using the same call-GL-before-commit pattern as
/// <see cref="GrnPaymentService"/> — the mirror image of it (that one credits a payable
/// clearing account when vendor-paid; this one credits the receivable asset when
/// customer-paid) — so a rejected/unreachable GL post rolls back the whole payment atomically.
/// </summary>
public sealed class SalesInvoicePaymentService : ISalesInvoicePaymentService
{
    private readonly IRepository<SalesInvoice> _invoices;
    private readonly IRepository<SalesInvoicePayment> _payments;
    private readonly IGeneralLedgerService _gl;
    private readonly ICurrentUser _currentUser;
    private readonly IUnitOfWork _uow;
    private readonly string _glAccountsReceivableCode;

    public SalesInvoicePaymentService(
        IRepository<SalesInvoice> invoices, IRepository<SalesInvoicePayment> payments, IGeneralLedgerService gl,
        ICurrentUser currentUser, IUnitOfWork uow, IConfiguration configuration)
    {
        _invoices = invoices;
        _payments = payments;
        _gl = gl;
        _currentUser = currentUser;
        _uow = uow;
        _glAccountsReceivableCode = configuration["SalesInvoice:GlAccountsReceivableCode"] ?? string.Empty;
    }

    public async Task<SalesInvoicePaymentDto> PayAsync(int salesInvoiceId, CreateSalesInvoicePaymentRequest request, CancellationToken ct = default)
    {
        var invoice = await _invoices.Query().FirstOrDefaultAsync(i => i.SalesInvoiceId == salesInvoiceId, ct)
            ?? throw new NotFoundException($"Sales invoice {salesInvoiceId} was not found.");

        if (invoice.Status != SalesInvoiceStatus.Posted)
            throw new DomainException(ErrorCodes.SalesInvoiceNotPayable, $"Sales invoice {salesInvoiceId} must be posted before it can be paid.", 409);
        if (request.Amount > invoice.BalanceAmount)
            throw new DomainException(ErrorCodes.SalesInvoicePaymentExceedsBalance,
                $"Payment amount {request.Amount} exceeds the remaining balance {invoice.BalanceAmount}.", 400);

        var accounts = await _gl.ListBankAccountsAsync(ct);
        var account = accounts.FirstOrDefault(a => a.AccountId == request.GlBankAccountId)
            ?? throw new DomainException(ErrorCodes.SalesInvoiceBankAccountNotFound, $"Bank/cash account {request.GlBankAccountId} was not found.", 404);

        var actor = _currentUser.AuditUserId;
        var now = DateTime.UtcNow;

        var payment = await _uow.ExecuteInTransactionAsync(async token =>
        {
            // Same atomicity approach as GrnPaymentService.PayAsync: post to GL first, inside
            // this transaction, before anything is committed — a GL rejection/timeout rolls
            // the whole payment back with no partial local state.
            var period = await _gl.GetPeriodByDateAsync(DateOnly.FromDateTime(now), token);
            var posted = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest
            {
                PeriodId = period.PeriodId,
                EntryDate = DateOnly.FromDateTime(now),
                SourceModule = "SALES_INVOICE_PAYMENT",
                Reference = invoice.InvoiceNo,
                Narration = $"Payment against invoice {invoice.InvoiceNo}",
                Lines =
                [
                    new GlJournalEntryLineRequest(account.GlAccountCode, request.Amount, 0m, $"Payment against invoice {invoice.InvoiceNo}"),
                    new GlJournalEntryLineRequest(_glAccountsReceivableCode, 0m, request.Amount, $"Payment against invoice {invoice.InvoiceNo}")
                ]
            }, token);

            var entity = new SalesInvoicePayment
            {
                SalesInvoiceId = invoice.SalesInvoiceId,
                Amount = request.Amount,
                PaymentDate = now,
                GlBankAccountId = account.AccountId,
                BankAccountName = account.AccountName,
                Reference = request.Reference,
                GlJournalNo = posted.JournalNo,
                CreatedBy = actor,
                CreatedAt = now
            };
            await _payments.AddAsync(entity, token);

            invoice.PaidAmount += request.Amount;
            invoice.BalanceAmount -= request.Amount;

            return entity;
        }, ct);

        return Map(payment);
    }

    public async Task<IReadOnlyList<SalesInvoicePaymentDto>> ListAsync(int salesInvoiceId, CancellationToken ct = default)
    {
        if (!await _invoices.Query().AnyAsync(i => i.SalesInvoiceId == salesInvoiceId, ct))
            throw new NotFoundException($"Sales invoice {salesInvoiceId} was not found.");

        return await _payments.Query().AsNoTracking()
            .Where(p => p.SalesInvoiceId == salesInvoiceId)
            .OrderByDescending(p => p.SalesInvoicePaymentId)
            .Select(p => new SalesInvoicePaymentDto(
                p.SalesInvoicePaymentId, p.SalesInvoiceId, p.Amount, p.PaymentDate,
                p.GlBankAccountId, p.BankAccountName, p.Reference, p.GlJournalNo, p.CreatedAt))
            .ToListAsync(ct);
    }

    private static SalesInvoicePaymentDto Map(SalesInvoicePayment p) => new(
        p.SalesInvoicePaymentId, p.SalesInvoiceId, p.Amount, p.PaymentDate,
        p.GlBankAccountId, p.BankAccountName, p.Reference, p.GlJournalNo, p.CreatedAt);
}
