<?php

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Shapefile\ShapefileReader;

class SpatialFileConverter
{
    public static function convertToGeoJson(UploadedFile $file): ?array
    {
        $ext = strtolower($file->getClientOriginalExtension());
        $path = $file->getPathname();

        $geojson = match ($ext) {
            'geojson' => self::parseGeoJson($path),
            'kml' => self::kmlToGeoJson($path),
            'kmz' => self::kmzToGeoJson($path),
            'shp' => self::shapefileToGeoJson($path),
            'zip' => self::handleZip($path),
            default => null,
        };

        if (! $geojson) {
            return null;
        }

        return self::postProcess($geojson);
    }

    /* =======================
       POST PROCESADO PRO
    ======================= */

    private static function postProcess(array $geojson): array
    {
        $features = [];

        foreach ($geojson['features'] ?? [] as $feature) {

            if (! isset($feature['geometry'])) {
                continue;
            }

            $geometry = self::cleanGeometry($feature['geometry']);
            if (! $geometry) {
                continue;
            }

            $features[] = [
                'type' => 'Feature',
                'geometry' => $geometry,
                'properties' => self::normalizeProperties($feature['properties'] ?? []),
            ];
        }

        return [
            'type' => 'FeatureCollection',
            'features' => $features,
            'bbox' => self::calculateBBox($features),
            'centroid' => self::calculateCentroid($features),
        ];
    }

    /* =======================
       NORMALIZACIÓN
    ======================= */

    private static function normalizeProperties(array $props): array
    {
        return array_merge([
            'name' => '',
            'description' => '',
        ], $props);
    }

    /* =======================
       GEOMETRY CLEAN
    ======================= */

    private static function cleanGeometry(array $geom): ?array
    {
        if (! isset($geom['type'], $geom['coordinates'])) {
            return null;
        }

        if ($geom['type'] === 'Polygon') {
            $geom['coordinates'] = array_map(function ($ring) {
                if ($ring[0] !== end($ring)) {
                    $ring[] = $ring[0];
                }

                return $ring;
            }, $geom['coordinates']);
        }

        return $geom;
    }

    /* =======================
       BBOX
    ======================= */

    private static function calculateBBox(array $features): ?array
    {
        $coords = [];

        foreach ($features as $f) {
            $coords = array_merge($coords, self::flattenCoords($f['geometry']['coordinates']));
        }

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

        $lons = array_column($coords, 0);
        $lats = array_column($coords, 1);

        return [
            min($lons),
            min($lats),
            max($lons),
            max($lats),
        ];
    }

    /* =======================
       CENTROIDE SIMPLE
    ======================= */

    private static function calculateCentroid(array $features): ?array
    {
        $coords = [];

        foreach ($features as $f) {
            $coords = array_merge($coords, self::flattenCoords($f['geometry']['coordinates']));
        }

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

        $x = array_sum(array_column($coords, 0)) / count($coords);
        $y = array_sum(array_column($coords, 1)) / count($coords);

        return [$x, $y];
    }

    private static function flattenCoords($coords): array
    {
        $result = [];

        $iterator = function ($c) use (&$result, &$iterator) {
            if (! is_array($c)) {
                return;
            }

            if (isset($c[0]) && isset($c[1]) && is_numeric($c[0])) {
                $result[] = [$c[0], $c[1]];

                return;
            }

            foreach ($c as $item) {
                $iterator($item);
            }
        };

        $iterator($coords);

        return $result;
    }

    /* =======================
       GEOJSON
    ======================= */

    private static function parseGeoJson($path): ?array
    {
        $data = json_decode(file_get_contents($path), true);

        return json_last_error() === JSON_ERROR_NONE ? $data : null;
    }

    /* =======================
       KML (MEJORADO)
    ======================= */

    private static function kmlToGeoJson($path): ?array
    {
        libxml_use_internal_errors(true);
        $xml = simplexml_load_file($path);

        if (! $xml) {
            return null;
        }

        // Namespace-agnostic: usamos local-name() en el XPath para aceptar KMLs
        // con cualquier xmlns (opengis 2.2, earth.google 2.1/2.0, o sin xmlns).
        $placemarks = $xml->xpath('//*[local-name()="Placemark"]');
        if ($placemarks === false) {
            $placemarks = [];
        }

        $features = [];

        foreach ($placemarks as $pm) {
            $geom = self::parseKmlGeometry($pm);
            if (! $geom) {
                continue;
            }

            $features[] = [
                'type' => 'Feature',
                'geometry' => $geom,
                'properties' => [
                    'name' => trim((string) self::kmlChild($pm, 'name')),
                    'description' => trim((string) self::kmlChild($pm, 'description')),
                ],
            ];
        }

        return ['type' => 'FeatureCollection', 'features' => $features];
    }

