using System.ComponentModel.DataAnnotations;
using foodmarket.Application.Common;
using foodmarket.Application.Inventory;
using foodmarket.Domain.Inventory;
using foodmarket.Domain.Sales;
using foodmarket.Infrastructure.Persistence;
using foodmarket.Api.Infrastructure.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace foodmarket.Api.Controllers.Sales;

/// <summary>Оптовая отгрузка (Demand). Списывает товар со склада в адрес
/// юрлица-контрагента. Зеркалит <see cref="RetailSalesController"/>, но
/// без RetailPoint/Cashier и с другим способом оплаты (Credit вместо Mixed/Bonus).
/// Множ. возвратов нет в MVP — учётный шаг без подтверждения дебиторки.</summary>
[ApiController]
[Authorize]
[Route("api/sales/demands")]
public class DemandsController : ControllerBase
{
    private readonly AppDbContext _db;
    private readonly IStockService _stock;

    public DemandsController(AppDbContext db, IStockService stock)
    {
        _db = db;
        _stock = stock;
    }

    public record DemandListRow(
        Guid Id, string Number, DateTime Date, DemandStatus Status,
        Guid CustomerId, string CustomerName,
        Guid StoreId, string StoreName,
        Guid CurrencyId, string CurrencyCode,
        decimal Total, decimal PaidAmount,
        DemandPayment Payment, int LineCount,
        DateTime? PostedAt);

    public record DemandLineDto(
        Guid? Id, Guid ProductId, string? ProductName, string? ProductArticle, string? UnitSymbol,
        decimal Quantity, decimal UnitPrice, decimal Discount, decimal LineTotal,
        decimal VatPercent, int SortOrder,
        decimal? StockAtStore);

    public record DemandDto(
        Guid Id, string Number, DateTime Date, DemandStatus Status,
        Guid CustomerId, string CustomerName,
        Guid StoreId, string StoreName,
        Guid CurrencyId, string CurrencyCode,
        DemandPayment Payment, decimal Subtotal, decimal DiscountTotal,
        decimal Total, decimal PaidAmount,
        string? Notes, DateTime? PostedAt,
        IReadOnlyList<DemandLineDto> Lines);

    public record DemandLineInput(
        Guid ProductId,
        [Range(0, 1e10)] decimal Quantity,
        [Range(0, 1e10)] decimal UnitPrice,
        [Range(0, 1e10)] decimal Discount,
        [Range(0, 100)] decimal VatPercent);

    public record DemandInput(
        DateTime Date,
        Guid CustomerId, Guid StoreId, Guid CurrencyId,
        DemandPayment Payment,
        [Range(0, 1e10)] decimal PaidAmount,
        string? Notes,
        IReadOnlyList<DemandLineInput> Lines);

    [HttpGet]
    public async Task<ActionResult<PagedResult<DemandListRow>>> List(
        [FromQuery] PagedRequest req,
        [FromQuery] DemandStatus? status,
        [FromQuery] Guid? customerId,
        [FromQuery] Guid? storeId,
        CancellationToken ct)
    {
        var q = from d in _db.Demands.AsNoTracking()
                join cp in _db.Counterparties on d.CustomerId equals cp.Id
                join st in _db.Stores on d.StoreId equals st.Id
                join cu in _db.Currencies on d.CurrencyId equals cu.Id
                select new { d, cp, st, cu };

        if (status is not null) q = q.Where(x => x.d.Status == status);
        if (customerId is not null) q = q.Where(x => x.d.CustomerId == customerId);
        if (storeId is not null) q = q.Where(x => x.d.StoreId == storeId);
        if (!string.IsNullOrWhiteSpace(req.Search))
        {
            var s = req.Search.Trim().ToLower();
            q = q.Where(x => x.d.Number.ToLower().Contains(s) || x.cp.Name.ToLower().Contains(s));
        }

        var total = await q.CountAsync(ct);
        q = (req.Sort, req.Desc) switch
        {
            ("number", false) => q.OrderBy(x => x.d.Number),
            ("number", true) => q.OrderByDescending(x => x.d.Number),
            ("customer", false) => q.OrderBy(x => x.cp.Name).ThenByDescending(x => x.d.Date),
            ("customer", true) => q.OrderByDescending(x => x.cp.Name).ThenByDescending(x => x.d.Date),
            ("status", false) => q.OrderBy(x => x.d.Status).ThenByDescending(x => x.d.Date),
            ("status", true) => q.OrderByDescending(x => x.d.Status).ThenByDescending(x => x.d.Date),
            ("total", false) => q.OrderBy(x => x.d.Total).ThenByDescending(x => x.d.Date),
            ("total", true) => q.OrderByDescending(x => x.d.Total).ThenByDescending(x => x.d.Date),
            ("date", false) => q.OrderBy(x => x.d.Date).ThenBy(x => x.d.Number),
            _ => q.OrderByDescending(x => x.d.Date).ThenByDescending(x => x.d.Number),
        };
        var items = await q.Skip(req.Skip).Take(req.Take)
            .Select(x => new DemandListRow(
                x.d.Id, x.d.Number, x.d.Date, x.d.Status,
                x.cp.Id, x.cp.Name,
                x.st.Id, x.st.Name,
                x.cu.Id, x.cu.Code,
                x.d.Total, x.d.PaidAmount, x.d.Payment, x.d.Lines.Count,
                x.d.PostedAt))
            .ToListAsync(ct);

        return new PagedResult<DemandListRow> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
    }

