"""
Valhalla tile lifecycle management.

Handles building, caching, and packing of Valhalla routing tiles from OSM
PBF data.  Separated from the matcher to keep infrastructure concerns
independent of matching logic.
"""

import json
import logging
import os
import shutil
import subprocess
import sys
import tarfile
from pathlib import Path
from typing import Optional, Tuple

from ..osm_utils import download_region, geocode_place_to_bbox, generate_cache_key

logger = logging.getLogger(__name__)


def get_cache_dir(custom_dir: Optional[os.PathLike] = None) -> Path:
    """Resolve the directory used for caching tiles and OSM data.

    Resolution order:
      1. *custom_dir* argument
      2. ``WALKINGPANDAS_CACHE_DIR`` environment variable
      3. ``./cache/walkingpandas/valhalla`` (relative to cwd)

    Args:
        custom_dir: Explicit cache directory override.

    Returns:
        Path: Resolved cache directory.
    """
    if custom_dir:
        return Path(custom_dir)
    env = os.environ.get("WALKINGPANDAS_CACHE_DIR")
    if env:
        return Path(env)
    return Path.cwd() / "cache" / "walkingpandas" / "valhalla"


def write_valhalla_config(config_path: Path, tile_dir: Path) -> None:
    """Write the minimal ``valhalla.json`` required for tile building.

    Args:
        config_path: Destination path for the config file.
        tile_dir: Directory where Valhalla tiles are stored.
    """
    config = {
        "mjolnir": {
            "tile_dir": str(tile_dir.resolve()),
            "max_cache_size": 1_000_000_000,
            "id_table_size": 1_300_000_000,
            "use_lru_mem_cache": False,
            "hierarchy": True,
            "shortcuts": True,
            "include_pedestrian": True,
            "include_bicycle": True,
            "include_driving": True,
            "data_processing": {
                "infer_internal_intersections": True,
                "infer_turn_channels": True,
                "apply_country_overrides": True,
                "use_admin_db": False,
            },
        }
    }
    config_path.write_text(json.dumps(config, indent=2))


def build_tiles(config_path: Path, pbf_path: Path) -> None:
    """Run the PyValhalla tile builder sub-process.

    Args:
        config_path: Path to the ``valhalla.json`` config.
        pbf_path: Path to the OSM PBF input file.

    Raises:
        RuntimeError: If the build fails or ``valhalla_build_tiles`` is
            not found.
    """
    logger.info(
        "Building Valhalla tiles from %s (this may take a few minutes)...",
        pbf_path.name,
    )

    try:
        subprocess.run(
            [
                sys.executable,
                "-m",
                "valhalla",
                "valhalla_build_tiles",
                "-c",
                str(config_path),
                str(pbf_path),
            ],
            check=True,
            capture_output=True,
            text=True,
            timeout=3600,
        )
        logger.info("Successfully built Valhalla tiles.")
    except subprocess.CalledProcessError as e:
        logger.error(
            "Tile building failed!\nSTDOUT:\n%s\nSTDERR:\n%s", e.stdout, e.stderr
        )
        raise RuntimeError(
            f"valhalla_build_tiles failed. See logs above. Error: {e}"
        ) from e
    except FileNotFoundError:
        raise RuntimeError(
            "valhalla_build_tiles not found. Ensure pyvalhalla is installed."
        ) from None


def pack_to_tar(tile_dir: Path, tar_path: Path) -> None:
    """Pack the generated tile directory into a portable ``.tar`` file.

    Args:
        tile_dir: Directory containing the built tiles.
        tar_path: Destination path for the ``.tar`` archive.
    """
    logger.info("Packing tiles into %s ...", tar_path.name)
    with tarfile.open(tar_path, "w") as tf:
        for f in tile_dir.rglob("*"):
            if f.is_file():
                tf.add(str(f), arcname=f.relative_to(tile_dir).as_posix())


def ensure_tiles(
    bbox: Optional[Tuple[float, float, float, float]] = None,
    place: Optional[str] = None,
    osm_pbf: Optional[os.PathLike] = None,
    cache_dir: Optional[os.PathLike] = None,
) -> Tuple[Path, Optional[Path]]:
    """Return paths to a Valhalla tile archive and the OSM XML file.

    Builds tiles from scratch on a cache miss; returns the cached archive
    on a hit.  Exactly one of *bbox*, *place*, or *osm_pbf* must be given.

    Args:
        bbox: ``(north, south, east, west)`` bounding box.
        place: Free-text place name (geocoded to a bbox internally).
        osm_pbf: Path to a pre-existing ``.osm.pbf`` file.
        cache_dir: Override for the tile cache directory.

    Returns:
        ``(tar_path, xml_path)`` -- *xml_path* may be ``None`` when the
        caller supplied a pre-existing PBF.

    Raises:
        ValueError: If not exactly one of *bbox*, *place*, or *osm_pbf*
            is provided.
        FileNotFoundError: If *osm_pbf* is given but does not exist.
    """
    if sum(x is not None for x in (bbox, place, osm_pbf)) != 1:
        raise ValueError("Exactly one of bbox, place, or osm_pbf must be provided.")

    cache_path = get_cache_dir(cache_dir)
    cache_path.mkdir(parents=True, exist_ok=True)

    key = generate_cache_key(bbox=bbox, place=place, osm_pbf=osm_pbf)
    tar_path = cache_path / f"{key}.tar"
    xml_path = cache_path / f"{key}.osm"

    if tar_path.exists():
        logger.info("Using cached Valhalla tiles from %s", tar_path)
        return tar_path, xml_path if xml_path.exists() else None

    logger.info("Cache miss for %s. Starting tile build process...", key)

    pbf_path: Path
    if osm_pbf is not None:
        pbf_path = Path(osm_pbf).resolve()
        if not pbf_path.exists():
            raise FileNotFoundError(f"Provided osm_pbf not found: {pbf_path}")
        xml_path = None
    else:
        pbf_path = cache_path / f"{key}.osm.pbf"
        target_bbox = bbox if bbox else geocode_place_to_bbox(place)
        logger.debug("Delegating download to osm_utils...")
        download_region(
            bbox=target_bbox, output_xml=str(xml_path), output_pbf=str(pbf_path)
        )

    tile_dir = cache_path / key
    tile_dir.mkdir(parents=True, exist_ok=True)
    config_path = tile_dir / "valhalla.json"

    write_valhalla_config(config_path, tile_dir)
    build_tiles(config_path, pbf_path)
    pack_to_tar(tile_dir, tar_path)

    shutil.rmtree(tile_dir, ignore_errors=True)
    logger.info("Tiles successfully built and cached at %s", tar_path)

    return tar_path, xml_path
