"""
REST API for S-57 ENC chart management.
"""
import asyncio
import io
import logging
import shutil
import tempfile
from pathlib import Path

import httpx
from fastapi import APIRouter, HTTPException, UploadFile, File
from fastapi.responses import JSONResponse

from services.chart_manager import (
    install_from_zip, install_from_enc, list_cells,
    delete_cell, get_all_features, get_all_depths,
    get_all_land, get_all_hazards, get_all_zones,
    CHARTS_DIR, set_meta, get_region,
    install_from_csv_zip, install_from_csv_dir,
)

router = APIRouter(prefix="/charts", tags=["charts"])
log    = logging.getLogger(__name__)

# Florida/Miami NOAA ENC cells — name → description
NOAA_FLORIDA_CELLS = {
    "US5FL17M": "Miami Harbor",
    "US5FL16M": "Biscayne Bay",
    "US4FL15M": "Florida Keys — East",
    "US4FL16M": "Florida Keys — West",
    "US5FL18M": "Port Everglades (Fort Lauderdale)",
    "US5FL19M": "Lake Worth / Palm Beach",
    "US4FL14M": "Florida Bay",
    "US5FL13M": "Card Sound / Barnes Sound",
    "US4FL13M": "Straits of Florida",
    "US5FL12M": "Hawk Channel East",
    "US3FL04M": "Florida Straits Overview",
}

NOAA_BASE = "https://charts.noaa.gov/ENCs/{cell}.zip"

