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

namespace foodmarket.Api.Controllers.Purchases;

/// <summary>Возврат поставщику. Списывает товар со склада (как Supply, но
/// со знаком минус и movement type SupplierReturn). Опционально ссылается на
/// исходную приёмку через <c>referenceSupplyId</c>.</summary>
[ApiController]
[Authorize]
[Route("api/purchases/supplier-returns")]
public class SupplierReturnsController : ControllerBase
{
    private readonly AppDbContext _db;
    private readonly IStockService _stock;

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

    public record SupplierReturnListRow(
        Guid Id, string Number, DateTime Date, SupplierReturnStatus Status,
        Guid SupplierId, string SupplierName,
        Guid StoreId, string StoreName,
        Guid CurrencyId, string CurrencyCode,
        decimal Total, int LineCount,
        DateTime? PostedAt,
        Guid? ReferenceSupplyId, string? ReferenceSupplyNumber);

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

    public record SupplierReturnDto(
        Guid Id, string Number, DateTime Date, SupplierReturnStatus Status,
        Guid SupplierId, string SupplierName,
        Guid StoreId, string StoreName,
        Guid CurrencyId, string CurrencyCode,
        Guid? ReferenceSupplyId, string? ReferenceSupplyNumber,
        string? Notes,
        decimal Total, DateTime? PostedAt,
        IReadOnlyList<SupplierReturnLineDto> Lines);

    public record SupplierReturnLineInput(
        Guid ProductId,
        [Range(0, 1e10)] decimal Quantity,
        [Range(0, 1e10)] decimal UnitPrice);

    public record SupplierReturnInput(
        DateTime Date,
        Guid SupplierId, Guid StoreId, Guid CurrencyId,
        Guid? ReferenceSupplyId,
        string? Notes,
        IReadOnlyList<SupplierReturnLineInput> Lines);

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

        if (status is not null) q = q.Where(x => x.r.Status == status);
        if (supplierId is not null) q = q.Where(x => x.r.SupplierId == supplierId);
        if (storeId is not null) q = q.Where(x => x.r.StoreId == storeId);
        if (!string.IsNullOrWhiteSpace(req.Search))
        {
            var s = req.Search.Trim().ToLower();
            q = q.Where(x => x.r.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.r.Number),
            ("number", true) => q.OrderByDescending(x => x.r.Number),
            ("supplier", false) => q.OrderBy(x => x.cp.Name).ThenByDescending(x => x.r.Date),
            ("supplier", true) => q.OrderByDescending(x => x.cp.Name).ThenByDescending(x => x.r.Date),
            ("status", false) => q.OrderBy(x => x.r.Status).ThenByDescending(x => x.r.Date),
            ("status", true) => q.OrderByDescending(x => x.r.Status).ThenByDescending(x => x.r.Date),
            ("total", false) => q.OrderBy(x => x.r.Total).ThenByDescending(x => x.r.Date),
            ("total", true) => q.OrderByDescending(x => x.r.Total).ThenByDescending(x => x.r.Date),
            ("date", false) => q.OrderBy(x => x.r.Date).ThenBy(x => x.r.Number),
            _ => q.OrderByDescending(x => x.r.Date).ThenByDescending(x => x.r.Number),
        };
        var items = await q
            .Skip(req.Skip).Take(req.Take)
            .Select(x => new SupplierReturnListRow(
                x.r.Id, x.r.Number, x.r.Date, x.r.Status,
                x.cp.Id, x.cp.Name,
                x.st.Id, x.st.Name,
                x.cu.Id, x.cu.Code,
                x.r.Total,
                x.r.Lines.Count,
                x.r.PostedAt,
                x.r.ReferenceSupplyId,
                x.r.ReferenceSupplyId == null ? null : _db.Supplies.Where(s => s.Id == x.r.ReferenceSupplyId).Select(s => s.Number).FirstOrDefault()))
            .ToListAsync(ct);

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

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

