using Meezi.API.Models.Reports;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace Meezi.API.Services;

public interface IDailyReportService
{
    Task<DailyReportSnapshotDto> GenerateReportAsync(
        string cafeId,
        string branchId,
        DateOnly date,
        CancellationToken cancellationToken = default);

    Task<DailyReportSnapshotDto?> GetReportAsync(
        string cafeId,
        string branchId,
        DateOnly date,
        CancellationToken cancellationToken = default);

    Task<IReadOnlyList<DailyReportSnapshotDto>> GetReportRangeAsync(
        string cafeId,
        string? branchId,
        DateOnly startDate,
        DateOnly endDate,
        CancellationToken cancellationToken = default);

    Task<DailyReportSummaryDto> GetSummaryAsync(
        string cafeId,
        int days,
        CancellationToken cancellationToken = default);
}

public class DailyReportService : IDailyReportService
{
    private static readonly OrderStatus ClosedOrderStatus = OrderStatus.Delivered;

    private readonly AppDbContext _db;
    private readonly ILogger<DailyReportService> _logger;

    public DailyReportService(AppDbContext db, ILogger<DailyReportService> logger)
    {
        _db = db;
        _logger = logger;
    }

    public async Task<DailyReportSnapshotDto> GenerateReportAsync(
        string cafeId,
        string branchId,
        DateOnly date,
        CancellationToken cancellationToken = default)
    {
        await EnsureBranchAsync(cafeId, branchId, cancellationToken);

        var (utcStart, utcEnd) = IranCalendar.GetUtcRangeForIranDay(date);
        var metrics = await ComputeMetricsAsync(cafeId, branchId, utcStart, utcEnd, cancellationToken);

        var existing = await _db.DailyReports.FirstOrDefaultAsync(
            r => r.CafeId == cafeId && r.BranchId == branchId && r.Date == date,
            cancellationToken);

        var now = DateTime.UtcNow;
        if (existing is null)
        {
            existing = new DailyReport
            {
                CafeId = cafeId,
                BranchId = branchId,
                Date = date,
                CreatedAt = now
            };
            _db.DailyReports.Add(existing);
        }

        ApplyMetrics(existing, metrics, now);
        await _db.SaveChangesAsync(cancellationToken);

        _logger.LogInformation(
            "Daily report generated for cafe {CafeId} branch {BranchId} date {Date}",
            cafeId, branchId, date);

        return ToDto(existing);
    }

    public async Task<DailyReportSnapshotDto?> GetReportAsync(
        string cafeId,
        string branchId,
        DateOnly date,
        CancellationToken cancellationToken = default)
    {
        var row = await _db.DailyReports.AsNoTracking()
            .FirstOrDefaultAsync(
                r => r.CafeId == cafeId && r.BranchId == branchId && r.Date == date,
                cancellationToken);

        return row is null ? null : ToDto(row);
    }

    public async Task<IReadOnlyList<DailyReportSnapshotDto>> GetReportRangeAsync(
        string cafeId,
        string? branchId,
        DateOnly startDate,
        DateOnly endDate,
        CancellationToken cancellationToken = default)
    {
        var query = _db.DailyReports.AsNoTracking()
            .Where(r => r.CafeId == cafeId && r.Date >= startDate && r.Date <= endDate);

        if (!string.IsNullOrEmpty(branchId))
            query = query.Where(r => r.BranchId == branchId);

        var rows = await query.OrderBy(r => r.Date).ThenBy(r => r.BranchId).ToListAsync(cancellationToken);
        return rows.Select(ToDto).ToList();
    }

    public async Task<DailyReportSummaryDto> GetSummaryAsync(
        string cafeId,
        int days,
        CancellationToken cancellationToken = default)
    {
        days = Math.Clamp(days, 1, 365);
        var today = IranCalendar.TodayInIran;
        var from = today.AddDays(-(days - 1));

        var rows = await GetReportRangeAsync(cafeId, null, from, today, cancellationToken);

        if (rows.Count == 0)
        {
            return new DailyReportSummaryDto(
                days, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, []);
        }

        return new DailyReportSummaryDto(
            days,
            rows.Sum(r => r.TotalRevenue),
            rows.Sum(r => r.CashRevenue),
            rows.Sum(r => r.CardRevenue),
            rows.Sum(r => r.CreditRevenue),
            rows.Sum(r => r.TotalOrders),
            rows.Sum(r => r.TotalOrders) > 0
                ? rows.Sum(r => r.TotalRevenue) / rows.Sum(r => r.TotalOrders)
                : 0,
            rows.Sum(r => r.TotalVoids),
            rows.Sum(r => r.VoidAmount),
            rows.Sum(r => r.TotalExpenses),
            rows.Sum(r => r.NetIncome),
            rows);
    }

    private async Task EnsureBranchAsync(string cafeId, string branchId, CancellationToken ct)
    {
        var exists = await _db.Branches.AnyAsync(
            b => b.Id == branchId && b.CafeId == cafeId && b.IsActive, ct);
        if (!exists)
            throw new InvalidOperationException("Branch not found.");
    }

