namespace OmsorgCore.Application.Services;

using OmsorgCore.Application.Abstractions;
using OmsorgCore.Domain.Entities;
using OmsorgCore.Domain.Enums;

public class AssignmentService : IAssignmentService
{
    private const int AssumedHoursPerDay = 8;

    private readonly IAssignmentRepository _assignmentRepository;
    private readonly IOrderRepository _orderRepository;
    private readonly IEmployeeRepository _employeeRepository;
    private readonly IAbsenceRepository _absenceRepository;
    private readonly IContractRepository _contractRepository;
    private readonly IValueListRepository _valueListRepository;
    private readonly IAssignmentValidationSettingsRepository _validationSettingsRepository;

    public AssignmentService(
        IAssignmentRepository assignmentRepository,
        IOrderRepository orderRepository,
        IEmployeeRepository employeeRepository,
        IAbsenceRepository absenceRepository,
        IContractRepository contractRepository,
        IValueListRepository valueListRepository,
        IAssignmentValidationSettingsRepository validationSettingsRepository)
    {
        _assignmentRepository = assignmentRepository;
        _orderRepository = orderRepository;
        _employeeRepository = employeeRepository;
        _absenceRepository = absenceRepository;
        _contractRepository = contractRepository;
        _valueListRepository = valueListRepository;
        _validationSettingsRepository = validationSettingsRepository;
    }

    public async Task<(IReadOnlyList<Assignment> Items, int TotalCount)> GetPagedAsync(
        Guid? orderId = null,
        Guid? employeeId = null,
        DateOnly? fromDate = null,
        DateOnly? toDate = null,
        int page = 1,
        int pageSize = 20,
        CancellationToken cancellationToken = default)
    {
        return await _assignmentRepository.GetPagedAsync(
            orderId, employeeId, fromDate, toDate, page, pageSize, cancellationToken);
    }

    public async Task<Assignment?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
    {
        return await _assignmentRepository.GetByIdAsync(id, cancellationToken);
    }

    public async Task<CreateAssignmentResult> CreateAsync(
        Guid orderId,
        Guid employeeId,
        DateOnly startDate,
        DateOnly endDate,
        string? note,
        CancellationToken cancellationToken = default)
    {
        // Validate Order exists
        var order = await _orderRepository.GetByIdAsync(orderId, cancellationToken);
        if (order is null)
        {
            return new CreateAssignmentResult(false, CreateAssignmentFailureReason.OrderNotFound, null, Array.Empty<AssignmentConflict>());
        }

        // Validate Employee exists
        var employee = await _employeeRepository.GetByIdAsync(employeeId, cancellationToken);
        if (employee is null)
        {
            return new CreateAssignmentResult(false, CreateAssignmentFailureReason.EmployeeNotFound, null, Array.Empty<AssignmentConflict>());
        }

        // Validate date range
        if (endDate < startDate)
        {
            return new CreateAssignmentResult(false, CreateAssignmentFailureReason.InvalidDateRange, null, Array.Empty<AssignmentConflict>());
        }

        // Validate dates are within Order's range
        var orderEndDate = order.EndDate ?? DateOnly.MaxValue;
        if (startDate < order.StartDate || endDate > orderEndDate)
        {
            return new CreateAssignmentResult(false, CreateAssignmentFailureReason.DateOutsideOrderRange, null, Array.Empty<AssignmentConflict>());
        }

        var conflicts = await CheckConflictsAsync(order, employee, startDate, endDate, excludeAssignmentId: null, cancellationToken);
        if (conflicts.Any(c => c.Severity == ValidationSeverity.Error))
        {
            return new CreateAssignmentResult(false, CreateAssignmentFailureReason.ValidationConflict, null, conflicts);
        }

        // Create assignment
        var assignment = new Assignment
        {
            OrderId = orderId,
            EmployeeId = employeeId,
            StartDate = startDate,
            EndDate = endDate,
            Note = note
        };

        await _assignmentRepository.AddAsync(assignment, cancellationToken);
        await _assignmentRepository.SaveChangesAsync(cancellationToken);

        // Reload to include the Employee navigation for the result
        var reloaded = await _assignmentRepository.GetByIdAsync(assignment.Id, cancellationToken);
        return new CreateAssignmentResult(true, CreateAssignmentFailureReason.None, reloaded, conflicts);
    }

