<?php

namespace App\Controller;

use App\Entity\CoastalPoint;
use App\Service\Forecast\ImpactScoreService;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/api/spots', name: 'api_spots_')]
class SpotScoreController extends AbstractController
{
    public function __construct(
        private EntityManagerInterface $em,
        private ImpactScoreService     $scoreService,
        private Connection             $connection,
    ) {}

    /**
     * GET /api/spots/overview
     *
     * Retourne tous les spots avec leur dernier score en UNE seule requête.
     * Remplace les N appels individuels à /api/spots/{id}/score pour le rendu
     * initial de la carte.
     *
     * Cache HTTP 30 min (scores mis à jour au max 1-2×/jour par ingestion).
     */
    // priority: 10 — empêche API Platform GET /api/spots/{id} de capturer /overview
    #[Route('/overview', name: 'overview', methods: ['GET'], priority: 10)]
    public function overview(): JsonResponse
    {
        $rows = $this->connection->fetchAllAssociative(
            "SELECT
                cp.id::text,
                cp.name,
                cp.type,
                cp.region,
                ST_X(cp.geometry::geometry)  AS longitude,
                ST_Y(cp.geometry::geometry)  AS latitude,
                ist.score,
                ist.level,
                ROUND(CAST(ist.distance_to_nearest_sargassum AS numeric), 2) AS distance_km
             FROM coastal_point cp
             LEFT JOIN LATERAL (
                 SELECT score, level, distance_to_nearest_sargassum
                 FROM impact_score
                 WHERE coastal_point_id = cp.id
                 ORDER BY timestamp DESC
                 LIMIT 1
             ) ist ON true
             WHERE cp.region = 'Guadeloupe'
             ORDER BY cp.name"
        );

        $response = $this->json($rows);
        $response->headers->set('Cache-Control', 'public, max-age=1800, stale-while-revalidate=300');

        return $response;
    }

    /**
     * GET /api/spots/{id}/score
     *
     * Retourne le score courant + les scores par horizon (H+6/12/24/48).
     * Utilise le cache Redis (TTL 3h).
     * Paramètre optionnel : ?at=YYYY-MM-DDTHH:MM:SSZ
     */
    #[Route('/{id}/score', name: 'score', methods: ['GET'])]
    public function score(string $id, Request $request): JsonResponse
    {
        $spot = $this->em->getRepository(CoastalPoint::class)->find($id);
        if ($spot === null) {
            return $this->json(['error' => 'Spot not found'], 404);
        }

        $atParam = $request->query->get('at');

        // Si ?at= spécifié, on ne peut pas utiliser le cache
        if ($atParam !== null) {
            try {
                $at = new \DateTimeImmutable($atParam);
            } catch (\Exception) {
                return $this->json(['error' => 'Invalid "at" parameter format'], 400);
            }
            $current = $this->loadScoreAt($id, $at);
        } else {
            $current = $this->scoreService->getCachedScore($id);
        }

        $patches = $this->loadPatches($id);

        // La distance issue des patches (ST_Dump live) est plus fiable que
        // distance_to_nearest_sargassum stockée (peut venir d'une obs lointaine).
        // On l'utilise comme référence si des patches sont détectés à proximité.
        if ($current !== null && !empty($patches)) {
            $minPatchDist = min(array_column($patches, 'distanceKm'));
            $current['distance'] = $minPatchDist;
        }

        $currentDistanceKm = $current !== null ? $current['distance'] : null;

        return $this->json([
            'spotId'   => (string) $spot->getId(),
            'name'     => $spot->getName(),
            'type'     => $spot->getType(),
            'region'   => $spot->getRegion(),
            'score'    => $current,
            'patches'  => $patches,
            'horizons' => $this->loadHorizonScores($id, $currentDistanceKm),
        ]);
    }

    // -------------------------------------------------------------------------