    [HttpGet("{id:guid}")]
    public async Task<ActionResult<DemandDto>> Get(Guid id, CancellationToken ct)
    {
        var dto = await GetInternal(id, ct);
        return dto is null ? NotFound() : Ok(dto);
    }

    [HttpPost, RequiresPermission("DemandsEdit")]
    public async Task<ActionResult<DemandDto>> Create([FromBody] DemandInput input, CancellationToken ct)
    {
        if (RequiredGuid.FirstMissing(
                (nameof(input.CustomerId), input.CustomerId),
                (nameof(input.StoreId), input.StoreId),
                (nameof(input.CurrencyId), input.CurrencyId)) is { } missing)
            return BadRequest(new { error = $"Поле {missing} обязательно.", field = missing });
        if (input.Lines is null || input.Lines.Count == 0)
            return BadRequest(new { error = "Отгрузка должна содержать хотя бы одну позицию." });

        var number = await GenerateNumberAsync(input.Date, ct);
        var demand = new Demand
        {
            Number = number,
            Date = input.Date,
            Status = DemandStatus.Draft,
            CustomerId = input.CustomerId,
            StoreId = input.StoreId,
            CurrencyId = input.CurrencyId,
            Payment = input.Payment,
            PaidAmount = input.PaidAmount,
            Notes = input.Notes,
        };
        ApplyLines(demand, input.Lines);
        _db.Demands.Add(demand);
        if (await SaveOrFkErrorAsync(ct) is { } err) return err;
        var dto = await GetInternal(demand.Id, ct);
        return CreatedAtAction(nameof(Get), new { id = demand.Id }, dto);
    }

    private async Task<ActionResult?> SaveOrFkErrorAsync(CancellationToken ct)
    {
        try
        {
            await _db.SaveChangesAsync(ct);
            return null;
        }
        catch (DbUpdateException ex) when (ex.InnerException is Npgsql.PostgresException pg && pg.SqlState == "23503")
        {
            var name = pg.ConstraintName ?? "";
            string field = name.Contains("Customer") ? "customerId"
                         : name.Contains("Store") ? "storeId"
                         : name.Contains("Currency") ? "currencyId"
                         : name.Contains("Product") ? "productId"
                         : "(unknown)";
            return BadRequest(new { error = $"Связанная запись не найдена: {field}.", field, constraint = name });
        }
    }

    [HttpPut("{id:guid}"), RequiresPermission("DemandsEdit")]
    public async Task<IActionResult> Update(Guid id, [FromBody] DemandInput input, CancellationToken ct)
    {
        if (RequiredGuid.FirstMissing(
                (nameof(input.CustomerId), input.CustomerId),
                (nameof(input.StoreId), input.StoreId),
                (nameof(input.CurrencyId), input.CurrencyId)) is { } missing)
            return BadRequest(new { error = $"Поле {missing} обязательно.", field = missing });
        if (input.Lines is null || input.Lines.Count == 0)
            return BadRequest(new { error = "Отгрузка должна содержать хотя бы одну позицию." });

        var demand = await _db.Demands.FirstOrDefaultAsync(d => d.Id == id, ct);
        if (demand is null) return NotFound();
        if (demand.Status != DemandStatus.Draft)
            return Conflict(new { error = "Только черновик может быть изменён. Сначала отмени проведение." });

        demand.Date = input.Date;
        demand.CustomerId = input.CustomerId;
        demand.StoreId = input.StoreId;
        demand.CurrencyId = input.CurrencyId;
        demand.Payment = input.Payment;
        demand.PaidAmount = input.PaidAmount;
        demand.Notes = input.Notes;

        // Удаляем старые строки через ExecuteDelete (минует трекер), добавляем новые
        // напрямую в DbSet — тот же паттерн что в RetailSale.Update (см. P1-8 fix).
        await _db.DemandLines.Where(l => l.DemandId == demand.Id).ExecuteDeleteAsync(ct);
        ApplyLines(demand, input.Lines);

        if (await SaveOrFkErrorAsync(ct) is { } err) return err;
        return NoContent();
    }

