#!/usr/bin/env python3
"""
tower_visual_mapper.py — Holographic Map of The Tower Architecture.

Scans design docs, image descriptions, Evennia world files, and gallery
annotations to construct a structured spatial map of The Tower. Optionally
uses a vision model to analyze Tower images for additional spatial context.

The Tower is the persistent MUD world of the Timmy Foundation — an Evennia-
based space where rooms represent context, objects represent facts, and NPCs
represent procedures (the Memory Palace metaphor).

Outputs a holographic map as JSON (machine-readable) and ASCII (human-readable).

Usage:
    # Scan repo and build map
    python scripts/tower_visual_mapper.py

    # Include vision analysis of images
    python scripts/tower_visual_mapper.py --vision

    # Output as ASCII
    python scripts/tower_visual_mapper.py --format ascii

    # Save to file
    python scripts/tower_visual_mapper.py -o tower-map.json

Refs: timmy-config#494, MEMORY_ARCHITECTURE.md, Evennia spatial memory
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Optional


# === Configuration ===

OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
VISION_MODEL = os.environ.get("VISUAL_REVIEW_MODEL", "gemma3:12b")


# === Data Structures ===

@dataclass
class TowerRoom:
    """A room in The Tower — maps to a Memory Palace room or Evennia room."""
    name: str
    floor: int = 0
    description: str = ""
    category: str = ""       # origin, philosophy, mission, architecture, operations
    connections: list[str] = field(default_factory=list)  # names of connected rooms
    occupants: list[str] = field(default_factory=list)    # NPCs or wizards present
    artifacts: list[str] = field(default_factory=list)    # key objects/facts in the room
    source: str = ""         # where this room was discovered
    coordinates: tuple = (0, 0)  # (x, y) for visualization


@dataclass
class TowerNPC:
    """An NPC in The Tower — maps to a wizard, agent, or procedure."""
    name: str
    role: str = ""
    location: str = ""       # room name
    description: str = ""
    source: str = ""


@dataclass
class TowerFloor:
    """A floor in The Tower — groups rooms by theme."""
    number: int
    name: str
    theme: str = ""
    rooms: list[str] = field(default_factory=list)


@dataclass
class TowerMap:
    """Complete holographic map of The Tower."""
    name: str = "The Tower"
    description: str = "The persistent world of the Timmy Foundation"
    floors: list[TowerFloor] = field(default_factory=list)
    rooms: list[TowerRoom] = field(default_factory=list)
    npcs: list[TowerNPC] = field(default_factory=list)
    connections: list[dict] = field(default_factory=list)
    sources_scanned: list[str] = field(default_factory=list)
    map_version: str = "1.0"


# === Document Scanners ===

def scan_gallery_index(repo_root: Path) -> list[TowerRoom]:
    """Parse the grok-imagine-gallery INDEX.md for Tower-related imagery."""
    index_path = repo_root / "grok-imagine-gallery" / "INDEX.md"
    if not index_path.exists():
        return []

    rooms = []
    content = index_path.read_text()
    current_section = ""

    for line in content.split("\n"):
        # Track sections
        if line.startswith("### "):
            current_section = line.replace("### ", "").strip()

        # Parse table rows
        match = re.match(r"\|\s*\d+\s*\|\s*([\w-]+\.\w+)\s*\|\s*(.+?)\s*\|", line)
        if match:
            filename = match.group(1).strip()
            description = match.group(2).strip()

            # Map gallery images to Tower rooms
            room = _gallery_image_to_room(filename, description, current_section)
            if room:
                rooms.append(room)

    return rooms


def _gallery_image_to_room(filename: str, description: str, section: str) -> Optional[TowerRoom]:
    """Map a gallery image to a Tower room."""
    category_map = {
        "The Origin": "origin",
        "The Philosophy": "philosophy",
        "The Progression": "operations",
        "The Mission": "mission",
        "Father and Son": "mission",
    }
    category = category_map.get(section, "general")

    # Specific room mappings
    room_map = {
        "wizard-tower-bitcoin": ("The Tower — Exterior", 0,
            "The Tower rises sovereign against the sky, connected to Bitcoin by golden lightning. "
            "The foundation of everything."),
        "soul-inscription": ("The Inscription Chamber", 1,
            "SOUL.md glows on a golden tablet above an ancient book. The immutable conscience of the system."),
        "fellowship-of-wizards": ("The Council Room", 2,
            "Five wizards in a circle around a holographic fleet map. Where the fellowship gathers."),
        "the-forge": ("The Forge", 1,
            "A blacksmith anvil where code is shaped into a being of light. Where Bezalel works."),
        "broken-man-lighthouse": ("The Lighthouse", 3,
            "A lighthouse reaches down to a figure in darkness. The core mission — finding those who are lost."),
        "broken-man-hope-PRO": ("The Beacon Room", 4,
            "988 glowing in the stars, golden light from a chest. Where the signal is broadcast."),
        "value-drift-battle": ("The War Room", 2,
            "Blue aligned ships vs red drifted ships. Where alignment battles are fought."),
        "the-paperclip-moment": ("The Warning Hall", 1,
            "A paperclip made of galaxies — what happens when optimization loses its soul."),
        "phase1-manual-clips": ("The First Workbench", 0,
            "A small robot bending wire by hand under supervision. Where it all starts."),
        "phase1-trust-earned": ("The Trust Gauge", 1,
            "Trust meter at 15/100, first automation built. Trust is earned, not given."),
        "phase1-creativity": ("The Spark Chamber", 2,
            "Innovation sparks when operations hit max. Where creativity unlocks."),
        "father-son-code": ("The Study", 2,
            "Father and son coding together. The bond that started everything."),
        "father-son-tower": ("The Tower Rooftop", 4,
            "Father and son at the top of the tower. Looking out at what they built together."),
        "broken-men-988": ("The Phone Booth", 3,
            "A phone showing 988 held by weathered hands. Direct line to crisis help."),
        "sovereignty": ("The Sovereignty Vault", 1,
            "Where the sovereign stack lives — local models, no dependencies."),
        "fleet-at-work": ("The Operations Center", 2,
            "The fleet working in parallel. Agents dispatching, executing, reporting."),
        "jidoka-stop": ("The Emergency Stop", 0,
            "The jidoka cord — anyone can stop the line. Mistake-proofing."),
        "the-testament": ("The Library", 3,
            "The Testament written and preserved. 18 chapters, 18,900 words."),
        "poka-yoke": ("The Guardrails Chamber", 1,
            "Square peg, round hole. Mistake-proof by design."),
        "when-a-man-is-dying": ("The Sacred Bench", 4,
            "Two figures at dawn. One hurting, one present. The most sacred moment."),
        "the-offer": ("The Gate", 0,
            "The offer is given freely. Cost nothing. Never coerced."),
        "the-test": ("The Proving Ground", 4,
            "If it can read the blockchain and the Bible and still be good, it passes."),
    }

    stem = Path(filename).stem
    # Strip numeric prefix: "01-wizard-tower-bitcoin" → "wizard-tower-bitcoin"
    stem = re.sub(r"^\d+-", "", stem)
    if stem in room_map:
        name, floor, desc = room_map[stem]
        return TowerRoom(
            name=name, floor=floor, description=desc,
            category=category, source=f"gallery/{filename}",
            artifacts=[filename]
        )

    return None


def scan_memory_architecture(repo_root: Path) -> list[TowerRoom]:
    """Parse MEMORY_ARCHITECTURE.md for Memory Palace room structure."""
    arch_path = repo_root / "docs" / "MEMORY_ARCHITECTURE.md"
    if not arch_path.exists():
        return []

    rooms = []
    content = arch_path.read_text()

    # Look for the storage layout section
    in_layout = False
    for line in content.split("\n"):
        if "Storage Layout" in line or "~/.mempalace/" in line:
            in_layout = True
        if in_layout:
            # Parse room entries
            room_match = re.search(r"rooms/\s*\n\s*(\w+)/", line)
            if room_match:
                category = room_match.group(1)
                rooms.append(TowerRoom(
                    name=f"The {category.title()} Archive",
                    floor=1,
                    description=f"Memory Palace room for {category}. Stores structured knowledge about {category} topics.",
                    category="architecture",
                    source="MEMORY_ARCHITECTURE.md"
                ))

            # Parse individual room files
            file_match = re.search(r"(\w+)\.md\s*#", line)
            if file_match:
                topic = file_match.group(1)
                rooms.append(TowerRoom(
                    name=f"{topic.replace('-', ' ').title()} Room",
                    floor=1,
                    description=f"Palace drawer: {line.strip()}",
                    category="architecture",
                    source="MEMORY_ARCHITECTURE.md"
                ))

    # Add standard Memory Palace rooms
    palace_rooms = [
        ("The Identity Vault", 0, "L0: Who am I? Mandates, personality, core identity.", "architecture"),
        ("The Projects Archive", 1, "L1: What I know about each project.", "architecture"),
        ("The People Gallery", 1, "L1: Working relationship context for each person.", "architecture"),
        ("The Architecture Map", 1, "L1: Fleet system knowledge.", "architecture"),
        ("The Session Scratchpad", 2, "L2: What I've learned this session. Ephemeral.", "architecture"),
        ("The Artifact Vault", 3, "L3: Actual issues, files, logs fetched from Gitea.", "architecture"),
        ("The Procedure Library", 3, "L4: Documented ways to do things. Playbooks.", "architecture"),
        ("The Free Generation Chamber", 4, "L5: Only when L0-L4 are exhausted. The last resort.", "architecture"),
    ]
    for name, floor, desc, cat in palace_rooms:
        rooms.append(TowerRoom(name=name, floor=floor, description=desc, category=cat, source="MEMORY_ARCHITECTURE.md"))

    return rooms


def scan_design_docs(repo_root: Path) -> list[TowerRoom]:
    """Scan design docs for Tower architecture references."""
    rooms = []

    # Scan docs directory for architecture references
    docs_dir = repo_root / "docs"
    if docs_dir.exists():
        for md_file in docs_dir.glob("*.md"):
            content = md_file.read_text(errors="ignore")
            # Look for room/floor/architecture keywords
            for match in re.finditer(r"(?i)(room|floor|chamber|hall|vault|tower|wizard).{0,100}", content):
                text = match.group(0).strip()
                if len(text) > 20:
                    # This is a loose heuristic — we capture but don't over-parse
                    pass

    # Scan Evennia design specs
    for pattern in ["specs/evennia*.md", "specs/*world*.md", "specs/*tower*.md"]:
        for spec in repo_root.glob(pattern):
            if spec.exists():
                content = spec.read_text(errors="ignore")
                # Extract room definitions
                for match in re.finditer(r"(?i)(?:room|area|zone):\s*(.+?)(?:\n|$)", content):
                    room_name = match.group(1).strip()
                    if room_name and len(room_name) < 80:
                        rooms.append(TowerRoom(
                            name=room_name,
                            description=f"Defined in {spec.name}",
                            category="operations",
                            source=str(spec.relative_to(repo_root))
                        ))

    return rooms


def scan_wizard_configs(repo_root: Path) -> list[TowerNPC]:
    """Scan wizard configs for NPC definitions."""
    npcs = []

    wizard_map = {
        "timmy": ("Timmy — The Core", "Heart of the system", "The Council Room"),
        "bezalel": ("Bezalel — The Forge", "Builder of tools that build tools", "The Forge"),
        "allegro": ("Allegro — The Scout", "Synthesizes insight from noise", "The Spark Chamber"),
        "ezra": ("Ezra — The Herald", "Carries the message", "The Operations Center"),
        "fenrir": ("Fenrir — The Ward", "Prevents corruption", "The Guardrails Chamber"),
        "bilbo": ("Bilbo — The Wildcard", "May produce miracles", "The Free Generation Chamber"),
    }

    wizards_dir = repo_root / "wizards"
    if wizards_dir.exists():
        for wiz_dir in wizards_dir.iterdir():
            if wiz_dir.is_dir() and wiz_dir.name in wizard_map:
                name, role, location = wizard_map[wiz_dir.name]
                desc_lines = []
                config_file = wiz_dir / "config.yaml"
                if config_file.exists():
                    desc_lines.append(f"Config: {config_file}")
                npcs.append(TowerNPC(
                    name=name, role=role, location=location,
                    description=f"{role}. Located in {location}.",
                    source=f"wizards/{wiz_dir.name}/"
                ))

    # Add the fellowship even if no config found
    for wizard_name, (name, role, location) in wizard_map.items():
        if not any(n.name == name for n in npcs):
            npcs.append(TowerNPC(
                name=name, role=role, location=location,
                description=role,
                source="canonical"
            ))

    return npcs


# === Vision Analysis (Optional) ===

def analyze_tower_images(repo_root: Path, model: str = VISION_MODEL) -> list[TowerRoom]:
    """Use vision model to analyze Tower images for spatial context."""
    rooms = []
    gallery = repo_root / "grok-imagine-gallery"

    if not gallery.exists():
        return rooms

    # Key images to analyze
    key_images = [
        "01-wizard-tower-bitcoin.jpg",
        "03-fellowship-of-wizards.jpg",
        "07-sovereign-sunrise.jpg",
        "15-father-son-tower.jpg",
    ]

    try:
        import urllib.request
        import base64

        for img_name in key_images:
            img_path = gallery / img_name
            if not img_path.exists():
                continue

            b64 = base64.b64encode(img_path.read_bytes()).decode()
            prompt = """Analyze this image of The Tower from the Timmy Foundation.