    /**
     * Score à un instant précis (sans cache).
     *
     * @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, etaHours: int|null, computedAt: string}|null
     */
    private function loadScoreAt(string $spotId, \DateTimeImmutable $at): ?array
    {
        $row = $this->connection->fetchAssociative(
            'SELECT score, level, distance_to_nearest_sargassum AS distance,
                    density_estimate AS density, trend, eta_hours, timestamp AS computed_at
             FROM impact_score
             WHERE coastal_point_id = :spotId AND timestamp <= :at
             ORDER BY timestamp DESC LIMIT 1',
            ['spotId' => $spotId, 'at' => $at->format('Y-m-d H:i:s')]
        );

        return $row !== false ? $this->formatScore($row) : null;
    }

    /**
     * Scores estimés par horizon — même formule 4 composantes que computeScore() :
     *   distance (40pts) + densité observation (30pts) + vitesse d'approche (20pts) + tendance (10pts)
     * Le tout multiplié par la confiance du modèle (décroissante avec l'horizon).
     *
     * @return array<string, array{score: int, level: string, distanceKm: float, confidence: float, validAt: string}>|null
     */
    private function loadHorizonScores(string $spotId, ?float $currentDistanceKm): ?array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT
                f.time_horizon                                                    AS horizon,
                ROUND(CAST(ST_Distance(
                    cp.geometry::geography,
                    f.geometry::geography
                ) / 1000 AS numeric), 2)                                          AS distance_km,
                f.confidence,
                f.valid_at,
                COALESCE(o.coverage_area, 0)                                      AS coverage_area
             FROM sargassum_forecast f
             JOIN coastal_point cp         ON cp.id = :spotId
             JOIN sargassum_observation o  ON o.id  = f.source_observation_id
             WHERE f.source_observation_id = (
                 SELECT o2.id FROM sargassum_observation o2
                 INNER JOIN sargassum_forecast ff ON ff.source_observation_id = o2.id,
                            coastal_point cp3
                 WHERE cp3.id = :spotId
                   AND ST_Distance(o2.geometry::geography, cp3.geometry::geography) < 300000
                 ORDER BY ST_Distance(o2.geometry::geography, cp3.geometry::geography) ASC,
                          o2.detected_at DESC
                 LIMIT 1
             )
             ORDER BY f.time_horizon ASC',
            ['spotId' => $spotId]
        );

        if (empty($rows)) {
            return null;
        }

        $horizons      = [];
        $prevDistKm    = $currentDistanceKm; // distance de référence = "now"
        $prevHorizon   = 0;

        foreach ($rows as $row) {
            $distKm      = (float) $row['distance_km'];
            $distM       = $distKm * 1000;
            $confidence  = (float) $row['confidence'];
            $coverageKm2 = (float) $row['coverage_area'];
            $horizon     = (int) $row['horizon'];

            // Composante distance (même seuil 50km que computeScore)
            $distanceScore = max(0.0, 1.0 - $distM / 50_000) * 40;

            // Composante densité : même observation source, coverage identique
            $densityScore = min($coverageKm2 / 100, 1.0) * 30;

            // Composante vitesse d'approche par rapport à l'horizon précédent
            $hoursElapsed = max($horizon - $prevHorizon, 1);
            $approachKmh  = $prevDistKm !== null
                ? ($prevDistKm - $distKm) / $hoursElapsed
                : 0.0;
            $velocityScore = min(max($approachKmh, 0.0) / 5.0, 1.0) * 20;

            // Composante tendance
            $trendScore = match (true) {
                $approachKmh > 0.2  => 10,  // approche
                $approachKmh < -0.2 => 0,   // s'éloigne
                default             => 5,   // stable
            };

            // Confiance du modèle appliquée à l'ensemble (l'observation "now" est certaine, pas la prévision)
            $raw   = $distanceScore + $densityScore + $velocityScore + $trendScore;
            $score = max(0, min(100, (int) round($raw * $confidence)));

            $cf = fn(float $v): int => (int) round($v * $confidence);

            $horizons['h' . $horizon] = [
                'score'      => $score,
                'level'      => $this->scoreToLevel($score),
                'distanceKm' => $distKm,
                'confidence' => $confidence,
                'validAt'    => $row['valid_at'],
                'breakdown'  => [
                    'distance' => $cf($distanceScore),
                    'density'  => $cf($densityScore),
                    'velocity' => $cf($velocityScore),
                    'trend'    => $cf($trendScore),
                ],
            ];

            $prevDistKm  = $distKm;
            $prevHorizon = $horizon;
        }

        return $horizons;
    }

    /**
     * @param array<string, mixed> $row
     * @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, etaHours: int|null, computedAt: string, breakdown: array{distance: int, density: int, velocity: int, trend: int}|null}
     */
    private function formatScore(array $row): array
    {
        $distKm  = $row['distance'] !== null ? (float) $row['distance']  : null;
        $densKm2 = $row['density']  !== null ? (float) $row['density']   : null;
        $trend   = $row['trend'];
        $total   = (int) $row['score'];

        $breakdown = null;
        if ($distKm !== null && $densKm2 !== null && $trend !== null) {
            $dScore = (int) round(max(0.0, 1.0 - $distKm * 1000 / 50_000) * 40);
            $nScore = (int) round(min($densKm2 / 100, 1.0) * 30);
            $tScore = match ($trend) { 'increasing' => 10, 'stable' => 5, default => 0 };
            $vScore = max(0, $total - $dScore - $nScore - $tScore);
            $breakdown = ['distance' => $dScore, 'density' => $nScore, 'velocity' => $vScore, 'trend' => $tScore];
        }

        return [
            'value'      => $total,
            'level'      => $row['level'],
            'distance'   => $distKm,
            'density'    => $densKm2,
            'trend'      => $trend,
            'etaHours'   => $row['eta_hours'] !== null ? (int) $row['eta_hours']   : null,
            'computedAt' => $row['computed_at'],
            'breakdown'  => $breakdown,
        ];
    }

    /**
     * Décompose la dernière observation en patches individuels (ST_Dump)
     * et retourne les 5 plus proches du spot dans un rayon de 200 km.
     *
     * @return array<int, array{areaKm2: float, distanceKm: float}>|null
     */
    private function loadPatches(string $spotId): ?array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT
                ROUND(CAST(ST_Area(dump.geom::geography) / 1000000 AS numeric), 2)  AS area_km2,
                ROUND(CAST(ST_Distance(cp.geometry::geography, dump.geom::geography) / 1000 AS numeric), 2) AS distance_km
             FROM coastal_point cp
             CROSS JOIN LATERAL (
                 SELECT (ST_Dump(o.geometry)).geom AS geom
                 FROM sargassum_observation o
                 WHERE o.id = (
                     SELECT o2.id FROM sargassum_observation o2
                     INNER JOIN sargassum_forecast ff ON ff.source_observation_id = o2.id,
                                coastal_point cp3
                     WHERE cp3.id = :spotId
                       AND ST_Distance(o2.geometry::geography, cp3.geometry::geography) < 300000
                     ORDER BY ST_Distance(o2.geometry::geography, cp3.geometry::geography) ASC,
                              o2.detected_at DESC
                     LIMIT 1
                 )
             ) dump
             WHERE cp.id = :spotId
               AND ST_Distance(cp.geometry::geography, dump.geom::geography) < 200000
             ORDER BY distance_km ASC
             LIMIT 5',
            ['spotId' => $spotId]
        );

        if (empty($rows)) {
            return null;
        }

        return array_map(
            fn(array $r) => ['areaKm2' => (float) $r['area_km2'], 'distanceKm' => (float) $r['distance_km']],
            $rows
        );
    }

    private function scoreToLevel(int $score): string
    {
        return match (true) {
            $score <= 15 => 'low',
            $score <= 55 => 'medium',
            default      => 'high',
        };
    }
}
