<?php

namespace App\Service\Forecast;

use App\Entity\CoastalPoint;
use App\Entity\ImpactScore;
use App\Entity\SargassumForecast;
use App\Entity\SargassumObservation;
use App\Service\Push\PushNotificationService;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

/**
 * Calcule et persiste les ImpactScore pour tous les CoastalPoints,
 * en prenant en compte l'observation courante et les forecasts.
 *
 * Formule :
 *   score = distance_score(40) + density_score(30) + velocity_score(20) + trend_score(10)
 *
 *   distance_score : max(0, 1 - distance/50km) × 40
 *   density_score  : min(coverage_area_km2 / 100, 1) × 30
 *   velocity_score : max(0, approach_speed_km_h / 5) capped at 1, × 20
 *   trend_score    : 10 si increasing, 5 si stable, 0 si decreasing
 */
class ImpactScoreService
{
    private const MAX_DISTANCE_M = 50_000;   // 50 km : au-delà score distance = 0
    private const MAX_RELEVANT_M = 300_000;  // 300 km : au-delà, on ne touche pas au score existant
    private const MAX_DENSITY_KM2 = 100;   // 100 km² = densité maximale
    private const MAX_APPROACH_KMH = 5;    // 5 km/h = vitesse d'approche max
    private const CACHE_TTL = 10_800;      // 3h
    private const ETA_BUFFER_M = 5_000;    // 5 km : seuil "impact imminent" sur le spot

    // Bonus feedback : +8 pts si présence confirmée dans les 6h et dans rayon 20km
    private const FEEDBACK_RADIUS_M  = 20_000;
    private const FEEDBACK_WINDOW_H  = 6;
    private const FEEDBACK_BONUS     = 8;
    private const HIGH_SCORE_THRESHOLD = 70;

    public function __construct(
        private Connection $connection,
        private EntityManagerInterface $em,
        private CacheInterface $cache,
        private LoggerInterface $logger,
        private PushNotificationService $push,
    ) {}

    /**
     * Calcule et sauvegarde les scores pour tous les CoastalPoints
     * à partir d'une observation et de ses forecasts.
     *
     * @param array<int, SargassumForecast> $forecasts
     */
    public function computeForObservation(SargassumObservation $observation, array $forecasts): void
    {
        $spots = $this->em->getRepository(CoastalPoint::class)->findAll();

        if (empty($spots)) {
            $this->logger->info('No coastal points found, skipping score computation');
            return;
        }

        $obsId = (string) $observation->getId();

        foreach ($spots as $spot) {
            $spotId = (string) $spot->getId();

            try {
                // Ne pas écraser le score d'un spot si cette observation est trop loin.
                // Cela évite qu'une ingestion distante remplace un bon score existant.
                $distanceCheck = $this->getDistanceToNearestSargassum($spotId, $obsId);
                if ($distanceCheck === null || $distanceCheck > self::MAX_RELEVANT_M) {
                    continue;
                }

                $score = $this->computeScore($spot, $obsId, $forecasts);
                $this->persistScore($spot, $score);
                $this->invalidateCache($spotId);

                // Alerte push si score passe en "high"
                if ($score['score'] >= self::HIGH_SCORE_THRESHOLD) {
                    $this->push->notifySpot($spot, $score['score'], $score['level']);
                }
            } catch (\Throwable $e) {
                $this->logger->error('Score computation failed', [
                    'spot'  => $spot->getName(),
                    'error' => $e->getMessage(),
                ]);
            }
        }

        $this->em->flush();
    }

    /**
     * Retourne le score mis en cache pour un spot (TTL 3h).
     *
     * @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, etaHours: int|null, computedAt: string}|null
     */
    public function getCachedScore(string $spotId): ?array
    {
        $key = $this->cacheKey($spotId);

        return $this->cache->get($key, function (ItemInterface $item) use ($spotId) {
            $item->expiresAfter(self::CACHE_TTL);
            return $this->loadLatestScore($spotId);
        });
    }

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

