using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace foodmarket.Api.Controllers.Reports;

/// <summary>Отчёт «Остатки на дату».
///
/// Восстанавливает остаток (Product, Store) на произвольный момент,
/// агрегируя журнал <see cref="Domain.Inventory.StockMovement"/> до этой даты.
/// Текущий <see cref="Domain.Inventory.Stock"/> является результатом
/// Σ движений (инвариант учёта), поэтому подсчёт на «сейчас» совпадает с
/// материализованным остатком; подсчёт на прошлую дату — реконструкция.
///
/// Edge-cases:
/// • Дата в будущем — возвращаем текущий остаток (Σ до сейчас): движений
///   из будущего нет, фильтр на это пропускает.
/// • Дата раньше первой операции — возвращаем нули (или вообще не отдаём
///   строку, потому что и таких пар (Product, Store) не было).
///
/// Стоимость остатка считаем по последнему известному `UnitCost` движения
/// до этой даты ИЛИ снимаем с `Product.Cost` если в движениях не было ни
/// одной партии (например, оприходование без UnitCost). Это компромисс
/// для приближённой оценки — точная FIFO/LIFO потребует партий, которые
/// в текущем учёте не ведутся.</summary>
[ApiController]
[Authorize]
[Route("api/reports/stock")]
public class StockReportController : ControllerBase
{
    private readonly AppDbContext _db;
    public StockReportController(AppDbContext db) => _db = db;

    public record StockRow(
        Guid ProductId, string ProductName, string? ProductArticle, string? UnitName,
        Guid StoreId, string StoreName,
        decimal Quantity, decimal Cost, decimal Value);

    [HttpGet]
    public async Task<ActionResult<IReadOnlyList<StockRow>>> Get(
        [FromQuery] DateTime? date,
        [FromQuery] Guid? storeId,
        [FromQuery] Guid? productGroupId,
        [FromQuery] bool includeZero = false,
        CancellationToken ct = default)
    {
        var on = date ?? DateTime.UtcNow;
        return Ok(await BuildAsync(on, storeId, productGroupId, includeZero, ct));
    }

    [HttpGet("export")]
    public async Task<IActionResult> Export(
        [FromQuery] DateTime? date,
        [FromQuery] Guid? storeId,
        [FromQuery] Guid? productGroupId,
        [FromQuery] bool includeZero = false,
        [FromQuery] string format = "csv",
        CancellationToken ct = default)
    {
        var on = date ?? DateTime.UtcNow;
        var rows = await BuildAsync(on, storeId, productGroupId, includeZero, ct);
        var name = $"stock-{on:yyyyMMdd}";
        var headers = new[] { "ProductId", "Товар", "Артикул", "Ед.", "StoreId", "Склад", "Кол-во", "Цена", "Стоимость" };
        return format.ToLower() switch
        {
            "xlsx" => ReportExport.Xlsx(rows, name, "Stock", headers),
            _      => ReportExport.Csv(rows, name, headers),
        };
    }

    private async Task<List<StockRow>> BuildAsync(
        DateTime on, Guid? storeId, Guid? productGroupId, bool includeZero, CancellationToken ct)
    {
        // Σ Quantity и последний UnitCost (через max OccurredAt) на (Product, Store)
        // — два отдельных запроса; объединяем в памяти. Альтернатива через window
        // functions требует sql-raw; на типовых объёмах (десятки тыс. движений)
        // двойной round-trip с агрегатом дешевле и читабельнее.
        var movQ = _db.StockMovements.AsNoTracking().Where(m => m.OccurredAt <= on);
        if (storeId is not null) movQ = movQ.Where(m => m.StoreId == storeId);

        var qtyByPair = await movQ
            .GroupBy(m => new { m.ProductId, m.StoreId })
            .Select(g => new { g.Key.ProductId, g.Key.StoreId, Qty = g.Sum(m => m.Quantity) })
            .ToListAsync(ct);

        // Последний UnitCost (max OccurredAt с не-null UnitCost) на пару.
        var lastCostsRaw = await movQ
            .Where(m => m.UnitCost != null)
            .GroupBy(m => new { m.ProductId, m.StoreId })
            .Select(g => new
            {
                g.Key.ProductId,
                g.Key.StoreId,
                LastCost = g.OrderByDescending(m => m.OccurredAt).Select(m => m.UnitCost).First(),
            })
            .ToListAsync(ct);
        var lastCosts = lastCostsRaw
            .ToDictionary(x => (x.ProductId, x.StoreId), x => x.LastCost ?? 0m);

        if (productGroupId is not null)
        {
            // Фильтр по группе — отдельным запросом, чтобы получить ProductIds.
            var allowed = await _db.Products.AsNoTracking()
                .Where(p => p.ProductGroupId == productGroupId)
                .Select(p => p.Id)
                .ToListAsync(ct);
            var set = allowed.ToHashSet();
            qtyByPair = qtyByPair.Where(x => set.Contains(x.ProductId)).ToList();
        }

        if (!includeZero) qtyByPair = qtyByPair.Where(x => x.Qty != 0m).ToList();

        // Подтаскиваем имена товара/склада.
        var productIds = qtyByPair.Select(x => x.ProductId).Distinct().ToList();
        var storeIds = qtyByPair.Select(x => x.StoreId).Distinct().ToList();
        var products = await _db.Products.AsNoTracking()
            .Where(p => productIds.Contains(p.Id))
            .Join(_db.UnitsOfMeasure.AsNoTracking(), p => p.UnitOfMeasureId, u => u.Id,
                (p, u) => new { p.Id, p.Name, p.Article, p.Cost, UnitName = u.Name })
            .ToListAsync(ct);
        var stores = await _db.Stores.AsNoTracking()
            .Where(s => storeIds.Contains(s.Id))
            .Select(s => new { s.Id, s.Name })
            .ToListAsync(ct);

        var pMap = products.ToDictionary(p => p.Id);
        var sMap = stores.ToDictionary(s => s.Id);

        return qtyByPair
            .Select(x =>
            {
                pMap.TryGetValue(x.ProductId, out var p);
                sMap.TryGetValue(x.StoreId, out var s);
                lastCosts.TryGetValue((x.ProductId, x.StoreId), out var cost);
                if (cost == 0m && p is not null) cost = p.Cost;
                return new StockRow(
                    x.ProductId, p?.Name ?? "(unknown)", p?.Article, p?.UnitName,
                    x.StoreId, s?.Name ?? "(unknown)",
                    x.Qty, cost, x.Qty * cost);
            })
            .OrderBy(r => r.ProductName)
            .ThenBy(r => r.StoreName)
            .ToList();
    }
}
