<?php

namespace App\Service\Forecast;

use App\Entity\SargassumForecast;
use App\Entity\SargassumObservation;
use App\Service\Weather\OpenMeteoClient;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;

/**
 * Simule la dérive des sargasses à partir d'une observation.
 *
 * Algorithme :
 *   1. Échantillonnage PostGIS (ST_GeneratePoints) → N points internes
 *   2. Pour chaque point, déplacement horaire via vecteur vent Open-Meteo
 *      (modèle Stokes : 3% vitesse vent = contribution sargassum)
 *   3. Reconstruction du polygone par ST_ConcaveHull des points déplacés
 *   4. Sauvegarde d'un SargassumForecast par horizon (H+6/12/24/48)
 */
class DriftSimulationService
{
    private const HORIZONS          = [6, 12, 24, 48];
    private const SAMPLE_POINTS     = 30;
    private const WIND_FACTOR       = 0.03;   // 3% du vent = dérive Stokes
    private const MODEL_VERSION     = '1.0.0';
    private const CONCAVITY         = 0.8;    // paramètre ST_ConcaveHull (0=convex, 1=très concave)

    public function __construct(
        private OpenMeteoClient      $weather,
        private Connection           $connection,
        private EntityManagerInterface $em,
        private LoggerInterface      $logger,
    ) {}

    /**
     * Calcule et persiste les forecasts pour une observation.
     *
     * @return SargassumForecast[]
     */
    public function computeForecasts(SargassumObservation $observation): array
    {
        $id = (string) $observation->getId();

        // Centroïde de l'observation → point de référence pour Open-Meteo
        $centroid = $this->getCentroid($id);
        if ($centroid === null) {
            $this->logger->warning('Centroid unavailable, skipping forecast', ['id' => $id]);
            return [];
        }

        // Vents horaires sur 3 jours (72h)
        $windData = $this->weather->getHourlyWind($centroid['lat'], $centroid['lng']);

        // Points échantillonnés à l'intérieur du polygone
        $samplePoints = $this->samplePoints($id);
        if (empty($samplePoints)) {
            $this->logger->warning('No sample points, skipping forecast', ['id' => $id]);
            return [];
        }

        $forecasts    = [];
        $currentPoints = $samplePoints; // état courant des points
        $prevHorizon  = 0;

        foreach (self::HORIZONS as $horizon) {
            // Déplace les points heure par heure de $prevHorizon à $horizon
            $currentPoints = $this->displacePoints($currentPoints, $windData, $prevHorizon, $horizon);

            // Reconstruit le polygone depuis les points déplacés
            $geometry = $this->reconstructPolygon($currentPoints);
            if ($geometry === null) {
                $this->logger->info("No polygon for H+{$horizon}", ['id' => $id]);
                $prevHorizon = $horizon;
                continue;
            }

            // Vecteur de dérive moyen sur cette période
            $driftVector = $this->avgDriftVector($windData, $prevHorizon, $horizon);

            $forecast = new SargassumForecast();
            $forecast->setSourceObservation($observation);
            $forecast->setGeometry($geometry);
            $forecast->setValidAt($observation->getDetectedAt()->modify("+{$horizon} hours"));
            $forecast->setModelVersion(self::MODEL_VERSION);
            $forecast->setTimeHorizon($horizon);
            $forecast->setConfidence($this->computeConfidence($horizon));
            $forecast->setDriftVectorAvg(
                sprintf('SRID=4326;POINT(%f %f)', $driftVector['lng'], $driftVector['lat'])
            );

            $this->em->persist($forecast);
            $forecasts[]  = $forecast;
            $prevHorizon  = $horizon;
        }

        $this->em->flush();

        return $forecasts;
    }

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

    private function getCentroid(string $observationId): ?array
    {
        $row = $this->connection->fetchAssociative(
            'SELECT ST_X(ST_Centroid(geometry)) AS lng,
                    ST_Y(ST_Centroid(geometry)) AS lat
             FROM sargassum_observation WHERE id = :id',
            ['id' => $observationId]
        );

        return ($row !== false) ? ['lat' => (float) $row['lat'], 'lng' => (float) $row['lng']] : null;
    }