# Catálogo local NOAA: cell_id → (west, south, east, north, scale, description)
# Fuente: NOAA Office of Coast Survey ENC catalog
NOAA_CATALOG: list[dict] = [
  # ── FLORIDA / MIAMI ───────────────────────────────────────────────────────
  {"id":"US5FL17M","name":"Miami Harbor",                 "scale":10000,  "w":-80.20,"s":25.73,"e":-80.10,"n":25.82},
  {"id":"US5FL16M","name":"Biscayne Bay",                 "scale":40000,  "w":-80.32,"s":25.42,"e":-80.08,"n":25.78},
  {"id":"US5FL18M","name":"Port Everglades",              "scale":10000,  "w":-80.15,"s":26.05,"e":-80.06,"n":26.13},
  {"id":"US5FL19M","name":"Lake Worth — Palm Beach",      "scale":10000,  "w":-80.10,"s":26.67,"e":-80.01,"n":26.76},
  {"id":"US4FL15M","name":"Florida Keys — East",          "scale":80000,  "w":-80.60,"s":24.40,"e":-79.80,"n":25.50},
  {"id":"US4FL16M","name":"Florida Keys — West",          "scale":80000,  "w":-82.00,"s":24.40,"e":-80.60,"n":25.20},
  {"id":"US4FL14M","name":"Florida Bay",                  "scale":80000,  "w":-81.50,"s":24.80,"e":-80.30,"n":25.70},
  {"id":"US5FL13M","name":"Card Sound / Barnes Sound",    "scale":20000,  "w":-80.50,"s":25.10,"e":-80.25,"n":25.45},
  {"id":"US4FL13M","name":"Straits of Florida",           "scale":150000, "w":-81.00,"s":24.00,"e":-79.50,"n":26.00},
  {"id":"US3FL04M","name":"Florida Straits Overview",     "scale":600000, "w":-85.00,"s":22.00,"e":-78.00,"n":28.00},
  {"id":"US5FL20M","name":"Miami Beach / Government Cut", "scale":5000,   "w":-80.14,"s":25.75,"e":-80.10,"n":25.78},
  {"id":"US5FL21M","name":"Fort Lauderdale — New River",  "scale":10000,  "w":-80.20,"s":26.07,"e":-80.09,"n":26.15},
  {"id":"US5FL14M","name":"Key West Harbor",              "scale":15000,  "w":-81.86,"s":24.52,"e":-81.72,"n":24.60},
  {"id":"US5FL15M","name":"Key West Approaches",         "scale":40000,  "w":-82.20,"s":24.30,"e":-81.50,"n":24.80},
  {"id":"US4FL17M","name":"Tampa Bay",                    "scale":80000,  "w":-82.80,"s":27.40,"e":-82.20,"n":28.10},
  {"id":"US5FL01M","name":"Tampa Bay — Port Tampa",       "scale":15000,  "w":-82.60,"s":27.83,"e":-82.40,"n":27.95},
  {"id":"US5FL04M","name":"Jacksonville",                 "scale":15000,  "w":-81.70,"s":30.27,"e":-81.55,"n":30.42},
  # ── NEW YORK / NEW JERSEY ────────────────────────────────────────────────
  {"id":"US5NY01M","name":"New York Harbor",              "scale":10000,  "w":-74.15,"s":40.57,"e":-73.95,"n":40.72},
  {"id":"US5NY02M","name":"Upper New York Bay",           "scale":10000,  "w":-74.10,"s":40.63,"e":-73.95,"n":40.75},
  {"id":"US5NY13M","name":"New York — Lower Bay",         "scale":20000,  "w":-74.20,"s":40.48,"e":-73.90,"n":40.68},
  {"id":"US4NY10M","name":"New York Approaches",          "scale":80000,  "w":-74.50,"s":40.20,"e":-73.50,"n":41.00},
  {"id":"US5NY05M","name":"East River",                   "scale":10000,  "w":-74.02,"s":40.65,"e":-73.90,"n":40.80},
  {"id":"US5NY06M","name":"Hudson River — New York",      "scale":10000,  "w":-74.05,"s":40.68,"e":-73.95,"n":40.85},
  {"id":"US5NJ01M","name":"Port Newark / Elizabeth",      "scale":10000,  "w":-74.18,"s":40.62,"e":-74.08,"n":40.72},
  {"id":"US5NJ02M","name":"Newark Bay",                   "scale":10000,  "w":-74.18,"s":40.64,"e":-74.05,"n":40.73},
  {"id":"US3NY01M","name":"New York — Long Island Sound Overview","scale":400000,"w":-74.50,"s":40.00,"e":-71.50,"n":41.50},
  # ── CHESAPEAKE / MID-ATLANTIC ────────────────────────────────────────────
  {"id":"US5MD01M","name":"Baltimore Harbor",             "scale":10000,  "w":-76.65,"s":39.20,"e":-76.50,"n":39.32},
  {"id":"US4MD01M","name":"Chesapeake Bay — Northern",    "scale":80000,  "w":-76.70,"s":38.80,"e":-75.80,"n":39.70},
  {"id":"US4VA01M","name":"Chesapeake Bay — Southern",    "scale":80000,  "w":-76.70,"s":36.80,"e":-75.80,"n":38.00},
  {"id":"US5VA01M","name":"Norfolk — Hampton Roads",      "scale":15000,  "w":-76.40,"s":36.82,"e":-76.20,"n":37.00},
  {"id":"US5PA01M","name":"Philadelphia",                 "scale":10000,  "w":-75.20,"s":39.88,"e":-75.06,"n":39.98},
  # ── NEW ENGLAND ──────────────────────────────────────────────────────────
  {"id":"US5MA01M","name":"Boston Harbor",                "scale":10000,  "w":-71.10,"s":42.28,"e":-70.95,"n":42.40},
  {"id":"US4MA01M","name":"Cape Cod — Approaches",       "scale":80000,  "w":-71.20,"s":41.40,"e":-69.80,"n":42.50},
  {"id":"US5RI01M","name":"Providence — Narragansett Bay","scale":15000,  "w":-71.48,"s":41.60,"e":-71.35,"n":41.85},
  # ── GULF COAST ───────────────────────────────────────────────────────────
  {"id":"US5LA01M","name":"New Orleans — Lower Mississippi","scale":10000,"w":-89.85,"s":29.85,"e":-89.70,"n":30.05},
  {"id":"US5TX01M","name":"Houston Ship Channel",         "scale":10000,  "w":-95.10,"s":29.70,"e":-94.90,"n":29.85},
  {"id":"US5TX02M","name":"Galveston Bay",                "scale":10000,  "w":-94.95,"s":29.25,"e":-94.65,"n":29.60},
  {"id":"US4TX01M","name":"Texas — Gulf Coast",           "scale":80000,  "w":-97.50,"s":25.50,"e":-93.50,"n":30.00},
  {"id":"US5MS01M","name":"Pascagoula",                   "scale":10000,  "w":-88.65,"s":30.28,"e":-88.48,"n":30.38},
  {"id":"US5AL01M","name":"Mobile Bay",                   "scale":20000,  "w":-88.15,"s":30.50,"e":-87.90,"n":30.75},
  # ── WEST COAST ───────────────────────────────────────────────────────────
  {"id":"US5CA01M","name":"Los Angeles — Long Beach",     "scale":15000,  "w":-118.30,"s":33.70,"e":-118.10,"n":33.82},
  {"id":"US5CA02M","name":"San Diego Bay",                "scale":10000,  "w":-117.22,"s":32.60,"e":-117.10,"n":32.75},
  {"id":"US5CA03M","name":"San Francisco Bay",            "scale":20000,  "w":-122.55,"s":37.60,"e":-122.15,"n":37.90},
  {"id":"US5CA04M","name":"San Francisco — Oakland",      "scale":10000,  "w":-122.42,"s":37.75,"e":-122.22,"n":37.86},
  {"id":"US5OR01M","name":"Columbia River — Astoria",     "scale":15000,  "w":-124.00,"s":46.10,"e":-123.75,"n":46.30},
  {"id":"US5WA01M","name":"Seattle — Elliott Bay",        "scale":10000,  "w":-122.45,"s":47.55,"e":-122.30,"n":47.66},
  {"id":"US5WA02M","name":"Puget Sound — Tacoma",         "scale":15000,  "w":-122.55,"s":47.15,"e":-122.35,"n":47.35},
  {"id":"US4WA01M","name":"Puget Sound Overview",         "scale":80000,  "w":-123.30,"s":46.80,"e":-122.00,"n":48.60},
  # ── ALASKA ───────────────────────────────────────────────────────────────
  {"id":"US5AK01M","name":"Anchorage",                    "scale":15000,  "w":-150.10,"s":61.15,"e":-149.75,"n":61.32},
  {"id":"US4AK01M","name":"Cook Inlet — Overview",        "scale":80000,  "w":-153.00,"s":59.00,"e":-148.00,"n":63.00},
  # ── GREAT LAKES ──────────────────────────────────────────────────────────
  {"id":"US5MI01M","name":"Detroit River",                "scale":10000,  "w":-83.15,"s":42.22,"e":-82.95,"n":42.40},
  {"id":"US5IL01M","name":"Chicago Harbor",               "scale":10000,  "w":-87.68,"s":41.82,"e":-87.55,"n":41.92},
  {"id":"US5OH01M","name":"Cleveland Harbor",             "scale":10000,  "w":-81.75,"s":41.48,"e":-81.60,"n":41.56},
  # ── CARIBBEAN / PUERTO RICO ──────────────────────────────────────────────
  {"id":"US5PR01M","name":"San Juan Harbor",              "scale":10000,  "w":-66.15,"s":18.43,"e":-66.05,"n":18.50},
  {"id":"US4PR01M","name":"Puerto Rico — North Coast",    "scale":80000,  "w":-67.50,"s":17.80,"e":-65.00,"n":18.80},
  {"id":"US5VI01M","name":"Charlotte Amalie — St. Thomas","scale":10000, "w":-64.97,"s":18.30,"e":-64.88,"n":18.37},
  # ── HAWAII ───────────────────────────────────────────────────────────────
  {"id":"US5HI01M","name":"Honolulu Harbor",              "scale":10000,  "w":-157.90,"s":21.27,"e":-157.83,"n":21.34},
  {"id":"US4HI01M","name":"Oahu — Overview",              "scale":80000,  "w":-158.50,"s":21.00,"e":-157.50,"n":21.80},
]


