using System.Globalization;
using Meezi.API.Models.Reports;
using Meezi.API.Utils;
using Meezi.Core.Enums;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using OfficeOpenXml;

namespace Meezi.API.Services;

public interface IReportService
{
    Task<DailyReportDto> GetDailyReportAsync(string cafeId, string dateJalali, CancellationToken cancellationToken = default);
    Task<MonthlyReportDto> GetMonthlyReportAsync(string cafeId, string monthJalali, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<TrendDayDto>> GetTrendAsync(string cafeId, int days, CancellationToken cancellationToken = default);
    Task<byte[]> ExportExcelAsync(string cafeId, string monthJalali, CancellationToken cancellationToken = default);
}

public class ReportService : IReportService
{
    private static readonly OrderStatus[] RevenueStatuses =
    [
        OrderStatus.Confirmed,
        OrderStatus.Preparing,
        OrderStatus.Ready,
        OrderStatus.Delivered
    ];

    private readonly AppDbContext _db;

    public ReportService(AppDbContext db) => _db = db;

    public async Task<DailyReportDto> GetDailyReportAsync(
        string cafeId,
        string dateJalali,
        CancellationToken cancellationToken = default)
    {
        if (!JalaliCalendarHelper.TryParseJalaliDate(dateJalali, out var y, out var m, out var d))
        {
            var today = JalaliCalendarHelper.TodayJalali();
            y = today.Year;
            m = today.Month;
            d = today.Day;
            dateJalali = JalaliCalendarHelper.FormatJalaliDate(y, m, d);
        }

        var (utcStart, utcEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliDay(y, m, d);
        var orders = await LoadRevenueOrdersAsync(cafeId, utcStart, utcEnd, cancellationToken);

        var newCustomers = await _db.Customers
            .Where(c => c.CafeId == cafeId && c.CreatedAt >= utcStart && c.CreatedAt < utcEnd)
            .CountAsync(cancellationToken);

        var returningCustomers = await CountReturningCustomersAsync(cafeId, utcStart, utcEnd, cancellationToken);
        var topItems = await GetTopItemsAsync(cafeId, utcStart, utcEnd, 5, cancellationToken);

        return new DailyReportDto(
            dateJalali,
            orders.Count,
            newCustomers,
            returningCustomers,
            orders.Sum(o => o.Total),
            orders.Sum(o => o.TaxTotal),
            orders.Sum(o => o.DiscountAmount),
            topItems);
    }

    public async Task<MonthlyReportDto> GetMonthlyReportAsync(
        string cafeId,
        string monthJalali,
        CancellationToken cancellationToken = default)
    {
        if (!JalaliCalendarHelper.TryParseJalaliMonth(monthJalali, out var y, out var m))
        {
            var today = JalaliCalendarHelper.TodayJalali();
            y = today.Year;
            m = today.Month;
            monthJalali = JalaliCalendarHelper.FormatJalaliMonth(y, m);
        }

        var (utcStart, utcEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliMonth(y, m);
        var daysInMonth = new PersianCalendar().GetDaysInMonth(y, m);
        var salaryCosts = await GetSalaryCostsForGregorianMonthAsync(cafeId, utcStart, cancellationToken);
        var dailyCost = daysInMonth > 0 ? salaryCosts / daysInMonth : 0m;

        var breakdown = new List<DailyBreakdownDto>();
        decimal totalRevenue = 0;

        for (var day = 1; day <= daysInMonth; day++)
        {
            var (dayStart, dayEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliDay(y, m, day);
            var dayOrders = await LoadRevenueOrdersAsync(cafeId, dayStart, dayEnd, cancellationToken);
            var revenue = dayOrders.Sum(o => o.Total);
            totalRevenue += revenue;
            breakdown.Add(new DailyBreakdownDto(
                JalaliCalendarHelper.FormatJalaliDate(y, m, day),
                revenue,
                dailyCost));
        }

        var totalCosts = salaryCosts;
        return new MonthlyReportDto(
            monthJalali,
            breakdown,
            totalRevenue,
            totalCosts,
            salaryCosts,
            0m,
            totalRevenue - totalCosts);
    }

    public async Task<IReadOnlyList<TrendDayDto>> GetTrendAsync(
        string cafeId,
        int days,
        CancellationToken cancellationToken = default)
    {
        days = Math.Clamp(days, 1, 31);
        var (y, m, d) = JalaliCalendarHelper.TodayJalali();
        var persian = new PersianCalendar();
        var cursor = persian.ToDateTime(y, m, d, 0, 0, 0, 0);
        var monthYear = $"{cursor:yyyy-MM}";
        var salaryCosts = await _db.EmployeeSalaries
            .Include(s => s.Employee)
            .Where(s => s.Employee.CafeId == cafeId && s.MonthYear == monthYear)
            .SumAsync(s => s.NetSalary, cancellationToken);
        var daysInMonth = persian.GetDaysInMonth(y, m);
        var dailyCost = daysInMonth > 0 ? salaryCosts / daysInMonth : 0m;

        var result = new List<TrendDayDto>();
        for (var i = days - 1; i >= 0; i--)
        {
            var localDay = cursor.AddDays(-i);
            var jy = persian.GetYear(localDay);
            var jm = persian.GetMonth(localDay);
            var jd = persian.GetDayOfMonth(localDay);
            var (utcStart, utcEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliDay(jy, jm, jd);
            var orders = await LoadRevenueOrdersAsync(cafeId, utcStart, utcEnd, cancellationToken);
            result.Add(new TrendDayDto(
                JalaliCalendarHelper.FormatJalaliDate(jy, jm, jd),
                orders.Sum(o => o.Total),
                dailyCost));
        }

        return result;
    }

    public async Task<byte[]> ExportExcelAsync(
        string cafeId,
        string monthJalali,
        CancellationToken cancellationToken = default)
    {
        if (!JalaliCalendarHelper.TryParseJalaliMonth(monthJalali, out var y, out var m))
            throw new ArgumentException("Invalid Jalali month.", nameof(monthJalali));

        ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
        var (utcStart, utcEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliMonth(y, m);

        var orders = await _db.Orders
            .Include(o => o.Items)
            .ThenInclude(i => i.MenuItem)
            .Include(o => o.Customer)
            .Where(o => o.CafeId == cafeId && RevenueStatuses.Contains(o.Status))
            .Where(o => o.CreatedAt >= utcStart && o.CreatedAt < utcEnd)
            .OrderBy(o => o.CreatedAt)
            .ToListAsync(cancellationToken);

        var customers = await _db.Customers
            .Where(c => c.CafeId == cafeId && c.CreatedAt >= utcStart && c.CreatedAt < utcEnd)
            .ToListAsync(cancellationToken);

        var salaries = await _db.EmployeeSalaries
            .Include(s => s.Employee)
            .Where(s => s.Employee.CafeId == cafeId && s.MonthYear == $"{utcStart:yyyy-MM}")
            .ToListAsync(cancellationToken);

        using var package = new ExcelPackage();

        var sales = package.Workbook.Worksheets.Add("Sales");
        sales.Cells[1, 1].Value = "OrderId";
        sales.Cells[1, 2].Value = "Date";
        sales.Cells[1, 3].Value = "Total";
        sales.Cells[1, 4].Value = "Tax";
        sales.Cells[1, 5].Value = "Discount";
        var row = 2;
        foreach (var order in orders)
        {
            var jalali = JalaliCalendarHelper.ToJalali(order.CreatedAt);
            sales.Cells[row, 1].Value = order.Id;
            sales.Cells[row, 2].Value = JalaliCalendarHelper.FormatJalaliDate(jalali.Year, jalali.Month, jalali.Day);
            sales.Cells[row, 3].Value = order.Total;
            sales.Cells[row, 4].Value = order.TaxTotal;
            sales.Cells[row, 5].Value = order.DiscountAmount;
            row++;
        }

        var items = package.Workbook.Worksheets.Add("Items");
        items.Cells[1, 1].Value = "Item";
        items.Cells[1, 2].Value = "Quantity";
        items.Cells[1, 3].Value = "Revenue";
        row = 2;
        foreach (var line in orders.SelectMany(o => o.Items))
        {
            items.Cells[row, 1].Value = line.MenuItem.Name;
            items.Cells[row, 2].Value = line.Quantity;
            items.Cells[row, 3].Value = line.UnitPrice * line.Quantity;
            row++;
        }

        var customersSheet = package.Workbook.Worksheets.Add("Customers");
        customersSheet.Cells[1, 1].Value = "Name";
        customersSheet.Cells[1, 2].Value = "Phone";
        customersSheet.Cells[1, 3].Value = "Created";
        row = 2;
        foreach (var customer in customers)
        {
            var jalali = JalaliCalendarHelper.ToJalali(customer.CreatedAt);
            customersSheet.Cells[row, 1].Value = customer.Name;
            customersSheet.Cells[row, 2].Value = customer.Phone;
            customersSheet.Cells[row, 3].Value = JalaliCalendarHelper.FormatJalaliDate(jalali.Year, jalali.Month, jalali.Day);
            row++;
        }

        var employeesSheet = package.Workbook.Worksheets.Add("Employees");
        employeesSheet.Cells[1, 1].Value = "Employee";
        employeesSheet.Cells[1, 2].Value = "Month";
        employeesSheet.Cells[1, 3].Value = "NetSalary";
        employeesSheet.Cells[1, 4].Value = "Paid";
        row = 2;
        foreach (var salary in salaries)
        {
            employeesSheet.Cells[row, 1].Value = salary.Employee.Name;
            employeesSheet.Cells[row, 2].Value = salary.MonthYear;
            employeesSheet.Cells[row, 3].Value = salary.NetSalary;
            employeesSheet.Cells[row, 4].Value = salary.IsPaid ? "Yes" : "No";
            row++;
        }

        return await package.GetAsByteArrayAsync(cancellationToken);
    }

    private async Task<List<Core.Entities.Order>> LoadRevenueOrdersAsync(
        string cafeId,
        DateTime utcStart,
        DateTime utcEnd,
        CancellationToken cancellationToken)
    {
        return await _db.Orders
            .Where(o => o.CafeId == cafeId && RevenueStatuses.Contains(o.Status))
            .Where(o => o.CreatedAt >= utcStart && o.CreatedAt < utcEnd)
            .ToListAsync(cancellationToken);
    }

    private async Task<int> CountReturningCustomersAsync(
        string cafeId,
        DateTime utcStart,
        DateTime utcEnd,
        CancellationToken cancellationToken)
    {
        var customerIds = await _db.Orders
            .Where(o => o.CafeId == cafeId && o.CustomerId != null)
            .Where(o => RevenueStatuses.Contains(o.Status))
            .Where(o => o.CreatedAt >= utcStart && o.CreatedAt < utcEnd)
            .Select(o => o.CustomerId!)
            .Distinct()
            .ToListAsync(cancellationToken);

        if (customerIds.Count == 0)
            return 0;

        return await _db.Customers
            .Where(c => c.CafeId == cafeId && customerIds.Contains(c.Id) && c.CreatedAt < utcStart)
            .CountAsync(cancellationToken);
    }

    private async Task<IReadOnlyList<TopItemDto>> GetTopItemsAsync(
        string cafeId,
        DateTime utcStart,
        DateTime utcEnd,
        int take,
        CancellationToken cancellationToken)
    {
        var lines = await _db.OrderItems
            .Include(i => i.MenuItem)
            .Include(i => i.Order)
            .Where(i => i.Order.CafeId == cafeId && RevenueStatuses.Contains(i.Order.Status))
            .Where(i => i.Order.CreatedAt >= utcStart && i.Order.CreatedAt < utcEnd)
            .ToListAsync(cancellationToken);

        return lines
            .GroupBy(i => i.MenuItemId)
            .Select(g => new TopItemDto(
                g.Key,
                g.First().MenuItem.Name,
                g.Sum(x => x.Quantity),
                g.Sum(x => x.UnitPrice * x.Quantity)))
            .OrderByDescending(x => x.Revenue)
            .Take(take)
            .ToList();
    }

    private async Task<decimal> GetSalaryCostsForGregorianMonthAsync(
        string cafeId,
        DateTime utcStart,
        CancellationToken cancellationToken)
    {
        var monthYear = utcStart.ToString("yyyy-MM");
        return await _db.EmployeeSalaries
            .Include(s => s.Employee)
            .Where(s => s.Employee.CafeId == cafeId && s.MonthYear == monthYear)
            .SumAsync(s => s.NetSalary, cancellationToken);
    }
}
