<?php

namespace App\Service\Forecast;

use App\Entity\SargassumForecast;
use App\Entity\SargassumObservation;
use App\Service\Ocean\OceanCurrentClient;
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 v3 (ST_Translate + courants océaniques) :
 *   Déplacement total = vent Stokes (3% vent) + courant de surface OSCAR (100%)
 *
 *   Pour chaque heure :
 *     dx_m = (wind_speed × 0.03 × sin(wind_dir) + ocean_u) × 3600
 *     dy_m = (wind_speed × 0.03 × cos(wind_dir) + ocean_v) × 3600
 *
 *   Puis ST_Translate(geometry, cumul_dx_deg, cumul_dy_deg) sur la géométrie source.
 *   Fallback wind-only si OSCAR est indisponible.
 */
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 = '3.0.0';

    public function __construct(
        private OpenMeteoClient       $weather,
        private OceanCurrentClient    $ocean,
        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']);

        // Courant de surface OSCAR — constant sur tous les horizons (courants Caraïbes lents)
        // Retourne null si OSCAR indisponible → fallback wind-only (u=0, v=0)
        $current = $this->ocean->getSurfaceCurrent($centroid['lat'], $centroid['lng']);
        $oceanU  = $current['u'] ?? 0.0; // m/s Est-Ouest
        $oceanV  = $current['v'] ?? 0.0; // m/s Nord-Sud

        if ($current !== null) {
            $this->logger->info('Ocean current applied', [
                'u_ms' => round($oceanU, 3),
                'v_ms' => round($oceanV, 3),
                'speed_kmh' => round(sqrt($oceanU ** 2 + $oceanV ** 2) * 3.6, 2),
            ]);
        }

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

        foreach (self::HORIZONS as $horizon) {
            // Cumul déplacement de $prevHorizon à $horizon
            for ($h = $prevHorizon; $h < $horizon; $h++) {
                $wind   = $windData[$h] ?? ['speed' => 0, 'direction' => 0];
                $dirRad = deg2rad($wind['direction']);

                // Composante vent (dérive de Stokes)
                $windU = $wind['speed'] * self::WIND_FACTOR * sin($dirRad); // m/s Est-Ouest
                $windV = $wind['speed'] * self::WIND_FACTOR * cos($dirRad); // m/s Nord-Sud

                // Déplacement total sur 1 heure = vent Stokes + courant océanique
                $totalDxM += ($windU + $oceanU) * 3600;
                $totalDyM += ($windV + $oceanV) * 3600;
            }

            // Conversion m → degrés (au centroïde)
            $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, $oceanU, $oceanV);

            $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 via ST_Translate (PostGIS).
     * Conserve la structure MULTIPOLYGON — ST_Distance retourne la vraie distance
     * au patch le plus proche, pas au centroïde global.
     */
    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 && is_string($row['wkt']) && $row['wkt'] !== '')
            ? 'SRID=4326;' . $row['wkt']
            : null;
    }

    /**
     * Vecteur de dérive moyen (composantes lat/lng en m/s) sur [0..toHour],
     * incluant vent Stokes + courant océanique.
     *
     * @param array<int, array{speed: float, direction: float}> $windData
     * @return array{lat: float, lng: float}
     */
    private function avgDriftVector(
        array $windData,
        int   $fromHour,
        int   $toHour,
        float $oceanU,
        float $oceanV,
    ): array {
        $count  = max($toHour - $fromHour, 1);
        $sumLng = 0.0;
        $sumLat = 0.0;

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

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

    /** 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,
        };
    }
}