    [HttpDelete("{id:guid}"), RequiresPermission("DemandsEdit")]
    public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
    {
        var demand = await _db.Demands.FirstOrDefaultAsync(d => d.Id == id, ct);
        if (demand is null) return NotFound();
        if (demand.Status != DemandStatus.Draft)
            return Conflict(new { error = "Нельзя удалить проведённый документ. Сначала отмени проведение." });
        _db.Demands.Remove(demand);
        await _db.SaveChangesAsync(ct);
        return NoContent();
    }

    [HttpPost("{id:guid}/post"), RequiresPermission("DemandsPost")]
    public async Task<IActionResult> Post(Guid id, CancellationToken ct)
    {
        var demand = await _db.Demands.Include(d => d.Lines).FirstOrDefaultAsync(d => d.Id == id, ct);
        if (demand is null) return NotFound();
        if (demand.Status == DemandStatus.Posted) return Conflict(new { error = "Документ уже проведён." });
        if (demand.Lines.Count == 0) return BadRequest(new { error = "Нельзя провести документ без строк." });

        // Защита от ухода в минус.
        var byProduct = demand.Lines.GroupBy(l => l.ProductId)
            .Select(g => new { ProductId = g.Key, Qty = g.Sum(x => x.Quantity) }).ToList();
        var productIds = byProduct.Select(x => x.ProductId).ToList();
        var stocks = await _db.Stocks
            .Where(s => s.StoreId == demand.StoreId && productIds.Contains(s.ProductId))
            .ToDictionaryAsync(s => s.ProductId, s => s.Quantity, ct);
        var conflicts = new List<object>();
        foreach (var x in byProduct)
        {
            stocks.TryGetValue(x.ProductId, out var avail);
            if (avail < x.Qty)
            {
                var name = await _db.Products.Where(p => p.Id == x.ProductId).Select(p => p.Name).FirstOrDefaultAsync(ct);
                conflicts.Add(new
                {
                    productId = x.ProductId, productName = name,
                    requested = x.Qty, available = avail,
                });
            }
        }
        if (conflicts.Count > 0)
            return Conflict(new { error = "Недостаточно остатка для проведения отгрузки.", lines = conflicts });

        await using var tx = await _db.Database.BeginTransactionAsync(
            System.Data.IsolationLevel.Serializable, ct);

        foreach (var line in demand.Lines)
        {
            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: demand.StoreId,
                Quantity: -line.Quantity,
                Type: MovementType.WholesaleSale,
                DocumentType: "demand",
                DocumentId: demand.Id,
                DocumentNumber: demand.Number,
                UnitCost: line.UnitPrice,
                OccurredAt: demand.Date), ct);
        }

        demand.Status = DemandStatus.Posted;
        demand.PostedAt = DateTime.UtcNow;
        try
        {
            await _db.SaveChangesAsync(ct);
            await tx.CommitAsync(ct);
        }
        catch (Exception ex) when (IsSerializationConflict(ex))
        {
            foodmarket.Api.Infrastructure.Observability.AppMetrics.IncrementError("demand", "serialization");
            return Conflict(new { error = "Документ проводится параллельно. Повторите попытку." });
        }
        foodmarket.Api.Infrastructure.Observability.AppMetrics.IncrementPosted("demand");
        return NoContent();
    }

    [HttpPost("{id:guid}/unpost"), RequiresPermission("DemandsPost")]
    public async Task<IActionResult> Unpost(Guid id, CancellationToken ct)
    {
        var demand = await _db.Demands.Include(d => d.Lines).FirstOrDefaultAsync(d => d.Id == id, ct);
        if (demand is null) return NotFound();
        if (demand.Status != DemandStatus.Posted) return Conflict(new { error = "Документ не проведён." });

        foreach (var line in demand.Lines)
        {
            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: demand.StoreId,
                Quantity: line.Quantity,
                Type: MovementType.WholesaleSale,
                DocumentType: "demand-reversal",
                DocumentId: demand.Id,
                DocumentNumber: demand.Number,
                UnitCost: line.UnitPrice,
                OccurredAt: DateTime.UtcNow,
                Notes: $"Отмена отгрузки {demand.Number}"), ct);
        }

        demand.Status = DemandStatus.Draft;
        demand.PostedAt = null;
        await _db.SaveChangesAsync(ct);
        return NoContent();
    }

    private static bool IsSerializationConflict(Exception ex)
    {
        for (Exception? e = ex; e is not null; e = e.InnerException)
        {
            if (e is System.Data.Common.DbException { SqlState: "40001" or "40P01" })
                return true;
        }
        return false;
    }

    private void ApplyLines(Demand demand, IReadOnlyList<DemandLineInput> input)
    {
        var order = 0;
        decimal subtotal = 0m, discountTotal = 0m;
        foreach (var l in input)
        {
            var lineTotal = l.Quantity * l.UnitPrice - l.Discount;
            // Прямой Add в DbSet — тот же паттерн что в RetailSale.ApplyLines:
            // через nav-collection EF8 в комбинации с client-side Id путается.
            _db.DemandLines.Add(new DemandLine
            {
                DemandId = demand.Id,
                ProductId = l.ProductId,
                Quantity = l.Quantity,
                UnitPrice = l.UnitPrice,
                Discount = l.Discount,
                LineTotal = lineTotal,
                VatPercent = l.VatPercent,
                SortOrder = order++,
            });
            subtotal += l.Quantity * l.UnitPrice;
            discountTotal += l.Discount;
        }
        demand.Subtotal = subtotal;
        demand.DiscountTotal = discountTotal;
        demand.Total = subtotal - discountTotal;
    }

    private async Task<string> GenerateNumberAsync(DateTime date, CancellationToken ct)
    {
        var prefix = $"ОТГ-{date.Year}-";
        var lastNumber = await _db.Demands
            .Where(d => d.Number.StartsWith(prefix))
            .OrderByDescending(d => d.Number)
            .Select(d => d.Number)
            .FirstOrDefaultAsync(ct);
        var seq = 1;
        if (lastNumber is not null && int.TryParse(lastNumber[prefix.Length..], out var last))
            seq = last + 1;
        return $"{prefix}{seq:D6}";
    }

    private async Task<DemandDto?> GetInternal(Guid id, CancellationToken ct)
    {
        var row = await (from d in _db.Demands.AsNoTracking()
                         join cp in _db.Counterparties on d.CustomerId equals cp.Id
                         join st in _db.Stores on d.StoreId equals st.Id
                         join cu in _db.Currencies on d.CurrencyId equals cu.Id
                         where d.Id == id
                         select new { d, cp, st, cu }).FirstOrDefaultAsync(ct);
        if (row is null) return null;

        var lines = await (from l in _db.DemandLines.AsNoTracking()
                           join p in _db.Products on l.ProductId equals p.Id
                           join u in _db.UnitsOfMeasure on p.UnitOfMeasureId equals u.Id
                           where l.DemandId == id
                           orderby l.SortOrder
                           select new DemandLineDto(
                               l.Id, l.ProductId, p.Name, p.Article, u.Name,
                               l.Quantity, l.UnitPrice, l.Discount, l.LineTotal,
                               l.VatPercent, l.SortOrder,
                               _db.Stocks.Where(s => s.ProductId == l.ProductId && s.StoreId == row.d.StoreId)
                                   .Select(s => (decimal?)s.Quantity).FirstOrDefault()))
                          .ToListAsync(ct);

        return new DemandDto(
            row.d.Id, row.d.Number, row.d.Date, row.d.Status,
            row.cp.Id, row.cp.Name,
            row.st.Id, row.st.Name,
            row.cu.Id, row.cu.Code,
            row.d.Payment, row.d.Subtotal, row.d.DiscountTotal,
            row.d.Total, row.d.PaidAmount,
            row.d.Notes, row.d.PostedAt,
            lines);
    }
}
