using ERPCore.Domain;
using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Stock;
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>
/// Stock-adjustment service — the highest-risk feature (02-SECURITY C.5). Auto-posts
/// with a mandatory reason code and user stamp. Line application (FIFO consume on a
/// decrease, layer create on an increase) + ledger posting is delegated to
/// <see cref="IStockMutator"/>. Runs in a single UoW transaction (NFR-02/05). Also posts
/// a real GL journal entry — increases and decreases post as separate Inventory/Gain and
/// Loss/Inventory lines respectively (never netted against each other), so a count that's
/// simultaneously over on one item and under on another shows both, not a false net.
/// </summary>
public sealed class AdjustmentService : IAdjustmentService
{
    private readonly IRepository<StockAdjustment> _adjustments;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly IRepository<Item> _items;
    private readonly IRepository<ReasonCode> _reasonCodes;
    private readonly IRepository<StockLedger> _ledger;
    private readonly IStockMutator _mutator;
    private readonly INumberSequenceService _numbers;
    private readonly ICurrentUser _currentUser;
    private readonly IUnitOfWork _uow;
    private readonly IGeneralLedgerService _gl;
    private readonly string _glInventoryAccountCode;
    private readonly string _glGainAccountCode;
    private readonly string _glLossAccountCode;

    public AdjustmentService(
        IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses, IRepository<Item> items,
        IRepository<ReasonCode> reasonCodes, IRepository<StockLedger> ledger, IStockMutator mutator,
        INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow,
        IGeneralLedgerService gl, IConfiguration configuration)
    {
        _adjustments = adjustments;
        _warehouses = warehouses;
        _items = items;
        _reasonCodes = reasonCodes;
        _ledger = ledger;
        _mutator = mutator;
        _numbers = numbers;
        _currentUser = currentUser;
        _uow = uow;
        _gl = gl;
        _glInventoryAccountCode = configuration["Adjustment:GlInventoryAccountCode"] ?? string.Empty;
        _glGainAccountCode = configuration["Adjustment:GlGainAccountCode"] ?? string.Empty;
        _glLossAccountCode = configuration["Adjustment:GlLossAccountCode"] ?? string.Empty;
    }

