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-count service (FR-STK-08). Create snapshots system quantities (immutable,
/// 02-SECURITY C.7); posting emits a variance <see cref="StockAdjustment"/> via the
/// shared <see cref="IStockMutator"/> (a variance is an adjustment in disguise, C.7)
/// and closes the count — all in one UoW transaction. GL posting for that variance
/// mirrors <see cref="AdjustmentService"/>'s (same account config, same gain/loss
/// split) since it's the exact same kind of document under the hood.
/// </summary>
public sealed class CountService : ICountService
{
    private const string VarianceReasonCode = "VAR";

    private readonly IRepository<StockCount> _counts;
    private readonly IRepository<StockAdjustment> _adjustments;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly IRepository<Item> _items;
    private readonly IRepository<ReasonCode> _reasonCodes;
    private readonly IFifoCostingService _fifo;
    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 CountService(
        IRepository<StockCount> counts, IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses,
        IRepository<Item> items, IRepository<ReasonCode> reasonCodes, IFifoCostingService fifo, IStockMutator mutator,
        INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow,
        IGeneralLedgerService gl, IConfiguration configuration)
    {
        _counts = counts;
        _adjustments = adjustments;
        _warehouses = warehouses;
        _items = items;
        _reasonCodes = reasonCodes;
        _fifo = fifo;
        _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>Same logic as <c>AdjustmentService</c>'s private method of the same name — see there for why
    /// gains/losses post as separate lines instead of a net.</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<CountSummaryDto>> ListAsync(
        PageQuery query, CountStatus? status, int? warehouseId, CancellationToken ct = default)
    {
        var q = _counts.Query().AsNoTracking();

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

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(c => c.CountId)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(c => new CountSummaryDto(
                c.CountId, c.DocNo, c.WarehouseId, c.CountType, c.Status,
                c.CreatedBy, c.CreatedAt, c.Lines.Count))
            .ToListAsync(ct);

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

    public async Task<CountDto?> GetAsync(int countId, CancellationToken ct = default)
    {
        var count = await _counts.Query().AsNoTracking().Include(c => c.Lines)
            .FirstOrDefaultAsync(c => c.CountId == countId, ct);
        return count is null ? null : Map(count);
    }

    public async Task<CountDto> CreateAsync(CreateCountRequest request, CancellationToken ct = default)
    {
        if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.WarehouseId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Warehouse {request.WarehouseId} does not exist.", 422);

        var itemIds = request.ItemIds.Distinct().ToList();
        foreach (var id in itemIds)
            if (!await _items.Query().AnyAsync(i => i.ItemId == id, ct))
                throw new DomainException(ErrorCodes.Validation, $"Item {id} does not exist.", 422);

        // Snapshot system quantities now (immutable once opened).
        var lines = new List<StockCountLine>();
        foreach (var id in itemIds)
        {
            var systemQty = await _fifo.GetOnHandAsync(id, request.WarehouseId, ct);
            lines.Add(new StockCountLine { ItemId = id, SystemQty = systemQty });
        }

        var count = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var docNo = await _numbers.NextAsync(DocumentTypes.Count, token);
            var entity = new StockCount
            {
                DocNo = docNo,
                WarehouseId = request.WarehouseId,
                CountType = request.CountType,
                Status = CountStatus.Draft,
                CreatedBy = _currentUser.AuditUserId,
                CreatedAt = DateTime.UtcNow,
                Lines = lines
            };
            await _counts.AddAsync(entity, token);
            return entity;
        }, ct);

        return Map(count);
    }

    public async Task<CountDto> EnterCountsAsync(int countId, EnterCountsRequest request, CancellationToken ct = default)
    {
        var count = await _counts.Query().Include(c => c.Lines)
            .FirstOrDefaultAsync(c => c.CountId == countId, ct)
            ?? throw new NotFoundException($"Count {countId} was not found.");
        if (count.Status == CountStatus.Posted)
            throw new ConflictException($"Count {countId} is already posted.");

        foreach (var input in request.Lines)
        {
            var line = count.Lines.FirstOrDefault(l => l.CountLineId == input.CountLineId)
                ?? throw new DomainException(ErrorCodes.Validation, $"Count line {input.CountLineId} is not on count {countId}.", 422);
            line.CountedQty = input.CountedQty;
            line.Variance = input.CountedQty - line.SystemQty;
        }

        count.Status = CountStatus.Counted;
        await _uow.SaveChangesAsync(ct);
        return Map(count);
    }

    public async Task<CountPostResultDto> PostAsync(int countId, CancellationToken ct = default)
    {
        var count = await _counts.Query().Include(c => c.Lines)
            .FirstOrDefaultAsync(c => c.CountId == countId, ct)
            ?? throw new NotFoundException($"Count {countId} was not found.");
        if (count.Status == CountStatus.Posted)
            throw new ConflictException($"Count {countId} is already posted.");
        if (count.Status != CountStatus.Counted)
            throw new ConflictException($"Count {countId} has no entered counts to post.");

        var deltas = count.Lines
            .Where(l => l.Variance.HasValue && l.Variance.Value != 0)
            .Select(l => new StockDelta(l.ItemId, l.BinId, null, l.Variance!.Value))
            .ToList();

        // No variances → just close the count, no adjustment.
        if (deltas.Count == 0)
        {
            count.Status = CountStatus.Posted;
            await _uow.SaveChangesAsync(ct);
            return new CountPostResultDto(count.CountId, count.Status, null, null, Array.Empty<int>());
        }

        var reason = await _reasonCodes.Query().AsNoTracking()
            .FirstOrDefaultAsync(r => r.Context == ReasonContext.Adjustment && r.Code == VarianceReasonCode, ct)
            ?? throw new DomainException(ErrorCodes.Validation, $"Reason code '{VarianceReasonCode}' (Count Variance) is not configured.", 422);

        var now = DateTime.UtcNow;
        var (adjustmentId, glJournalNo, ledgerEntries) = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var docNo = await _numbers.NextAsync(DocumentTypes.Adjustment, token);
            var adjustment = new StockAdjustment
            {
                DocNo = docNo,
                WarehouseId = count.WarehouseId,
                ReasonCodeId = reason.ReasonCodeId,
                Status = AdjustmentStatus.Posted,
                CreatedBy = _currentUser.AuditUserId,
                CreatedAt = now,
                Lines = deltas.Select(d => new StockAdjustmentLine
                {
                    ItemId = d.ItemId,
                    BinId = d.BinId,
                    QtyDelta = d.QtyDelta
                }).ToList()
            };
            await _adjustments.AddAsync(adjustment, token);
            await _uow.SaveChangesAsync(token); // flush for a valid ledger sourceDocId

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

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

            count.Status = CountStatus.Posted;
            return (adjustment.AdjustmentId, adjustment.GlJournalNo, refs);
        }, ct);

        // Map ledger ids after commit so they are populated.
        return new CountPostResultDto(count.CountId, count.Status, adjustmentId, glJournalNo, ledgerEntries.Select(r => r.LedgerId).ToList());
    }

    private static CountDto Map(StockCount c) => new(
        c.CountId, c.DocNo, c.WarehouseId, c.CountType, c.Status, c.CreatedBy, c.CreatedAt,
        c.Lines.OrderBy(l => l.CountLineId)
            .Select(l => new CountLineDto(l.CountLineId, l.ItemId, l.BinId, l.SystemQty, l.CountedQty, l.Variance)).ToList());
}
