<?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 v2 (ST_Translate) :
 *   1. Cumul du vecteur de dérive heure par heure (vent Open-Meteo × facteur Stokes 3%)
 *   2. Translation directe de la géométrie source via ST_Translate (PostGIS)
 *   3. Buffer croissant (√horizon) pour simuler la diffusion turbulente
 *   4. Sauvegarde d'un SargassumForecast par horizon (H+6/12/24/48)
 *
 * Avantages vs l'ancienne approche sample+ConcaveHull :
 *   - Déterministe : pas d'aléatoire sur le polygone, distances cohérentes
 *   - Conserve la structure MULTIPOLYGON : chaque patch reste localement correct
 *   - ST_Distance donne la vraie distance au patch le plus proche (pas au centroïde)
 */
class DriftSimulationService
{
    private const HORIZONS      = [6, 12, 24, 48];
    private const WIND_FACTOR   = 0.03;    // 3% vitesse vent = dérive de Stokes sargassum
    private const MODEL_VERSION = '2.0.0';

    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();

        $centroid = $this->getCentroid($id);
        if ($centroid === null) {
            $this->logger->warning('Centroid unavailable, skipping forecast', ['id' => $id]);
            return [];
        }

        $windData = $this->weather->getHourlyWind($centroid['lat'], $centroid['lng']);

        $forecasts   = [];
        $totalDxM    = 0.0;
        $totalDyM    = 0.0;
        $prevHorizon = 0;

        foreach (self::HORIZONS as $horizon) {
            // Cumul du déplacement en mètres de $prevHorizon à $horizon
            for ($h = $prevHorizon; $h < $horizon; $h++) {
                $wind    = $windData[$h] ?? ['speed' => 0, 'direction' => 0];
                $speed   = $wind['speed'] * self::WIND_FACTOR; // m/s
                $dirRad  = deg2rad($wind['direction']);
                $totalDxM += $speed * sin($dirRad) * 3600; // m/h Est-Ouest
                $totalDyM += $speed * cos($dirRad) * 3600; // m/h Nord-Sud
            }

            // Conversion m → degrés (au centroïde de l'observation)
            $dxDeg = $totalDxM / (111320.0 * cos(deg2rad($centroid['lat'])));
            $dyDeg = $totalDyM / 111320.0;

            $geometry = $this->buildForecastGeometry($id, $dxDeg, $dyDeg);
            if ($geometry === null) {
                $this->logger->info("No polygon for H+{$horizon}", ['id' => $id]);
                $prevHorizon = $horizon;
                continue;
            }

            $driftVector = $this->avgDriftVector($windData, 0, $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;
    }

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

    /** @return array{lat: float, lng: float}|null */
    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;
    }

    /**
     * Traduit la géométrie source de $dxDeg/$dyDeg degrés via ST_Translate.
     *
     * Pas de ST_Buffer ici : buffer sur geography produit des géométries volumineuses
     * et invalides sur de grands MULTIPOLYGON, corrompant les calculs ST_Distance.
     * ST_Translate seul conserve la structure et la taille de chaque patch individuel.
     */
    private function buildForecastGeometry(
        string $observationId,
        float  $dxDeg,
        float  $dyDeg,
    ): ?string {
        $row = $this->connection->fetchAssociative(
            'SELECT ST_AsText(
                ST_Multi(
                    ST_SimplifyPreserveTopology(
                        ST_Translate(geometry, :dx, :dy),
                        0.001
                    )
                )
             ) AS wkt
             FROM sargassum_observation WHERE id = :id',
            ['id' => $observationId, 'dx' => $dxDeg, 'dy' => $dyDeg]
        );

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

    /**
     * Vecteur de dérive moyen (composantes lat/lng en m/s) sur [fromHour..toHour].
     *
     * @param array<int, array{speed: float, direction: float}> $windData
     * @return array{lat: float, lng: float}
     */
    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,
        };
    }
}