    public async Task<IReadOnlyList<AssignmentConflict>> CheckConflictsAsync(
        Guid orderId,
        Guid employeeId,
        DateOnly startDate,
        DateOnly endDate,
        Guid? excludeAssignmentId = null,
        CancellationToken cancellationToken = default)
    {
        if (endDate < startDate)
        {
            return Array.Empty<AssignmentConflict>();
        }

        var order = await _orderRepository.GetByIdAsync(orderId, cancellationToken);
        var employee = await _employeeRepository.GetByIdAsync(employeeId, cancellationToken);
        if (order is null || employee is null)
        {
            return Array.Empty<AssignmentConflict>();
        }

        return await CheckConflictsAsync(order, employee, startDate, endDate, excludeAssignmentId, cancellationToken);
    }

    private async Task<IReadOnlyList<AssignmentConflict>> CheckConflictsAsync(
        Order order,
        Employee employee,
        DateOnly startDate,
        DateOnly endDate,
        Guid? excludeAssignmentId,
        CancellationToken cancellationToken)
    {
        var settings = await _validationSettingsRepository.GetOrCreateAsync(cancellationToken);
        var conflicts = new List<AssignmentConflict>();

        // Qualifikation: Employee.Qualification muss mindestens den Rang von Order.RequiredQualification haben
        // (SortOrder der gemeinsamen ValueList "Qualification", siehe omsorgCore/CLAUDE.md "Konfigurierbare Auswahllisten").
        if (!string.IsNullOrWhiteSpace(order.RequiredQualification))
        {
            var qualificationItems = await _valueListRepository.GetItemsAsync("Qualification", cancellationToken);
            var requiredItem = qualificationItems.FirstOrDefault(i => i.Value == order.RequiredQualification);
            var employeeItem = qualificationItems.FirstOrDefault(i => i.Value == employee.Qualification);

            if (requiredItem is not null && (employeeItem is null || employeeItem.SortOrder < requiredItem.SortOrder))
            {
                conflicts.Add(new AssignmentConflict(
                    AssignmentConflictType.Qualification,
                    settings.QualificationMode,
                    $"Mitarbeiterin erfüllt die benötigte Qualifikation \"{order.RequiredQualification}\" nicht (aktuell: \"{employee.Qualification ?? "keine"}\")."));
            }
        }

        // Abwesenheit (deckt "Verfügbarkeit" ab, siehe Plan): genehmigte Abwesenheit überlappt Zeitraum.
        var blockingAbsenceStatuses = (await _valueListRepository.GetItemsAsync("AbsenceStatus", cancellationToken))
            .Where(i => i.BlocksAssignment)
            .Select(i => i.Value)
            .ToList();

        if (await _absenceRepository.HasOverlappingAbsenceAsync(employee.Id, startDate, endDate, blockingAbsenceStatuses, cancellationToken))
        {
            conflicts.Add(new AssignmentConflict(
                AssignmentConflictType.Absence,
                settings.AbsenceMode,
                "Mitarbeiterin hat im Zuweisungszeitraum eine genehmigte Abwesenheit."));
        }

        // Überschneidung: jetzt auftragsübergreifend geprüft (nicht mehr nur derselbe Auftrag).
        if (await _assignmentRepository.HasOverlapAsync(employee.Id, startDate, endDate, excludeAssignmentId, cancellationToken))
        {
            conflicts.Add(new AssignmentConflict(
                AssignmentConflictType.Overlap,
                settings.OverlapMode,
                "Mitarbeiterin ist im Zuweisungszeitraum bereits einem anderen Auftrag zugewiesen."));
        }

        // Vertrag + Arbeitszeit: beide brauchen den aktiven Vertrag der Mitarbeiterin für den Zeitraum.
        var activeContract = await _contractRepository.GetActiveForEmployeeCoveringRangeAsync(employee.Id, startDate, endDate, cancellationToken);

        if (activeContract is null)
        {
            conflicts.Add(new AssignmentConflict(
                AssignmentConflictType.Contract,
                settings.ContractMode,
                "Mitarbeiterin hat keinen aktiven Vertrag, der den Zuweisungszeitraum abdeckt."));
        }
        else if (activeContract.WeeklyHours.HasValue)
        {
            var weeklyHourCap = activeContract.WeeklyHours.Value;
            var maxDaysPerWeek = (int)Math.Ceiling(weeklyHourCap / AssumedHoursPerDay);

            var current = startDate;
            while (current <= endDate)
            {
                var weekStart = GetIsoWeekStart(current);
                var weekEnd = weekStart.AddDays(6);

                var existingDays = await _assignmentRepository.GetAssignedDayCountInWeekAsync(
                    employee.Id, weekStart, weekEnd, excludeAssignmentId, cancellationToken);

                var newDaysInWeek = CountOverlapDays(startDate, endDate, weekStart, weekEnd);
                var totalDays = existingDays + newDaysInWeek;

                if (totalDays > maxDaysPerWeek)
                {
                    conflicts.Add(new AssignmentConflict(
                        AssignmentConflictType.WorkingHours,
                        settings.WorkingHoursMode,
                        $"Arbeitszeit-Näherung (8h/Tag) überschreitet in der Woche ab {weekStart:yyyy-MM-dd} die Vertrags-Wochenstunden ({weeklyHourCap}h): {totalDays} zugewiesene Tage statt max. {maxDaysPerWeek}."));
                    break;
                }

                current = weekEnd.AddDays(1);
            }
        }

        return conflicts;
    }

