<?php

namespace Modules\Transport\Services\Ai;

use App\Models\ActivityLogModel;
use App\Models\TransportAiPlanModel;
use App\Models\TransportAiPlanRouteModel;
use App\Models\TransportAiPlanStopModel;
use App\Models\TransportRouteModel;
use App\Models\TransportScheduleModel;
use App\Models\TransportScheduleStopModel;
use Config\Database;
use Throwable;

/**
 * Orchestrates AI plan generation and the manager approval workflow.
 *
 * The AI only ever produces draft plans. Official transport schedules are
 * created exclusively inside approve(), after a manager with the
 * transport_ai.approve permission (and TPIN) signs off.
 */
class TransportAiPlanService
{
    protected TransportAiPlanModel $plans;
    protected TransportAiPlanRouteModel $planRoutes;
    protected TransportAiPlanStopModel $planStops;
    protected TransportScheduleModel $schedules;
    protected TransportScheduleStopModel $scheduleStops;
    protected TransportRouteModel $routes;
    protected ActivityLogModel $activity;

    public function __construct()
    {
        $this->plans      = new TransportAiPlanModel();
        $this->planRoutes = new TransportAiPlanRouteModel();
        $this->planStops  = new TransportAiPlanStopModel();
        $this->schedules     = new TransportScheduleModel();
        $this->scheduleStops = new TransportScheduleStopModel();
        $this->routes        = new TransportRouteModel();
        $this->activity   = new ActivityLogModel();
    }

