using ERPCore.Common.Http;
using ERPCore.Domain;
using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Procurement;
using ERPCore.Infra.Auth;
using ERPCore.Infra.UoW;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services;

/// <summary>
/// Purchase-order service. Phase 1 auto-approves on creation (FR-PROC-04),
/// PO is freely editable while open (FR-PROC-05), totals are computed server-side
/// (02-SECURITY C.2), and cancel is blocked once any receipt exists.
/// </summary>
public sealed class PurchaseOrderService : IPurchaseOrderService
{
    private const string BaseCurrency = "LKR";

    private readonly IRepository<PurchaseOrder> _pos;
    private readonly IRepository<Vendor> _vendors;
    private readonly IRepository<Requisition> _requisitions;
    private readonly IRepository<Item> _items;
    private readonly IRepository<Uom> _uoms;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly INumberSequenceService _numbers;
    private readonly ICurrentUser _currentUser;
    private readonly IUnitOfWork _uow;

    public PurchaseOrderService(
        IRepository<PurchaseOrder> pos, IRepository<Vendor> vendors, IRepository<Requisition> requisitions,
        IRepository<Item> items, IRepository<Uom> uoms, IRepository<Warehouse> warehouses,
        INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow)
    {
        _pos = pos;
        _vendors = vendors;
        _requisitions = requisitions;
        _items = items;
        _uoms = uoms;
        _warehouses = warehouses;
        _numbers = numbers;
        _currentUser = currentUser;
        _uow = uow;
    }

    public async Task<PagedResponse<PurchaseOrderSummaryDto>> ListAsync(
        PageQuery query, PurchaseOrderStatus? status, int? vendorId, CancellationToken ct = default)
    {
        var q = _pos.Query().AsNoTracking().Include(p => p.Lines).AsQueryable();
        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(p => EF.Functions.ILike(p.DocNo, $"%{term}%"));
        }
        if (status is not null) q = q.Where(p => p.Status == status);
        if (vendorId is not null) q = q.Where(p => p.VendorId == vendorId);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(p => p.PoId)
            .Skip(query.Skip).Take(query.PageSize)
            .ToListAsync(ct);

        var summaries = rows.Select(p => new PurchaseOrderSummaryDto(
            p.PoId, p.DocNo, p.VendorId, p.Status, p.ApprovalRequired, p.CreatedAt, ComputeTotals(p.Lines))).ToList();