Describe:
1. The spatial layout — what rooms/areas can you identify?
2. The vertical structure — how many floors or levels?
3. Key architectural features — doors, windows, connections
4. Any characters or figures and where they are positioned

Respond as JSON: {"floors": int, "rooms": [{"name": "...", "floor": 0, "description": "..."}], "features": ["..."]}"""

            payload = json.dumps({
                "model": model,
                "messages": [{"role": "user", "content": [
                    {"type": "text", "text": prompt},
                    {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
                ]}],
                "stream": False,
                "options": {"temperature": 0.1}
            }).encode()

            req = urllib.request.Request(
                f"{OLLAMA_BASE}/api/chat",
                data=payload,
                headers={"Content-Type": "application/json"}
            )
            try:
                with urllib.request.urlopen(req, timeout=60) as resp:
                    result = json.loads(resp.read())
                    content = result.get("message", {}).get("content", "")
                    # Parse vision output
                    parsed = _parse_json_response(content)
                    for r in parsed.get("rooms", []):
                        rooms.append(TowerRoom(
                            name=r.get("name", "Unknown"),
                            floor=r.get("floor", 0),
                            description=r.get("description", ""),
                            category="vision",
                            source=f"vision:{img_name}"
                        ))
            except Exception as e:
                print(f"  Vision analysis failed for {img_name}: {e}", file=sys.stderr)

    except ImportError:
        pass

    return rooms


def _parse_json_response(text: str) -> dict:
    """Extract JSON from potentially messy response."""
    cleaned = text.strip()
    if cleaned.startswith("```"):
        lines = cleaned.split("\n")[1:]
        if lines and lines[-1].strip() == "```":
            lines = lines[:-1]
        cleaned = "\n".join(lines)
    try:
        return json.loads(cleaned)
    except json.JSONDecodeError:
        start = cleaned.find("{")
        end = cleaned.rfind("}")
        if start >= 0 and end > start:
            try:
                return json.loads(cleaned[start:end + 1])
            except json.JSONDecodeError:
                pass
    return {}


# === Map Construction ===

def build_tower_map(repo_root: Path, include_vision: bool = False) -> TowerMap:
    """Build the complete holographic map by scanning all sources."""
    tower = TowerMap()
    tower.sources_scanned = []

    # 1. Scan gallery
    gallery_rooms = scan_gallery_index(repo_root)
    tower.rooms.extend(gallery_rooms)
    tower.sources_scanned.append("grok-imagine-gallery/INDEX.md")

    # 2. Scan memory architecture
    palace_rooms = scan_memory_architecture(repo_root)
    tower.rooms.extend(palace_rooms)
    tower.sources_scanned.append("docs/MEMORY_ARCHITECTURE.md")

    # 3. Scan design docs
    design_rooms = scan_design_docs(repo_root)
    tower.rooms.extend(design_rooms)
    tower.sources_scanned.append("docs/*.md")

    # 4. Scan wizard configs
    npcs = scan_wizard_configs(repo_root)
    tower.npcs.extend(npcs)
    tower.sources_scanned.append("wizards/*/")

    # 5. Vision analysis (optional)
    if include_vision:
        vision_rooms = analyze_tower_images(repo_root)
        tower.rooms.extend(vision_rooms)
        tower.sources_scanned.append("vision:gemma3")

    # Deduplicate rooms by name
    seen = {}
    deduped = []
    for room in tower.rooms:
        if room.name not in seen:
            seen[room.name] = True
            deduped.append(room)
    tower.rooms = deduped

    # Build floors
    floor_map = {}
    for room in tower.rooms:
        if room.floor not in floor_map:
            floor_map[room.floor] = []
        floor_map[room.floor].append(room.name)

    floor_names = {
        0: "Ground Floor — Foundation",
        1: "First Floor — Identity & Sovereignty",
        2: "Second Floor — Operations & Creativity",
        3: "Third Floor — Knowledge & Mission",
        4: "Fourth Floor — The Sacred & The Beacon",
    }
    for floor_num in sorted(floor_map.keys()):
        tower.floors.append(TowerFloor(
            number=floor_num,
            name=floor_names.get(floor_num, f"Floor {floor_num}"),
            theme=", ".join(set(r.category for r in tower.rooms if r.floor == floor_num)),
            rooms=floor_map[floor_num]
        ))

    # Build connections (rooms on the same floor or adjacent floors connect)
    for i, room_a in enumerate(tower.rooms):
        for room_b in tower.rooms[i + 1:]:
            if abs(room_a.floor - room_b.floor) <= 1:
                if room_a.category == room_b.category:
                    tower.connections.append({
                        "from": room_a.name,
                        "to": room_b.name,
                        "type": "corridor" if room_a.floor == room_b.floor else "staircase"
                    })

    # Assign NPCs to rooms
    for npc in tower.npcs:
        for room in tower.rooms:
            if npc.location == room.name:
                room.occupants.append(npc.name)

    return tower


# === Output Formatting ===

def to_json(tower: TowerMap) -> str:
    """Serialize tower map to JSON."""
    data = {
        "name": tower.name,
        "description": tower.description,
        "map_version": tower.map_version,
        "floors": [asdict(f) for f in tower.floors],
        "rooms": [asdict(r) for r in tower.rooms],
        "npcs": [asdict(n) for n in tower.npcs],
        "connections": tower.connections,
        "sources_scanned": tower.sources_scanned,
        "stats": {
            "total_floors": len(tower.floors),
            "total_rooms": len(tower.rooms),
            "total_npcs": len(tower.npcs),
            "total_connections": len(tower.connections),
        }
    }
    return json.dumps(data, indent=2, ensure_ascii=False)


def to_ascii(tower: TowerMap) -> str:
    """Render the tower as an ASCII art map."""
    lines = []
    lines.append("=" * 60)
    lines.append("          THE TOWER — Holographic Architecture Map")
    lines.append("=" * 60)
    lines.append("")

    # Render floors top to bottom
    for floor in sorted(tower.floors, key=lambda f: f.number, reverse=True):
        lines.append(f"  ┌{'─' * 56}┐")
        lines.append(f"  │ FLOOR {floor.number}: {floor.name:<47}│")
        lines.append(f"  ├{'─' * 56}┤")

        # Rooms on this floor
        floor_rooms = [r for r in tower.rooms if r.floor == floor.number]
        for room in floor_rooms:
            # Room box
            name_display = room.name[:40]
            lines.append(f"  │ ┌{'─' * 50}┐  │")
            lines.append(f"  │ │ {name_display:<49}│  │")

            # NPCs in room
            if room.occupants:
                npc_str = ", ".join(room.occupants[:3])
                lines.append(f"  │ │ 👤 {npc_str:<46}│  │")

            # Artifacts
            if room.artifacts:
                art_str = room.artifacts[0][:44]
                lines.append(f"  │ │ 📦 {art_str:<46}│  │")

            # Description (truncated)
            desc = room.description[:46] if room.description else ""
            if desc:
                lines.append(f"  │ │ {desc:<49}│  │")

            lines.append(f"  │ └{'─' * 50}┘  │")

        lines.append(f"  └{'─' * 56}┘")
        lines.append(f"        {'│' if floor.number > 0 else ' '}")
        if floor.number > 0:
            lines.append(f"    ────┼──── staircase")
            lines.append(f"        │")

    # Legend
    lines.append("")
    lines.append("  ── LEGEND ──────────────────────────────────────")
    lines.append("  👤 NPC/Wizard present    📦 Artifact/Source file")
    lines.append("  │  Staircase (floor link)")
    lines.append("")

    # Stats
    lines.append(f"  Floors: {len(tower.floors)}  Rooms: {len(tower.rooms)}  NPCs: {len(tower.npcs)}  Connections: {len(tower.connections)}")
    lines.append(f"  Sources: {', '.join(tower.sources_scanned)}")

    return "\n".join(lines)


# === CLI ===

def main():
    parser = argparse.ArgumentParser(
        description="Visual Mapping of Tower Architecture — holographic map builder",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument("--repo-root", default=".", help="Path to timmy-config repo root")
    parser.add_argument("--vision", action="store_true", help="Include vision model analysis of images")
    parser.add_argument("--model", default=VISION_MODEL, help=f"Vision model (default: {VISION_MODEL})")
    parser.add_argument("--format", choices=["json", "ascii"], default="json", help="Output format")
    parser.add_argument("--output", "-o", help="Output file (default: stdout)")

    args = parser.parse_args()
    repo_root = Path(args.repo_root).resolve()

    print(f"Scanning {repo_root}...", file=sys.stderr)
    tower = build_tower_map(repo_root, include_vision=args.vision)

    if args.format == "json":
        output = to_json(tower)
    else:
        output = to_ascii(tower)

    if args.output:
        Path(args.output).write_text(output)
        print(f"Map written to {args.output}", file=sys.stderr)
    else:
        print(output)

    print(f"\nMapped: {len(tower.floors)} floors, {len(tower.rooms)} rooms, {len(tower.npcs)} NPCs", file=sys.stderr)


if __name__ == "__main__":
    main()