    /**
     * Run the full pipeline: optimize -> schedule -> validate -> summarize,
     * and persist the result as a draft plan.
     *
     * @param int[]|null $contractIds
     */
    public function generate(string $startDate, string $endDate, ?array $contractIds, ?int $actingUserId, ?string $title = null, int $batchLimit = 40): array
    {
        if (! strtotime($startDate) || ! strtotime($endDate)) {
            return ['success' => false, 'errors' => ['plan_start_date' => 'Provide a valid date range.']];
        }
        if ($endDate < $startDate) {
            return ['success' => false, 'errors' => ['plan_end_date' => 'End date must not be before the start date.']];
        }
        if ((strtotime($endDate) - strtotime($startDate)) / 86400 > 31) {
            return ['success' => false, 'errors' => ['plan_end_date' => 'Plans are limited to 31 days per run.']];
        }

        try {
            $optimizer = new RouteOptimizerService();
            $optimized = $optimizer->optimize($startDate, $endDate, $contractIds, $batchLimit);

            $scheduler = new SchedulingAssistantService($optimizer->getSettings());
            $scheduled = $scheduler->schedule($optimized['routes']);

            $routes = $scheduled['routes'];

            // Optimizer data-input issues become warning-level conflicts so the
            // manager sees everything in one panel.
            $conflicts = array_merge(
                array_map(static fn (array $issue): array => [
                    'severity'        => 'warning',
                    'type'            => $issue['type'],
                    'service_date'    => $issue['date'] ?? null,
                    'route_name'      => null,
                    'message'         => $issue['message'],
                    'recommendations' => [],
                ], $optimized['issues']),
                $scheduled['conflicts']
            );

            if ($routes === []) {
                return [
                    'success' => false,
                    'errors'  => ['generate' => 'No routes could be generated. ' . implode(' ', array_column($conflicts, 'message'))],
                ];
            }

            $totals = $this->totals($routes);

            $planData = [
                'number'                 => $this->plans->nextNumber(),
                'title'                  => $title !== null && trim($title) !== ''
                    ? trim($title)
                    : sprintf('Transportation plan %s to %s', $startDate, $endDate),
                'plan_start_date'        => $startDate,
                'plan_end_date'          => $endDate,
                'status'                 => 'draft',
                'total_routes'           => $totals['routes'],
                'total_employees'        => $totals['employees'],
                'total_distance_km'      => $totals['distance_km'],
                'total_duration_minutes' => $totals['duration_minutes'],
                'total_fuel_litres'      => $totals['fuel_litres'],
                'total_cost'             => $totals['cost'],
                'conflicts_json'         => json_encode($conflicts, JSON_UNESCAPED_UNICODE),
                'fleet_summary_json'     => json_encode([
                    'inputs'           => $optimized['inputs'],
                    'driver_workloads' => $scheduled['driver_workloads'],
                    'vehicles_used'    => count(array_unique(array_filter(array_column($routes, 'vehicle_id')))),
                    'drivers_used'     => count(array_unique(array_filter(array_column($routes, 'driver_id')))),
                ], JSON_UNESCAPED_UNICODE),
                'generated_by'           => $actingUserId,
                'created_by'             => $actingUserId,
                'updated_by'             => $actingUserId,
            ];

            $summaryService = new AiSummaryService($optimizer->getSettings());
            $planData['optimization_summary'] = $summaryService->summarize($planData, $routes, $conflicts);

            $db = Database::connect();
            $db->transStart();

            $this->plans->insert($planData, true);
            $planId = (int) $this->plans->getInsertID();

            foreach ($routes as $route) {
                $this->planRoutes->insert([
                    'plan_id'                    => $planId,
                    'service_date'               => $route['service_date'],
                    'route_name'                 => $route['route_name'],
                    'contract_id'                => $route['contract_id'],
                    'shift_id'                   => $route['shift_id'],
                    'origin_location_id'         => $route['origin_location_id'],
                    'destination_location_id'    => $route['destination_location_id'],
                    'vehicle_id'                 => $route['vehicle_id'],
                    'driver_id'                  => $route['driver_id'],
                    'employee_count'             => $route['employee_count'],
                    'total_distance_km'          => $route['total_distance_km'],
                    'estimated_duration_minutes' => $route['estimated_duration_minutes'],
                    'estimated_fuel_litres'      => $route['estimated_fuel_litres'],
                    'estimated_cost'             => $route['estimated_cost'],
                    'departure_time'             => $route['departure_time'],
                    'arrival_time'               => $route['arrival_time'],
                    'shift_start_time'           => $route['shift_start_time'],
                    'shift_end_time'             => $route['shift_end_time'],
                    'return_departure_time'      => $route['return_departure_time'],
                    'return_arrival_time'        => $route['return_arrival_time'],
                    'route_geometry'             => $route['route_geometry'] !== null ? json_encode($route['route_geometry']) : null,
                    'optimization_notes'         => $route['optimization_notes'],
                ], true);
                $planRouteId = (int) $this->planRoutes->getInsertID();

                foreach ($route['stops'] as $order => $stop) {
                    $this->planStops->insert([
                        'plan_route_id'        => $planRouteId,
                        'stop_order'           => $order + 1,
                        'employee_id'          => $stop['employee_id'],
                        'latitude'             => $stop['latitude'],
                        'longitude'            => $stop['longitude'],
                        'address'              => $stop['address'],
                        'pickup_time'          => $stop['pickup_time'],
                        'leg_distance_km'      => $stop['leg_distance_km'],
                        'leg_duration_minutes' => $stop['leg_duration_minutes'],
                    ]);
                }
            }

            $db->transComplete();

            if (! $db->transStatus()) {
                return ['success' => false, 'errors' => ['generate' => 'Failed to store the generated plan.']];
            }

            $this->activity->log('transport_ai', 'generated', $actingUserId, "Generated AI transport plan #{$planId} ({$planData['number']}) with {$totals['routes']} routes", (string) $planId);

            return ['success' => true, 'id' => $planId];
        } catch (Throwable $e) {
            log_message('error', 'Transport AI: plan generation failed: ' . $e->getMessage() . "\n" . $e->getTraceAsString());

            return ['success' => false, 'errors' => ['generate' => 'Plan generation failed: ' . $e->getMessage()]];
        }
    }

