<?php

namespace App\Service\Ingestion;

use App\Entity\DataIngestionJob;
use App\Entity\SargassumObservation;
use App\Service\SentinelHub\SentinelHubClient;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

class IngestionService
{
    private const CLOUD_THRESHOLD     = 60.0;
    private const SARGASSUM_DN        = 1;
    private const SIMPLIFY_TOLERANCE  = 0.001; // degrés (~100m à l'équateur)
    private const MIN_RING_POINTS     = 8;     // filtre polygones < ~2 pixels (bruit)
    private const PROCESSING_VERSION  = '1.2.0';

    public function __construct(
        private SentinelHubClient $sentinelHub,
        private EntityManagerInterface $em,
        private LoggerInterface $logger,
        #[Autowire('%kernel.project_dir%')] private string $projectDir,
    ) {}

    /**
     * Tente l'ingestion Sentinel-2, puis Sentinel-3 OLCI en fallback si la
     * couverture nuageuse est trop élevée ou si la scène S2 est indisponible.
     */
    /** @param array<int, float> $bbox */
    public function ingestWithFallback(array $bbox, \DateTimeImmutable $date): void
    {
        try {
            $this->ingest($bbox, $date, 'Sentinel-2', 'sentinel-2-l2a');
        } catch (HighCloudCoverageException $e) {
            $this->logger->info('S2 cloud too high, falling back to Sentinel-3', ['reason' => $e->getMessage()]);
            $this->ingest($bbox, $date, 'Sentinel-3', 'sentinel-3-olci');
        }
    }

    /** @param array<int, float> $bbox */
    public function ingest(
        array $bbox,
        \DateTimeImmutable $date,
        string $source = 'Sentinel-2',
        string $collection = 'sentinel-2-l2a',
    ): void {
        $job = new DataIngestionJob();
        $job->setSource($source);
        $job->setTileId(sprintf('[%s]', implode(',', $bbox)));
        $this->em->persist($job);
        $this->em->flush();

        $tmpDir = null;

        try {
            // Étape 1 : vérification couverture nuageuse via Catalog API
            $cloudCoverage = $this->sentinelHub->getCloudCoverage($bbox, $date, $collection);
            $this->logger->info('Cloud coverage check', ['bbox' => $bbox, 'coverage' => $cloudCoverage, 'collection' => $collection]);

            if ($cloudCoverage > self::CLOUD_THRESHOLD) {
                $job->setStatus('failed');
                $job->setErrorMessage("Couverture nuageuse {$cloudCoverage}% > seuil " . self::CLOUD_THRESHOLD . '%');
                $this->em->flush();
                throw new HighCloudCoverageException(
                    "Cloud coverage {$cloudCoverage}% exceeds threshold for collection {$collection}"
                );
            }

            // Étape 2 : téléchargement du raster AFAI binaire
            $tmpDir = $this->projectDir . '/var/ingestion/' . uniqid('ingest_', true);
            mkdir($tmpDir, 0755, true);
            $tifPath     = $tmpDir . '/afai.tif';
            $geojsonPath = $tmpDir . '/polygons.geojson';

            $metadata = $this->sentinelHub->downloadAfaiBinary($bbox, $date, $tifPath, $collection);
            $this->logger->info('GeoTIFF downloaded', ['path' => $tifPath, 'tile' => $metadata['tileId']]);

            // Étape 3 : vectorisation via GDAL
            $this->polygonize($tifPath, $geojsonPath);

            // Étape 4 : construction du MULTIPOLYGON depuis le GeoJSON
            $geometry = $this->buildMultiPolygon($geojsonPath);

            if ($geometry === null) {
                $job->setStatus('success');
                $job->setProcessedAt(new \DateTimeImmutable());
                $job->setErrorMessage('Aucune sargasse détectée dans cette scène');
                $this->em->flush();
                $this->logger->info('No sargassum detected', ['bbox' => $bbox]);
                return;
            }

            // Étape 5 : persistance
            $observation = new SargassumObservation();
            $observation->setGeometry(json_encode($geometry));
            $observation->setDetectedAt($date);
            $observation->setSource($source);
            $observation->setTileId($metadata['tileId']);
            $observation->setCloudCoverage($cloudCoverage);
            $observation->setConfidence(0.8);
            $observation->setAfaiMean($metadata['afaiMean']);
            $observation->setAfaiStd($metadata['afaiStd']);
            $observation->setProcessingVersion(self::PROCESSING_VERSION);

            $this->em->persist($observation);
            $this->em->flush();

            // Étape 6 : simplification + bbox + aire via PostGIS
            $this->updateSpatialMeta((string) $observation->getId());

            $job->setStatus('success');
            $job->setProcessedAt(new \DateTimeImmutable());
            $this->em->flush();

            $this->logger->info('Ingestion OK', ['observation_id' => (string) $observation->getId()]);

        } catch (\Throwable $e) {
            $job->setStatus('failed');
            $job->incrementRetry();
            $job->setErrorMessage($e->getMessage());
            $this->em->flush();
            $this->logger->error('Ingestion failed', ['error' => $e->getMessage()]);
            throw $e;

        } finally {
            if ($tmpDir !== null && is_dir($tmpDir)) {
                $this->cleanup($tmpDir);
            }
        }
    }

