<?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/{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);
        }

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

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

    /**
     * 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 à partir des forecasts les plus récents.
     * Retourne null si aucun forecast disponible.
     *
     * @return array<string, array{score: int, level: string, distanceKm: float, confidence: float, validAt: string}>|null
     */
    private function loadHorizonScores(string $spotId): ?array
    {
        // Dernière observation avec forecasts
        $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
             FROM sargassum_forecast f
             JOIN coastal_point cp ON cp.id = :spotId
             WHERE f.source_observation_id = (
                 SELECT o.id FROM sargassum_observation o
                 INNER JOIN sargassum_forecast ff ON ff.source_observation_id = o.id
                 ORDER BY o.detected_at DESC LIMIT 1
             )
             ORDER BY f.time_horizon ASC',
            ['spotId' => $spotId]
        );

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

        $horizons = [];
        foreach ($rows as $row) {
            $distKm     = (float) $row['distance_km'];
            $confidence = (float) $row['confidence'];
            // Score pondéré par la confiance : un horizon lointain moins certain
            // ne peut pas afficher un risque supérieur à un horizon proche plus fiable
            $score = max(0, (int) round((1 - min($distKm / 50, 1)) * 100 * $confidence));
            $horizons['h' . $row['horizon']] = [
                'score'      => $score,
                'level'      => $this->scoreToLevel($score),
                'distanceKm' => $distKm,
                'confidence' => $confidence,
                'validAt'    => $row['valid_at'],
            ];
        }

        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}
     */
    private function formatScore(array $row): array
    {
        return [
            'value'      => (int) $row['score'],
            'level'      => $row['level'],
            'distance'   => $row['distance']  !== null ? (float) $row['distance']  : null,
            'density'    => $row['density']   !== null ? (float) $row['density']   : null,
            'trend'      => $row['trend'],
            'etaHours'   => $row['eta_hours'] !== null ? (int) $row['eta_hours']   : null,
            'computedAt' => $row['computed_at'],
        ];
    }

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