using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.UoW;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services.Hrm;

/// <summary>
/// Leave request service (FR-HR-LV-02). Approving a request increments the
/// employee's <see cref="LeaveBalance.TakenDays"/> (docs/12-BACKEND-HRM.md §6) —
/// this is also the event Attendance's OnLeave classification reads back via
/// <see cref="FindApprovedLeaveCoveringAsync"/>.
/// </summary>
public sealed class LeaveRequestService : ILeaveRequestService
{
    private readonly IRepository<LeaveRequest> _requests;
    private readonly IRepository<Employee> _employees;
    private readonly IRepository<LeaveType> _leaveTypes;
    private readonly ILeaveBalanceService _balances;
    private readonly INumberSequenceService _numberSequence;
    private readonly IUnitOfWork _uow;

    public LeaveRequestService(
        IRepository<LeaveRequest> requests, IRepository<Employee> employees, IRepository<LeaveType> leaveTypes,
        ILeaveBalanceService balances, INumberSequenceService numberSequence, IUnitOfWork uow)
    {
        _requests = requests;
        _employees = employees;
        _leaveTypes = leaveTypes;
        _balances = balances;
        _numberSequence = numberSequence;
        _uow = uow;
    }

    public async Task<PagedResponse<LeaveRequestDto>> ListAsync(
        PageQuery query, int? employeeId, LeaveRequestStatus? status, CancellationToken ct = default)
    {
        var q = _requests.Query().AsNoTracking().Include(r => r.Employee).Include(r => r.LeaveType).AsQueryable();
        if (employeeId is not null) q = q.Where(r => r.EmployeeId == employeeId);
        if (status is not null) q = q.Where(r => r.Status == status);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(r => r.CreatedAt)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(r => Map(r))
            .ToListAsync(ct);

        return PagedResponse<LeaveRequestDto>.Create(rows, query.Page, query.PageSize, total);
    }

    public async Task<LeaveRequestDto?> GetAsync(int leaveRequestId, CancellationToken ct = default)
    {
        var request = await _requests.Query().AsNoTracking()
            .Include(r => r.Employee).Include(r => r.LeaveType)
            .FirstOrDefaultAsync(r => r.LeaveRequestId == leaveRequestId, ct);
        return request is null ? null : Map(request);
    }

    public async Task<LeaveRequestDto> CreateAsync(CreateLeaveRequestRequest request, int actorUserId, CancellationToken ct = default)
    {
        if (!await _employees.Query().AnyAsync(e => e.EmployeeId == request.EmployeeId, ct))
            throw new NotFoundException($"Employee {request.EmployeeId} was not found.");
        if (!await _leaveTypes.Query().AnyAsync(t => t.LeaveTypeId == request.LeaveTypeId, ct))
            throw new NotFoundException($"Leave type {request.LeaveTypeId} was not found.");
        if (request.EndDate < request.StartDate)
            throw new DomainException(ErrorCodes.Validation, "End date cannot be before start date.", 422);

        var docNo = await _numberSequence.NextAsync("LV", ct);

        // Simplification: calendar-day count (inclusive), not business-day aware —
        // flagged for a future improvement, not silently assumed correct for payroll.
        var daysCount = (decimal)(request.EndDate.Date - request.StartDate.Date).Days + 1;

        var leaveRequest = new LeaveRequest
        {
            DocNo = docNo,
            EmployeeId = request.EmployeeId,
            LeaveTypeId = request.LeaveTypeId,
            StartDate = request.StartDate.Date,
            EndDate = request.EndDate.Date,
            DaysCount = daysCount,
            Reason = request.Reason?.Trim(),
            Status = LeaveRequestStatus.Draft,
            CreatedBy = actorUserId,
            CreatedAt = DateTime.UtcNow
        };

        await _requests.AddAsync(leaveRequest, ct);
        await _uow.SaveChangesAsync(ct);

        return Map(leaveRequest);
    }

    public async Task<LeaveRequestDto> SubmitAsync(int leaveRequestId, CancellationToken ct = default)
    {
        var request = await GetTrackedAsync(leaveRequestId, ct);
        if (request.Status != LeaveRequestStatus.Draft)
            throw new DomainException(ErrorCodes.Conflict, "Only a Draft leave request can be submitted.", 409);

        request.Status = LeaveRequestStatus.Submitted;
        await _uow.SaveChangesAsync(ct);
        return Map(request);
    }

    public async Task<LeaveRequestDto> ApproveAsync(int leaveRequestId, int actorUserId, CancellationToken ct = default)
    {
        var request = await GetTrackedAsync(leaveRequestId, ct);
        if (request.Status != LeaveRequestStatus.Submitted)
            throw new DomainException(ErrorCodes.Conflict, "Only a Submitted leave request can be approved.", 409);

        request.Status = LeaveRequestStatus.Approved;
        request.ApprovedBy = actorUserId;
        request.ApprovedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);

        await _balances.IncrementTakenDaysAsync(request.EmployeeId, request.LeaveTypeId, request.StartDate.Year, request.DaysCount, ct);

        return Map(request);
    }

    public async Task<LeaveRequestDto> RejectAsync(int leaveRequestId, string reason, int actorUserId, CancellationToken ct = default)
    {
        var request = await GetTrackedAsync(leaveRequestId, ct);
        if (request.Status != LeaveRequestStatus.Submitted)
            throw new DomainException(ErrorCodes.Conflict, "Only a Submitted leave request can be rejected.", 409);

        request.Status = LeaveRequestStatus.Rejected;
        request.ApprovedBy = actorUserId;
        request.ApprovedAt = DateTime.UtcNow;
        request.RejectionReason = reason.Trim();
        await _uow.SaveChangesAsync(ct);

        return Map(request);
    }

    public async Task<LeaveRequestDto> CancelAsync(int leaveRequestId, CancellationToken ct = default)
    {
        var request = await GetTrackedAsync(leaveRequestId, ct);
        if (request.Status is not (LeaveRequestStatus.Draft or LeaveRequestStatus.Submitted))
            throw new DomainException(ErrorCodes.Conflict, "Only a Draft or Submitted leave request can be cancelled.", 409);

        request.Status = LeaveRequestStatus.Cancelled;
        await _uow.SaveChangesAsync(ct);
        return Map(request);
    }

    public async Task<LeaveRequest?> FindApprovedLeaveCoveringAsync(int employeeId, DateTime date, CancellationToken ct = default)
    {
        var day = date.Date;
        return await _requests.Query().AsNoTracking()
            .Include(r => r.LeaveType)
            .FirstOrDefaultAsync(r => r.EmployeeId == employeeId && r.Status == LeaveRequestStatus.Approved
                && r.StartDate <= day && r.EndDate >= day, ct);
    }

    private async Task<LeaveRequest> GetTrackedAsync(int leaveRequestId, CancellationToken ct)
        => await _requests.GetByIdAsync(leaveRequestId, ct)
            ?? throw new NotFoundException($"Leave request {leaveRequestId} was not found.");

    private static LeaveRequestDto Map(LeaveRequest r) => new(
        r.LeaveRequestId, r.DocNo, r.EmployeeId, r.Employee?.FullName, r.LeaveTypeId, r.LeaveType?.Name,
        r.StartDate, r.EndDate, r.DaysCount, r.Reason, r.Status, r.ApprovedBy, r.ApprovedAt, r.RejectionReason, r.CreatedAt);
}
