using ERPCore.Common.Http;
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>
/// Employee (staff) service (FR-HR-MD-02/03, docs/13-BACKEND-HRM-API.md §3). Distinct
/// from <see cref="IUserManagementService"/> (system login accounts) — the two are
/// linked only via the optional, explicit <see cref="Employee.UserId"/> (docs/12-BACKEND-HRM.md A.5).
/// </summary>
public sealed class EmployeeService : IEmployeeService
{
    private readonly IRepository<Employee> _employees;
    private readonly IRepository<User> _users;
    private readonly IRepository<EmployeeBankDetail> _bankDetails;
    private readonly IUnitOfWork _uow;

    public EmployeeService(
        IRepository<Employee> employees, IRepository<User> users, IRepository<EmployeeBankDetail> bankDetails, IUnitOfWork uow)
    {
        _employees = employees;
        _users = users;
        _bankDetails = bankDetails;
        _uow = uow;
    }

    public async Task<PagedResponse<EmployeeListItemDto>> ListAsync(
        PageQuery query, EmployeeStatus? status, int? departmentId, int? designationId, int? branchId, CancellationToken ct = default)
    {
        var q = _employees.Query().AsNoTracking()
            .Include(e => e.Department).Include(e => e.Designation)
            .Include(e => e.EmploymentType).Include(e => e.Branch)
            .AsQueryable();

        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(e => EF.Functions.ILike(e.FullName, $"%{term}%") || EF.Functions.ILike(e.EmployeeCode, $"%{term}%"));
        }
        if (status is not null) q = q.Where(e => e.Status == status);
        if (departmentId is not null) q = q.Where(e => e.DepartmentId == departmentId);
        if (designationId is not null) q = q.Where(e => e.DesignationId == designationId);
        if (branchId is not null) q = q.Where(e => e.BranchId == branchId);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderBy(e => e.FullName)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(e => new EmployeeListItemDto(
                e.EmployeeId, e.EmployeeCode, e.FullName, e.Email,
                e.DepartmentId, e.Department!.Name, e.DesignationId, e.Designation!.Name,
                e.EmploymentTypeId, e.EmploymentType!.Name, e.BranchId, e.Branch != null ? e.Branch.Name : null,
                e.Status, e.UserId != null, e.HireDate))
            .ToListAsync(ct);

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

    public async Task<ETagged<EmployeeDetailDto>?> GetAsync(int employeeId, CancellationToken ct = default)
    {
        var employee = await _employees.Query().AsNoTracking().FirstOrDefaultAsync(e => e.EmployeeId == employeeId, ct);
        return employee is null ? null : new ETagged<EmployeeDetailDto>(Map(employee), employee.RowVersion);
    }

    public async Task<ETagged<EmployeeDetailDto>> CreateAsync(CreateEmployeeRequest request, int actorUserId, CancellationToken ct = default)
    {
        var code = request.EmployeeCode.Trim();
        if (await _employees.Query().AnyAsync(e => e.EmployeeCode.ToLower() == code.ToLower(), ct))
            throw new DomainException(ErrorCodes.EmployeeCodeDuplicate, $"An employee with code '{code}' already exists.", 400);

        int? linkedUserId = null;
        if (request.LinkUserId is not null)
        {
            var user = await _users.GetByIdAsync(request.LinkUserId.Value, ct)
                ?? throw new NotFoundException($"User {request.LinkUserId} was not found.");
            if (await _employees.Query().AnyAsync(e => e.UserId == user.UserId, ct))
                throw new DomainException(ErrorCodes.UserAlreadyLinked, $"User {user.UserId} already backs a different employee.", 409);
            linkedUserId = user.UserId;
        }

        var employee = new Employee
        {
            EmployeeCode = code,
            FullName = request.FullName.Trim(),
            Nic = request.Nic?.Trim(),
            DateOfBirth = request.DateOfBirth,
            Gender = request.Gender,
            Nationality = request.Nationality?.Trim(),
            Email = request.Email?.Trim(),
            PersonalMobile = request.PersonalMobile?.Trim(),
            AddressLine1 = request.AddressLine1?.Trim(),
            AddressLine2 = request.AddressLine2?.Trim(),
            City = request.City?.Trim(),
            PostalCode = request.PostalCode?.Trim(),
            Country = request.Country?.Trim(),
            EmergencyContactName = request.EmergencyContactName?.Trim(),
            EmergencyContactRelationship = request.EmergencyContactRelationship?.Trim(),
            EmergencyContactPhone = request.EmergencyContactPhone?.Trim(),
            HireDate = request.HireDate,
            DepartmentId = request.DepartmentId,
            DesignationId = request.DesignationId,
            EmploymentTypeId = request.EmploymentTypeId,
            BranchId = request.BranchId,
            WorkShiftId = request.WorkShiftId,
            ReportingManagerId = request.ReportingManagerId,
            EpfNumber = request.EpfNumber?.Trim(),
            EtfNumber = request.EtfNumber?.Trim(),
            TaxIdentificationNumber = request.TaxIdentificationNumber?.Trim(),
            UserId = linkedUserId,
            Status = EmployeeStatus.Active,
            CreatedBy = actorUserId,
            CreatedAt = DateTime.UtcNow
        };

        await _employees.AddAsync(employee, ct);
        await _uow.SaveChangesAsync(ct);

        return new ETagged<EmployeeDetailDto>(Map(employee), employee.RowVersion);
    }