@router.get("/cells")
def get_cells():
    return list_cells()


@router.get("/florida-catalog")
def florida_catalog():
    installed = {c["id"] for c in list_cells()}
    return [
        {"id": k, "description": v, "installed": k in installed}
        for k, v in NOAA_FLORIDA_CELLS.items()
    ]


@router.get("/noaa-catalog")
def noaa_catalog():
    installed = {c["id"] for c in list_cells()}
    return [
        {"id": c["id"], "description": c["name"], "scale": c["scale"], "installed": c["id"] in installed}
        for c in NOAA_CATALOG
    ]


@router.get("/noaa-nearby")
def noaa_nearby(west: float, south: float, east: float, north: float):
    """
    Return NOAA ENC cells that intersect the given bbox.
    Checks both the hardcoded catalog AND installed cells' actual bounding boxes.
    """
    cells_info  = list_cells()
    installed   = {c["id"] for c in cells_info}
    # Build actual-bbox map for installed cells
    actual_bbox = {c["id"]: c["bbox"] for c in cells_info if c["bbox"]}

    seen    = set()
    matches = []

    # 1. Installed cells with real bounding boxes
    for cell_id, bbox in actual_bbox.items():
        w, s, e, n = bbox
        if e < west or w > east or n < south or s > north:
            continue
        # Look up catalog name if available
        name  = next((c["name"] for c in NOAA_CATALOG if c["id"] == cell_id), cell_id)
        scale = next((c["scale"] for c in NOAA_CATALOG if c["id"] == cell_id), 0)
        matches.append({"id": cell_id, "name": name, "scale": scale, "installed": True})
        seen.add(cell_id)

    # 2. Catalog cells not yet installed
    for cell in NOAA_CATALOG:
        if cell["id"] in seen:
            continue
        if cell["e"] < west or cell["w"] > east:
            continue
        if cell["n"] < south or cell["s"] > north:
            continue
        matches.append({
            "id":        cell["id"],
            "name":      cell["name"],
            "scale":     cell["scale"],
            "installed": cell["id"] in installed,
        })

    matches.sort(key=lambda c: c["scale"])
    return matches