    [HttpPost, RequiresPermission("SuppliesEdit")]
    public async Task<ActionResult<SupplierReturnDto>> Create([FromBody] SupplierReturnInput input, CancellationToken ct)
    {
        if (RequiredGuid.FirstMissing(
                (nameof(input.SupplierId), input.SupplierId),
                (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 = "Возврат должен содержать хотя бы одну позицию." });

        if (input.ReferenceSupplyId is { } refId)
        {
            var refSupply = await _db.Supplies.AsNoTracking().FirstOrDefaultAsync(s => s.Id == refId, ct);
            if (refSupply is null)
                return BadRequest(new { error = "Исходная приёмка не найдена.", field = "referenceSupplyId" });
            if (refSupply.Status != SupplyStatus.Posted)
                return BadRequest(new { error = "Можно возвращать только из проведённой приёмки.", field = "referenceSupplyId" });
            if (refSupply.SupplierId != input.SupplierId)
                return BadRequest(new { error = "Поставщик возврата должен совпадать с поставщиком исходной приёмки.", field = "supplierId" });
        }

        var number = await GenerateNumberAsync(input.Date, ct);
        var r = new SupplierReturn
        {
            Number = number,
            Date = input.Date,
            Status = SupplierReturnStatus.Draft,
            SupplierId = input.SupplierId,
            StoreId = input.StoreId,
            CurrencyId = input.CurrencyId,
            ReferenceSupplyId = input.ReferenceSupplyId,
            Notes = input.Notes,
        };
        var order = 0;
        foreach (var l in input.Lines)
        {
            r.Lines.Add(new SupplierReturnLine
            {
                ProductId = l.ProductId,
                Quantity = l.Quantity,
                UnitPrice = l.UnitPrice,
                LineTotal = l.Quantity * l.UnitPrice,
                SortOrder = order++,
            });
        }
        r.Total = r.Lines.Sum(x => x.LineTotal);

        _db.SupplierReturns.Add(r);
        if (await SaveOrFkErrorAsync(ct) is { } err) return err;
        var dto = await GetInternal(r.Id, ct);
        return CreatedAtAction(nameof(Get), new { id = r.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("Supplier") ? "supplierId"
                         : name.Contains("Store") ? "storeId"
                         : name.Contains("Currency") ? "currencyId"
                         : name.Contains("Product") ? "productId"
                         : name.Contains("Supply") ? "referenceSupplyId"
                         : "(unknown)";
            return BadRequest(new { error = $"Связанная запись не найдена: {field}.", field, constraint = name });
        }
    }

    [HttpPut("{id:guid}"), RequiresPermission("SuppliesEdit")]
    public async Task<IActionResult> Update(Guid id, [FromBody] SupplierReturnInput input, CancellationToken ct)
    {
        if (RequiredGuid.FirstMissing(
                (nameof(input.SupplierId), input.SupplierId),
                (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 r = await _db.SupplierReturns.Include(x => x.Lines).FirstOrDefaultAsync(x => x.Id == id, ct);
        if (r is null) return NotFound();
        if (r.Status != SupplierReturnStatus.Draft)
            return Conflict(new { error = "Только черновик может быть изменён. Сначала отмени проведение." });

        r.Date = input.Date;
        r.SupplierId = input.SupplierId;
        r.StoreId = input.StoreId;
        r.CurrencyId = input.CurrencyId;
        r.ReferenceSupplyId = input.ReferenceSupplyId;
        r.Notes = input.Notes;

        // ExecuteDelete + DbSet.Add — иначе EF8 на nav-collection путается и
        // UPDATE supplier_returns падает с DbUpdateConcurrencyException «0 rows».
        // Тот же паттерн что в Supplies/Demands/RetailSales/Enters/Losses.Update.
        await _db.SupplierReturnLines.Where(l => l.SupplierReturnId == r.Id).ExecuteDeleteAsync(ct);
        var order = 0;
        decimal total = 0;
        foreach (var l in input.Lines)
        {
            var lineTotal = l.Quantity * l.UnitPrice;
            _db.SupplierReturnLines.Add(new SupplierReturnLine
            {
                SupplierReturnId = r.Id,
                ProductId = l.ProductId,
                Quantity = l.Quantity,
                UnitPrice = l.UnitPrice,
                LineTotal = lineTotal,
                SortOrder = order++,
            });
            total += lineTotal;
        }
        r.Total = total;

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

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

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

        // Защита от ухода в минус.
        var byProduct = r.Lines.GroupBy(l => l.ProductId)
            .Select(g => new { ProductId = g.Key, Quantity = g.Sum(x => x.Quantity) }).ToList();
        var productIds = byProduct.Select(x => x.ProductId).ToList();
        var stocks = await _db.Stocks
            .Where(s => s.StoreId == r.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 available);
            if (available < x.Quantity)
            {
                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,
                    returnQty = x.Quantity,
                    available,
                });
            }
        }
        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 r.Lines)
        {
            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: r.StoreId,
                Quantity: -line.Quantity,
                Type: MovementType.SupplierReturn,
                DocumentType: "supplier-return",
                DocumentId: r.Id,
                DocumentNumber: r.Number,
                UnitCost: line.UnitPrice,
                OccurredAt: r.Date), ct);
        }

