<?php

namespace Modules\Transport\Services\Ai;

use App\Models\TransportAiSettingsModel;
use App\Models\TransportDriverModel;
use App\Models\TransportScheduleModel;
use App\Models\TransportVehicleModel;

/**
 * AI Scheduling Assistant.
 *
 * Takes the optimized route candidates and assigns the best available driver
 * to each one, balancing workloads and avoiding conflicts with existing
 * transport schedules. Every schedule is validated before being presented;
 * violations become structured conflict entries with ranked replacement
 * driver/vehicle recommendations.
 */
class SchedulingAssistantService
{
    /**
     * Vehicle type => license class tokens that qualify (case-insensitive).
     * Types without an entry accept any valid license.
     */
    protected const LICENSE_REQUIREMENTS = [
        'bus'        => ['d', 'd1', 'de', 'bus', 'heavy'],
        'truck'      => ['c', 'c1', 'ce', 'truck', 'heavy'],
        'motorcycle' => ['a', 'a1', 'm', 'motorcycle'],
    ];

    protected TransportDriverModel $drivers;
    protected TransportVehicleModel $vehicles;
    protected TransportScheduleModel $schedules;
    protected array $settings;

    public function __construct(?array $settings = null)
    {
        $this->drivers   = new TransportDriverModel();
        $this->vehicles  = new TransportVehicleModel();
        $this->schedules = new TransportScheduleModel();
        $this->settings  = $settings ?? (new TransportAiSettingsModel())->getSettings();
    }