import re as _re
_CELL_RE = _re.compile(r'^US[1-5][A-Z]{2}\d{2}M$')

@router.post("/download-noaa/{cell_id}")
async def download_noaa(cell_id: str):
    cell_id = cell_id.upper()
    if not _CELL_RE.match(cell_id):
        raise HTTPException(400, f"Invalid cell_id format: {cell_id}")
    url = NOAA_BASE.format(cell=cell_id)
    log.info("Downloading NOAA ENC %s from %s", cell_id, url)
    try:
        async with httpx.AsyncClient(timeout=120) as client:
            r = await client.get(url, follow_redirects=True)
        if r.status_code == 404:
            raise HTTPException(404, f"Cell {cell_id} not found on NOAA server")
        if r.status_code != 200:
            raise HTTPException(502, f"NOAA returned {r.status_code}")
    except httpx.TimeoutException:
        raise HTTPException(504, "Timeout downloading from NOAA")

    with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
        tmp.write(r.content)
        tmp_path = Path(tmp.name)

    try:
        installed = await asyncio.get_event_loop().run_in_executor(
            None, install_from_zip, tmp_path)
    except Exception as e:
        raise HTTPException(500, str(e))
    finally:
        tmp_path.unlink(missing_ok=True)

    return {"installed": installed}


def _zip_contains_csvs(zip_path: Path) -> bool:
    """Return True if the ZIP has *.csv files but no *.000 ENC files."""
    import zipfile as _zf
    with _zf.ZipFile(zip_path) as z:
        names = z.namelist()
    has_csv = any(n.lower().endswith(".csv") for n in names)
    has_enc = any(n.upper().endswith(".000") for n in names)
    return has_csv and not has_enc