    /// <summary>Builds and posts the GL journal for a <see cref="StockAdjustment"/>'s stock movement.
    /// <see cref="CountService"/>'s variance posting mirrors this exact logic for the same reason
    /// (it creates a <see cref="StockAdjustment"/> through the same <see cref="IStockMutator"/> call).</summary>
    private async Task<string?> PostAdjustmentJournalAsync(
        string docNo, DateTime now, IReadOnlyList<StockLedger> refs, CancellationToken ct)
    {
        var gain = refs.Where(r => r.Direction == Direction.In).Sum(r => r.Value);
        var loss = refs.Where(r => r.Direction == Direction.Out).Sum(r => r.Value);
        if (gain <= 0 && loss <= 0) return null;

        var period = await _gl.GetPeriodByDateAsync(DateOnly.FromDateTime(now), ct);
        var lines = new List<GlJournalEntryLineRequest>();
        if (gain > 0)
        {
            lines.Add(new GlJournalEntryLineRequest(_glInventoryAccountCode, gain, 0m, $"Adjustment {docNo} — stock increase"));
            lines.Add(new GlJournalEntryLineRequest(_glGainAccountCode, 0m, gain, $"Adjustment {docNo} — inventory gain"));
        }
        if (loss > 0)
        {
            lines.Add(new GlJournalEntryLineRequest(_glLossAccountCode, loss, 0m, $"Adjustment {docNo} — inventory loss"));
            lines.Add(new GlJournalEntryLineRequest(_glInventoryAccountCode, 0m, loss, $"Adjustment {docNo} — stock decrease"));
        }

        var result = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest
        {
            PeriodId = period.PeriodId,
            EntryDate = DateOnly.FromDateTime(now),
            SourceModule = "ADJUSTMENT",
            Reference = docNo,
            Narration = $"Stock adjustment {docNo}",
            Lines = lines
        }, ct);
        return result.JournalNo;
    }

    public async Task<PagedResponse<AdjustmentSummaryDto>> ListAsync(
        PageQuery query, int? warehouseId, int? reasonCodeId, CancellationToken ct = default)
    {
        var q = _adjustments.Query().AsNoTracking();

        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(a => EF.Functions.ILike(a.DocNo, $"%{term}%"));
        }
        if (warehouseId is not null) q = q.Where(a => a.WarehouseId == warehouseId);
        if (reasonCodeId is not null) q = q.Where(a => a.ReasonCodeId == reasonCodeId);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(a => a.AdjustmentId)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(a => new AdjustmentSummaryDto(
                a.AdjustmentId, a.DocNo, a.WarehouseId, a.ReasonCodeId, a.Status,
                a.CreatedBy, a.CreatedAt, a.Lines.Count, a.GlJournalNo))
            .ToListAsync(ct);

        return PagedResponse<AdjustmentSummaryDto>.Create(rows, query.Page, query.PageSize, total);
    }

    public async Task<AdjustmentDto?> GetAsync(int adjustmentId, CancellationToken ct = default)
    {
        var adjustment = await _adjustments.Query().AsNoTracking()
            .Include(a => a.Lines)
            .FirstOrDefaultAsync(a => a.AdjustmentId == adjustmentId, ct);
        if (adjustment is null) return null;

        // The ledger reference is polymorphic (docs/10 C.9) — there is no FK to follow,
        // so the refs this adjustment posted are recovered by source-doc lookup.
        var ledgerRefs = await _ledger.Query().AsNoTracking()
            .Where(l => l.SourceDocType == DocumentTypes.Adjustment && l.SourceDocId == adjustmentId)
            .OrderBy(l => l.LedgerId)
            .Select(l => l.LedgerId)
            .ToListAsync(ct);

        return ToDto(adjustment, ledgerRefs);
    }

    public async Task<AdjustmentDto> CreateAsync(CreateAdjustmentRequest request, CancellationToken ct = default)
    {
        if (request.ReasonCodeId is null)
            throw new DomainException(ErrorCodes.ReasonCodeRequired, "A reason code is required for an adjustment.", 400);

        if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422);

        var reason = await _reasonCodes.Query().AsNoTracking().FirstOrDefaultAsync(r => r.ReasonCodeId == request.ReasonCodeId, ct)
            ?? throw new DomainException(ErrorCodes.Validation, $"Reason code {request.ReasonCodeId} does not exist.", 422);
        if (reason.Context != ReasonContext.Adjustment)
            throw new DomainException(ErrorCodes.Validation, $"Reason code {request.ReasonCodeId} is not an Adjustment reason.", 422);

        foreach (var line in request.Lines)
        {
            if (line.QtyDelta == 0)
                throw new DomainException(ErrorCodes.Validation, "qtyDelta cannot be zero.", 422);
            if (!await _items.Query().AnyAsync(i => i.ItemId == line.ItemId, ct))
                throw new DomainException(ErrorCodes.Validation, $"Item {line.ItemId} does not exist.", 422);
        }

        var now = DateTime.UtcNow;
        var deltas = request.Lines
            .Select(l => new StockDelta(l.ItemId, l.BinId, l.BatchId, l.QtyDelta)).ToList();

        var (adjustment, ledgerRefs) = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var docNo = await _numbers.NextAsync(DocumentTypes.Adjustment, token);
            var entity = new StockAdjustment
            {
                DocNo = docNo,
                WarehouseId = request.WarehouseId,
                ReasonCodeId = request.ReasonCodeId.Value,
                Status = AdjustmentStatus.Posted,
                CreatedBy = _currentUser.AuditUserId,
                CreatedAt = now,
                Lines = request.Lines.Select(l => new StockAdjustmentLine
                {
                    ItemId = l.ItemId,
                    BinId = l.BinId,
                    BatchId = l.BatchId,
                    QtyDelta = l.QtyDelta
                }).ToList()
            };
            await _adjustments.AddAsync(entity, token);
            await _uow.SaveChangesAsync(token); // flush so AdjustmentId is a valid ledger sourceDocId

            var refs = await _mutator.ApplyAsync(request.WarehouseId, DocumentTypes.Adjustment, entity.AdjustmentId, now, deltas, token);

            entity.GlJournalNo = await PostAdjustmentJournalAsync(docNo, now, refs, token);
            if (entity.GlJournalNo is not null) entity.GlPostedAt = now;

            return (entity, refs);
        }, ct);

        return ToDto(adjustment, ledgerRefs.Select(l => l.LedgerId).ToList());
    }

    private static AdjustmentDto ToDto(StockAdjustment a, IReadOnlyList<int> ledgerRefs) => new(
        a.AdjustmentId, a.DocNo, a.WarehouseId, a.ReasonCodeId, a.Status, a.CreatedBy, a.CreatedAt,
        a.GlJournalNo, a.GlPostedAt,
        a.Lines.OrderBy(l => l.AdjLineId)
            .Select(l => new AdjustmentLineDto(l.AdjLineId, l.ItemId, l.BinId, l.BatchId, l.QtyDelta)).ToList(),
        ledgerRefs);
}