    private function polygonize(string $tifPath, string $geojsonPath): void
    {
        $maskPath = dirname($tifPath) . '/mask.tif';

        // Créer un masque binaire : pixels DN=1 → valeur 1, tout le reste → nodata (0)
        // Cela permet à gdal_polygonize de n'extraire QUE les pixels sargasses,
        // évitant les faux positifs issus de la vectorisation de tout le tile.
        $maskProcess = new Process([
            'gdal_calc.py',
            '-A', $tifPath,
            '--outfile', $maskPath,
            '--calc', 'A==1',
            '--type', 'Byte',
            '--NoDataValue', '0',
            '--quiet',
        ]);
        $maskProcess->setTimeout(60);
        $maskProcess->run();

        $args = [
            'gdal_polygonize.py',
            $tifPath,
            '-f', 'GeoJSON',
            '-q',
            $geojsonPath,
        ];

        if ($maskProcess->isSuccessful() && file_exists($maskPath)) {
            // Insérer -mask avant le fichier de sortie
            array_splice($args, 1, 0, ['-mask', $maskPath]);
        }

        $process = new Process($args);
        $process->setTimeout(120);
        $process->run();

        if (file_exists($maskPath)) {
            unlink($maskPath);
        }

        if (!$process->isSuccessful()) {
            throw new ProcessFailedException($process);
        }
    }

    /**
     * @return array{type: string, coordinates: array<int, mixed>}|null
     */
    private function buildMultiPolygon(string $geojsonPath): ?array
    {
        if (!file_exists($geojsonPath)) {
            return null;
        }

        $contents = file_get_contents($geojsonPath);
        if ($contents === false) {
            return null;
        }
        $data = json_decode($contents, true);

        if (empty($data['features'])) {
            return null;
        }

        $polygons = [];
        foreach ($data['features'] as $feature) {
            if (($feature['properties']['DN'] ?? 0) !== self::SARGASSUM_DN) {
                continue;
            }

            $geom = $feature['geometry'];

            if ($geom['type'] === 'Polygon') {
                // Ignorer les polygones trop petits (bruit : quelques pixels isolés)
                if (count($geom['coordinates'][0] ?? []) >= self::MIN_RING_POINTS) {
                    $polygons[] = $geom['coordinates'];
                }
            } elseif ($geom['type'] === 'MultiPolygon') {
                foreach ($geom['coordinates'] as $poly) {
                    if (count($poly[0] ?? []) >= self::MIN_RING_POINTS) {
                        $polygons[] = $poly;
                    }
                }
            }
        }

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

        return ['type' => 'MultiPolygon', 'coordinates' => $polygons];
    }

    private const MIN_POLYGON_AREA_M2 = 1_000_000; // 1 km² — filtre résidus AFAI

    private function updateSpatialMeta(string $id): void
    {
        // 1) Filtrer les micro-polygones < MIN_POLYGON_AREA_M2 avant simplification.
        //    ST_CollectionExtract(..., 3) extrait uniquement les Polygon pour éviter
        //    que ST_Collect retourne une GeometryCollection incompatible avec la colonne.
        //    ST_Multi force le type MultiPolygon même pour un seul polygone résiduel.
        $this->em->getConnection()->executeStatement(
            "UPDATE sargassum_observation
             SET geometry = (
                SELECT COALESCE(
                    ST_Multi(ST_CollectionExtract(ST_Collect(geom), 3)),
                    ST_GeomFromText('MULTIPOLYGON EMPTY', 4326)
                )
                FROM (
                    SELECT (ST_Dump(geometry)).geom AS geom
                    FROM sargassum_observation
                    WHERE id = :id
                ) parts
                WHERE ST_Area(parts.geom::geography) >= :minArea
             )
             WHERE id = :id",
            ['id' => $id, 'minArea' => self::MIN_POLYGON_AREA_M2]
        );

        // 2) Simplifier + calculer bbox et surface finale
        $this->em->getConnection()->executeStatement(
            'UPDATE sargassum_observation
             SET
                geometry      = ST_SimplifyPreserveTopology(geometry, :tolerance),
                bbox          = ST_Envelope(geometry),
                coverage_area = ROUND(CAST(ST_Area(geometry::geography) / 1000000 AS numeric), 2)
             WHERE id = :id
               AND NOT ST_IsEmpty(geometry)',
            ['id' => $id, 'tolerance' => self::SIMPLIFY_TOLERANCE]
        );

        // 3) Supprimer l'observation si plus rien ne reste après filtrage
        $this->em->getConnection()->executeStatement(
            'DELETE FROM sargassum_observation
             WHERE id = :id AND (ST_IsEmpty(geometry) OR geometry IS NULL)',
            ['id' => $id]
        );
    }

    private function cleanup(string $dir): void
    {
        foreach (glob($dir . '/*') ?: [] as $file) {
            if (is_file($file)) {
                unlink($file);
            }
        }
        rmdir($dir);
    }
}
