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

namespace foodmarket.Api.Controllers.Inventory;

/// <summary>Перемещение (Transfer): атомарное движение товара между складами.
/// Post создаёт ПАРУ движений в <see cref="StockMovement"/>: TransferOut с
/// отрицательным Quantity из FromStore + TransferIn с положительным в
/// ToStore. Серьёзный кейс: post→unpost не должен оставить orphan-движений
/// (только один из двух) — оба создаются и оба «отменяются» в одной
/// транзакции.</summary>
[ApiController]
[Authorize]
[Route("api/inventory/transfers")]
public class TransfersController : ControllerBase
{
    private readonly AppDbContext _db;
    private readonly IStockService _stock;

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

    public record TransferListRow(
        Guid Id, string Number, DateTime Date, TransferStatus Status,
        Guid FromStoreId, string FromStoreName,
        Guid ToStoreId, string ToStoreName,
        decimal Total, int LineCount,
        DateTime? PostedAt);

    public record TransferLineDto(
        Guid? Id, Guid ProductId, string? ProductName, string? ProductArticle,
        string? UnitSymbol,
        decimal Quantity, decimal UnitCost, decimal LineTotal, int SortOrder,
        decimal? StockAtFrom);

    public record TransferDto(
        Guid Id, string Number, DateTime Date, TransferStatus Status,
        Guid FromStoreId, string FromStoreName,
        Guid ToStoreId, string ToStoreName,
        string? Notes,
        decimal Total, DateTime? PostedAt,
        IReadOnlyList<TransferLineDto> Lines);

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

    public record TransferInput(
        DateTime Date, Guid FromStoreId, Guid ToStoreId,
        string? Notes,
        IReadOnlyList<TransferLineInput> Lines);

