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

namespace Meezi.API.Services;

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

public interface IShiftService
{
    Task<ShiftServiceResult<ShiftDto>> OpenShiftAsync(
        string cafeId,
        string branchId,
        decimal openingCash,
        string userId,
        CancellationToken cancellationToken = default);

    Task<ShiftServiceResult<ShiftDto>> CloseShiftAsync(
        string cafeId,
        string shiftId,
        decimal closingCash,
        string userId,
        CancellationToken cancellationToken = default);

    Task<ShiftDto?> GetCurrentShiftAsync(
        string cafeId,
        string branchId,
        CancellationToken cancellationToken = default);

    Task<IReadOnlyList<CashTransactionDto>?> GetTransactionsAsync(
        string cafeId,
        string shiftId,
        CancellationToken cancellationToken = default);

    Task<ShiftServiceResult<CashTransactionDto>> RecordTransactionAsync(
        string cafeId,
        string shiftId,
        CashTransactionType type,
        PaymentMethod method,
        decimal amount,
        string createdByUserId,
        string? referenceId = null,
        string? note = null,
        CancellationToken cancellationToken = default);

    Task<ShiftServiceResult<Shift>> RequireOpenShiftForBranchAsync(
        string cafeId,
        string branchId,
        CancellationToken cancellationToken = default);
}

public class ShiftService : IShiftService
{
    private readonly AppDbContext _db;

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

    public async Task<ShiftServiceResult<ShiftDto>> OpenShiftAsync(
        string cafeId,
        string branchId,
        decimal openingCash,
        string userId,
        CancellationToken cancellationToken = default)
    {
        var branch = await _db.Branches.FirstOrDefaultAsync(
            b => b.Id == branchId && b.CafeId == cafeId && b.IsActive,
            cancellationToken);
        if (branch is null)
            return new ShiftServiceResult<ShiftDto>(false, null, "BRANCH_NOT_FOUND", "branchId");

        var hasOpen = await _db.RegisterShifts.AnyAsync(
            s => s.BranchId == branchId && s.CafeId == cafeId && s.Status == ShiftStatus.Open,
            cancellationToken);
        if (hasOpen)
            return new ShiftServiceResult<ShiftDto>(false, null, "SHIFT_ALREADY_OPEN", "branchId");

        var employeeExists = await _db.Employees.AnyAsync(
            e => e.Id == userId && e.CafeId == cafeId,
            cancellationToken);
        if (!employeeExists)
            return new ShiftServiceResult<ShiftDto>(false, null, "USER_NOT_FOUND", "userId");

        var now = DateTime.UtcNow;
        var shift = new Shift
        {
            CafeId = cafeId,
            BranchId = branchId,
            OpenedByUserId = userId,
            OpenedAt = now,
            OpeningCash = openingCash,
            ExpectedCash = openingCash,
            Status = ShiftStatus.Open,
            CreatedAt = now
        };

        _db.RegisterShifts.Add(shift);
        await _db.SaveChangesAsync(cancellationToken);

        return new ShiftServiceResult<ShiftDto>(true, ToDto(shift));
    }

    public async Task<ShiftServiceResult<ShiftDto>> CloseShiftAsync(
        string cafeId,
        string shiftId,
        decimal closingCash,
        string userId,
        CancellationToken cancellationToken = default)
    {
        var shift = await _db.RegisterShifts
            .Include(s => s.Transactions)
            .FirstOrDefaultAsync(s => s.Id == shiftId && s.CafeId == cafeId, cancellationToken);

        if (shift is null)
            return new ShiftServiceResult<ShiftDto>(false, null, "SHIFT_NOT_FOUND");

        if (shift.Status != ShiftStatus.Open)
            return new ShiftServiceResult<ShiftDto>(false, null, "SHIFT_ALREADY_CLOSED");

        shift.ExpectedCash = CalculateExpectedCash(shift.OpeningCash, shift.Transactions);
        shift.ClosingCash = closingCash;
        shift.Discrepancy = closingCash - shift.ExpectedCash;
        shift.ClosedByUserId = userId;
        shift.ClosedAt = DateTime.UtcNow;
        shift.Status = ShiftStatus.Closed;

        await _db.SaveChangesAsync(cancellationToken);

        return new ShiftServiceResult<ShiftDto>(true, ToDto(shift));
    }

