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

namespace Meezi.API.Services;

public interface IHrService
{
    Task<IReadOnlyList<EmployeeSummaryDto>> GetEmployeesAsync(
        string cafeId,
        string? branchId = null,
        CancellationToken cancellationToken = default);
    Task<EmployeeSummaryDto?> GetEmployeeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
    Task<bool> EmployeeBelongsToCafeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);

    Task<TodayShiftDto?> GetTodayShiftAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
    Task<AttendanceDto?> ClockInAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
    Task<AttendanceDto?> ClockOutAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<AttendanceDto>> GetAttendanceAsync(string cafeId, string? employeeId, DateOnly? from, DateOnly? to, CancellationToken cancellationToken = default);

    Task<IReadOnlyList<ShiftDto>> GetShiftsAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<ShiftDto>> UpsertShiftsAsync(string cafeId, string employeeId, UpsertShiftsRequest request, CancellationToken cancellationToken = default);

    Task<IReadOnlyList<LeaveRequestDto>> GetLeaveRequestsAsync(string cafeId, LeaveStatus? status, CancellationToken cancellationToken = default);
    Task<LeaveRequestDto?> CreateLeaveRequestAsync(string cafeId, string employeeId, CreateLeaveRequest request, CancellationToken cancellationToken = default);
    Task<LeaveRequestDto?> ReviewLeaveRequestAsync(string cafeId, string leaveId, string reviewerId, ReviewLeaveRequest request, CancellationToken cancellationToken = default);

    Task<IReadOnlyList<EmployeeSalaryDto>> GetSalariesAsync(string cafeId, string? monthYear, CancellationToken cancellationToken = default);
    Task<EmployeeSalaryDto?> CreateSalaryAsync(string cafeId, CreateSalaryRequest request, CancellationToken cancellationToken = default);
    Task<EmployeeSalaryDto?> MarkSalaryPaidAsync(string cafeId, string salaryId, CancellationToken cancellationToken = default);
}

public class HrService : IHrService
{
    private readonly AppDbContext _db;

    public HrService(AppDbContext db)
    {
        _db = db;
    }

    public async Task<IReadOnlyList<EmployeeSummaryDto>> GetEmployeesAsync(
        string cafeId,
        string? branchId = null,
        CancellationToken cancellationToken = default)
    {
        var query = _db.Employees.Where(e => e.CafeId == cafeId);
        if (!string.IsNullOrEmpty(branchId))
            query = query.Where(e => e.BranchId == branchId);
        var list = await query.OrderBy(e => e.Name).ToListAsync(cancellationToken);
        return list.Select(e => new EmployeeSummaryDto(e.Id, e.Name, e.Phone, e.Role, e.BaseSalary)).ToList();
    }

    public async Task<EmployeeSummaryDto?> GetEmployeeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
    {
        var e = await _db.Employees.FirstOrDefaultAsync(x => x.Id == employeeId && x.CafeId == cafeId, cancellationToken);
        return e is null ? null : new EmployeeSummaryDto(e.Id, e.Name, e.Phone, e.Role, e.BaseSalary);
    }

    public Task<bool> EmployeeBelongsToCafeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default) =>
        _db.Employees.AnyAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken);

    public async Task<TodayShiftDto?> GetTodayShiftAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
    {
        if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken))
            return null;

        var day = (int)DateTime.UtcNow.DayOfWeek;
        var shift = await _db.EmployeeSchedules
            .FirstOrDefaultAsync(s => s.EmployeeId == employeeId && s.DayOfWeek == day, cancellationToken);

        var type = shift?.ShiftType ?? ShiftType.DayOff;
        return new TodayShiftDto(type, ShiftLabel(type));
    }

    public async Task<AttendanceDto?> ClockInAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
    {
        var employee = await _db.Employees
            .FirstOrDefaultAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken);
        if (employee is null) return null;

        var today = DateOnly.FromDateTime(DateTime.UtcNow);
        var attendance = await _db.Attendances
            .FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.Date == today, cancellationToken);

        if (attendance is null)
        {
            attendance = new Attendance
            {
                EmployeeId = employeeId,
                Date = today,
                ClockIn = DateTime.UtcNow
            };
            _db.Attendances.Add(attendance);
        }
        else if (attendance.ClockIn is null)
        {
            attendance.ClockIn = DateTime.UtcNow;
        }
        else
        {
            return ToAttendanceDto(attendance, employee.Name);
        }

        await _db.SaveChangesAsync(cancellationToken);
        return ToAttendanceDto(attendance, employee.Name);
    }

    public async Task<AttendanceDto?> ClockOutAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
    {
        var employee = await _db.Employees
            .FirstOrDefaultAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken);
        if (employee is null) return null;

        var today = DateOnly.FromDateTime(DateTime.UtcNow);
        var attendance = await _db.Attendances
            .FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.Date == today, cancellationToken);

        if (attendance?.ClockIn is null) return null;

        attendance.ClockOut = DateTime.UtcNow;
        await _db.SaveChangesAsync(cancellationToken);
        return ToAttendanceDto(attendance, employee.Name);
    }

    public async Task<IReadOnlyList<AttendanceDto>> GetAttendanceAsync(
        string cafeId,
        string? employeeId,
        DateOnly? from,
        DateOnly? to,
        CancellationToken cancellationToken = default)
    {
        var query = _db.Attendances
            .Include(a => a.Employee)
            .Where(a => a.Employee.CafeId == cafeId);

        if (!string.IsNullOrEmpty(employeeId))
            query = query.Where(a => a.EmployeeId == employeeId);

        if (from.HasValue)
            query = query.Where(a => a.Date >= from.Value);
        if (to.HasValue)
            query = query.Where(a => a.Date <= to.Value);

        var list = await query.OrderByDescending(a => a.Date).Take(100).ToListAsync(cancellationToken);
        return list.Select(a => ToAttendanceDto(a, a.Employee.Name)).ToList();
    }

    public async Task<IReadOnlyList<ShiftDto>> GetShiftsAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default)
    {
        if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken))
            return [];

        var shifts = await _db.EmployeeSchedules
            .Where(s => s.EmployeeId == employeeId)
            .OrderBy(s => s.DayOfWeek)
            .ToListAsync(cancellationToken);
        return shifts.Select(s => new ShiftDto(s.DayOfWeek, s.ShiftType)).ToList();
    }

    public async Task<IReadOnlyList<ShiftDto>> UpsertShiftsAsync(
        string cafeId,
        string employeeId,
        UpsertShiftsRequest request,
        CancellationToken cancellationToken = default)
    {
        if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken))
            return [];

        var existing = await _db.EmployeeSchedules.Where(s => s.EmployeeId == employeeId).ToListAsync(cancellationToken);
        _db.EmployeeSchedules.RemoveRange(existing);

        foreach (var s in request.Shifts)
        {
            _db.EmployeeSchedules.Add(new EmployeeSchedule
            {
                EmployeeId = employeeId,
                DayOfWeek = s.DayOfWeek,
                ShiftType = s.ShiftType
            });
        }

        await _db.SaveChangesAsync(cancellationToken);
        return request.Shifts.ToList();
    }

    public async Task<IReadOnlyList<LeaveRequestDto>> GetLeaveRequestsAsync(
        string cafeId,
        LeaveStatus? status,
        CancellationToken cancellationToken = default)
    {
        var query = _db.LeaveRequests
            .Include(l => l.Employee)
            .Where(l => l.Employee.CafeId == cafeId);

        if (status.HasValue)
            query = query.Where(l => l.Status == status.Value);

        var list = await query.OrderByDescending(l => l.CreatedAt).ToListAsync(cancellationToken);
        return list.Select(l => ToLeaveDto(l)).ToList();
    }

    public async Task<LeaveRequestDto?> CreateLeaveRequestAsync(
        string cafeId,
        string employeeId,
        CreateLeaveRequest request,
        CancellationToken cancellationToken = default)
    {
        if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken))
            return null;

        var employee = await _db.Employees.FindAsync([employeeId], cancellationToken);
        var entity = new LeaveRequest
        {
            EmployeeId = employeeId,
            StartDate = request.StartDate,
            EndDate = request.EndDate,
            Reason = request.Reason,
            Status = LeaveStatus.Pending
        };

        _db.LeaveRequests.Add(entity);
        await _db.SaveChangesAsync(cancellationToken);
        return ToLeaveDto(entity, employee!.Name);
    }

    public async Task<LeaveRequestDto?> ReviewLeaveRequestAsync(
        string cafeId,
        string leaveId,
        string reviewerId,
        ReviewLeaveRequest request,
        CancellationToken cancellationToken = default)
    {
        var entity = await _db.LeaveRequests
            .Include(l => l.Employee)
            .FirstOrDefaultAsync(l => l.Id == leaveId && l.Employee.CafeId == cafeId, cancellationToken);

        if (entity is null) return null;

        entity.Status = request.Status;
        entity.ReviewedBy = reviewerId;
        await _db.SaveChangesAsync(cancellationToken);
        return ToLeaveDto(entity);
    }

    public async Task<IReadOnlyList<EmployeeSalaryDto>> GetSalariesAsync(
        string cafeId,
        string? monthYear,
        CancellationToken cancellationToken = default)
    {
        var query = _db.EmployeeSalaries
            .Include(s => s.Employee)
            .Where(s => s.Employee.CafeId == cafeId);

        if (!string.IsNullOrEmpty(monthYear))
            query = query.Where(s => s.MonthYear == monthYear);

        var list = await query.OrderByDescending(s => s.MonthYear).ToListAsync(cancellationToken);
        return list.Select(s => ToSalaryDto(s)).ToList();
    }

    public async Task<EmployeeSalaryDto?> CreateSalaryAsync(
        string cafeId,
        CreateSalaryRequest request,
        CancellationToken cancellationToken = default)
    {
        if (!await EmployeeBelongsToCafeAsync(cafeId, request.EmployeeId, cancellationToken))
            return null;

        var employee = await _db.Employees.FindAsync([request.EmployeeId], cancellationToken);
        var net = request.BaseSalary + request.OvertimePay - request.Deductions;

        var entity = new EmployeeSalary
        {
            EmployeeId = request.EmployeeId,
            MonthYear = request.MonthYear,
            BaseSalary = request.BaseSalary,
            OvertimePay = request.OvertimePay,
            Deductions = request.Deductions,
            NetSalary = net,
            IsPaid = false
        };

        _db.EmployeeSalaries.Add(entity);
        await _db.SaveChangesAsync(cancellationToken);
        return ToSalaryDto(entity, employee!.Name);
    }

    public async Task<EmployeeSalaryDto?> MarkSalaryPaidAsync(string cafeId, string salaryId, CancellationToken cancellationToken = default)
    {
        var entity = await _db.EmployeeSalaries
            .Include(s => s.Employee)
            .FirstOrDefaultAsync(s => s.Id == salaryId && s.Employee.CafeId == cafeId, cancellationToken);

        if (entity is null) return null;

        entity.IsPaid = true;
        await _db.SaveChangesAsync(cancellationToken);
        return ToSalaryDto(entity);
    }

    private static string ShiftLabel(ShiftType type) => type switch
    {
        ShiftType.Morning => "08:00 - 16:00",
        ShiftType.Evening => "16:00 - 00:00",
        _ => "Day off"
    };

    private static AttendanceDto ToAttendanceDto(Attendance a, string name) => new(
        a.Id, a.EmployeeId, name, a.Date, a.ClockIn, a.ClockOut, a.Notes);

    private static LeaveRequestDto ToLeaveDto(LeaveRequest l, string? name = null) => new(
        l.Id,
        l.EmployeeId,
        name ?? l.Employee?.Name ?? "",
        l.StartDate,
        l.EndDate,
        l.Reason,
        l.Status,
        l.ReviewedBy,
        l.CreatedAt);

    private static EmployeeSalaryDto ToSalaryDto(EmployeeSalary s, string? name = null) => new(
        s.Id,
        s.EmployeeId,
        name ?? s.Employee?.Name ?? "",
        s.MonthYear,
        s.BaseSalary,
        s.OvertimePay,
        s.Deductions,
        s.NetSalary,
        s.IsPaid);
}