    /**
     * @param array<int, SargassumForecast> $forecasts
     * @return array{score: int, level: string, distance: float|null, density: float, trend: string, etaHours: int|null}
     */
    private function computeScore(CoastalPoint $spot, string $obsId, array $forecasts): array
    {
        $spotId = (string) $spot->getId();

        // Distance au sargassum le plus proche (mètres)
        $distanceM = $this->getDistanceToNearestSargassum($spotId, $obsId);

        if ($distanceM === null) {
            return $this->emptyScore();
        }

        $coverageKm2 = $this->getCoverageArea($obsId);
        $forecastH6  = $this->findForecastByHorizon($forecasts, 6);

        // Vitesse d'approche : delta distance entre now et H+6 (km/h)
        $approachKmh = 0.0;
        if ($forecastH6 !== null) {
            $distH6M = $this->getDistanceToNearestForecast($spotId, (string) $forecastH6->getId());
            if ($distH6M !== null) {
                $approachKmh = ($distanceM - $distH6M) / 1000 / 6; // km/h (positif = approche)
            }
        }

        // Tendance
        $previousDistance = $this->getPreviousDistance($spotId);
        $trend = match (true) {
            $previousDistance === null           => 'stable',
            $distanceM < $previousDistance * 0.9 => 'increasing',
            $distanceM > $previousDistance * 1.1 => 'decreasing',
            default                              => 'stable',
        };

        // ETA côtier
        $etaHours = $this->computeEta($spotId, $distanceM, $forecasts);

        // Calcul des composantes
        $distanceScore = max(0.0, 1.0 - $distanceM / self::MAX_DISTANCE_M) * 40;
        $densityScore  = min($coverageKm2 / self::MAX_DENSITY_KM2, 1.0) * 30;
        $velocityScore = min(max($approachKmh, 0.0) / self::MAX_APPROACH_KMH, 1.0) * 20;
        $trendScore    = match ($trend) { 'increasing' => 10, 'stable' => 5, default => 0 };

        // Bonus feedback terrain récent
        $feedbackBonus = $this->getFeedbackBonus($spotId);

        $total = (int) round($distanceScore + $densityScore + $velocityScore + $trendScore + $feedbackBonus);
        $total = max(0, min(100, $total));

        return [
            'score'    => $total,
            'level'    => $this->scoreToLevel($total),
            'distance' => round($distanceM / 1000, 2),
            'density'  => round($coverageKm2, 2),
            'trend'    => $trend,
            'etaHours' => $etaHours,
        ];
    }

    /**
     * Calcule l'horizon (en h) auquel les sargasses devraient atteindre le spot.
     * Retourne 0 si impact déjà en cours, null si aucun impact prévu dans 48h.
     *
     * @param array<int, SargassumForecast> $forecasts
     */
    private function computeEta(string $spotId, float $currentDistanceM, array $forecasts): ?int
    {
        if ($currentDistanceM < self::ETA_BUFFER_M) {
            return 0; // impact en cours
        }

        foreach ([6, 12, 24, 48] as $horizon) {
            $forecast = $this->findForecastByHorizon($forecasts, $horizon);
            if ($forecast === null) {
                continue;
            }

            $distM = $this->getDistanceToNearestForecast($spotId, (string) $forecast->getId());
            if ($distM !== null && $distM < self::ETA_BUFFER_M) {
                return $horizon;
            }
        }

        return null;
    }

    private function getDistanceToNearestSargassum(string $spotId, string $obsId): ?float
    {
        $row = $this->connection->fetchAssociative(
            'SELECT ST_Distance(
                cp.geometry::geography,
                o.geometry::geography
             ) AS distance_m
             FROM coastal_point cp, sargassum_observation o
             WHERE cp.id = :spotId AND o.id = :obsId',
            ['spotId' => $spotId, 'obsId' => $obsId]
        );