    public async Task<ShiftDto?> GetCurrentShiftAsync(
        string cafeId,
        string branchId,
        CancellationToken cancellationToken = default)
    {
        var shift = await _db.RegisterShifts
            .AsNoTracking()
            .FirstOrDefaultAsync(
                s => s.CafeId == cafeId && s.BranchId == branchId && s.Status == ShiftStatus.Open,
                cancellationToken);

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

    public async Task<IReadOnlyList<CashTransactionDto>?> GetTransactionsAsync(
        string cafeId,
        string shiftId,
        CancellationToken cancellationToken = default)
    {
        var shiftExists = await _db.RegisterShifts.AnyAsync(
            s => s.Id == shiftId && s.CafeId == cafeId,
            cancellationToken);
        if (!shiftExists) return null;

        var rows = await _db.CashTransactions
            .AsNoTracking()
            .Where(t => t.ShiftId == shiftId && t.CafeId == cafeId)
            .OrderBy(t => t.CreatedAt)
            .ToListAsync(cancellationToken);

        return rows.Select(ToTransactionDto).ToList();
    }

    public async Task<ShiftServiceResult<CashTransactionDto>> RecordTransactionAsync(
        string cafeId,
        string shiftId,
        CashTransactionType type,
        PaymentMethod method,
        decimal amount,
        string createdByUserId,
        string? referenceId = null,
        string? note = null,
        CancellationToken cancellationToken = default)
    {
        if (amount <= 0)
            return new ShiftServiceResult<CashTransactionDto>(false, null, "INVALID_AMOUNT", "amount");

        var shift = await _db.RegisterShifts.FirstOrDefaultAsync(
            s => s.Id == shiftId && s.CafeId == cafeId,
            cancellationToken);

        if (shift is null)
            return new ShiftServiceResult<CashTransactionDto>(false, null, "SHIFT_NOT_FOUND");

        if (shift.Status != ShiftStatus.Open)
            return new ShiftServiceResult<CashTransactionDto>(false, null, "SHIFT_ALREADY_CLOSED");

        var tx = new CashTransaction
        {
            CafeId = cafeId,
            BranchId = shift.BranchId,
            ShiftId = shiftId,
            Type = type,
            Method = method,
            Amount = amount,
            ReferenceId = referenceId,
            Note = note,
            CreatedByUserId = createdByUserId,
            CreatedAt = DateTime.UtcNow
        };

        _db.CashTransactions.Add(tx);
        await _db.SaveChangesAsync(cancellationToken);

        return new ShiftServiceResult<CashTransactionDto>(true, ToTransactionDto(tx));
    }

    public async Task<ShiftServiceResult<Shift>> RequireOpenShiftForBranchAsync(
        string cafeId,
        string branchId,
        CancellationToken cancellationToken = default)
    {
        var shift = await _db.RegisterShifts.FirstOrDefaultAsync(
            s => s.CafeId == cafeId && s.BranchId == branchId && s.Status == ShiftStatus.Open,
            cancellationToken);

        if (shift is null)
            return new ShiftServiceResult<Shift>(false, null, "NO_OPEN_SHIFT", "branchId");

        return new ShiftServiceResult<Shift>(true, shift);
    }

    internal static decimal CalculateExpectedCash(decimal openingCash, IEnumerable<CashTransaction> transactions)
    {
        var cashPayments = transactions
            .Where(t => t.Type == CashTransactionType.OrderPayment && t.Method == PaymentMethod.Cash)
            .Sum(t => t.Amount);

        var withdrawals = transactions
            .Where(t => t.Type == CashTransactionType.Withdrawal)
            .Sum(t => t.Amount);

        return openingCash + cashPayments - withdrawals;
    }

    private static ShiftDto ToDto(Shift s) => new(
        s.Id,
        s.CafeId,
        s.BranchId,
        s.OpenedByUserId,
        s.ClosedByUserId,
        s.OpenedAt,
        s.ClosedAt,
        s.OpeningCash,
        s.ClosingCash,
        s.ExpectedCash,
        s.Discrepancy,
        s.Status);

    private static CashTransactionDto ToTransactionDto(CashTransaction t) => new(
        t.Id,
        t.ShiftId,
        t.BranchId,
        t.Type,
        t.Method,
        t.Amount,
        t.ReferenceId,
        t.Note,
        t.CreatedByUserId,
        t.CreatedAt);
}
