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

namespace foodmarket.Api.Controllers.Reports;

/// <summary>Отчёт ABC-анализ.
///
/// Распределяет товары по классам A/B/C по правилу Парето:
///   A — топ-товары, дающие 80% накопительной метрики;
///   B — следующие 15% (порог 95% накопительно);
///   C — оставшиеся 5%.
///
/// Параметр <c>metric</c>:
///   • <c>revenue</c> — выручка (по умолчанию);
///   • <c>profit</c>  — прибыль (выручка − себестоимость);
///   • <c>margin</c>  — суммарная маржа в деньгах (то же что profit, но
///     отсортированы по убыванию margin per unit; для совместимости с
///     требованием отдельной кнопки в UI оставляем как алиас).
///
/// Возвраты учтены со знаком: net-метрика для периода.</summary>
[ApiController]
[Authorize]
[Route("api/reports/abc")]
public class AbcReportController : ControllerBase
{
    private readonly AppDbContext _db;
    public AbcReportController(AppDbContext db) => _db = db;

    public record AbcRow(
        Guid ProductId, string ProductName, string? ProductArticle,
        decimal MetricValue,
        decimal Share,            // % от общей метрики (0..100)
        decimal CumulativeShare,  // накопительный % (0..100)
        string AbcClass,          // "A" | "B" | "C"
        int Rank);

    [HttpGet]
    public async Task<ActionResult<IReadOnlyList<AbcRow>>> Get(
        [FromQuery] DateTime? from,
        [FromQuery] DateTime? to,
        [FromQuery] string metric = "revenue",
        [FromQuery] Guid? storeId = null,
        [FromQuery] Guid? productGroupId = null,
        CancellationToken ct = default)
    {
        var range = ResolveRange(from, to);
        return Ok(await BuildAsync(range, metric, storeId, productGroupId, ct));
    }

    [HttpGet("export")]
    public async Task<IActionResult> Export(
        [FromQuery] DateTime? from,
        [FromQuery] DateTime? to,
        [FromQuery] string metric = "revenue",
        [FromQuery] Guid? storeId = null,
        [FromQuery] Guid? productGroupId = null,
        [FromQuery] string format = "csv",
        CancellationToken ct = default)
    {
        var range = ResolveRange(from, to);
        var rows = await BuildAsync(range, metric, storeId, productGroupId, ct);
        var name = $"abc-{metric}-{range.From:yyyyMMdd}-{range.To:yyyyMMdd}";
        var headers = new[]
        {
            "ProductId", "Товар", "Артикул", "Метрика", "Доля,%", "Накоп.доля,%", "Класс", "Ранг",
        };
        return format.ToLower() switch
        {
            "xlsx" => ReportExport.Xlsx(rows, name, "ABC", headers),
            _      => ReportExport.Csv(rows, name, headers),
        };
    }

    private static DateRange ResolveRange(DateTime? from, DateTime? to)
    {
        // См. SalesReportController.ResolveRange — то же self-doc.
        static DateTime AsUtc(DateTime d) => d.Kind switch
        {
            DateTimeKind.Utc => d,
            DateTimeKind.Local => d.ToUniversalTime(),
            _ => DateTime.SpecifyKind(d, DateTimeKind.Utc),
        };
        var t = to.HasValue ? AsUtc(to.Value) : DateTime.UtcNow;
        var f = from.HasValue ? AsUtc(from.Value) : t.AddDays(-30);
        return new DateRange(f, t);
    }

    private async Task<List<AbcRow>> BuildAsync(
        DateRange range, string metric, Guid? storeId, Guid? productGroupId, CancellationToken ct)
    {
        var q = from l in _db.RetailSaleLines.AsNoTracking()
                join s in _db.RetailSales.AsNoTracking() on l.RetailSaleId equals s.Id
                join p in _db.Products.AsNoTracking() on l.ProductId equals p.Id
                where s.Status == RetailSaleStatus.Posted
                      && s.Date >= range.From && s.Date <= range.To
                select new { l, s, p };

        if (storeId is not null) q = q.Where(x => x.s.StoreId == storeId);
        if (productGroupId is not null) q = q.Where(x => x.p.ProductGroupId == productGroupId);

        var raw = await q
            .Select(x => new
            {
                x.l.ProductId,
                x.p.Name,
                x.p.Article,
                x.p.Cost,
                Revenue = x.s.IsReturn ? -x.l.LineTotal : x.l.LineTotal,
                Quantity = x.s.IsReturn ? -x.l.Quantity : x.l.Quantity,
            })
            .ToListAsync(ct);

        // Группируем по product, считаем метрику.
        var grouped = raw
            .GroupBy(r => new { r.ProductId, r.Name, r.Article, r.Cost })
            .Select(g =>
            {
                var rev = g.Sum(x => x.Revenue);
                var qty = g.Sum(x => x.Quantity);
                var cost = g.Key.Cost * qty;
                var prof = rev - cost;
                var value = metric.ToLower() switch
                {
                    "profit" or "margin" => prof,
                    _ => rev,
                };
                return new
                {
                    g.Key.ProductId,
                    g.Key.Name,
                    g.Key.Article,
                    MetricValue = value,
                };
            })
            // Только положительные значения: товары с net-убытком или нулевой
            // выручкой не имеют смысла в ABC (никакого вклада в Парето). При
            // желании можно отдельным флагом включить и отрицательные.
            .Where(x => x.MetricValue > 0m)
            .OrderByDescending(x => x.MetricValue)
            .ToList();

        var total = grouped.Sum(x => x.MetricValue);
        if (total <= 0m) return new();

        var rows = new List<AbcRow>(grouped.Count);
        decimal cum = 0m;
        var rank = 1;
        foreach (var g in grouped)
        {
            // Pre-cumulative: где мы СТОЯЛИ до добавления этого товара. Это
            // правильная Парето-граница: товар принадлежит классу A, если он
            // нужен чтобы пересечь порог 80% (cumBefore < 80%). Без этого
            // единственный товар (cumShare=100%) уезжал бы в C, хотя он
            // полностью покрывает Парето сам по себе.
            var cumBefore = cum / total * 100m;
            cum += g.MetricValue;
            var share = g.MetricValue / total * 100m;
            var cumShare = cum / total * 100m;
            var cls = cumBefore < 80m ? "A"
                    : cumBefore < 95m ? "B"
                    : "C";
            rows.Add(new AbcRow(
                g.ProductId, g.Name, g.Article,
                Math.Round(g.MetricValue, 4),
                Math.Round(share, 2),
                Math.Round(cumShare, 2),
                cls,
                rank++));
        }
        return rows;
    }
}
