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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace foodmarket.Api.Controllers.Purchases;

[ApiController]
[Authorize]
[Route("api/purchases/supplies")]
public class SuppliesController : ControllerBase
{
    private readonly AppDbContext _db;
    private readonly IStockService _stock;

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

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

    public record SupplyLineDto(
        Guid? Id, Guid ProductId, string? ProductName, string? ProductArticle,
        string? ProductBarcode,
        string? UnitSymbol,
        decimal Quantity, decimal UnitPrice, decimal LineTotal, int SortOrder,
        bool RetailPriceManuallyOverridden, decimal? RetailPriceOverride,
        decimal? CurrentRetailPrice);

    public record SupplyDto(
        Guid Id, string Number, DateTime Date, SupplyStatus Status,
        Guid SupplierId, string SupplierName,
        Guid StoreId, string StoreName,
        Guid CurrencyId, string CurrencyCode,
        string? Notes,
        decimal Total, DateTime? PostedAt,
        IReadOnlyList<SupplyLineDto> Lines);

    public record SupplyLineInput(
        Guid ProductId,
        [Range(0, 1e10)] decimal Quantity,
        [Range(0, 1e10)] decimal UnitPrice,
        bool RetailPriceManuallyOverridden = false,
        [Range(0, 1e10)] decimal? RetailPriceOverride = null);
    public record SupplyInput(
        DateTime Date, Guid SupplierId, Guid StoreId, Guid CurrencyId,
        string? Notes,
        IReadOnlyList<SupplyLineInput> Lines);

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

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

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

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

    [HttpPost, Authorize(Roles = "Admin,Storekeeper")]
    public async Task<ActionResult<SupplyDto>> Create([FromBody] SupplyInput input, CancellationToken ct)
    {
        if (input.Lines is null || input.Lines.Count == 0)
            return BadRequest(new { error = "Приёмка должна содержать хотя бы одну позицию." });
        var number = await GenerateNumberAsync(input.Date, ct);
        var allowFractional = await _db.Organizations.Select(o => o.AllowFractionalPrices).FirstOrDefaultAsync(ct);
        var supply = new Supply
        {
            Number = number,
            Date = input.Date,
            Status = SupplyStatus.Draft,
            SupplierId = input.SupplierId,
            StoreId = input.StoreId,
            CurrencyId = input.CurrencyId,
            Notes = input.Notes,
        };

        var order = 0;
        foreach (var l in input.Lines)
        {
            var unitPrice = allowFractional ? l.UnitPrice : Math.Round(l.UnitPrice, 0, MidpointRounding.AwayFromZero);
            supply.Lines.Add(new SupplyLine
            {
                ProductId = l.ProductId,
                Quantity = l.Quantity,
                UnitPrice = unitPrice,
                LineTotal = l.Quantity * unitPrice,
                SortOrder = order++,
                RetailPriceManuallyOverridden = l.RetailPriceManuallyOverridden,
                RetailPriceOverride = l.RetailPriceOverride.HasValue
                    ? (allowFractional ? l.RetailPriceOverride : Math.Round(l.RetailPriceOverride.Value, 0, MidpointRounding.AwayFromZero))
                    : null,
            });
        }
        supply.Total = supply.Lines.Sum(x => x.LineTotal);

        _db.Supplies.Add(supply);
        await _db.SaveChangesAsync(ct);
        var dto = await GetInternal(supply.Id, ct);
        return CreatedAtAction(nameof(Get), new { id = supply.Id }, dto);
    }

    [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Storekeeper")]
    public async Task<IActionResult> Update(Guid id, [FromBody] SupplyInput input, CancellationToken ct)
    {
        if (input.Lines is null || input.Lines.Count == 0)
            return BadRequest(new { error = "Приёмка должна содержать хотя бы одну позицию." });
        var supply = await _db.Supplies.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct);
        if (supply is null) return NotFound();
        if (supply.Status != SupplyStatus.Draft)
            return Conflict(new { error = "Только черновик может быть изменён. Сначала отмени проведение." });

        supply.Date = input.Date;
        supply.SupplierId = input.SupplierId;
        supply.StoreId = input.StoreId;
        supply.CurrencyId = input.CurrencyId;
        supply.Notes = input.Notes;

        // Replace lines wholesale (simple, idempotent).
        _db.SupplyLines.RemoveRange(supply.Lines);
        supply.Lines.Clear();
        var allowFractional = await _db.Organizations.Select(o => o.AllowFractionalPrices).FirstOrDefaultAsync(ct);
        var order = 0;
        foreach (var l in input.Lines)
        {
            var unitPrice = allowFractional ? l.UnitPrice : Math.Round(l.UnitPrice, 0, MidpointRounding.AwayFromZero);
            supply.Lines.Add(new SupplyLine
            {
                SupplyId = supply.Id,
                ProductId = l.ProductId,
                Quantity = l.Quantity,
                UnitPrice = unitPrice,
                LineTotal = l.Quantity * unitPrice,
                SortOrder = order++,
                RetailPriceManuallyOverridden = l.RetailPriceManuallyOverridden,
                RetailPriceOverride = l.RetailPriceOverride.HasValue
                    ? (allowFractional ? l.RetailPriceOverride : Math.Round(l.RetailPriceOverride.Value, 0, MidpointRounding.AwayFromZero))
                    : null,
            });
        }
        supply.Total = supply.Lines.Sum(x => x.LineTotal);

        await _db.SaveChangesAsync(ct);
        return NoContent();
    }

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

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

        var allowFractional = await _db.Organizations.Select(o => o.AllowFractionalPrices).FirstOrDefaultAsync(ct);