    public function submitForApproval(int $id, ?int $actingUserId): array
    {
        $plan = $this->plans->find($id);
        if (! $plan) {
            return ['success' => false, 'error' => 'Plan not found.'];
        }
        if ($plan['status'] !== 'draft') {
            return ['success' => false, 'error' => 'Only draft plans can be submitted for approval.'];
        }

        $this->plans->update($id, [
            'status'           => 'pending_approval',
            'rejection_reason' => null,
            'updated_by'       => $actingUserId,
        ]);
        $this->activity->log('transport_ai', 'submitted', $actingUserId, "Submitted AI transport plan #{$id} for approval", (string) $id);

        return ['success' => true];
    }

    /**
     * Manager approval: the only path that publishes AI output to the official
     * transport schedule.
     */
    public function approve(int $id, ?int $actingUserId): array
    {
        $plan = $this->plans->find($id);
        if (! $plan) {
            return ['success' => false, 'error' => 'Plan not found.'];
        }
        if ($plan['status'] !== 'pending_approval') {
            return ['success' => false, 'error' => 'Only plans pending approval can be approved.'];
        }

        $routes = $this->planRoutes->getForPlan($id);
        $stops  = $this->planStops->getForPlanGrouped($id);

        $db = Database::connect();
        $db->transStart();

        $published = 0;
        foreach ($routes as $route) {
            // Persist the optimized route as a reusable transport route record.
            $routeId = null;
            if ($route['origin_location_id'] || $route['destination_location_id']) {
                $this->routes->insert([
                    'number'                     => $this->routes->nextNumber(),
                    'name'                       => $route['route_name'],
                    'origin_location_id'         => $route['origin_location_id'],
                    'destination_location_id'    => $route['destination_location_id'],
                    'distance_km'                => $route['total_distance_km'],
                    'estimated_duration_minutes' => $route['estimated_duration_minutes'],
                    'route_geometry'             => $route['route_geometry'],
                    'status'                     => 'active',
                    'notes'                      => 'Generated by AI transport plan ' . $plan['number'],
                    'created_by'                 => $actingUserId,
                    'updated_by'                 => $actingUserId,
                ], true);
                $routeId = (int) $this->routes->getInsertID();
            }

            $routeStops = $stops[(int) $route['id']] ?? [];

            // Red change indicator on the scheduling screen:
            // driver already had trips that day (their schedule changed),
            // otherwise "new". Cleared once seen.
            $aiFlag = 'new';
            if (! empty($route['driver_id'])) {
                $existingTrips = $this->schedules
                    ->where('driver_id', (int) $route['driver_id'])
                    ->where('schedule_date', $route['service_date'])
                    ->whereIn('status', TransportScheduleModel::OPEN)
                    ->countAllResults();
                if ($existingTrips > 0) {
                    $aiFlag = 'updated';
                }
            }

            $this->schedules->insert([
                'number'                 => $this->schedules->nextNumber(),
                'title'                  => $route['route_name'],
                'route_id'               => $routeId,
                'vehicle_id'             => $route['vehicle_id'],
                'driver_id'              => $route['driver_id'],
                'schedule_date'          => $route['service_date'],
                'departure_time'         => $route['departure_time'],
                'arrival_time'           => $route['arrival_time'],
                'shift_start_time'       => $route['shift_start_time'] ?? null,
                'shift_end_time'         => $route['shift_end_time'] ?? null,
                'return_departure_time'  => $route['return_departure_time'] ?? null,
                'return_arrival_time'    => $route['return_arrival_time'] ?? null,
                'priority'               => 'normal',
                'status'                 => 'planned',
                'ai_flag'                => $aiFlag,
                'notes'                  => "From AI plan {$plan['number']}.",
                'created_by'             => $actingUserId,
                'updated_by'             => $actingUserId,
            ], true);
            $scheduleId = (int) $this->schedules->getInsertID();

            foreach ($routeStops as $stop) {
                $this->scheduleStops->insert([
                    'schedule_id'          => $scheduleId,
                    'stop_order'           => (int) $stop['stop_order'],
                    'employee_id'          => (int) $stop['employee_id'],
                    'latitude'             => $stop['latitude'],
                    'longitude'            => $stop['longitude'],
                    'address'              => $stop['address'],
                    'pickup_time'          => $stop['pickup_time'],
                    'leg_distance_km'      => $stop['leg_distance_km'],
                    'leg_duration_minutes' => $stop['leg_duration_minutes'],
                ]);
            }

            $this->planRoutes->update((int) $route['id'], ['published_schedule_id' => $scheduleId]);
            $published++;
        }

        $this->plans->update($id, [
            'status'      => 'approved',
            'approved_by' => $actingUserId,
            'approved_at' => date('Y-m-d H:i:s'),
            'updated_by'  => $actingUserId,
        ]);

        $db->transComplete();

        if (! $db->transStatus()) {
            return ['success' => false, 'error' => 'Failed to publish the approved plan.'];
        }

        $this->activity->log('transport_ai', 'approved', $actingUserId, "Approved AI transport plan #{$id}; published {$published} schedules", (string) $id);

        return ['success' => true, 'published' => $published];
    }