    [HttpGet]
    public async Task<ActionResult<PagedResult<TransferListRow>>> List(
        [FromQuery] PagedRequest req,
        [FromQuery] TransferStatus? status,
        [FromQuery] Guid? fromStoreId,
        [FromQuery] Guid? toStoreId,
        CancellationToken ct)
    {
        var q = from t in _db.Transfers.AsNoTracking()
                join fs in _db.Stores on t.FromStoreId equals fs.Id
                join ts in _db.Stores on t.ToStoreId equals ts.Id
                select new { t, fs, ts };

        if (status is not null) q = q.Where(x => x.t.Status == status);
        if (fromStoreId is not null) q = q.Where(x => x.t.FromStoreId == fromStoreId);
        if (toStoreId is not null) q = q.Where(x => x.t.ToStoreId == toStoreId);
        if (!string.IsNullOrWhiteSpace(req.Search))
        {
            var s = req.Search.Trim().ToLower();
            q = q.Where(x => x.t.Number.ToLower().Contains(s));
        }

        var total = await q.CountAsync(ct);
        q = (req.Sort, req.Desc) switch
        {
            ("number", false) => q.OrderBy(x => x.t.Number),
            ("number", true) => q.OrderByDescending(x => x.t.Number),
            ("status", false) => q.OrderBy(x => x.t.Status).ThenByDescending(x => x.t.Date),
            ("status", true) => q.OrderByDescending(x => x.t.Status).ThenByDescending(x => x.t.Date),
            ("total", false) => q.OrderBy(x => x.t.Total).ThenByDescending(x => x.t.Date),
            ("total", true) => q.OrderByDescending(x => x.t.Total).ThenByDescending(x => x.t.Date),
            ("date", false) => q.OrderBy(x => x.t.Date).ThenBy(x => x.t.Number),
            _ => q.OrderByDescending(x => x.t.Date).ThenByDescending(x => x.t.Number),
        };
        var items = await q
            .Skip(req.Skip).Take(req.Take)
            .Select(x => new TransferListRow(
                x.t.Id, x.t.Number, x.t.Date, x.t.Status,
                x.fs.Id, x.fs.Name,
                x.ts.Id, x.ts.Name,
                x.t.Total,
                x.t.Lines.Count,
                x.t.PostedAt))
            .ToListAsync(ct);

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

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

    [HttpPost, RequiresPermission("TransferEdit")]
    public async Task<ActionResult<TransferDto>> Create([FromBody] TransferInput input, CancellationToken ct)
    {
        if (RequiredGuid.FirstMissing(
                (nameof(input.FromStoreId), input.FromStoreId),
                (nameof(input.ToStoreId), input.ToStoreId)) is { } missing)
            return BadRequest(new { error = $"Поле {missing} обязательно.", field = missing });
        if (input.FromStoreId == input.ToStoreId)
            return BadRequest(new { error = "Склад-отправитель и склад-получатель должны различаться.", field = "toStoreId" });
        if (input.Lines is null || input.Lines.Count == 0)
            return BadRequest(new { error = "Перемещение должно содержать хотя бы одну позицию." });

        var number = await GenerateNumberAsync(input.Date, ct);
        var t = new Transfer
        {
            Number = number,
            Date = input.Date,
            Status = TransferStatus.Draft,
            FromStoreId = input.FromStoreId,
            ToStoreId = input.ToStoreId,
            Notes = input.Notes,
        };
        var order = 0;
        foreach (var l in input.Lines)
        {
            t.Lines.Add(new TransferLine
            {
                ProductId = l.ProductId,
                Quantity = l.Quantity,
                UnitCost = l.UnitCost,
                LineTotal = l.Quantity * l.UnitCost,
                SortOrder = order++,
            });
        }
        t.Total = t.Lines.Sum(x => x.LineTotal);

        _db.Transfers.Add(t);
        if (await SaveOrFkErrorAsync(ct) is { } err) return err;
        var dto = await GetInternal(t.Id, ct);
        return CreatedAtAction(nameof(Get), new { id = t.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("FromStore") ? "fromStoreId"
                         : name.Contains("ToStore") ? "toStoreId"
                         : name.Contains("Product") ? "productId"
                         : "(unknown)";
            return BadRequest(new { error = $"Связанная запись не найдена: {field}.", field, constraint = name });
        }
    }

    [HttpPut("{id:guid}"), RequiresPermission("TransferEdit")]
    public async Task<IActionResult> Update(Guid id, [FromBody] TransferInput input, CancellationToken ct)
    {
        if (RequiredGuid.FirstMissing(
                (nameof(input.FromStoreId), input.FromStoreId),
                (nameof(input.ToStoreId), input.ToStoreId)) is { } missing)
            return BadRequest(new { error = $"Поле {missing} обязательно.", field = missing });
        if (input.FromStoreId == input.ToStoreId)
            return BadRequest(new { error = "Склад-отправитель и склад-получатель должны различаться.", field = "toStoreId" });
        if (input.Lines is null || input.Lines.Count == 0)
            return BadRequest(new { error = "Перемещение должно содержать хотя бы одну позицию." });

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

        t.Date = input.Date;
        t.FromStoreId = input.FromStoreId;
        t.ToStoreId = input.ToStoreId;
        t.Notes = input.Notes;

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

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

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

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

        // Проверка: на FromStore хватает товара (нельзя уйти в минус).
        var byProduct = t.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 == t.FromStoreId && productIds.Contains(s.ProductId))
            .ToDictionaryAsync(s => s.ProductId, s => s.Quantity, ct);
        var conflicts = new List<object>();
        foreach (var r in byProduct)
        {
            stocks.TryGetValue(r.ProductId, out var available);
            if (available < r.Quantity)
            {
                var name = await _db.Products.Where(p => p.Id == r.ProductId)
                    .Select(p => p.Name).FirstOrDefaultAsync(ct);
                conflicts.Add(new
                {
                    productId = r.ProductId,
                    productName = name,
                    requested = r.Quantity,
                    available,
                });
            }
        }
        if (conflicts.Count > 0)
        {
            return Conflict(new
            {
                error = "На складе-отправителе недостаточно товара.",
                lines = conflicts,
            });
        }

        // Атомарная транзакция: пара движений (Out + In) — либо обе, либо ни одной.
        await using var tx = await _db.Database.BeginTransactionAsync(
            System.Data.IsolationLevel.Serializable, ct);
        var now = DateTime.UtcNow;

        foreach (var line in t.Lines)
        {
            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: t.FromStoreId,
                Quantity: -line.Quantity,
                Type: MovementType.TransferOut,
                DocumentType: "transfer-out",
                DocumentId: t.Id,
                DocumentNumber: t.Number,
                UnitCost: line.UnitCost,
                OccurredAt: t.Date), ct);
            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: t.ToStoreId,
                Quantity: line.Quantity,
                Type: MovementType.TransferIn,
                DocumentType: "transfer-in",
                DocumentId: t.Id,
                DocumentNumber: t.Number,
                UnitCost: line.UnitCost,
                OccurredAt: t.Date), ct);
        }

        t.Status = TransferStatus.Posted;
        t.PostedAt = now;
        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("TransferEdit")]
    public async Task<IActionResult> Unpost(Guid id, CancellationToken ct)
    {
        var t = await _db.Transfers.Include(x => x.Lines).FirstOrDefaultAsync(x => x.Id == id, ct);
        if (t is null) return NotFound();
        if (t.Status != TransferStatus.Posted) return Conflict(new { error = "Документ не проведён." });

        // Reverse: на ToStore должен хватать товар (могли уже использовать).
        var byProduct = t.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 == t.ToStoreId && productIds.Contains(s.ProductId))
            .ToDictionaryAsync(s => s.ProductId, s => s.Quantity, ct);
        var conflicts = new List<object>();
        foreach (var r in byProduct)
        {
            stocks.TryGetValue(r.ProductId, out var available);
            if (available < r.Quantity)
            {
                var name = await _db.Products.Where(p => p.Id == r.ProductId)
                    .Select(p => p.Name).FirstOrDefaultAsync(ct);
                conflicts.Add(new
                {
                    productId = r.ProductId,
                    productName = name,
                    reverseQty = r.Quantity,
                    availableAtTo = 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 t.Lines)
        {
            // Возвращаем на FromStore (+Quantity).
            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: t.FromStoreId,
                Quantity: line.Quantity,
                Type: MovementType.TransferOut,
                DocumentType: "transfer-out-reversal",
                DocumentId: t.Id,
                DocumentNumber: t.Number,
                UnitCost: line.UnitCost,
                OccurredAt: DateTime.UtcNow,
                Notes: $"Отмена проведения документа {t.Number}"), ct);
            // Снимаем с ToStore (-Quantity).
            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: t.ToStoreId,
                Quantity: -line.Quantity,
                Type: MovementType.TransferIn,
                DocumentType: "transfer-in-reversal",
                DocumentId: t.Id,
                DocumentNumber: t.Number,
                UnitCost: line.UnitCost,
                OccurredAt: DateTime.UtcNow,
                Notes: $"Отмена проведения документа {t.Number}"), ct);
        }

        t.Status = TransferStatus.Draft;
        t.PostedAt = null;
        try
        {
            await _db.SaveChangesAsync(ct);
            await tx.CommitAsync(ct);
        }
        catch (Exception ex) when (IsSerializationConflict(ex))
        {
            return Conflict(new { error = "Документ обрабатывается параллельно. Повторите попытку." });
        }
        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.Transfers
            .Where(t => t.Number.StartsWith(prefix))
            .OrderByDescending(t => t.Number)
            .Select(t => t.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<TransferDto?> GetInternal(Guid id, CancellationToken ct)
    {
        var row = await (from t in _db.Transfers.AsNoTracking()
                         join fs in _db.Stores on t.FromStoreId equals fs.Id
                         join ts in _db.Stores on t.ToStoreId equals ts.Id
                         where t.Id == id
                         select new { t, fs, ts }).FirstOrDefaultAsync(ct);
        if (row is null) return null;

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

        return new TransferDto(
            row.t.Id, row.t.Number, row.t.Date, row.t.Status,
            row.fs.Id, row.fs.Name,
            row.ts.Id, row.ts.Name,
            row.t.Notes,
            row.t.Total, row.t.PostedAt,
            lines);
    }
}