    private static DateOnly GetIsoWeekStart(DateOnly date)
    {
        var daysSinceMonday = ((int)date.DayOfWeek + 6) % 7;
        return date.AddDays(-daysSinceMonday);
    }

    private static int CountOverlapDays(DateOnly aStart, DateOnly aEnd, DateOnly bStart, DateOnly bEnd)
    {
        var rangeStart = aStart > bStart ? aStart : bStart;
        var rangeEnd = aEnd < bEnd ? aEnd : bEnd;
        return rangeEnd < rangeStart ? 0 : rangeEnd.DayNumber - rangeStart.DayNumber + 1;
    }

    public async Task<Assignment?> UpdateAsync(
        Guid id,
        string? note,
        CancellationToken cancellationToken = default)
    {
        var assignment = await _assignmentRepository.GetByIdAsync(id, cancellationToken);
        if (assignment is null)
        {
            return null;
        }

        assignment.Note = note;

        await _assignmentRepository.UpdateAsync(assignment, cancellationToken);
        await _assignmentRepository.SaveChangesAsync(cancellationToken);

        return assignment;
    }

    public async Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
    {
        var deleted = await _assignmentRepository.SoftDeleteAsync(id, cancellationToken);
        if (deleted)
        {
            await _assignmentRepository.SaveChangesAsync(cancellationToken);
        }
        return deleted;
    }

    public async Task<IReadOnlyList<Assignment>> GetDeletedAsync(string? search = null, CancellationToken cancellationToken = default)
    {
        return await _assignmentRepository.GetDeletedAsync(search, cancellationToken);
    }

    public async Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
    {
        var restored = await _assignmentRepository.RestoreAsync(id, cancellationToken);
        if (restored)
        {
            await _assignmentRepository.SaveChangesAsync(cancellationToken);
        }
        return restored;
    }

    public Task SaveChangesAsync(CancellationToken cancellationToken = default)
        => _assignmentRepository.SaveChangesAsync(cancellationToken);
}