    public async Task<ETagged<EmployeeDetailDto>> UpdateAsync(
        int employeeId, UpdateEmployeeRequest request, uint expectedRowVersion, int actorUserId, CancellationToken ct = default)
    {
        var employee = await _employees.GetByIdAsync(employeeId, ct)
            ?? throw new NotFoundException($"Employee {employeeId} was not found.");

        if (employee.RowVersion != expectedRowVersion)
            throw new DomainException(ErrorCodes.ConcurrencyConflict, "The employee was modified by another request.", 412);

        employee.FullName = request.FullName.Trim();
        employee.Nic = request.Nic?.Trim();
        employee.DateOfBirth = request.DateOfBirth;
        employee.Gender = request.Gender;
        employee.Nationality = request.Nationality?.Trim();
        employee.Email = request.Email?.Trim();
        employee.PersonalMobile = request.PersonalMobile?.Trim();
        employee.AddressLine1 = request.AddressLine1?.Trim();
        employee.AddressLine2 = request.AddressLine2?.Trim();
        employee.City = request.City?.Trim();
        employee.PostalCode = request.PostalCode?.Trim();
        employee.Country = request.Country?.Trim();
        employee.EmergencyContactName = request.EmergencyContactName?.Trim();
        employee.EmergencyContactRelationship = request.EmergencyContactRelationship?.Trim();
        employee.EmergencyContactPhone = request.EmergencyContactPhone?.Trim();
        employee.ConfirmationDate = request.ConfirmationDate;
        employee.LastWorkingDate = request.LastWorkingDate;
        employee.DepartmentId = request.DepartmentId;
        employee.DesignationId = request.DesignationId;
        employee.EmploymentTypeId = request.EmploymentTypeId;
        employee.BranchId = request.BranchId;
        employee.WorkShiftId = request.WorkShiftId;
        employee.ReportingManagerId = request.ReportingManagerId;
        employee.EpfNumber = request.EpfNumber?.Trim();
        employee.EtfNumber = request.EtfNumber?.Trim();
        employee.TaxIdentificationNumber = request.TaxIdentificationNumber?.Trim();
        employee.UpdatedBy = actorUserId;
        employee.UpdatedAt = DateTime.UtcNow;

        try
        {
            await _uow.SaveChangesAsync(ct);
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new DomainException(ErrorCodes.ConcurrencyConflict, "The employee was modified by another request.", 412);
        }

        return new ETagged<EmployeeDetailDto>(Map(employee), employee.RowVersion);
    }

    public async Task SetStatusAsync(int employeeId, EmployeeStatus status, CancellationToken ct = default)
    {
        var employee = await _employees.GetByIdAsync(employeeId, ct)
            ?? throw new NotFoundException($"Employee {employeeId} was not found.");

        employee.Status = status;
        employee.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);
    }

    public async Task<List<EmployeeBankDetailDto>> ListBankDetailsAsync(int employeeId, CancellationToken ct = default)
    {
        return await _bankDetails.Query().AsNoTracking()
            .Where(b => b.EmployeeId == employeeId)
            .Select(b => new EmployeeBankDetailDto(
                b.EmployeeBankDetailId, b.BankName, b.BranchName, b.AccountNumber, b.AccountHolderName,
                b.SwiftCode, b.IsPrimary, b.Status))
            .ToListAsync(ct);
    }

    public async Task<List<EmployeeBankDetailDto>> ReplaceBankDetailsAsync(
        int employeeId, ReplaceEmployeeBankDetailsRequest request, CancellationToken ct = default)
    {
        if (!await _employees.Query().AnyAsync(e => e.EmployeeId == employeeId, ct))
            throw new NotFoundException($"Employee {employeeId} was not found.");

        if (request.Items.Count(i => i.IsPrimary) > 1)
            throw new DomainException(ErrorCodes.Validation, "Only one bank detail row may be marked primary.", 422);

        var existing = await _bankDetails.Query().Where(b => b.EmployeeId == employeeId).ToListAsync(ct);
        foreach (var row in existing) _bankDetails.Remove(row);

        var result = new List<EmployeeBankDetail>();
        foreach (var item in request.Items)
        {
            var detail = new EmployeeBankDetail
            {
                EmployeeId = employeeId,
                BankName = item.BankName.Trim(),
                BranchName = item.BranchName.Trim(),
                AccountNumber = item.AccountNumber.Trim(),
                AccountHolderName = item.AccountHolderName.Trim(),
                SwiftCode = item.SwiftCode?.Trim(),
                IsPrimary = item.IsPrimary,
                Status = EntityStatus.Active,
                CreatedAt = DateTime.UtcNow
            };
            result.Add(detail);
            await _bankDetails.AddAsync(detail, ct);
        }

        await _uow.SaveChangesAsync(ct);

        return result.Select(b => new EmployeeBankDetailDto(
            b.EmployeeBankDetailId, b.BankName, b.BranchName, b.AccountNumber, b.AccountHolderName,
            b.SwiftCode, b.IsPrimary, b.Status)).ToList();
    }

    private static EmployeeDetailDto Map(Employee e) => new(
        e.EmployeeId, e.EmployeeCode, e.FullName, e.Nic, e.DateOfBirth, e.Gender, e.Nationality, e.ProfilePhotoPath,
        e.Email, e.PersonalMobile, e.AddressLine1, e.AddressLine2, e.City, e.PostalCode, e.Country,
        e.EmergencyContactName, e.EmergencyContactRelationship, e.EmergencyContactPhone,
        e.HireDate, e.ConfirmationDate, e.LastWorkingDate,
        e.DepartmentId, e.DesignationId, e.EmploymentTypeId, e.BranchId, e.WorkShiftId,
        e.ReportingManagerId, e.EpfNumber, e.EtfNumber, e.TaxIdentificationNumber,
        e.UserId, e.Status, e.CreatedAt, e.UpdatedAt);
}
