"""GodotIQ session — unified project state for MCP tools.

Bridges the parsers into a single cached state object. Created once
at server startup, provides all cross-reference queries the tools need.
"""

from __future__ import annotations

import logging
import os
from collections import defaultdict
from pathlib import Path

logger = logging.getLogger(__name__)

from godotiq.cache.file_cache import FileCache
from godotiq.config import load_config, GodotIQConfig
from godotiq.parsers.gd_parser import parse_gd, GdScript
from godotiq.parsers.project_index import scan_project, ProjectIndex
from godotiq.parsers.scene_resolver import (
    resolve_scene,
    calculate_world_transforms,
    ResolvedScene,
    WorldNode,
)
from godotiq.parsers.tscn_parser import parse_tscn, TscnScene


class GodotIQSession:
    """Holds loaded project state. Created once, cached, refreshed on file changes."""

    def __init__(self, project_root: str | Path) -> None:
        self.project_root = Path(project_root)
        self.cache = FileCache()
        self.config: GodotIQConfig = GodotIQConfig()
        self._index: ProjectIndex | None = None
        self._gd_scripts: dict[str, GdScript] = {}
        self._tscn_scenes: dict[str, TscnScene] = {}
        self._resolved_scenes: dict[str, tuple[ResolvedScene, list[WorldNode]]] = {}

    def load(self) -> None:
        """Full project scan + parse all .gd files with autoload awareness."""
        self._index = scan_project(self.project_root, self.cache)
        autoload_names = list(self._index.autoloads.keys())
        for script_path in self._index.scripts:
            abs_path = self._resolve_res_path(script_path)
            if abs_path.exists():
                try:
                    self._gd_scripts[script_path] = parse_gd(
                        str(abs_path), known_autoloads=autoload_names
                    )
                except Exception:
                    logger.warning("Failed to parse %s, skipping", script_path)
        config_path = self.project_root / ".godotiq.json"
        self.config = load_config(str(config_path))

    def _resolve_res_path(self, res_path: str) -> Path:
        """Convert a res:// path to an absolute filesystem path."""
        relative = res_path.removeprefix("res://")
        return self.project_root / relative

    def get_script(self, res_path: str) -> GdScript | None:
        """Return the parsed GdScript for the given res:// path, or None."""
        return self._gd_scripts.get(res_path)

    def get_scene(self, res_path: str) -> TscnScene | None:
        """Lazy parse + cache a .tscn scene."""
        if self._index and res_path in self._index.scenes:
            return self._index.scenes[res_path]
        if res_path in self._tscn_scenes:
            return self._tscn_scenes[res_path]
        abs_path = self._resolve_res_path(res_path)
        if abs_path.exists():
            scene = parse_tscn(str(abs_path))
            self._tscn_scenes[res_path] = scene
            return scene
        return None

    def resolve_scene_spatial(
        self, res_path: str
    ) -> tuple[ResolvedScene, list[WorldNode]] | None:
        """Lazy resolve + cache scene spatial data."""
        if res_path in self._resolved_scenes:
            return self._resolved_scenes[res_path]
        scene = self.get_scene(res_path)
        if scene is None:
            return None
        resolved = resolve_scene(scene, self.project_root)
        world_nodes = calculate_world_transforms(resolved)
        result = (resolved, world_nodes)
        self._resolved_scenes[res_path] = result
        return result

    def get_all_signal_definitions(self) -> dict[str, list[str]]:
        """Map signal names to the scripts that define them."""
        result: dict[str, list[str]] = defaultdict(list)
        for path, gd in self._gd_scripts.items():
            for sig in gd.signals:
                result[sig.name].append(path)
        return dict(result)

    def get_all_signal_emissions(self) -> dict[str, list[str]]:
        """Map signal names to the scripts that emit them."""
        result: dict[str, list[str]] = defaultdict(list)
        for path, gd in self._gd_scripts.items():
            for em in gd.signal_emissions:
                result[em.signal_name].append(path)
        return dict(result)

    def get_all_signal_connections(self) -> dict[str, list[str]]:
        """Map signal source to the scripts that connect to them."""
        result: dict[str, list[str]] = defaultdict(list)
        for path, gd in self._gd_scripts.items():
            for conn in gd.signal_connections:
                result[conn.signal_source].append(path)
        return dict(result)

    def get_autoload_dependents(self, autoload_name: str) -> list[str]:
        """Return list of res:// script paths referencing the given autoload."""
        result: list[str] = []
        for path, gd in self._gd_scripts.items():
            for ref in gd.autoload_refs:
                if ref.autoload_name == autoload_name:
                    result.append(path)
                    break
        return result

    def to_res_path(self, path: str) -> str:
        """Convert a filesystem path to a res:// path."""
        if path.startswith("res://"):
            return path
        p = Path(path)
        if p.is_absolute():
            try:
                relative = p.relative_to(self.project_root)
                return f"res://{relative}"
            except ValueError:
                return f"res://{path}"
        return f"res://{path}"

    def resolve_file_path(self, path: str) -> str | None:
        """Resolve a file identifier to a res:// path if it exists in the index."""
        res_path = self.to_res_path(path)
        if self._index is None:
            return None
        if res_path in self._gd_scripts:
            return res_path
        if res_path in self._index.scenes:
            return res_path
        if res_path in self._index.assets:
            return res_path
        return None


def discover_project_root() -> Path | None:
    """Discover the Godot project root directory.

    Strategy:
    1. Check GODOTIQ_PROJECT_ROOT env var
    2. Check cwd for .godotiq.json
    3. Walk up from cwd looking for project.godot
    """
    env_root = os.environ.get("GODOTIQ_PROJECT_ROOT")
    if env_root:
        p = Path(env_root)
        if (p / "project.godot").exists():
            return p

    cwd = Path.cwd()
    if (cwd / ".godotiq.json").exists():
        return cwd

    current = cwd
    while current != current.parent:
        if (current / "project.godot").exists():
            return current
        current = current.parent

    return None