    private async Task<DailyMetrics> ComputeMetricsAsync(
        string cafeId,
        string branchId,
        DateTime utcStart,
        DateTime utcEnd,
        CancellationToken cancellationToken)
    {
        var closedOrders = await _db.Orders
            .Where(o => o.CafeId == cafeId
                && o.BranchId == branchId
                && o.Status == ClosedOrderStatus
                && o.CreatedAt >= utcStart
                && o.CreatedAt < utcEnd)
            .Select(o => o.Id)
            .ToListAsync(cancellationToken);

        var orderItems = await _db.OrderItems
            .Include(i => i.MenuItem)
            .Include(i => i.Order)
            .Where(i => i.Order.CafeId == cafeId
                && i.Order.BranchId == branchId
                && i.Order.Status == ClosedOrderStatus
                && i.Order.CreatedAt >= utcStart
                && i.Order.CreatedAt < utcEnd)
            .ToListAsync(cancellationToken);

        var activeLines = orderItems.Where(i => !i.IsVoided).ToList();
        var voidedLines = orderItems.Where(i => i.IsVoided).ToList();

        var totalRevenue = activeLines.Sum(i => i.UnitPrice * i.Quantity);
        var totalOrders = closedOrders.Count;
        var avgOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0;

        var cashTx = await _db.CashTransactions
            .Where(t => t.CafeId == cafeId
                && t.BranchId == branchId
                && t.CreatedAt >= utcStart
                && t.CreatedAt < utcEnd)
            .ToListAsync(cancellationToken);

        var payments = cashTx.Where(t => t.Type == CashTransactionType.OrderPayment).ToList();
        var cashRevenue = payments.Where(t => t.Method == PaymentMethod.Cash).Sum(t => t.Amount);
        var cardRevenue = payments.Where(t => t.Method == PaymentMethod.Card).Sum(t => t.Amount);
        var creditRevenue = payments.Where(t => t.Method == PaymentMethod.Credit).Sum(t => t.Amount);

        if (payments.Count == 0 && closedOrders.Count > 0)
        {
            var orderPayments = await _db.Payments
                .Include(p => p.Order)
                .Where(p => p.Order.CafeId == cafeId
                    && p.Order.BranchId == branchId
                    && p.Order.Status == ClosedOrderStatus
                    && p.Status == PaymentStatus.Completed
                    && p.Order.CreatedAt >= utcStart
                    && p.Order.CreatedAt < utcEnd)
                .ToListAsync(cancellationToken);

            cashRevenue = orderPayments.Where(p => p.Method == PaymentMethod.Cash).Sum(p => p.Amount);
            cardRevenue = orderPayments.Where(p => p.Method == PaymentMethod.Card).Sum(p => p.Amount);
            creditRevenue = orderPayments.Where(p => p.Method == PaymentMethod.Credit).Sum(p => p.Amount);
        }

        var totalExpenses = await _db.Expenses
            .Where(e => e.CafeId == cafeId
                && e.BranchId == branchId
                && e.CreatedAt >= utcStart
                && e.CreatedAt < utcEnd)
            .SumAsync(e => e.Amount, cancellationToken);

        var voidAmount = voidedLines.Sum(i => i.UnitPrice * i.Quantity);
        var netIncome = totalRevenue - totalExpenses - voidAmount;

        var topProducts = activeLines
            .GroupBy(i => i.MenuItemId)
            .Select(g => new TopProductEntry
            {
                ProductId = g.Key,
                Name = g.First().MenuItem.Name,
                Quantity = g.Sum(x => x.Quantity),
                Revenue = g.Sum(x => x.UnitPrice * x.Quantity)
            })
            .OrderByDescending(x => x.Revenue)
            .Take(10)
            .ToList();

        return new DailyMetrics(
            totalRevenue,
            cashRevenue,
            cardRevenue,
            creditRevenue,
            totalOrders,
            avgOrderValue,
            voidedLines.Count,
            voidAmount,
            totalExpenses,
            netIncome,
            topProducts);
    }

    private static void ApplyMetrics(DailyReport entity, DailyMetrics metrics, DateTime generatedAt)
    {
        entity.TotalRevenue = metrics.TotalRevenue;
        entity.CashRevenue = metrics.CashRevenue;
        entity.CardRevenue = metrics.CardRevenue;
        entity.CreditRevenue = metrics.CreditRevenue;
        entity.TotalOrders = metrics.TotalOrders;
        entity.AvgOrderValue = metrics.AvgOrderValue;
        entity.TotalVoids = metrics.TotalVoids;
        entity.VoidAmount = metrics.VoidAmount;
        entity.TotalExpenses = metrics.TotalExpenses;
        entity.NetIncome = metrics.NetIncome;
        entity.TopProducts = metrics.TopProducts;
        entity.GeneratedAt = generatedAt;
    }

    private static DailyReportSnapshotDto ToDto(DailyReport r) => new(
        r.Id,
        r.CafeId,
        r.BranchId,
        r.Date.ToString("yyyy-MM-dd"),
        r.TotalRevenue,
        r.CashRevenue,
        r.CardRevenue,
        r.CreditRevenue,
        r.TotalOrders,
        r.AvgOrderValue,
        r.TotalVoids,
        r.VoidAmount,
        r.TotalExpenses,
        r.NetIncome,
        r.TopProducts.Select(p => new TopProductSnapshotDto(
            p.ProductId, p.Name, p.Quantity, p.Revenue)).ToList(),
        r.GeneratedAt);

    private sealed record DailyMetrics(
        decimal TotalRevenue,
        decimal CashRevenue,
        decimal CardRevenue,
        decimal CreditRevenue,
        int TotalOrders,
        decimal AvgOrderValue,
        int TotalVoids,
        decimal VoidAmount,
        decimal TotalExpenses,
        decimal NetIncome,
        List<TopProductEntry> TopProducts);
}