    /**
     * Génère N points aléatoires à l'intérieur du polygone via PostGIS.
     *
     * @return array<int, array{lat: float, lng: float}>
     */
    private function samplePoints(string $observationId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT ST_X(geom) AS lng, ST_Y(geom) AS lat
             FROM (
                 SELECT (ST_Dump(ST_GeneratePoints(geometry, :n))).geom
                 FROM sargassum_observation WHERE id = :id
             ) AS pts',
            ['n' => self::SAMPLE_POINTS, 'id' => $observationId]
        );

        return array_map(fn($r) => ['lat' => (float) $r['lat'], 'lng' => (float) $r['lng']], $rows);
    }

    /**
     * Déplace un ensemble de points sur la période [$fromHour..$toHour]
     * en appliquant le vecteur vent heure par heure.
     *
     * @param array<int, array{lat: float, lng: float}> $points
     * @param array<int, array{speed: float, direction: float}> $windData
     * @return array<int, array{lat: float, lng: float}>
     */
    private function displacePoints(array $points, array $windData, int $fromHour, int $toHour): array
    {
        for ($h = $fromHour; $h < $toHour; $h++) {
            $wind = $windData[$h] ?? ['speed' => 0, 'direction' => 0];

            $speed     = $wind['speed'] * self::WIND_FACTOR; // m/s
            $dirRad    = deg2rad($wind['direction']);

            // Déplacement en mètres sur 1 heure
            $dx = $speed * sin($dirRad) * 3600; // Est-Ouest (m)
            $dy = $speed * cos($dirRad) * 3600; // Nord-Sud  (m)

            foreach ($points as &$pt) {
                // Conversion m → degrés
                $pt['lat'] += $dy / 111320;
                $pt['lng'] += $dx / (111320 * cos(deg2rad($pt['lat'])));
            }
            unset($pt);
        }

        return $points;
    }

    /**
     * Reconstruit un MULTIPOLYGON depuis un nuage de points via PostGIS.
     */
    private function reconstructPolygon(array $points): ?string
    {
        if (count($points) < 3) {
            return null;
        }

        $wktPoints = implode(',', array_map(
            fn($p) => sprintf('%f %f', $p['lng'], $p['lat']),
            $points
        ));

        $row = $this->connection->fetchAssociative(
            "SELECT ST_AsText(
                ST_Multi(
                    ST_ConcaveHull(
                        ST_GeomFromText('MULTIPOINT({$wktPoints})', 4326),
                        :concavity
                    )
                )
            ) AS wkt",
            ['concavity' => self::CONCAVITY]
        );

        return ($row !== false && $row['wkt']) ? 'SRID=4326;' . $row['wkt'] : null;
    }

    /**
     * Calcule le vecteur de dérive moyen (composantes lat/lng) sur une période.
     */
    private function avgDriftVector(array $windData, int $fromHour, int $toHour): array
    {
        $count = $toHour - $fromHour;
        $sumLat = 0.0;
        $sumLng = 0.0;

        for ($h = $fromHour; $h < $toHour; $h++) {
            $wind   = $windData[$h] ?? ['speed' => 0, 'direction' => 0];
            $speed  = $wind['speed'] * self::WIND_FACTOR;
            $dirRad = deg2rad($wind['direction']);
            $sumLat += $speed * cos($dirRad);
            $sumLng += $speed * sin($dirRad);
        }

        return ['lat' => $sumLat / max($count, 1), 'lng' => $sumLng / max($count, 1)];
    }

    /** Confiance décroissante avec l'horizon temporel. */
    private function computeConfidence(int $horizon): float
    {
        return match ($horizon) {
            6       => 0.85,
            12      => 0.75,
            24      => 0.60,
            48      => 0.45,
            default => 0.50,
        };
    }
}