        await using var tx = await _db.Database.BeginTransactionAsync(ct);
        var now = DateTime.UtcNow;

        foreach (var line in supply.Lines)
        {
            var product = await _db.Products
                .Include(p => p.ProductGroup)
                .Include(p => p.Prices)
                .FirstAsync(p => p.Id == line.ProductId, ct);

            // Текущее общее количество по всем складам (до этой приёмки).
            var currentQty = await _db.Stocks
                .Where(s => s.ProductId == line.ProductId)
                .SumAsync(s => (decimal?)s.Quantity, ct) ?? 0m;

            // 1. Cost — скользящее среднее.
            var totalQty = currentQty + line.Quantity;
            var newCost = totalQty == 0m || product.Cost == 0m && currentQty == 0m
                ? line.UnitPrice
                : (currentQty * product.Cost + line.Quantity * line.UnitPrice) / totalQty;
            product.Cost = Math.Round(newCost, 4, MidpointRounding.AwayFromZero);

            // 2. ReferencePrice — автозаполнение при первой приёмке.
            if (product.ReferencePrice is null)
            {
                product.ReferencePrice = line.UnitPrice;
                product.ReferencePriceUpdatedAt = now;
            }
            product.LastSupplyAt = now;

            // 3. Розничная: либо явный override строки, либо автонаценка по группе.
            if (line.RetailPriceManuallyOverridden && line.RetailPriceOverride.HasValue)
            {
                SetDefaultRetail(product, line.RetailPriceOverride.Value, supply.CurrencyId);
            }
            else if (product.ProductGroup?.MarkupPercent is decimal pct)
            {
                var raw = product.Cost * (1m + pct / 100m);
                var newRetail = allowFractional
                    ? Math.Ceiling(raw * 100m) / 100m
                    : Math.Ceiling(raw);
                SetDefaultRetail(product, newRetail, supply.CurrencyId);
            }

            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: supply.StoreId,
                Quantity: line.Quantity,
                Type: MovementType.Supply,
                DocumentType: "supply",
                DocumentId: supply.Id,
                DocumentNumber: supply.Number,
                UnitCost: line.UnitPrice,
                OccurredAt: supply.Date), ct);
        }

        supply.Status = SupplyStatus.Posted;
        supply.PostedAt = now;
        await _db.SaveChangesAsync(ct);
        await tx.CommitAsync(ct);
        return NoContent();
    }

    /// <summary>Записывает значение в дефолтный розничный PriceType. Если в списке
    /// цен у товара такой записи нет — создаёт её. Дефолтным считается PriceType
    /// с IsSystem=true; если такого нет — первый IsRetail; иначе — первый
    /// PriceType в списке. Currency берётся из приёмки (или из существующей записи).</summary>
    private void SetDefaultRetail(foodmarket.Domain.Catalog.Product p, decimal value, Guid fallbackCurrencyId)
    {
        var defaultType = _db.PriceTypes
            .OrderByDescending(pt => pt.IsSystem)
            .ThenByDescending(pt => pt.IsRetail)
            .ThenBy(pt => pt.SortOrder)
            .ThenBy(pt => pt.Name)
            .FirstOrDefault();
        if (defaultType is null) return;
        var existing = p.Prices.FirstOrDefault(x => x.PriceTypeId == defaultType.Id);
        if (existing is null)
        {
            p.Prices.Add(new foodmarket.Domain.Catalog.ProductPrice
            {
                PriceTypeId = defaultType.Id,
                Amount = value,
                CurrencyId = fallbackCurrencyId,
            });
        }
        else
        {
            existing.Amount = value;
        }
    }

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

        // Reverse: negative movements with same document reference
        foreach (var line in supply.Lines)
        {
            await _stock.ApplyMovementAsync(new StockMovementDraft(
                ProductId: line.ProductId,
                StoreId: supply.StoreId,
                Quantity: -line.Quantity,
                Type: MovementType.Supply,
                DocumentType: "supply-reversal",
                DocumentId: supply.Id,
                DocumentNumber: supply.Number,
                UnitCost: line.UnitPrice,
                OccurredAt: DateTime.UtcNow,
                Notes: $"Отмена проведения документа {supply.Number}"), ct);
        }

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

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

        // CurrentRetailPrice — текущая розничная цена товара (дефолтный PriceType),
        // отображается в строке приёмки как «Розничная (из карточки)».
        var lines = await (from l in _db.SupplyLines.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.SupplyId == id
                           orderby l.SortOrder
                           select new SupplyLineDto(
                               l.Id, l.ProductId, p.Name, p.Article,
                               // Основной штрихкод (IsPrimary=true), иначе первый по порядку.
                               p.Barcodes.OrderByDescending(b => b.IsPrimary).Select(b => b.Code).FirstOrDefault(),
                               u.Name,
                               l.Quantity, l.UnitPrice, l.LineTotal, l.SortOrder,
                               l.RetailPriceManuallyOverridden, l.RetailPriceOverride,
                               p.Prices
                                   .OrderByDescending(pr => pr.PriceType!.IsSystem)
                                   .ThenByDescending(pr => pr.PriceType!.IsRetail)
                                   .ThenBy(pr => pr.PriceType!.SortOrder)
                                   .ThenBy(pr => pr.PriceType!.Name)
                                   .Select(pr => (decimal?)pr.Amount)
                                   .FirstOrDefault()))
                          .ToListAsync(ct);

        return new SupplyDto(
            row.s.Id, row.s.Number, row.s.Date, row.s.Status,
            row.cp.Id, row.cp.Name,
            row.st.Id, row.st.Name,
            row.cu.Id, row.cu.Code,
            row.s.Notes,
            row.s.Total, row.s.PostedAt,
            lines);
    }
}