    /**
     * Assign drivers to route candidates and validate the whole schedule.
     *
     * @param array $routes Route candidates from RouteOptimizerService.
     *
     * @return array{routes: array, conflicts: array, driver_workloads: array}
     */
    public function schedule(array $routes): array
    {
        $conflicts = [];

        $drivers = $this->drivers->scopeForList(null, 'active')->findAll();
        $fleet   = $this->vehicles->where('status', 'active')->where('capacity >', 0)->findAll();
        $fleetById = [];
        foreach ($fleet as $vehicle) {
            $fleetById[(int) $vehicle['id']] = $vehicle;
        }

        $dates = array_values(array_unique(array_column($routes, 'service_date')));
        $existing = $this->loadExistingSchedules($dates);

        // Chronological assignment keeps workload balancing meaningful.
        usort($routes, static function (array $a, array $b): int {
            return [$a['service_date'], $a['departure_min']] <=> [$b['service_date'], $b['departure_min']];
        });

        $driverWindows  = []; // [date][driver_id] => [[from,to],...]
        $driverDaily    = []; // [date][driver_id] => minutes
        $driverWeekly   = []; // [driver_id] => minutes
        $vehicleWindows = []; // [date][vehicle_id] => [[from,to],...]
        $maxDaily       = (int) $this->settings['driver_max_daily_minutes'];

        foreach ($routes as &$route) {
            $date = $route['service_date'];
            $busyFrom = (int) $route['departure_min'];
            $busyTo   = (int) $route['return_arrival_min'];
            $workMinutes = $this->routeWorkMinutes($route);

            // --- Vehicle re-check against existing official schedules.
            if ($route['vehicle_id'] !== null) {
                $vehicleId = (int) $route['vehicle_id'];
                $existingWindows = $existing['vehicles'][$date][$vehicleId] ?? [];
                $planWindows     = $vehicleWindows[$date][$vehicleId] ?? [];

                if ($this->windowsOverlap(array_merge($existingWindows, $planWindows), $busyFrom, $busyTo)) {
                    $replacement  = $this->recommendVehicles($route, $fleet, $vehicleWindows, $existing);
                    $originalName = $route['vehicle']['registration_number'] ?? ('#' . $vehicleId);

                    if ($replacement['vehicle_id'] !== null) {
                        $newVehicle = $fleetById[$replacement['vehicle_id']];
                        $conflicts[] = $this->conflict('warning', 'vehicle_replaced', $route, sprintf(
                            'Vehicle %s was already booked in this time window; replaced with %s (%s, capacity %d).',
                            $originalName,
                            $newVehicle['registration_number'],
                            $newVehicle['vehicle_type'],
                            (int) $newVehicle['capacity']
                        ), []);
                        $route['optimization_notes'] = trim(($route['optimization_notes'] ?? '') . sprintf(
                            ' Scheduler replaced %s with %s due to a booking overlap.',
                            $originalName,
                            $newVehicle['registration_number']
                        ));
                        $route['vehicle_id'] = $replacement['vehicle_id'];
                        $route['vehicle']    = $newVehicle;
                    } else {
                        $conflicts[] = $this->conflict('error', 'vehicle_overlap', $route, sprintf(
                            'Vehicle %s is already booked in this time window and no replacement is free.',
                            $originalName
                        ), $replacement['messages']);
                        $route['vehicle_id'] = null;
                        $route['vehicle']    = null;
                    }
                }
            }

            if ($route['vehicle_id'] === null) {
                $replacement = $this->recommendVehicles($route, $fleet, $vehicleWindows, $existing);
                if ($replacement['vehicle_id'] !== null) {
                    $route['vehicle_id'] = $replacement['vehicle_id'];
                    $route['vehicle']    = $fleetById[$replacement['vehicle_id']];
                } else {
                    $conflicts[] = $this->conflict('error', 'no_vehicle', $route, sprintf(
                        'No available vehicle with capacity for %d passengers in this time window.',
                        $route['employee_count']
                    ), $replacement['messages']);
                }
            }

            if ($route['vehicle_id'] !== null) {
                $vehicleWindows[$date][(int) $route['vehicle_id']][] = [$busyFrom, $busyTo];
            }

            // --- Driver assignment.
            $assignment = $this->pickDriver($route, $drivers, $existing, $driverWindows, $driverDaily, $driverWeekly, $maxDaily);

            if ($assignment['driver'] !== null) {
                $driver = $assignment['driver'];
                $driverId = (int) $driver['id'];
                $route['driver_id']   = $driverId;
                $route['driver_name'] = $driver['employee_name'];
                $route['driver']      = $driver;

                $driverWindows[$date][$driverId][] = [$busyFrom, $busyTo];
                $driverDaily[$date][$driverId] = ($driverDaily[$date][$driverId] ?? 0) + $workMinutes;
                $driverWeekly[$driverId]       = ($driverWeekly[$driverId] ?? 0) + $workMinutes;
            } else {
                $route['driver_id']   = null;
                $route['driver_name'] = null;
                $route['driver']      = null;
                $conflicts[] = $this->conflict('error', 'no_driver', $route, 'No available driver qualifies for this route.', $assignment['recommendations']);
            }
        }
        unset($route);

        // --- Final validation pass over the assembled schedule.
        foreach ($this->validate($routes, $existing, $maxDaily) as $conflict) {
            $conflicts[] = $conflict;
        }

        return [
            'routes'           => $routes,
            'conflicts'        => $conflicts,
            'driver_workloads' => $this->summarizeWorkloads($routes),
        ];
    }

    // ------------------------------------------------------------------
    // Driver selection
    // ------------------------------------------------------------------

