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

namespace Meezi.API.Services;

public record ExpenseServiceResult<T>(bool Success, T? Data, string? ErrorCode = null, string? Field = null);

public interface IExpenseService
{
    Task<ExpenseServiceResult<ExpenseDto>> CreateExpenseAsync(
        string cafeId,
        CreateExpenseRequest request,
        string userId,
        CancellationToken cancellationToken = default);

    Task<ExpenseListResult> GetExpensesAsync(
        string cafeId,
        string branchId,
        DateOnly from,
        DateOnly to,
        int page,
        int pageSize,
        CancellationToken cancellationToken = default);

    Task<ExpenseServiceResult<bool>> DeleteExpenseAsync(
        string cafeId,
        string expenseId,
        CancellationToken cancellationToken = default);
}

public class ExpenseService : IExpenseService
{
    private readonly AppDbContext _db;
    private readonly IShiftService _shifts;

    public ExpenseService(AppDbContext db, IShiftService shifts)
    {
        _db = db;
        _shifts = shifts;
    }

    public async Task<ExpenseServiceResult<ExpenseDto>> CreateExpenseAsync(
        string cafeId,
        CreateExpenseRequest request,
        string userId,
        CancellationToken cancellationToken = default)
    {
        var branch = await _db.Branches.FirstOrDefaultAsync(
            b => b.Id == request.BranchId && b.CafeId == cafeId && b.IsActive,
            cancellationToken);
        if (branch is null)
            return new ExpenseServiceResult<ExpenseDto>(false, null, "BRANCH_NOT_FOUND", "branchId");

        if (!string.IsNullOrWhiteSpace(request.ShiftId))
        {
            var shift = await _db.RegisterShifts.FirstOrDefaultAsync(
                s => s.Id == request.ShiftId && s.CafeId == cafeId,
                cancellationToken);
            if (shift is null)
                return new ExpenseServiceResult<ExpenseDto>(false, null, "SHIFT_NOT_FOUND", "shiftId");
            if (shift.BranchId != request.BranchId)
                return new ExpenseServiceResult<ExpenseDto>(false, null, "SHIFT_BRANCH_MISMATCH", "shiftId");
            if (shift.Status != ShiftStatus.Open)
                return new ExpenseServiceResult<ExpenseDto>(false, null, "SHIFT_ALREADY_CLOSED", "shiftId");
        }

        var now = DateTime.UtcNow;
        var expense = new Expense
        {
            CafeId = cafeId,
            BranchId = request.BranchId,
            ShiftId = request.ShiftId,
            Category = request.Category,
            Amount = request.Amount,
            Note = request.Note,
            ReceiptImageUrl = request.ReceiptImageUrl,
            CreatedByUserId = userId,
            CreatedAt = now
        };

        _db.Expenses.Add(expense);
        await _db.SaveChangesAsync(cancellationToken);

        if (!string.IsNullOrWhiteSpace(request.ShiftId))
        {
            var withdrawalNote = $"expense:{expense.Category}";
            if (!string.IsNullOrWhiteSpace(request.Note))
                withdrawalNote += $" — {request.Note}";

            var tx = await _shifts.RecordTransactionAsync(
                cafeId,
                request.ShiftId,
                CashTransactionType.Withdrawal,
                PaymentMethod.Cash,
                request.Amount,
                userId,
                referenceId: expense.Id,
                note: withdrawalNote,
                cancellationToken);

            if (!tx.Success)
            {
                _db.Expenses.Remove(expense);
                await _db.SaveChangesAsync(cancellationToken);
                return new ExpenseServiceResult<ExpenseDto>(
                    false, null, tx.ErrorCode ?? "WITHDRAWAL_FAILED", tx.Field);
            }
        }

        return new ExpenseServiceResult<ExpenseDto>(true, ToDto(expense));
    }

    public async Task<ExpenseListResult> GetExpensesAsync(
        string cafeId,
        string branchId,
        DateOnly from,
        DateOnly to,
        int page,
        int pageSize,
        CancellationToken cancellationToken = default)
    {
        page = Math.Max(1, page);
        pageSize = Math.Clamp(pageSize, 1, 100);

        var (utcStart, _) = IranCalendar.GetUtcRangeForIranDay(from);
        var (_, utcEnd) = IranCalendar.GetUtcRangeForIranDay(to);

        var query = _db.Expenses.AsNoTracking()
            .Where(e => e.CafeId == cafeId
                && e.BranchId == branchId
                && e.CreatedAt >= utcStart
                && e.CreatedAt < utcEnd);

        var total = await query.CountAsync(cancellationToken);
        var rows = await query
            .OrderByDescending(e => e.CreatedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync(cancellationToken);

        return new ExpenseListResult(rows.Select(ToDto).ToList(), total);
    }

    public async Task<ExpenseServiceResult<bool>> DeleteExpenseAsync(
        string cafeId,
        string expenseId,
        CancellationToken cancellationToken = default)
    {
        var expense = await _db.Expenses.FirstOrDefaultAsync(
            e => e.Id == expenseId && e.CafeId == cafeId,
            cancellationToken);

        if (expense is null)
            return new ExpenseServiceResult<bool>(false, false, "NOT_FOUND");

        var now = DateTime.UtcNow;
        expense.DeletedAt = now;

        var withdrawal = await _db.CashTransactions.FirstOrDefaultAsync(
            t => t.CafeId == cafeId
                && t.ReferenceId == expenseId
                && t.Type == CashTransactionType.Withdrawal,
            cancellationToken);
        if (withdrawal is not null)
            withdrawal.DeletedAt = now;

        await _db.SaveChangesAsync(cancellationToken);
        return new ExpenseServiceResult<bool>(true, true);
    }

    private static ExpenseDto ToDto(Expense e) => new(
        e.Id,
        e.CafeId,
        e.BranchId,
        e.ShiftId,
        e.Category,
        e.Amount,
        e.Note,
        e.ReceiptImageUrl,
        e.CreatedByUserId,
        e.CreatedAt);
}