@router.post("/upload")
async def upload_chart(file: UploadFile = File(...)):
    """
    Universal chart upload.

    Accepts:
      • .000  — single S-57 ENC cell
      • .zip  — either a NOAA ENC zip (contains .000) OR a CSV-based custom
                chart zip (contains *.csv, no .000).  The ZIP auto-detection
                determines which parser is used.
    """
    suffix = Path(file.filename or "").suffix.lower()
    if suffix not in (".zip", ".000"):
        raise HTTPException(400, "Only .zip or .000 files accepted")

    data = await file.read()
    with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
        tmp.write(data)
        tmp_path = Path(tmp.name)

    try:
        if suffix == ".zip":
            # Auto-detect: CSV zip vs ENC zip
            if _zip_contains_csvs(tmp_path):
                installed = await asyncio.get_event_loop().run_in_executor(
                    None, install_from_csv_zip, tmp_path)
            else:
                installed = await asyncio.get_event_loop().run_in_executor(
                    None, install_from_zip, tmp_path)
        else:
            orig_name = Path(file.filename).stem.upper() if file.filename else None
            cell_id = await asyncio.get_event_loop().run_in_executor(
                None, install_from_enc, tmp_path, orig_name)
            installed = [cell_id]
    except Exception as e:
        raise HTTPException(500, str(e))
    finally:
        tmp_path.unlink(missing_ok=True)

    return {"installed": installed}


@router.post("/upload-csv")
async def upload_csv_chart(file: UploadFile = File(...),
                           cell_id: str | None = None):
    """
    Upload a ZIP archive containing CSV navigation-aid files to create a
    custom chart cell.  Use this when your source data is in DIMAR / custom
    CSV format rather than S-57 .000.

    The cell_id query parameter overrides the inferred name from the ZIP.

    Workflow:
      1. Edit BOYLAT.csv, BOYCAR.csv, BOYSPEC.csv, etc. in your local
         capas_ctg/ folder.
      2. Zip the entire folder.
      3. POST the zip here (optionally with ?cell_id=BAHIA_DE_CARTAGENA).
      4. AidsMonitoring reads the CSVs directly, preserving all light
         attributes (LITCHR, SIGPER, VALNMR …) without GDAL round-trip loss.
    """
    if not (file.filename or "").lower().endswith(".zip"):
        raise HTTPException(400, "Only .zip files accepted for CSV upload")

    data = await file.read()
    with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
        tmp.write(data)
        tmp_path = Path(tmp.name)

    try:
        if cell_id:
            # Extract to temp dir then install with explicit cell_id
            import zipfile as _zf
            import tempfile as _tf
            with _tf.TemporaryDirectory() as td:
                td_p = Path(td)
                with _zf.ZipFile(tmp_path) as z:
                    for member in z.namelist():
                        if member.lower().endswith(".csv"):
                            data_bytes = z.read(member)
                            (td_p / Path(member).name).write_bytes(data_bytes)
                installed_id = await asyncio.get_event_loop().run_in_executor(
                    None, install_from_csv_dir, td_p, cell_id)
            installed = [installed_id]
        else:
            installed = await asyncio.get_event_loop().run_in_executor(
                None, install_from_csv_zip, tmp_path)
    except Exception as e:
        raise HTTPException(500, str(e))
    finally:
        tmp_path.unlink(missing_ok=True)

    return {"installed": installed}


@router.delete("/cells/{cell_id}")
def remove_cell(cell_id: str):
    delete_cell(cell_id)
    return {"deleted": cell_id.upper()}


@router.patch("/cells/{cell_id}/region")
def set_cell_region(cell_id: str, region: str):
    region = (region or "").upper().strip()
    if region not in ("A", "B"):
        raise HTTPException(400, "region must be 'A' or 'B'")
    try:
        set_meta(cell_id, region=region)
    except FileNotFoundError:
        raise HTTPException(404, f"Cell {cell_id} not installed")
    return {"id": cell_id.upper(), "region": region}


# Chart caches are rebuilt at server startup / explicit /rebuild-cache.
# Between rebuilds, GeoJSON content is stable, so we let the browser cache
# the response for an hour. ETag/304 would be safer but max-age is enough
# for typical operator sessions and avoids redownloading 350 MB of depths.
_CHART_CACHE_HEADERS = {"Cache-Control": "private, max-age=3600"}