    /**
     * @return array{driver: ?array, recommendations: string[]}
     */
    protected function pickDriver(array $route, array $drivers, array $existing, array $driverWindows, array $driverDaily, array $driverWeekly, int $maxDaily): array
    {
        $date        = $route['service_date'];
        $busyFrom    = (int) $route['departure_min'];
        $busyTo      = (int) $route['return_arrival_min'];
        $workMinutes = $this->routeWorkMinutes($route);
        $vehicleType = $route['vehicle']['vehicle_type'] ?? null;

        $eligible = [];
        $blocked  = [];

        foreach ($drivers as $driver) {
            $driverId = (int) $driver['id'];
            $name     = $driver['employee_name'] ?? $driver['number'];

            if (! empty($driver['license_expiry']) && $driver['license_expiry'] < $date) {
                $blocked[] = "{$name}: license expired {$driver['license_expiry']}.";

                continue;
            }

            if ($vehicleType !== null && ! $this->licenseMatchesVehicle($driver['license_class'] ?? null, $vehicleType)) {
                $blocked[] = "{$name}: license class '" . ($driver['license_class'] ?: 'none') . "' does not qualify for a {$vehicleType}.";

                continue;
            }

            $windows = array_merge(
                $existing['drivers'][$date][$driverId] ?? [],
                $driverWindows[$date][$driverId] ?? []
            );
            if ($this->windowsOverlap($windows, $busyFrom, $busyTo)) {
                $blocked[] = "{$name}: already scheduled in this time window.";

                continue;
            }

            $daily = ($driverDaily[$date][$driverId] ?? 0) + ($existing['driver_minutes'][$date][$driverId] ?? 0);
            if ($daily + $workMinutes > $maxDaily) {
                $blocked[] = "{$name}: would exceed the daily working limit of {$maxDaily} minutes.";

                continue;
            }

            $eligible[] = [
                'driver' => $driver,
                'weekly' => $driverWeekly[$driverId] ?? 0,
                'daily'  => $daily,
            ];
        }

        if ($eligible === []) {
            return ['driver' => null, 'recommendations' => array_slice($blocked, 0, 5)];
        }

        // Workload balancing: least weekly minutes first, then least daily.
        usort($eligible, static fn (array $a, array $b): int => [$a['weekly'], $a['daily']] <=> [$b['weekly'], $b['daily']]);

        return ['driver' => $eligible[0]['driver'], 'recommendations' => []];
    }

    public function licenseMatchesVehicle(?string $licenseClass, string $vehicleType): bool
    {
        $required = self::LICENSE_REQUIREMENTS[$vehicleType] ?? null;
        if ($required === null) {
            return true; // cars, vans, other: any valid license
        }

        $class = strtolower(trim((string) $licenseClass));
        if ($class === '') {
            return false;
        }

        foreach ($required as $token) {
            if (str_contains($class, $token)) {
                return true;
            }
        }

        return false;
    }

    // ------------------------------------------------------------------
    // Vehicle replacement recommendations
    // ------------------------------------------------------------------

    /**
     * @return array{vehicle_id: ?int, messages: string[]}
     */
    protected function recommendVehicles(array $route, array $fleet, array $vehicleWindows, array $existing): array
    {
        $date     = $route['service_date'];
        $busyFrom = (int) $route['departure_min'];
        $busyTo   = (int) $route['return_arrival_min'];
        $needed   = (int) $route['employee_count'];

        $candidates = [];
        $messages   = [];

        foreach ($fleet as $vehicle) {
            if ((int) $vehicle['capacity'] < $needed) {
                continue;
            }
            $vehicleId = (int) $vehicle['id'];
            $windows = array_merge(
                $existing['vehicles'][$date][$vehicleId] ?? [],
                $vehicleWindows[$date][$vehicleId] ?? []
            );
            if ($this->windowsOverlap($windows, $busyFrom, $busyTo)) {
                continue;
            }
            $candidates[] = $vehicle;
        }

        // Best fit: least spare capacity first.
        usort($candidates, static fn (array $a, array $b): int => (int) $a['capacity'] <=> (int) $b['capacity']);

        foreach (array_slice($candidates, 0, 3) as $candidate) {
            $messages[] = sprintf(
                'Replacement vehicle available: %s (%s, capacity %d).',
                $candidate['registration_number'],
                $candidate['vehicle_type'],
                (int) $candidate['capacity']
            );
        }

        if ($candidates === []) {
            $maintenance = $this->vehicles->where('status', 'maintenance')->where('capacity >=', $needed)->findAll(3);
            foreach ($maintenance as $vehicle) {
                $messages[] = sprintf(
                    'Vehicle %s (capacity %d) fits but is under maintenance; expediting its service would resolve this.',
                    $vehicle['registration_number'],
                    (int) $vehicle['capacity']
                );
            }
        }

        return [
            'vehicle_id' => $candidates !== [] ? (int) $candidates[0]['id'] : null,
            'messages'   => $messages,
        ];
    }