        r.Status = SupplierReturnStatus.Posted;
        r.PostedAt = DateTime.UtcNow;
        try
        {
            await _db.SaveChangesAsync(ct);
            await tx.CommitAsync(ct);
        }
        catch (Exception ex) when (IsSerializationConflict(ex))
        {
            return Conflict(new { error = "Документ проводится параллельно. Повторите попытку." });
        }
        return NoContent();
    }

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

        // Reverse — добавляем товар обратно, проверка на минус не нужна (только растёт).
        foreach (var line in r.Lines)
        {
            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: r.StoreId,
                Quantity: line.Quantity,
                Type: MovementType.SupplierReturn,
                DocumentType: "supplier-return-reversal",
                DocumentId: r.Id,
                DocumentNumber: r.Number,
                UnitCost: line.UnitPrice,
                OccurredAt: DateTime.UtcNow,
                Notes: $"Отмена возврата {r.Number}"), ct);
        }

        r.Status = SupplierReturnStatus.Draft;
        r.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 async Task<string> GenerateNumberAsync(DateTime date, CancellationToken ct)
    {
        var year = date.Year;
        var prefix = $"ВП-{year}-";
        var lastNumber = await _db.SupplierReturns
            .Where(r => r.Number.StartsWith(prefix))
            .OrderByDescending(r => r.Number)
            .Select(r => r.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<SupplierReturnDto?> GetInternal(Guid id, CancellationToken ct)
    {
        var row = await (from r in _db.SupplierReturns.AsNoTracking()
                         join cp in _db.Counterparties on r.SupplierId equals cp.Id
                         join st in _db.Stores on r.StoreId equals st.Id
                         join cu in _db.Currencies on r.CurrencyId equals cu.Id
                         where r.Id == id
                         select new { r, cp, st, cu }).FirstOrDefaultAsync(ct);
        if (row is null) return null;

        string? refNumber = row.r.ReferenceSupplyId is null ? null
            : await _db.Supplies.Where(s => s.Id == row.r.ReferenceSupplyId).Select(s => s.Number).FirstOrDefaultAsync(ct);

        var lines = await (from l in _db.SupplierReturnLines.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.SupplierReturnId == id
                           orderby l.SortOrder
                           select new SupplierReturnLineDto(
                               l.Id, l.ProductId, p.Name, p.Article, u.Name,
                               l.Quantity, l.UnitPrice, l.LineTotal, l.SortOrder,
                               _db.Stocks.Where(s => s.ProductId == l.ProductId && s.StoreId == row.r.StoreId)
                                   .Select(s => (decimal?)s.Quantity).FirstOrDefault()))
                          .ToListAsync(ct);

        return new SupplierReturnDto(
            row.r.Id, row.r.Number, row.r.Date, row.r.Status,
            row.cp.Id, row.cp.Name,
            row.st.Id, row.st.Name,
            row.cu.Id, row.cu.Code,
            row.r.ReferenceSupplyId, refNumber,
            row.r.Notes,
            row.r.Total, row.r.PostedAt,
            lines);
    }
}
