"""The City: open modern cities for a custom 3D game (GTA-like streets, downtowns, traffic and crime).

A custom 3D game whose words name an open modern city gets the city kit (JavaScript ES modules,
built from the city source as `citykit.tar.gz`, code only) and the builder contract joins the brief. The builder places
streets, buildings and traffic with `createCity` (CITY.md) instead of drawing them. Player vehicles come from the Garage
kit when it is attached. The Engine stages attachments at `.forge/attachments/att<N>/original.<ext>` in order, like
games_garage and games_town. A missing bundle = no city kit, the build goes on as before.
"""

from __future__ import annotations

import base64
import hashlib
import re
from pathlib import Path
from typing import Any

DIST = Path("/srv/forge-assets/dist")  # gex44 host tree, bind-mounted read-only into the Factory

try:  # the Factory package path; tests import the sibling file
    from forge_corp.api.routes.games_town import town_kind
except ImportError:  # pragma: no cover
    from games_town import town_kind

_CITY_RE = re.compile(
    r"\b(gta|grand theft(?: auto)?|open[- ]city|open[- ]world city|downtown|crime city|gangsters?|mafia|taxi driver|"
    r"city streets?|city blocks?|metropolis|metropolises|skyscrapers?|new york|hong kong|big city|modern city|urban crime|"
    r"los angeles|chicago|miami)\b", re.I)
# these are crime-game words, a city only when the game also says city, streets or urban
_CRIME_RE = re.compile(r"\b(heists?|police chases?|car chases?|getaways?|cops?|police)\b", re.I)
_CITY_CONTEXT_RE = re.compile(r"\b(city|cities|urban|streets?|avenues?|downtown)\b", re.I)
_NOT_CITY_RE = re.compile(
    r"\b(city[- ]builders?|city[- ]building|cities[: ]+skylines|sim ?city|city sims?|tycoon|chess|checkers|board game|"
    r"racing (?:circuits?|tracks?)|race tracks?|grand prix|formula (?:1|one)|circuits?|side[- ]scroll\w*|pixel|2d|"
    r"space|spaceships?|starships?|aliens?|asteroids?|galax\w*)\b", re.I)


def _card_text(card: Any, prompt: str = "") -> str:
    world = getattr(card, "world", None)
    parts = [getattr(card, "title", ""), getattr(card, "pitch", ""), getattr(card, "genre", ""), getattr(card, "core_loop", ""),
             getattr(card, "first_version", ""), " ".join(getattr(card, "mood", None) or []),
             getattr(world, "setting", None) or "", getattr(world, "look", None) or "", prompt or ""]
    return " ".join(str(p) for p in parts if p)


def city_fits(card: Any) -> bool:
    world = getattr(card, "world", None)
    return (getattr(card, "lane", None) == "custom" and getattr(card, "stage", None) == "none" and world is not None
            and getattr(world, "view", None) in ("first", "third", "top"))


def city_kind(card: Any, prompt: str = "") -> str | None:
    """'city' or None, deterministic from the words. A medieval or Breton settlement is never a city, and neither is a
    racing circuit, a city builder or a side view game."""
    text = _card_text(card, prompt)
    if town_kind(card, prompt) is not None or _NOT_CITY_RE.search(text):
        return None
    if _CITY_RE.search(text):
        return "city"
    if _CRIME_RE.search(text) and _CITY_CONTEXT_RE.search(text):
        return "city"
    return None


def city_sha(assets_dir: Path) -> str | None:
    """sha256 of the city kit tarball (the one `citykit.sha` names), for meta; None when the kit is not vendored."""
    try:
        return (assets_dir / "citykit.sha").read_text().strip() or None
    except OSError:
        pass
    try:
        return hashlib.sha256((assets_dir / "citykit.tar.gz").read_bytes()).hexdigest()
    except OSError:
        return None


def _data_url(mime: str, data: bytes) -> str:
    return f"data:{mime};base64," + base64.b64encode(data).decode()


def city_contract(code_id: str, garage: bool) -> str:
    """The builder contract for the city kit. code_id is the attachment id (att2...)."""
    cars = (" The player's own vehicles come from the Garage kit (`createVehicle`, see the Garage contract); the city brings "
            "the streets, buildings and its own moving traffic."
            if garage else " No Garage kit is attached, so the cars are the city kit's own; never draw a car by hand.")
    return (
        " THE CITY COMES FROM OUR CITY KIT. Attachment " + code_id + " is the city kit (JavaScript ES modules, three.js, a gzipped tar). First run: `mkdir -p src/lib/city && tar -xzf .forge/attachments/" + code_id +
        "/original.* -C src/lib/city`, then read `src/lib/city/CITY.md` and build the city with `createCity` from "
        "`src/lib/city/game.mjs`. Never draw buildings or roads yourself: the kit lays the streets, blocks and buildings. "
        "Spawn the player at the city's `spawn`. Collide with the returned `colliders` and with `carPoses()` for every car "
        "on the road." + cars + " Do not edit src/lib/city."
    )


def city_attachments(card: Any, first_index: int, prompt: str = "", assets_dir: Path = DIST, garage: bool = False
                     ) -> tuple[list[dict[str, Any]], str, dict] | None:
    """(attachments, contract, meta) or None. first_index = attachments already in the brief (the Engine numbers att1,
    att2... in list order). garage = the Garage kit is attached too. None when the game is not a city or the bundle is
    missing: the city kit is a bonus, never a reason to fail a build."""
    if not city_fits(card):
        return None
    if city_kind(card, prompt) is None:
        return None
    try:
        code = (assets_dir / "citykit.tar.gz").read_bytes()
    except OSError:
        return None
    code_id = f"att{first_index + 1}"
    atts = [
        {"url": _data_url("application/gzip", code), "filename": "citykit.tar.gz", "alt": "City kit (JavaScript, three.js)",
         "role": "code to extract into src/lib/city and use as is"},
    ]
    return atts, city_contract(code_id, garage), {"kind": "city", "sha": city_sha(assets_dir)}
