using System.Globalization;
using foodmarket.Domain.Inventory;
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>Отчёт «Прибыль».
///
/// Выручка − себестоимость, маржа, рентабельность по периодам / товарам /
/// группам товаров. Cost-snapshot берётся из <see cref="StockMovement.UnitCost"/>
/// сопоставленного по (documentId, productId): движение тип `RetailSale`
/// или `CustomerReturn` несёт UnitCost = `RetailSaleLine.UnitPrice`… НО для
/// расчёта прибыли нам нужна СЕБЕСТОИМОСТЬ, а не цена продажи. Поэтому
/// фактически берём `Product.Cost` (текущее скользящее среднее) как
/// COGS-snapshot — это приближение; точный COGS требует партий или
/// fetch'а на момент продажи. Документируем как компромисс.
///
/// Multi-tenant: query-filter работает прозрачно.</summary>
[ApiController]
[Authorize]
[Route("api/reports/profit")]
public class ProfitReportController : ControllerBase
{
    private readonly AppDbContext _db;
    public ProfitReportController(AppDbContext db) => _db = db;

    public record ProfitRow(
        string Key, string Label,
        decimal Revenue, decimal Cost, decimal Profit,
        decimal MarginPercent, decimal Quantity);

    private record FlatRow(
        Guid SaleId, DateTime Date,
        Guid ProductId, string ProductName, string? ProductArticle,
        Guid? ProductGroupId, string? ProductGroupName,
        decimal Revenue, decimal Cost, decimal Quantity);

    [HttpGet]
    public async Task<ActionResult<IReadOnlyList<ProfitRow>>> Get(
        [FromQuery] DateTime? from,
        [FromQuery] DateTime? to,
        [FromQuery] string groupBy = "period:day",
        [FromQuery] Guid? storeId = null,
        [FromQuery] Guid? productGroupId = null,
        CancellationToken ct = default)
    {
        var range = ResolveRange(from, to);
        var flat = await FetchAsync(range, storeId, productGroupId, ct);
        return Ok(Group(flat, groupBy));
    }

    [HttpGet("export")]
    public async Task<IActionResult> Export(
        [FromQuery] DateTime? from,
        [FromQuery] DateTime? to,
        [FromQuery] string groupBy = "period:day",
        [FromQuery] Guid? storeId = null,
        [FromQuery] Guid? productGroupId = null,
        [FromQuery] string format = "csv",
        CancellationToken ct = default)
    {
        var range = ResolveRange(from, to);
        var flat = await FetchAsync(range, storeId, productGroupId, ct);
        var rows = Group(flat, groupBy);
        var name = $"profit-{groupBy.Replace(':', '-')}-{range.From:yyyyMMdd}-{range.To:yyyyMMdd}";
        var headers = new[] { "Ключ", "Группа", "Выручка", "Себестоимость", "Прибыль", "Маржа,%", "Количество" };
        return format.ToLower() switch
        {
            "xlsx" => ReportExport.Xlsx(rows, name, "Profit", headers),
            _      => ReportExport.Csv(rows, name, headers),
        };
    }

    private static DateRange ResolveRange(DateTime? from, DateTime? to)
    {
        var t = to ?? DateTime.UtcNow;
        var f = from ?? t.AddDays(-30);
        return new DateRange(f, t);
    }

    private async Task<List<FlatRow>> FetchAsync(
        DateRange range, Guid? storeId, Guid? productGroupId, CancellationToken ct)
    {
        // Берём строки проведённых чеков с присоединённым Product (для Cost-snapshot
        // и группы) и ProductGroup (для имени). Возврат вычитается из выручки и
        // из COGS (продали по цене X с себестоимостью C → returned восстанавливает).
        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 flat = await q
            .Select(x => new FlatRow(
                x.s.Id, x.s.Date,
                x.l.ProductId, x.p.Name, x.p.Article,
                x.p.ProductGroupId,
                x.p.ProductGroupId == null ? null
                    : _db.ProductGroups.Where(g => g.Id == x.p.ProductGroupId).Select(g => g.Name).FirstOrDefault(),
                x.s.IsReturn ? -x.l.LineTotal : x.l.LineTotal,
                // COGS-snapshot: Quantity × Product.Cost (текущее скользящее среднее).
                // Документировано как приближение; точный COGS требует партий.
                (x.s.IsReturn ? -x.l.Quantity : x.l.Quantity) * x.p.Cost,
                x.s.IsReturn ? -x.l.Quantity : x.l.Quantity))
            .ToListAsync(ct);

        return flat;
    }

    private static List<ProfitRow> Group(List<FlatRow> flat, string groupBy)
    {
        IEnumerable<IGrouping<(string Key, string Label), FlatRow>> grouped;
        switch (groupBy)
        {
            case "period:day":
                grouped = flat.GroupBy(x => (
                    Key: x.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
                    Label: x.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)));
                break;
            case "period:week":
                var cal = CultureInfo.InvariantCulture.Calendar;
                grouped = flat.GroupBy(x =>
                {
                    var w = cal.GetWeekOfYear(x.Date, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
                    return (Key: $"{x.Date.Year:0000}-W{w:00}", Label: $"{x.Date.Year:0000} нед. {w:00}");
                });
                break;
            case "period:month":
                grouped = flat.GroupBy(x => (
                    Key: $"{x.Date.Year:0000}-{x.Date.Month:00}",
                    Label: $"{x.Date.Year:0000}-{x.Date.Month:00}"));
                break;
            case "product":
                grouped = flat.GroupBy(x => (
                    Key: x.ProductId.ToString(),
                    Label: x.ProductName + (x.ProductArticle is not null ? " · " + x.ProductArticle : "")));
                break;
            case "group":
                grouped = flat.GroupBy(x => (
                    Key: (x.ProductGroupId ?? Guid.Empty).ToString(),
                    Label: string.IsNullOrEmpty(x.ProductGroupName) ? "Без группы" : x.ProductGroupName));
                break;
            default:
                return new();
        }

        return grouped
            .Select(g =>
            {
                var revenue = g.Sum(x => x.Revenue);
                var cost = g.Sum(x => x.Cost);
                var profit = revenue - cost;
                // Защита от деления на ноль: при нулевой выручке маржа = 0
                // (а не NaN/Infinity). Бывает при чисто возвратных периодах,
                // когда продажи отсутствуют.
                var margin = revenue == 0m ? 0m : Math.Round(profit / revenue * 100m, 2);
                return new ProfitRow(
                    g.Key.Key, g.Key.Label,
                    revenue, cost, profit, margin,
                    g.Sum(x => x.Quantity));
            })
            .OrderByDescending(r => r.Profit)
            .ToList();
    }
}