    // ------------------------------------------------------------------
    // Validation
    // ------------------------------------------------------------------

    /**
     * Full rule check over the assembled schedule. Assignment already avoids
     * most violations, so anything caught here is surfaced as a conflict.
     */
    protected function validate(array $routes, array $existing, int $maxDaily): array
    {
        $conflicts = [];
        $driverWindows  = [];
        $vehicleWindows = [];
        $driverDaily    = [];

        foreach ($routes as $route) {
            $date = $route['service_date'];
            $from = (int) $route['departure_min'];
            $to   = (int) $route['return_arrival_min'];

            // Employees must arrive before their shift begins.
            if ((int) $route['arrival_min'] >= $this->timeToMinutes($route['shift_start_time'])) {
                $conflicts[] = $this->conflict('error', 'late_arrival', $route, sprintf(
                    'Arrival %s is not before the %s shift start.',
                    substr($route['arrival_time'], 0, 5),
                    substr($route['shift_start_time'], 0, 5)
                ), ['Move the departure earlier or use a closer depot/vehicle start point.']);
            }

            // Vehicle rules.
            if ($route['vehicle'] !== null) {
                $vehicle = $route['vehicle'];
                if (($vehicle['status'] ?? 'active') === 'maintenance') {
                    $conflicts[] = $this->conflict('error', 'vehicle_maintenance', $route, sprintf('Vehicle %s is under maintenance.', $vehicle['registration_number']), []);
                }
                if ((int) $vehicle['capacity'] < (int) $route['employee_count']) {
                    $conflicts[] = $this->conflict('error', 'capacity_exceeded', $route, sprintf(
                        'Vehicle capacity %d is below the %d passengers on this route.',
                        (int) $vehicle['capacity'],
                        (int) $route['employee_count']
                    ), []);
                }

                $vehicleId = (int) $route['vehicle_id'];
                if ($this->windowsOverlap($vehicleWindows[$date][$vehicleId] ?? [], $from, $to)) {
                    $conflicts[] = $this->conflict('error', 'vehicle_overlap', $route, sprintf('Vehicle %s has overlapping plan routes.', $vehicle['registration_number']), []);
                }
                $vehicleWindows[$date][$vehicleId][] = [$from, $to];
            }

            // Driver rules.
            if ($route['driver'] !== null) {
                $driver   = $route['driver'];
                $driverId = (int) $route['driver_id'];
                $name     = $route['driver_name'] ?? $driver['number'];

                if (! empty($driver['license_expiry']) && $driver['license_expiry'] < $date) {
                    $conflicts[] = $this->conflict('error', 'license_expired', $route, sprintf('%s\'s license expired on %s.', $name, $driver['license_expiry']), []);
                }

                if ($route['vehicle'] !== null && ! $this->licenseMatchesVehicle($driver['license_class'] ?? null, $route['vehicle']['vehicle_type'])) {
                    $conflicts[] = $this->conflict('error', 'license_mismatch', $route, sprintf(
                        '%s\'s license class (%s) does not match the assigned %s.',
                        $name,
                        $driver['license_class'] ?: 'none',
                        $route['vehicle']['vehicle_type']
                    ), []);
                }

                $allWindows = array_merge($existing['drivers'][$date][$driverId] ?? [], $driverWindows[$date][$driverId] ?? []);
                if ($this->windowsOverlap($allWindows, $from, $to)) {
                    $conflicts[] = $this->conflict('error', 'driver_overlap', $route, sprintf('%s has overlapping schedules.', $name), []);
                }
                $driverWindows[$date][$driverId][] = [$from, $to];

                $driverDaily[$date][$driverId] = ($driverDaily[$date][$driverId] ?? 0) + $this->routeWorkMinutes($route);
                if ($driverDaily[$date][$driverId] > $maxDaily) {
                    $conflicts[] = $this->conflict('warning', 'overtime_risk', $route, sprintf(
                        '%s reaches %d working minutes on %s (limit %d).',
                        $name,
                        $driverDaily[$date][$driverId],
                        $date,
                        $maxDaily
                    ), ['Reassign one of this driver\'s routes to a driver with spare hours.']);
                }
            }
        }

        return $conflicts;
    }

