"""The Town: settlements for a custom 3D game (villages, hamlets, towns and cities of the medieval and Breton styles).

A custom 3D game whose words name a settlement (a village, a hamlet, a castle town, a tavern, a smithy, a cathedral...)
gets the town kit (JavaScript ES modules, built by `tools/pack/pack_townkit.sh`) and its style's
models and textures as attachments, and the builder contract joins the brief. The builder places houses from the kit
with `settle` + `buildSettlement` (SETTLEMENT.md) instead of drawing them from boxes. The Engine stages attachments at
`.forge/attachments/att<N>/original.<ext>` in order, like games_garage and games_library. A missing bundle = no town 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

_KIND_BRETON_RE = re.compile(
    r"\b(breton|bretons|brittany|britanny|bretagne|finistere|morbihan|fishing (?:village|town|port|harbou?r)s?)\b", re.I)
_KIND_MEDIEVAL_RE = re.compile(
    r"\b(medieval|mediaeval|middle ages?|fantasy (?:town|village|city|kingdom|realm)s?|castle towns?|kingdom(?! hearts)|kingdoms|"
    r"knights?|knightly|taverns?|blacksmiths?|smithy|smithies|bakery|bakeries|market towns?|cathedrals?|abbeys?|monasteries|"
    r"monastery|peasants?|feudal|hamlets?|villages?|serfs?|thatched)\b", re.I)
_NOT_TOWN_RE = re.compile(
    r"\b(space|spaceships?|starships?|sci-?fi|galax\w*|asteroids?|aliens?|cyber\w*|modern city|downtown|skyscrapers?|metropolis|"
    r"new york|hong kong|racing (?:circuit|track)|race ?tracks?|grand prix|formula (?:1|one)|football|soccer|basketball|"
    r"baseball|tennis|golf\w*|skate\w*|supermarkets?|airports?|office blocks?|shopping (?:mall|centre|center))\b", re.I)

_TIER_WORDS = (
    ("city", re.compile(r"\b(cities|city|capitals?|cathedrals?|walled|metropolis)\b", re.I)),
    ("town", re.compile(r"\b(towns?)\b", re.I)),
    ("village", re.compile(r"\b(villages?)\b", re.I)),
    ("hamlet", re.compile(r"\b(hamlets?)\b", re.I)),
)
_SITE_WORDS = (
    ("coast", re.compile(r"\b(coasts?|coastal|seaside|shore|shoreline|harbou?rs?|bay|cliffs?)\b", re.I)),
    ("river", re.compile(r"\b(rivers?|riverside|streams?|fords?|lakes?|springs?)\b", re.I)),
    ("hill", re.compile(r"\b(hills?|hilltops?|hillsides?|mountains?|perched)\b", re.I)),
    ("spur", re.compile(r"\b(spurs?|ridges?|promontor\w*)\b", re.I)),
    ("crossroads", re.compile(r"\b(crossroads?|crossings?|junctions?|intersections?)\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 town_kind(card: Any, prompt: str = "") -> str | None:
    """'breton', 'medieval' or None, deterministic from the words. Modern, space and sport words win over settlement words."""
    text = _card_text(card, prompt)
    if _NOT_TOWN_RE.search(text):
        return None
    if _KIND_BRETON_RE.search(text):
        return "breton"
    if _KIND_MEDIEVAL_RE.search(text):
        return "medieval"
    return None


def town_tier(card: Any, prompt: str = "") -> str:
    """'hamlet', 'village', 'town' or 'city'. The largest word wins; a settlement with no size word is a village."""
    text = _card_text(card, prompt)
    for tier, pattern in _TIER_WORDS:
        if pattern.search(text):
            return tier
    return "village"


def town_site(card: Any, prompt: str = "") -> str:
    """'coast', 'river', 'hill', 'spur', 'crossroads' or 'plain': water first, then high ground, then a crossing."""
    text = _card_text(card, prompt)
    for site, pattern in _SITE_WORDS:
        if pattern.search(text):
            return site
    return "plain"


def town_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 town_sha(assets_dir: Path) -> str | None:
    """sha256 of the code tarball (the one `townkit.sha` names), for meta; None when the kit is not vendored."""
    try:
        return (assets_dir / "townkit.sha").read_text().strip() or None
    except OSError:
        pass
    try:
        return hashlib.sha256((assets_dir / "townkit.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()


_STYLE_NOTES = {
    "medieval": "Medieval style: timber-framed and stone houses with tile roofs, Romanesque and Gothic churches, walls and gates.",
    "breton": "Breton style: granite and slate houses with blue-grey slate roofs and painted timber, a fishing-village look.",
}


def town_contract(code_id: str, models_id: str, kind: str, tier: str, site: str) -> str:
    """The builder contract for the town kit. code_id / models_id are attachment ids (att2...)."""
    return (
        " SETTLEMENTS COME FROM OUR TOWN KIT. Attachment " + code_id + " is the town kit (JavaScript ES modules, three.js, a "
        "gzipped tar) and attachment " + models_id + " holds its " + kind + " style models and textures (a gzipped "
        "tar). First run: `mkdir -p src/lib/town && tar -xzf .forge/attachments/" + code_id + "/original.* -C src/lib/town && "
        "tar -xzf .forge/attachments/" + models_id + "/original.* -C public`, then read `src/lib/town/TOWN.md` and "
        "`src/lib/town/SETTLEMENT.md`. " + _STYLE_NOTES[kind] + " This world is a " + tier + " on a " + site + ": "
        "build the whole world with ONE call: `const town = await createTown(THREE, { renderer, GLTFLoader, KTX2Loader, style: '" + kind + "', "
        "tier: '" + tier + "', site: '" + site + "', seed })` from `@/lib/town/index.mjs` (three.js loaders from three/addons; kits "
        "load from /town/kits/, the KTX2 decoder from /town/basis/), then `scene.add(town.group)`. "
        "Never draw houses, walls, roads or churches from boxes: the kit places them. Spawn the "
        "player at `town.spawn` (the main square). Collide with `town.colliders`. Walk on `town.heightAt(x, z)`. Do not edit src/lib/town or public/town."
    )


def town_attachments(card: Any, first_index: int, prompt: str = "", assets_dir: Path = DIST) -> 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). None when the game has no settlement or a bundle is missing: the town kit is a bonus, never a
    reason to fail a build."""
    if not town_fits(card):
        return None
    kind = town_kind(card, prompt)
    if kind is None:
        return None
    try:
        code = (assets_dir / "townkit.tar.gz").read_bytes()
        models = (assets_dir / f"townkit-{kind}.tar.gz").read_bytes()
    except OSError:
        return None
    code_id, models_id = f"att{first_index + 1}", f"att{first_index + 2}"
    atts = [
        {"url": _data_url("application/gzip", code), "filename": "townkit.tar.gz", "alt": "Town kit (JavaScript, three.js)",
         "role": "code to extract into src/lib/town and use as is"},
        {"url": _data_url("application/gzip", models), "filename": f"townkit-{kind}.tar.gz",
         "alt": f"Town kit models and textures ({kind} style)", "role": "models to extract into public (public/town)"},
    ]
    tier, site = town_tier(card, prompt), town_site(card, prompt)
    contract = town_contract(code_id, models_id, kind, tier, site)
    return atts, contract, {"kind": kind, "tier": tier, "site": site, "sha": town_sha(assets_dir)}
