"""Compute bot "hiding spots" from the server's baked map.

Usage: python scripts/find-hide-spots.py [npz_path]

Reads the baked map npz (room_lookup + inflated walkable weights + meta), counts
each room's wall openings to classify through-rooms (>=2 exits) vs dead-ends, then
within each through-room picks the walkable cell that is deepest from the room's
exits and far from task points / spawns — a tucked corner that is off the
task-running routes but still has >=2 ways to flee. Corridors (the task routes
themselves) and >=4-exit junction hubs are excluded.

Prints a summary table and a ready-to-paste TS array for
`src/strategies/hide-spots.ts` (`HIDE_SPOTS`). Re-run whenever the map is rebaked.
Dependency: numpy only (run with the backend venv interpreter).
"""
import json
import math
import sys
from pathlib import Path

import numpy as np

DEFAULT_NPZ = r"I:\crab-kill-backend\config\clawclaw\clawclaw.tmj.baked.npz"

# Tunables (world px)
EXIT_MIN = 120.0       # candidate must be at least this deep from any exit (kills thin corridors)
SPAWN_MIN = 160.0      # keep clear of spawn clusters
TASK_MIN = 110.0       # don't sit on top of a task point (players doing it would see you)
MAX_EXITS = 3          # >=4 openings = a junction/hub: too much through-traffic to hide in
DEEP_CAP = 250.0       # diminishing returns on exit depth
TASK_CAP = 300.0       # diminishing returns on task clearance
TASK_W = 0.8
SAMPLE_STEP = 4        # evaluate every Nth tile inside a room (tile=4px -> 16px grid)
MIN_SPACING = 240.0    # spread chosen spots apart
MAX_SPOTS = 14
EXCLUDE_NAME_SUBSTR = ("走廊",)  # corridors are exactly the task-running routes -> never hide there


def union_find_components(cells):
    """8-connected component count over a set of (ty, tx) cells."""
    cellset = set(cells)
    parent = {c: c for c in cellset}

    def find(a):
        while parent[a] != a:
            parent[a] = parent[parent[a]]
            a = parent[a]
        return a

    for (ty, tx) in cellset:
        for dy in (-1, 0, 1):
            for dx in (-1, 0, 1):
                if dy == 0 and dx == 0:
                    continue
                nb = (ty + dy, tx + dx)
                if nb in cellset:
                    ra, rb = find((ty, tx)), find(nb)
                    if ra != rb:
                        parent[ra] = rb
    return len({find(c) for c in cellset})


def exit_cells_of(room_lookup, walk, r):
    """Walkable cells of room r that border a walkable cell outside room r."""
    room_mask = (room_lookup == r) & walk
    if not room_mask.any():
        return None, None
    outside = walk & (room_lookup != r)
    border = np.zeros_like(room_mask)
    border[:-1, :] |= room_mask[:-1, :] & outside[1:, :]
    border[1:, :] |= room_mask[1:, :] & outside[:-1, :]
    border[:, :-1] |= room_mask[:, :-1] & outside[:, 1:]
    border[:, 1:] |= room_mask[:, 1:] & outside[:, :-1]
    ys, xs = np.where(border)
    return room_mask, list(zip(ys.tolist(), xs.tolist()))


def best_cell_in_room(room_mask, opening_px, task_xy, spawn_xy, tw, th):
    ys, xs = np.where(room_mask)
    if len(ys) == 0:
        return None
    sel = (ys % SAMPLE_STEP == 0) & (xs % SAMPLE_STEP == 0)
    ys, xs = ys[sel], xs[sel]
    if len(ys) == 0:
        return None
    cx = xs * tw + tw / 2.0
    cy = ys * th + th / 2.0
    pts = np.stack([cx, cy], axis=1)

    op = np.array(opening_px, dtype=np.float64)
    exit_d = np.sqrt(((pts[:, None, :] - op[None, :, :]) ** 2).sum(-1)).min(1)
    task_d = np.sqrt(((pts[:, None, :] - task_xy[None, :, :]) ** 2).sum(-1)).min(1)
    spawn_d = np.sqrt(((pts[:, None, :] - spawn_xy[None, :, :]) ** 2).sum(-1)).min(1)

    ok = (exit_d >= EXIT_MIN) & (spawn_d >= SPAWN_MIN) & (task_d >= TASK_MIN)
    if not ok.any():
        return None
    score = np.minimum(exit_d, DEEP_CAP) + TASK_W * np.minimum(task_d, TASK_CAP)
    score = np.where(ok, score, -1e9)
    i = int(np.argmax(score))
    return (round(float(cx[i]), 1), round(float(cy[i]), 1), float(score[i]),
            float(exit_d[i]), float(task_d[i]), float(spawn_d[i]))


def main() -> None:
    npz_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_NPZ
    data = np.load(npz_path, allow_pickle=False)
    meta = json.loads(data["meta"].tobytes().decode("utf-8"))
    room_lookup = data["room_lookup"]
    weights = data["inflated_weights"]
    tw, th = meta["tile_width"], meta["tile_height"]
    room_names = meta["room_names"]
    walk = np.isfinite(weights)
    task_xy = np.array([[t["x"], t["y"]] for t in meta["task_points"]], dtype=np.float64)
    spawn_xy = np.array([[s["x"], s["y"]] for s in meta["spawn_points"]], dtype=np.float64)

    candidates = []
    for r in range(1, len(room_names)):
        room_mask, opening = exit_cells_of(room_lookup, walk, r)
        if room_mask is None or not opening:
            continue
        name = room_names[r]
        if any(s in name for s in EXCLUDE_NAME_SUBSTR):
            continue
        n_exits = union_find_components(opening)
        if n_exits < 2 or n_exits > MAX_EXITS:
            continue
        opening_px = [(tx * tw + tw / 2.0, ty * th + th / 2.0) for (ty, tx) in opening]
        best = best_cell_in_room(room_mask, opening_px, task_xy, spawn_xy, tw, th)
        if best is None:
            continue
        x, y, score, ed, td, sd = best
        candidates.append({"x": x, "y": y, "room": name, "exits": n_exits,
                           "score": round(score, 1), "exit_dist": round(ed, 1),
                           "task_dist": round(td, 1), "spawn_dist": round(sd, 1)})

    candidates.sort(key=lambda c: c["score"], reverse=True)
    chosen = []
    for c in candidates:
        if all(math.hypot(c["x"] - k["x"], c["y"] - k["y"]) >= MIN_SPACING for k in chosen):
            chosen.append(c)
        if len(chosen) >= MAX_SPOTS:
            break

    print(f"{len(candidates)} through-room candidates, {len(chosen)} chosen after spacing:\n")
    print(f"{'room':>8} {'x':>7} {'y':>7} {'exits':>5} {'exitD':>6} {'taskD':>6} {'spawnD':>6}")
    for c in chosen:
        print(f"{c['room']:>8} {c['x']:>7.0f} {c['y']:>7.0f} {c['exits']:>5} "
              f"{c['exit_dist']:>6.0f} {c['task_dist']:>6.0f} {c['spawn_dist']:>6.0f}")

    print("\n// paste into src/strategies/hide-spots.ts (HIDE_SPOTS):")
    for c in chosen:
        print(f"  {{ x: {c['x']:.0f}, y: {c['y']:.0f}, room: '{c['room']}' }},")


if __name__ == "__main__":
    main()