    public function reject(int $id, string $reason, ?int $actingUserId): array
    {
        $plan = $this->plans->find($id);
        if (! $plan) {
            return ['success' => false, 'error' => 'Plan not found.'];
        }
        if ($plan['status'] !== 'pending_approval') {
            return ['success' => false, 'error' => 'Only plans pending approval can be rejected.'];
        }

        $reason = trim($reason);
        if ($reason === '') {
            return ['success' => false, 'error' => 'A rejection reason is required.'];
        }

        $this->plans->update($id, [
            'status'           => 'rejected',
            'rejection_reason' => $reason,
            'updated_by'       => $actingUserId,
        ]);
        $this->activity->log('transport_ai', 'rejected', $actingUserId, "Rejected AI transport plan #{$id}: {$reason}", (string) $id);

        return ['success' => true];
    }

    public function delete(int $id, ?int $actingUserId): array
    {
        $plan = $this->plans->find($id);
        if (! $plan) {
            return ['success' => false, 'error' => 'Plan not found.'];
        }
        if (! in_array($plan['status'], ['draft', 'rejected'], true)) {
            return ['success' => false, 'error' => 'Only draft or rejected plans can be deleted.'];
        }

        $routeIds = array_column($this->planRoutes->where('plan_id', $id)->findAll(), 'id');

        $db = Database::connect();
        $db->transStart();
        $this->planStops->deleteForPlanRoutes($routeIds);
        $this->planRoutes->deleteForPlan($id);
        $this->plans->delete($id);
        $db->transComplete();

        $this->activity->log('transport_ai', 'deleted', $actingUserId, "Deleted AI transport plan #{$id}", (string) $id);

        return ['success' => true];
    }

    protected function totals(array $routes): array
    {
        $totals = ['routes' => count($routes), 'employees' => 0, 'distance_km' => 0.0, 'duration_minutes' => 0, 'fuel_litres' => 0.0, 'cost' => 0.0];
        foreach ($routes as $route) {
            $totals['employees']        += (int) $route['employee_count'];
            $totals['distance_km']      += (float) $route['total_distance_km'];
            $totals['duration_minutes'] += (int) $route['estimated_duration_minutes'];
            $totals['fuel_litres']      += (float) $route['estimated_fuel_litres'];
            $totals['cost']             += (float) $route['estimated_cost'];
        }
        $totals['distance_km'] = round($totals['distance_km'], 2);
        $totals['fuel_litres'] = round($totals['fuel_litres'], 2);
        $totals['cost']        = round($totals['cost'], 3);

        return $totals;
    }
}