        return ($row !== false) ? (float) $row['distance_m'] : null;
    }

    private function getDistanceToNearestForecast(string $spotId, string $forecastId): ?float
    {
        $row = $this->connection->fetchAssociative(
            'SELECT ST_Distance(
                cp.geometry::geography,
                f.geometry::geography
             ) AS distance_m
             FROM coastal_point cp, sargassum_forecast f
             WHERE cp.id = :spotId AND f.id = :forecastId',
            ['spotId' => $spotId, 'forecastId' => $forecastId]
        );

        return ($row !== false) ? (float) $row['distance_m'] : null;
    }

    private function getCoverageArea(string $obsId): float
    {
        $row = $this->connection->fetchAssociative(
            'SELECT coverage_area FROM sargassum_observation WHERE id = :id',
            ['id' => $obsId]
        );

        return ($row !== false && $row['coverage_area'] !== null) ? (float) $row['coverage_area'] : 0.0;
    }

    private function getPreviousDistance(string $spotId): ?float
    {
        // On cherche le score de la précédente "session" d'ingestion :
        // au moins 6h d'ancienneté pour éviter de comparer avec un recalcul immédiat,
        // et au plus 14 jours pour rester pertinent.
        $since = (new \DateTimeImmutable())->modify('-14 days')->format('Y-m-d H:i:s');
        $before = (new \DateTimeImmutable())->modify('-6 hours')->format('Y-m-d H:i:s');

        $row = $this->connection->fetchAssociative(
            'SELECT distance_to_nearest_sargassum
             FROM impact_score
             WHERE coastal_point_id = :spotId
               AND timestamp <= :before
               AND timestamp >= :since
             ORDER BY timestamp DESC
             LIMIT 1',
            ['spotId' => $spotId, 'before' => $before, 'since' => $since]
        );

        return ($row !== false && $row['distance_to_nearest_sargassum'] !== null)
            ? (float) $row['distance_to_nearest_sargassum']
            : null;
    }

    /** @param array{score: int, level: string, distance: float|null, density: float, trend: string, etaHours: int|null} $data */
    private function persistScore(CoastalPoint $spot, array $data): void
    {
        $s = new ImpactScore();
        $s->setCoastalPoint($spot);
        $s->setScore($data['score']);
        $s->setLevel($data['level']);
        $s->setDistanceToNearestSargassum($data['distance']);
        $s->setDensityEstimate($data['density']);
        $s->setTrend($data['trend']);
        $s->setEtaHours($data['etaHours']);

        $this->em->persist($s);
    }

    /** @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, etaHours: int|null, computedAt: string}|null */
    private function loadLatestScore(string $spotId): ?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
             ORDER BY timestamp DESC LIMIT 1',
            ['spotId' => $spotId]
        );

        if ($row === false) {
            return null;
        }

        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 invalidateCache(string $spotId): void
    {
        $this->cache->delete($this->cacheKey($spotId));
    }

    private function cacheKey(string $spotId): string
    {
        return 'impact_score_' . str_replace('-', '_', $spotId);
    }

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

    /** @return array{score: int, level: string, distance: null, density: float, trend: string, etaHours: null} */
    private function emptyScore(): array
    {
        return ['score' => 0, 'level' => 'low', 'distance' => null, 'density' => 0.0, 'trend' => 'stable', 'etaHours' => null];
    }

    private function getFeedbackBonus(string $spotId): float
    {
        $windowStart = (new \DateTimeImmutable())->modify('-' . self::FEEDBACK_WINDOW_H . ' hours');

        $row = $this->connection->fetchAssociative(
            'SELECT COUNT(*) AS positive_count
             FROM user_feedback uf
             JOIN coastal_point cp ON cp.id = :spotId
             WHERE uf.has_seaweed = true
               AND uf.timestamp >= :since
               AND ST_Distance(uf.location::geography, cp.geometry::geography) <= :radius',
            [
                'spotId' => $spotId,
                'since'  => $windowStart->format('Y-m-d H:i:s'),
                'radius' => self::FEEDBACK_RADIUS_M,
            ]
        );

        return ($row !== false && (int) $row['positive_count'] > 0) ? (float) self::FEEDBACK_BONUS : 0.0;
    }

    /** @param array<int, SargassumForecast> $forecasts */
    private function findForecastByHorizon(array $forecasts, int $horizon): ?SargassumForecast
    {
        foreach ($forecasts as $f) {
            if ($f->getTimeHorizon() === $horizon) {
                return $f;
            }
        }
        return null;
    }
}