        return PagedResponse<PurchaseOrderSummaryDto>.Create(summaries, query.Page, query.PageSize, total);
    }

    public async Task<ETagged<PurchaseOrderDto>?> GetAsync(int poId, CancellationToken ct = default)
    {
        var po = await _pos.Query().AsNoTracking()
            .Include(p => p.Lines)
            .FirstOrDefaultAsync(p => p.PoId == poId, ct);
        return po is null ? null : new ETagged<PurchaseOrderDto>(Map(po), po.RowVersion);
    }

    public async Task<ETagged<PurchaseOrderDto>> CreateAsync(CreatePurchaseOrderRequest request, CancellationToken ct = default)
    {
        await ValidateReferencesAsync(request.VendorId, request.RequisitionId, request.Lines, ct);
        var actor = _currentUser.AuditUserId;

        var po = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var docNo = await _numbers.NextAsync(DocumentTypes.PurchaseOrder, token);
            var entity = new PurchaseOrder
            {
                DocNo = docNo,
                VendorId = request.VendorId,
                RequisitionId = request.RequisitionId,
                // Phase 1: approvalRequired defaults off → auto-approved on creation (FR-PROC-04),
                // unless the caller explicitly saves a Draft (editable/deletable until submitted).
                ApprovalRequired = false,
                Status = request.SaveAsDraft ? PurchaseOrderStatus.Draft : PurchaseOrderStatus.Approved,
                CreatedBy = actor,
                CreatedAt = DateTime.UtcNow,
                Lines = request.Lines.Select(ToLine).ToList()
            };
            await _pos.AddAsync(entity, token);
            return entity;
        }, ct);

        return new ETagged<PurchaseOrderDto>(Map(po), po.RowVersion);
    }

    public async Task<ETagged<PurchaseOrderDto>> UpdateAsync(
        int poId, UpdatePurchaseOrderRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var po = await _pos.Query()
            .Include(p => p.Lines)
            .FirstOrDefaultAsync(p => p.PoId == poId, ct)
            ?? throw new NotFoundException($"Purchase order {poId} was not found.");

        if (po.RowVersion != expectedRowVersion)
            throw new DomainException(ErrorCodes.ConcurrencyConflict, "The purchase order was modified by another request.", 412);

        if (!IsEditable(po.Status))
            throw new DomainException(ErrorCodes.PoNotEditable, $"Purchase order {poId} is {po.Status} and cannot be edited.", 409);

        await ValidateReferencesAsync(request.VendorId, request.RequisitionId, request.Lines, ct);

        po.VendorId = request.VendorId;
        po.RequisitionId = request.RequisitionId;
        po.UpdatedAt = DateTime.UtcNow;

        // Full line replacement (Phase 1: no receipts yet, so qtyReceived is 0 on every line).
        po.Lines.Clear();
        foreach (var input in request.Lines)
            po.Lines.Add(ToLine(input));

        try
        {
            await _uow.SaveChangesAsync(ct);
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new DomainException(ErrorCodes.ConcurrencyConflict, "The purchase order was modified by another request.", 412);
        }

        return new ETagged<PurchaseOrderDto>(Map(po), po.RowVersion);
    }

    public async Task<PurchaseOrderDto> ApproveAsync(int poId, CancellationToken ct = default)
    {
        var po = await _pos.Query()
            .Include(p => p.Lines)
            .FirstOrDefaultAsync(p => p.PoId == poId, ct)
            ?? throw new NotFoundException($"Purchase order {poId} was not found.");

        // Phase 1 no-op: POs are already Approved on creation. Kept for the future
        // approval workflow (PendingApproval → Approved) — FR-PROC-04.
        if (po.Status == PurchaseOrderStatus.PendingApproval)
        {
            po.Status = PurchaseOrderStatus.Approved;
            po.UpdatedAt = DateTime.UtcNow;
            await _uow.SaveChangesAsync(ct);
        }

        return Map(po);
    }

    public async Task<PurchaseOrderDto> CancelAsync(int poId, string? reason, CancellationToken ct = default)
    {
        var po = await _pos.Query()
            .Include(p => p.Lines)
            .FirstOrDefaultAsync(p => p.PoId == poId, ct)
            ?? throw new NotFoundException($"Purchase order {poId} was not found.");

        if (po.Lines.Any(l => l.QtyReceived > 0))
            throw new ConflictException($"Purchase order {poId} cannot be cancelled because goods have been received against it.");

        if (po.Status != PurchaseOrderStatus.Cancelled)
        {
            po.Status = PurchaseOrderStatus.Cancelled;
            po.UpdatedAt = DateTime.UtcNow;
            await _uow.SaveChangesAsync(ct);
        }

        return Map(po);
    }

    public async Task<PurchaseOrderDto> SubmitAsync(int poId, CancellationToken ct = default)
    {
        var po = await _pos.Query()
            .Include(p => p.Lines)
            .FirstOrDefaultAsync(p => p.PoId == poId, ct)
            ?? throw new NotFoundException($"Purchase order {poId} was not found.");

        if (po.Status != PurchaseOrderStatus.Draft)
            throw new DomainException(ErrorCodes.PoNotEditable, $"Purchase order {poId} is {po.Status} and cannot be submitted.", 409);

        // Phase 1: no value gate, so a submitted draft goes straight to Approved (FR-PROC-04).
        po.Status = PurchaseOrderStatus.Approved;
        po.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);

        return Map(po);
    }

    public async Task DeleteAsync(int poId, CancellationToken ct = default)
    {
        var po = await _pos.Query()
            .Include(p => p.Lines)
            .FirstOrDefaultAsync(p => p.PoId == poId, ct)
            ?? throw new NotFoundException($"Purchase order {poId} was not found.");

        if (po.Status != PurchaseOrderStatus.Draft)
            throw new DomainException(ErrorCodes.PoNotEditable, $"Purchase order {poId} is {po.Status} and cannot be deleted; only a Draft can be deleted.", 409);

        _pos.Remove(po);
        await _uow.SaveChangesAsync(ct);
    }

    // FR-PROC-05 (revised): a PO is editable/deletable only while Draft. Submitting locks it.
    // Supersedes Phase-1 Option B "freely editable while open" — see docs/10 FR-PROC-05.
    private static bool IsEditable(PurchaseOrderStatus status) => status is PurchaseOrderStatus.Draft;

    private static PoLine ToLine(CreatePoLineInput l) => new()
    {
        ItemId = l.ItemId,
        WarehouseId = l.WarehouseId,
        Qty = l.Qty,
        UnitPrice = l.UnitPrice,
        Tax = l.Tax,
        QtyReceived = 0
    };

    private static PoTotalsDto ComputeTotals(IEnumerable<PoLine> lines)
    {
        decimal sub = 0, tax = 0;
        foreach (var l in lines)
        {
            var net = l.Qty * l.UnitPrice;
            sub += net;
            tax += net * l.Tax;
        }
        sub = Math.Round(sub, 2, MidpointRounding.AwayFromZero);
        tax = Math.Round(tax, 2, MidpointRounding.AwayFromZero);
        return new PoTotalsDto(sub, tax, sub + tax, BaseCurrency);
    }

    private async Task ValidateReferencesAsync(
        int vendorId, int? requisitionId, IReadOnlyCollection<CreatePoLineInput> lines, CancellationToken ct)
    {
        var vendor = await _vendors.Query().AsNoTracking().FirstOrDefaultAsync(v => v.VendorId == vendorId, ct);
        if (vendor is null)
            throw new DomainException(ErrorCodes.Validation, $"Vendor {vendorId} does not exist.", 422);
        if (vendor.Status != EntityStatus.Active)
            throw new DomainException(ErrorCodes.Validation, $"Vendor {vendorId} is inactive.", 422);

        if (requisitionId is not null
            && !await _requisitions.Query().AnyAsync(r => r.RequisitionId == requisitionId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Requisition {requisitionId} does not exist.", 422);

        await EnsureAllExistAsync(_items.Query().Select(i => i.ItemId), lines.Select(l => l.ItemId), "Item", ct);
        await EnsureAllExistAsync(_warehouses.Query().Select(w => w.WarehouseId), lines.Select(l => l.WarehouseId), "Warehouse", ct);
    }

    private static async Task EnsureAllExistAsync(
        IQueryable<int> keySource, IEnumerable<int> requested, string label, CancellationToken ct)
    {
        var ids = requested.Distinct().ToList();
        var found = await keySource.Where(k => ids.Contains(k)).ToListAsync(ct);
        var missing = ids.Except(found).ToList();
        if (missing.Count > 0)
            throw new DomainException(ErrorCodes.Validation, $"{label}(s) not found: {string.Join(", ", missing)}.", 422);
    }

    private static PurchaseOrderDto Map(PurchaseOrder p) => new(
        p.PoId, p.DocNo, p.VendorId, p.RequisitionId, p.Status, p.ApprovalRequired,
        p.CreatedBy, p.CreatedAt, p.UpdatedAt, ComputeTotals(p.Lines),
        p.Lines.OrderBy(l => l.PoLineId).Select(l => new PoLineDto(
            l.PoLineId, l.ItemId, l.WarehouseId, l.Qty, l.UnitPrice, l.Tax, l.QtyReceived)).ToList());
}