    // ------------------------------------------------------------------
    // Helpers
    // ------------------------------------------------------------------

    /**
     * Existing open official schedules per date, as busy windows per driver
     * and vehicle plus accumulated driver minutes.
     */
    protected function loadExistingSchedules(array $dates): array
    {
        $result = ['drivers' => [], 'vehicles' => [], 'driver_minutes' => []];
        if ($dates === []) {
            return $result;
        }

        $rows = $this->schedules
            ->whereIn('schedule_date', $dates)
            ->whereIn('status', TransportScheduleModel::OPEN)
            ->findAll();

        foreach ($rows as $row) {
            if (empty($row['departure_time'])) {
                continue; // no time information to conflict on
            }
            $from = $this->timeToMinutes($row['departure_time']);
            $to   = ! empty($row['arrival_time'])
                ? $this->timeToMinutes($row['arrival_time'])
                : $from + 120; // assume two hours when open-ended
            if ($to <= $from) {
                $to += 1440;
            }

            $date = $row['schedule_date'];
            if (! empty($row['driver_id'])) {
                $result['drivers'][$date][(int) $row['driver_id']][] = [$from, $to];
                $result['driver_minutes'][$date][(int) $row['driver_id']] =
                    ($result['driver_minutes'][$date][(int) $row['driver_id']] ?? 0) + ($to - $from);
            }
            if (! empty($row['vehicle_id'])) {
                $result['vehicles'][$date][(int) $row['vehicle_id']][] = [$from, $to];
            }
        }

        return $result;
    }

    /**
     * Driver working minutes for a route: outbound trip + return trip.
     */
    protected function routeWorkMinutes(array $route): int
    {
        $outbound = (int) $route['arrival_min'] - (int) $route['departure_min'];
        $return   = (int) $route['return_arrival_min'] - (int) $route['return_departure_min'];

        return max(0, $outbound) + max(0, $return);
    }

    protected function summarizeWorkloads(array $routes): array
    {
        $workloads = [];
        foreach ($routes as $route) {
            if ($route['driver_id'] === null) {
                continue;
            }
            $key = (int) $route['driver_id'];
            $workloads[$key]['driver_id']   = $key;
            $workloads[$key]['driver_name'] = $route['driver_name'];
            $workloads[$key]['routes']      = ($workloads[$key]['routes'] ?? 0) + 1;
            $workloads[$key]['minutes']     = ($workloads[$key]['minutes'] ?? 0) + $this->routeWorkMinutes($route);
            $workloads[$key]['dates'][$route['service_date']] = true;
        }

        foreach ($workloads as &$workload) {
            $workload['days'] = count($workload['dates']);
            unset($workload['dates']);
        }

        return array_values($workloads);
    }

    protected function conflict(string $severity, string $type, array $route, string $message, array $recommendations): array
    {
        return [
            'severity'        => $severity,
            'type'            => $type,
            'service_date'    => $route['service_date'],
            'route_name'      => $route['route_name'],
            'message'         => $message,
            'recommendations' => $recommendations,
        ];
    }

    /**
     * @param array<int, array{0: int, 1: int}> $windows
     */
    protected function windowsOverlap(array $windows, int $from, int $to): bool
    {
        foreach ($windows as [$busyFrom, $busyTo]) {
            if ($from < $busyTo && $to > $busyFrom) {
                return true;
            }
        }

        return false;
    }

    protected function timeToMinutes(string $time): int
    {
        $parts = explode(':', $time);

        return ((int) $parts[0]) * 60 + (int) ($parts[1] ?? 0);
    }
}