    /** Descomprime un KMZ y parsea el .kml interno. */
    private static function kmzToGeoJson(string $path): ?array
    {
        $zip = new \ZipArchive;
        if ($zip->open($path) !== true) {
            return null;
        }

        $tmp = sys_get_temp_dir().'/kmz_'.uniqid();
        if (! @mkdir($tmp, 0777, true) && ! is_dir($tmp)) {
            $zip->close();

            return null;
        }
        $zip->extractTo($tmp);
        $zip->close();

        // Buscar el primer .kml (habitualmente doc.kml)
        $kml = null;
        $rii = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($tmp));
        foreach ($rii as $file) {
            if ($file->isFile() && strtolower($file->getExtension()) === 'kml') {
                $kml = $file->getPathname();
                break;
            }
        }
        $result = $kml ? self::kmlToGeoJson($kml) : null;

        // Limpieza
        self::rrmdir($tmp);

        return $result;
    }

    private static function rrmdir(string $dir): void
    {
        if (! is_dir($dir)) {
            return;
        }
        foreach (scandir($dir) ?: [] as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            $p = $dir.DIRECTORY_SEPARATOR.$item;
            is_dir($p) ? self::rrmdir($p) : @unlink($p);
        }
        @rmdir($dir);
    }

    /** Devuelve el primer hijo cuyo local-name coincida (sin depender del prefijo). */
    private static function kmlChild(\SimpleXMLElement $node, string $name): \SimpleXMLElement|string
    {
        $matches = $node->xpath('./*[local-name()="'.$name.'"]');

        return $matches ? $matches[0] : '';
    }

    private static function parseKmlGeometry($pm): ?array
    {
        // Todos los accesos van por xpath local-name para tolerar cualquier xmlns.
        $find = fn (string $tag) => $pm->xpath('./*[local-name()="'.$tag.'"]');
        $findDeep = fn (\SimpleXMLElement $n, string $tag) => $n->xpath('.//*[local-name()="'.$tag.'"]');

        if ($multi = $find('MultiGeometry')) {
            $geoms = [];
            foreach ($multi[0]->children() as $g) {
                $parsed = self::parseKmlGeometry($g);
                if ($parsed) {
                    $geoms[] = $parsed;
                }
            }

            return ['type' => 'GeometryCollection', 'geometries' => $geoms];
        }

        if ($point = $find('Point')) {
            $coords = self::parseKmlCoords((string) ($findDeep($point[0], 'coordinates')[0] ?? ''));

            return $coords ? ['type' => 'Point', 'coordinates' => $coords[0]] : null;
        }

        if ($line = $find('LineString')) {
            $coords = self::parseKmlCoords((string) ($findDeep($line[0], 'coordinates')[0] ?? ''));

            return $coords ? ['type' => 'LineString', 'coordinates' => $coords] : null;
        }

        if ($poly = $find('Polygon')) {
            $outer = $findDeep($poly[0], 'coordinates')[0] ?? '';
            $coords = self::parseKmlCoords((string) $outer);

            return $coords ? ['type' => 'Polygon', 'coordinates' => [$coords]] : null;
        }

        return null;
    }

    private static function parseKmlCoords(string $text): array
    {
        $coords = [];
        foreach (preg_split('/\s+/', trim($text)) as $pair) {
            $p = explode(',', $pair);
            if (count($p) >= 2) {
                $coords[] = [(float) $p[0], (float) $p[1]];
            }
        }

        return $coords;
    }

    /* =======================
       SHP REAL
    ======================= */

    private static function shapefileToGeoJson($path): ?array
    {
        try {
            $reader = new ShapefileReader($path);

            $features = [];

            while ($record = $reader->fetchRecord()) {
                if ($record->isDeleted()) {
                    continue;
                }

                $geom = json_decode($record->getGeometry()->toGeoJSON(), true);

                if (! $geom) {
                    continue;
                }

                $features[] = [
                    'type' => 'Feature',
                    'geometry' => $geom,
                    'properties' => $record->getDataArray(),
                ];
            }

            return ['type' => 'FeatureCollection', 'features' => $features];

        } catch (\Exception $e) {
            Log::error($e->getMessage());

            return null;
        }
    }

    /* =======================
       ZIP LIMPIO
    ======================= */

    private static function handleZip($zipPath): ?array
    {
        $zip = new \ZipArchive;

        if ($zip->open($zipPath) !== true) {
            return null;
        }

        $dir = sys_get_temp_dir().'/geo_'.uniqid();
        mkdir($dir);

        $zip->extractTo($dir);
        $zip->close();

        $result = null;

        foreach (scandir($dir) as $file) {
            $full = $dir.'/'.$file;
            $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));

            if ($ext === 'shp') {
                $result = self::shapefileToGeoJson($full);
                break;
            }

            if ($ext === 'kml') {
                $result = self::kmlToGeoJson($full);
                break;
            }
        }

        self::deleteDir($dir);

        return $result;
    }

    private static function deleteDir($dir)
    {
        foreach (glob("$dir/*") as $file) {
            is_dir($file) ? self::deleteDir($file) : unlink($file);
        }
        rmdir($dir);
    }
}
