<?php

declare(strict_types=1);

namespace App\Services;

use Database;
use PDO;

/**
 * Service global pour la gestion des secteurs
 *
 * Fournit des fonctions réutilisables pour :
 * - Géocoder une adresse via api-adresse.data.gouv.fr
 * - Trouver un secteur à partir de coordonnées GPS
 * - Trouver un secteur à partir d'une adresse
 */
class SectorService
{
    private PDO $db;

    public function __construct()
    {
        $this->db = Database::getInstance();
    }

    /**
     * Géocode une adresse via api-adresse.data.gouv.fr
     *
     * @param string $num Numéro de rue
     * @param string $bis Complément (bis, ter, etc.)
     * @param string $rue Nom de la rue
     * @param string $ville Nom de la ville
     * @param string $cp Code postal (pour vérifier le département)
     * @return array|null [lat, lng] ou null si non trouvé ou score trop faible
     */
    public function geocodeAddress(string $num, string $bis, string $rue, string $ville, string $cp): ?array
    {
        try {
            // Construire l'URL de l'API
            $query = trim($num . $bis) . ' ' . $rue . ' ' . $ville;
            $url = 'https://api-adresse.data.gouv.fr/search/?q=' . urlencode($query);

            LogService::info('[SectorService] Géocodage adresse', [
                'url' => $url,
                'adresse' => $query
            ]);

            // Appel à l'API
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_TIMEOUT, 10);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
            $json = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($httpCode !== 200 || empty($json)) {
                LogService::warning('[SectorService] Erreur API géocodage', [
                    'http_code' => $httpCode,
                    'adresse' => $query
                ]);
                return null;
            }

            $data = json_decode($json);

            if (empty($data->features)) {
                LogService::info('[SectorService] Aucun résultat de géocodage', [
                    'adresse' => $query
                ]);
                return null;
            }

            $score = $data->features[0]->properties->score ?? 0;

            // Vérifier le score (> 0.7 = 70% de confiance)
            if (floatval($score) <= 0.7) {
                LogService::info('[SectorService] Score géocodage trop faible', [
                    'score' => $score,
                    'adresse' => $query
                ]);
                return null;
            }

            // Vérifier le département
            $cpTrouve = $data->features[0]->properties->postcode ?? '';
            $deptTrouve = substr($cpTrouve, 0, 2);

            $cpAmicale = $cp;
            if (strlen($cpAmicale) == 4) {
                $cpAmicale = '0' . $cpAmicale;
            }
            $deptAmicale = substr($cpAmicale, 0, 2);

            if ($deptTrouve !== $deptAmicale) {
                LogService::warning('[SectorService] Département différent', [
                    'dept_trouve' => $deptTrouve,
                    'dept_attendu' => $deptAmicale,
                    'adresse' => $query
                ]);
                return null;
            }

            // Extraire les coordonnées [lng, lat] -> [lat, lng]
            $coordinates = $data->features[0]->geometry->coordinates;
            $lat = (float)$coordinates[1];
            $lng = (float)$coordinates[0];

            LogService::info('[SectorService] Géocodage réussi', [
                'lat' => $lat,
                'lng' => $lng,
                'score' => $score,
                'adresse' => $query
            ]);

            return ['lat' => $lat, 'lng' => $lng];

        } catch (\Exception $e) {
            LogService::error('[SectorService] Erreur géocodage', [
                'error' => $e->getMessage(),
                'adresse' => "$num$bis $rue $ville"
            ]);
            return null;
        }
    }

    /**
     * Trouve le secteur contenant une position GPS pour une opération donnée
     *
     * @param int $operationId ID de l'opération
     * @param float $lat Latitude
     * @param float $lng Longitude
     * @return int|null ID du secteur trouvé ou null
     */
    public function findSectorByGps(int $operationId, float $lat, float $lng): ?int
    {
        try {
            // Récupérer tous les secteurs de l'opération avec leur polygone
            $query = "SELECT id, sector FROM ope_sectors
                      WHERE fk_operation = :operation_id
                      AND chk_active = 1
                      AND sector IS NOT NULL
                      AND sector != ''";

            $stmt = $this->db->prepare($query);
            $stmt->execute(['operation_id' => $operationId]);
            $sectors = $stmt->fetchAll(PDO::FETCH_ASSOC);

            if (empty($sectors)) {
                LogService::info('[SectorService] Aucun secteur trouvé pour l\'opération', [
                    'operation_id' => $operationId
                ]);
                return null;
            }

            // Tester chaque secteur
            foreach ($sectors as $sector) {
                $polygon = $this->parseSectorPolygon($sector['sector']);

                if (empty($polygon)) {
                    continue;
                }

                if ($this->isPointInsidePolygon($lat, $lng, $polygon)) {
                    LogService::info('[SectorService] Secteur trouvé par GPS', [
                        'sector_id' => $sector['id'],
                        'operation_id' => $operationId,
                        'lat' => $lat,
                        'lng' => $lng
                    ]);
                    return (int)$sector['id'];
                }
            }

            LogService::info('[SectorService] Aucun secteur ne contient ce point GPS', [
                'operation_id' => $operationId,
                'lat' => $lat,
                'lng' => $lng,
                'nb_sectors_tested' => count($sectors)
            ]);

            return null;

        } catch (\Exception $e) {
            LogService::error('[SectorService] Erreur findSectorByGps', [
                'error' => $e->getMessage(),
                'operation_id' => $operationId,
                'lat' => $lat,
                'lng' => $lng
            ]);
            return null;
        }
    }

    /**
     * Trouve le secteur pour une adresse (géocodage + recherche GPS)
     *
     * @param int $operationId ID de l'opération
     * @param string $num Numéro de rue
     * @param string $bis Complément
     * @param string $rue Nom de la rue
     * @param string $ville Nom de la ville
     * @param string $cp Code postal
     * @return array|null ['sector_id' => int, 'gps_lat' => float, 'gps_lng' => float] ou null
     */
    public function findSectorByAddress(int $operationId, string $num, string $bis, string $rue, string $ville, string $cp): ?array
    {
        // Étape 1 : Géocoder l'adresse
        $coords = $this->geocodeAddress($num, $bis, $rue, $ville, $cp);

        if (!$coords) {
            return null;
        }

        // Étape 2 : Chercher le secteur avec les coordonnées obtenues
        $sectorId = $this->findSectorByGps($operationId, $coords['lat'], $coords['lng']);

        if (!$sectorId) {
            // Retourner quand même les coordonnées GPS trouvées (utiles pour mettre à jour le passage)
            return [
                'sector_id' => null,
                'gps_lat' => $coords['lat'],
                'gps_lng' => $coords['lng']
            ];
        }

        return [
            'sector_id' => $sectorId,
            'gps_lat' => $coords['lat'],
            'gps_lng' => $coords['lng']
        ];
    }

    /**
     * Parse le format de polygone stocké en base (lat/lng#lat/lng#...)
     *
     * @param string $sectorString Format "lat/lng#lat/lng#..."
     * @return array Array de ['lat' => float, 'lng' => float]
     */
    private function parseSectorPolygon(string $sectorString): array
    {
        $polygon = [];
        $points = explode('#', rtrim($sectorString, '#'));

        foreach ($points as $point) {
            if (!empty($point) && strpos($point, '/') !== false) {
                list($lat, $lng) = explode('/', $point);
                $polygon[] = [
                    'lat' => (float)$lat,
                    'lng' => (float)$lng
                ];
            }
        }

        return $polygon;
    }

    /**
     * Vérifie si un point est à l'intérieur d'un polygone
     * Utilise l'algorithme de ray casting
     *
     * @param float $lat Latitude du point
     * @param float $lng Longitude du point
     * @param array $polygon Array de ['lat' => float, 'lng' => float]
     * @return bool
     */
    private function isPointInsidePolygon(float $lat, float $lng, array $polygon): bool
    {
        $x = $lat;
        $y = $lng;
        $inside = false;
        $count = count($polygon);

        for ($i = 0, $j = $count - 1; $i < $count; $j = $i++) {
            $xi = $polygon[$i]['lat'];
            $yi = $polygon[$i]['lng'];
            $xj = $polygon[$j]['lat'];
            $yj = $polygon[$j]['lng'];

            $intersect = (($yi > $y) != ($yj > $y))
                && ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi);

            if ($intersect) {
                $inside = !$inside;
            }
        }

        return $inside;
    }
}
