using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Grn;
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>
/// Vendor payments against a confirmed GRN. Multiple installments are allowed until
/// <see cref="Grn.BalanceAmount"/> reaches zero. Each payment posts its own real GL
/// journal entry (Debit GRN Clearing / Credit the selected bank-or-cash account) before
/// being recorded, using the same call-GL-before-commit pattern as <see cref="GrnService.ConfirmAsync"/>
/// so a rejected/unreachable GL post rolls back the whole payment atomically.
/// </summary>
public sealed class GrnPaymentService : IGrnPaymentService
{
    private readonly IRepository<Grn> _grns;
    private readonly IRepository<GrnPayment> _payments;
    private readonly IGeneralLedgerService _gl;
    private readonly ICurrentUser _currentUser;
    private readonly IUnitOfWork _uow;
    private readonly string _glClearingAccountCode;

    public GrnPaymentService(
        IRepository<Grn> grns, IRepository<GrnPayment> payments, IGeneralLedgerService gl,
        ICurrentUser currentUser, IUnitOfWork uow, IConfiguration configuration)
    {
        _grns = grns;
        _payments = payments;
        _gl = gl;
        _currentUser = currentUser;
        _uow = uow;
        _glClearingAccountCode = configuration["Grn:GlClearingAccountCode"] ?? string.Empty;
    }

    public async Task<GrnPaymentDto> PayAsync(int grnId, CreateGrnPaymentRequest request, CancellationToken ct = default)
    {
        var grn = await _grns.Query().FirstOrDefaultAsync(g => g.GrnId == grnId, ct)
            ?? throw new NotFoundException($"GRN {grnId} was not found.");

        if (grn.Status == GrnStatus.Draft)
            throw new DomainException(ErrorCodes.GrnNotPayable, $"GRN {grnId} must be confirmed before it can be paid.", 409);
        if (request.Amount > grn.BalanceAmount)
            throw new DomainException(ErrorCodes.GrnPaymentExceedsBalance,
                $"Payment amount {request.Amount} exceeds the remaining balance {grn.BalanceAmount}.", 400);

        var accounts = await _gl.ListBankAccountsAsync(ct);
        var account = accounts.FirstOrDefault(a => a.AccountId == request.GlBankAccountId)
            ?? throw new DomainException(ErrorCodes.GrnBankAccountNotFound, $"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 GrnService.ConfirmAsync: 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 = "GRN_PAYMENT",
                Reference = grn.DocNo,
                Narration = $"Payment against GRN {grn.DocNo}",
                Lines =
                [
                    new GlJournalEntryLineRequest(_glClearingAccountCode, request.Amount, 0m, $"Payment against GRN {grn.DocNo}"),
                    new GlJournalEntryLineRequest(account.GlAccountCode, 0m, request.Amount, $"Payment against GRN {grn.DocNo}")
                ]
            }, token);

            var entity = new GrnPayment
            {
                GrnId = grn.GrnId,
                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);

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

            return entity;
        }, ct);

        return Map(payment);
    }

    public async Task<IReadOnlyList<GrnPaymentDto>> ListAsync(int grnId, CancellationToken ct = default)
    {
        if (!await _grns.Query().AnyAsync(g => g.GrnId == grnId, ct))
            throw new NotFoundException($"GRN {grnId} was not found.");

        return await _payments.Query().AsNoTracking()
            .Where(p => p.GrnId == grnId)
            .OrderByDescending(p => p.GrnPaymentId)
            .Select(p => new GrnPaymentDto(
                p.GrnPaymentId, p.GrnId, p.Amount, p.PaymentDate,
                p.GlBankAccountId, p.BankAccountName, p.Reference, p.GlJournalNo, p.CreatedAt))
            .ToListAsync(ct);
    }

    private static GrnPaymentDto Map(GrnPayment p) => new(
        p.GrnPaymentId, p.GrnId, p.Amount, p.PaymentDate,
        p.GlBankAccountId, p.BankAccountName, p.Reference, p.GlJournalNo, p.CreatedAt);
}