def _bbox(w, s, e, n):
    return (w, s, e, n) if None not in (w, s, e, n) else None


@router.get("/features")
def chart_features(w: float | None = None, s: float | None = None,
                   e: float | None = None, n: float | None = None):
    return JSONResponse(get_all_features(_bbox(w, s, e, n)),
                        headers=_CHART_CACHE_HEADERS)


@router.get("/depths")
def chart_depths(w: float | None = None, s: float | None = None,
                 e: float | None = None, n: float | None = None):
    return JSONResponse(get_all_depths(_bbox(w, s, e, n)),
                        headers=_CHART_CACHE_HEADERS)


@router.get("/land")
def chart_land(w: float | None = None, s: float | None = None,
               e: float | None = None, n: float | None = None):
    return JSONResponse(get_all_land(_bbox(w, s, e, n)),
                        headers=_CHART_CACHE_HEADERS)


@router.get("/hazards")
def chart_hazards(w: float | None = None, s: float | None = None,
                  e: float | None = None, n: float | None = None):
    return JSONResponse(get_all_hazards(_bbox(w, s, e, n)),
                        headers=_CHART_CACHE_HEADERS)


@router.get("/zones")
def chart_zones(w: float | None = None, s: float | None = None,
                e: float | None = None, n: float | None = None):
    return JSONResponse(get_all_zones(_bbox(w, s, e, n)),
                        headers=_CHART_CACHE_HEADERS)


@router.post("/rebuild-cache")
async def rebuild_cache():
    """Re-parse ALL installed ENC files and regenerate feature cache."""
    from services.chart_manager import CHARTS_DIR, _build_cache
    import asyncio
    rebuilt = []
    for cell_dir in CHARTS_DIR.iterdir():
        if not cell_dir.is_dir():
            continue
        enc_files = list(cell_dir.glob("*.000"))
        if not enc_files:
            continue
        await asyncio.get_event_loop().run_in_executor(
            None, _build_cache, cell_dir.name, enc_files[0])
        rebuilt.append(cell_dir.name)
    return {"rebuilt": rebuilt}


@router.post("/cells/{cell_id}/rebuild-from-csv")
async def rebuild_cell_from_csv(cell_id: str, file: UploadFile = File(...)):
    """
    Update an existing cell's features.geojson by re-uploading its CSV zip.
    Equivalent to DELETE + upload-csv but preserves meta.json settings
    (e.g. region override).
    """
    if not (file.filename or "").lower().endswith(".zip"):
        raise HTTPException(400, "Only .zip files accepted")
    data = await file.read()
    with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
        tmp.write(data)
        tmp_path = Path(tmp.name)
    try:
        import zipfile as _zf, tempfile as _tf
        with _tf.TemporaryDirectory() as td:
            td_p = Path(td)
            with _zf.ZipFile(tmp_path) as z:
                for member in z.namelist():
                    if member.lower().endswith(".csv"):
                        (td_p / Path(member).name).write_bytes(z.read(member))
            installed_id = await asyncio.get_event_loop().run_in_executor(
                None, install_from_csv_dir, td_p, cell_id)
    except Exception as e:
        raise HTTPException(500, str(e))
    finally:
        tmp_path.unlink(missing_ok=True)
    return {"rebuilt": installed_id}


@router.post("/cells/{cell_id}/rebuild")
async def rebuild_cell(cell_id: str):
    """Re-parse a single ENC cell and regenerate its feature cache."""
    from services.chart_manager import CHARTS_DIR, _build_cache
    import asyncio
    cell_dir = CHARTS_DIR / cell_id
    if not cell_dir.is_dir():
        from fastapi import HTTPException
        raise HTTPException(status_code=404, detail=f"Cell '{cell_id}' not found")
    enc_files = list(cell_dir.glob("*.000"))
    if not enc_files:
        from fastapi import HTTPException
        raise HTTPException(status_code=404, detail=f"No .000 file in cell '{cell_id}'")
    await asyncio.get_event_loop().run_in_executor(
        None, _build_cache, cell_id, enc_files[0])
    return {"rebuilt": cell_id}
