#!/usr/bin/env python3

# Convert spatial CSVs in cwd/input to EPSG:3031 shapefiles in cwd/output.
#
# Supported geometries:
#   - Latitude/Longitude points
#   - Rectangles from start/end coordinates
#   - Rectangles from N/S/E/W bounds
#
# Skips VME_risk_areas_taxa.csv (non-spatial)
#
# Install:
#   pip install pandas geopandas shapely

from __future__ import annotations

import csv
from pathlib import Path

import geopandas as gpd
import pandas as pd
from shapely.geometry import MultiPolygon, Point, box


# =============================================================================
# CONFIGURATION
# =============================================================================

BASE_DIR = Path.cwd()
INPUT_DIR = BASE_DIR / "input"
OUTPUT_DIR = BASE_DIR / "output"

SRC_CRS = "EPSG:4326"
OUT_CRS = "EPSG:3031"

SKIP_FILES = {"VME_risk_areas_taxa.csv"}
DATELINE_SPLIT_THRESHOLD_DEG = 180.0


# =============================================================================
# HELPERS
# =============================================================================

def sniff_delimiter(path: Path) -> str:
    sample = path.read_text(encoding="utf-8-sig", errors="replace")[:50_000]
    try:
        return csv.Sniffer().sniff(sample, delimiters=[",", "\t", ";", "|"]).delimiter
    except csv.Error:
        return ","


def to_float(value) -> float | None:
    if value is None:
        return None

    text = str(value).strip()
    if not text or text.upper() in {"NA", "N/A", "NULL"}:
        return None

    try:
        return float(text)
    except (TypeError, ValueError):
        return None


def remove_existing_shapefile(shp_path: Path) -> None:
    for ext in (".shp", ".shx", ".dbf", ".prj", ".cpg", ".qix", ".fix"):
        path = shp_path.with_suffix(ext)
        if path.exists():
            path.unlink()


def rectangle_from_coordinates(lon1, lat1, lon2, lat2):
    lon1, lat1 = to_float(lon1), to_float(lat1)
    lon2, lat2 = to_float(lon2), to_float(lat2)

    if None in (lon1, lat1, lon2, lat2):
        return None

    min_lat, max_lat = sorted((lat1, lat2))
    min_lon, max_lon = sorted((lon1, lon2))

    if max_lon - min_lon > DATELINE_SPLIT_THRESHOLD_DEG:
        return MultiPolygon([
            box(max_lon, min_lat, 180.0, max_lat),
            box(-180.0, min_lat, min_lon, max_lat),
        ])

    return box(min_lon, min_lat, max_lon, max_lat)


def point_from_lonlat(lon, lat):
    lon, lat = to_float(lon), to_float(lat)
    return None if lon is None or lat is None else Point(lon, lat)


# =============================================================================
# PROCESSING
# =============================================================================

def process_csv(csv_path: Path) -> None:
    df = pd.read_csv(
        csv_path,
        sep=sniff_delimiter(csv_path),
        dtype=str,
        encoding="utf-8-sig",
    )
    df.columns = [c.strip() for c in df.columns]

    has_bounds = all(c in df.columns for c in (
        "Northern Boundary", "Southern Boundary",
        "Eastern Boundary", "Western Boundary",
    ))
    has_pair = all(c in df.columns for c in (
        "Latitude start", "Longitude start",
        "Latitude end", "Longitude end",
    ))
    has_point = all(c in df.columns for c in ("Latitude", "Longitude"))

    geoms = []

    if has_bounds:
        for _, row in df.iterrows():
            geoms.append(rectangle_from_coordinates(
                row["Western Boundary"], row["Southern Boundary"],
                row["Eastern Boundary"], row["Northern Boundary"],
            ))

    elif has_pair:
        for _, row in df.iterrows():
            geoms.append(rectangle_from_coordinates(
                row["Longitude start"], row["Latitude start"],
                row["Longitude end"], row["Latitude end"],
            ))

    elif has_point:
        for _, row in df.iterrows():
            geoms.append(point_from_lonlat(
                row["Longitude"], row["Latitude"]
            ))

    else:
        print(f"[SKIP] {csv_path.name}: unrecognised spatial fields")
        return

    invalid = sum(g is None for g in geoms)

    gdf = gpd.GeoDataFrame(df, geometry=geoms, crs=SRC_CRS)
    gdf = gdf[~gdf.geometry.isna()].copy()

    if gdf.empty:
        print(f"[SKIP] {csv_path.name}: no valid geometries")
        return

    gdf = gdf.to_crs(OUT_CRS)

    output_path = OUTPUT_DIR / f"{csv_path.stem}.shp"
    remove_existing_shapefile(output_path)

    gdf.to_file(
        output_path,
        driver="ESRI Shapefile",
        index=False,
    )

    print(
        f"[OK] {csv_path.name} -> {output_path.name} | "
        f"features={len(gdf)} | skipped_invalid={invalid}"
    )


# =============================================================================
# MAIN
# =============================================================================

def main() -> None:
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

    csv_files = sorted(INPUT_DIR.glob("*.csv"))
    if not csv_files:
        raise FileNotFoundError(f"No CSV files found in:\n{INPUT_DIR}")

    for csv_path in csv_files:
        if csv_path.name in SKIP_FILES:
            print(f"[SKIP] {csv_path.name}: non-spatial lookup table")
            continue

        try:
            process_csv(csv_path)
        except Exception as error:
            print(f"[FAIL] {csv_path.name}: {error}")


if __name__ == "__main__":
    main()