"""Project Memory tools — project summary and file context.

These tools help AI agents understand and navigate a Godot project.
"""

from __future__ import annotations

from collections import Counter

from mcp.server.fastmcp import Context

from godotiq.parsers.gd_parser import GdScript
from godotiq.parsers.tscn_parser import TscnScene
from godotiq.license import gate_pro
from godotiq.server import mcp
from godotiq.session import GodotIQSession
from godotiq.tools.output_limit import apply_output_limit


def _read_main_scene(session: GodotIQSession) -> str:
    """Read run/main_scene from project.godot."""
    godot_file = session.project_root / "project.godot"
    if not godot_file.exists():
        return ""
    in_application = False
    for line in godot_file.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if line.startswith("[") and line.endswith("]"):
            in_application = line == "[application]"
            continue
        if in_application and line.startswith("run/main_scene="):
            value = line.split("=", 1)[1].strip().strip('"')
            return value
    return ""


def _build_brief(session: GodotIQSession) -> dict:
    """Build brief-level project summary."""
    idx = session._index
    return {
        "project_name": idx.project_name,
        "engine": session.config.project.get("engine", "godot_4"),
        "type": session.config.project.get("type", "unknown"),
        "counts": {
            "scenes": idx.total_scenes,
            "scripts": idx.total_scripts,
            "assets": idx.total_assets,
        },
        "autoloads": list(idx.autoloads.keys()),
    }


_MAX_SCENE_STRUCTURE = 10
_MAX_PUBLIC_FUNCTIONS = 5


def _build_normal(session: GodotIQSession) -> dict:
    """Build normal-level additions (architecture only)."""
    idx = session._index

    # Architecture block
    main_scene = _read_main_scene(session)
    architecture = {
        "main_scene": main_scene,
        "autoload_details": dict(idx.autoloads),
        "scene_tree": [
            s for s in idx.scenes
            if not any(s in instances for instances in idx.scene_instances.values())
        ],
        "key_systems": list(idx.autoloads.keys()),
    }

    return {
        "architecture": architecture,
    }


def _build_full(session: GodotIQSession) -> dict:
    """Build full-level additions (scene_structure, autoload_analysis, file_tree, detected_patterns)."""
    idx = session._index

    # Scene structure: top N scenes by node count
    scene_entries = []
    for res_path, scene in idx.scenes.items():
        scene_entries.append((res_path, scene, len(scene.nodes)))
    scene_entries.sort(key=lambda e: e[2], reverse=True)
    scene_structure: dict[str, dict] = {}
    for res_path, scene, _count in scene_entries[:_MAX_SCENE_STRUCTURE]:
        scene_structure[res_path] = {
            "root_type": scene.root.type if scene.root else "",
            "total_nodes": len(scene.nodes),
            "instances": scene.get_all_instances(),
            "scripts": scene.get_all_scripts(),
        }

    # Autoload analysis
    autoload_analysis: dict[str, dict] = {}
    for name, script_path in idx.autoloads.items():
        gd = session.get_script(script_path)
        if gd is None:
            referenced_by = session.get_autoload_dependents(name)
            autoload_analysis[name] = {
                "script_path": script_path,
                "class_name": "",
                "extends": "",
                "signals": [],
                "signals_defined": 0,
                "signals_emitted": 0,
                "public_functions": [],
                "referenced_by": referenced_by,
                "referenced_by_count": len(referenced_by),
            }
            continue
        public_funcs = [
            {"name": f.name, "args": len(f.args), "return_type": f.return_type}
            for f in gd.functions
            if not f.is_private
        ][:_MAX_PUBLIC_FUNCTIONS]
        referenced_by = session.get_autoload_dependents(name)
        autoload_analysis[name] = {
            "script_path": script_path,
            "class_name": gd.class_name,
            "extends": gd.extends,
            "signals": [s.name for s in gd.signals],
            "signals_defined": len(gd.signals),
            "signals_emitted": len(gd.signal_emissions),
            "public_functions": public_funcs,
            "referenced_by": referenced_by,
            "referenced_by_count": len(referenced_by),
        }

    # File tree
    asset_types: Counter[str] = Counter()
    for _path, info in idx.assets.items():
        asset_types[info.get("type", "resource")] += 1

    file_tree = {
        "scenes": list(idx.scenes.keys()),
        "scripts": list(idx.scripts),
        "assets_by_type": dict(asset_types),
    }

    # Detected patterns
    has_serialize = False
    uses_signals = False
    uses_groups = False
    for gd in session._gd_scripts.values():
        if any(f.name in ("serialize", "deserialize") for f in gd.functions):
            has_serialize = True
        if gd.signals:
            uses_signals = True
        if has_serialize and uses_signals:
            break
    for scene in idx.scenes.values():
        if uses_groups:
            break
        for node in scene.nodes:
            if node.groups:
                uses_groups = True
                break

    detected_patterns = {
        "has_serialize": has_serialize,
        "uses_signals": uses_signals,
        "uses_autoloads": bool(idx.autoloads),
        "uses_groups": uses_groups,
    }

    # Signal health summary
    all_definitions = session.get_all_signal_definitions()
    all_emissions = session.get_all_signal_emissions()
    all_connections_raw = session.get_all_signal_connections()

    # Build project-defined signal set (signals declared in .gd files)
    project_signals: set[str] = set()
    for gd in session._gd_scripts.values():
        for sig in gd.signals:
            project_signals.add(sig.name)

    # Normalize connection keys: "EconomyManager.cash_changed" -> "cash_changed"
    all_connections_by_name: dict[str, list[str]] = {}
    for source, scripts in all_connections_raw.items():
        bare_name = source.rsplit(".", 1)[-1] if "." in source else source
        if bare_name in all_connections_by_name:
            all_connections_by_name[bare_name].extend(scripts)
        else:
            all_connections_by_name[bare_name] = list(scripts)

    total_defined = sum(len(v) for v in all_definitions.values())
    total_emissions = sum(len(v) for v in all_emissions.values())
    total_connections = sum(len(v) for v in all_connections_by_name.values())

    # Orphan = project-defined signal emitted but never connected-to
    orphan_count = sum(
        1 for sig_name in all_emissions
        if sig_name in project_signals and sig_name not in all_connections_by_name
    )

    # Top signals by listener count — project-defined only
    top_signals = sorted(
        [
            {"name": sig, "listeners": len(scripts)}
            for sig, scripts in all_connections_by_name.items()
            if sig in project_signals
        ],
        key=lambda e: e["listeners"],
        reverse=True,
    )[:5]

    signal_health = {
        "total_defined": total_defined,
        "total_emissions": total_emissions,
        "total_connections": total_connections,
        "orphan_count": orphan_count,
        "top_signals": top_signals,
    }

    # Project health
    autoload_scripts = set(idx.autoloads.values())
    scripts_without_class_name = sum(
        1
        for path, gd in session._gd_scripts.items()
        if path not in autoload_scripts and not gd.class_name
    )

    health = {
        "orphan_signals": orphan_count,
        "scripts_without_class_name": scripts_without_class_name,
        "note": f"{orphan_count} signal(s) emitted but never connected to; "
        f"{scripts_without_class_name} non-autoload script(s) without class_name",
    }

    return {
        "scene_structure": scene_structure,
        "autoload_analysis": autoload_analysis,
        "file_tree": file_tree,
        "detected_patterns": detected_patterns,
        "signal_health": signal_health,
        "health": health,
    }


def _build_project_summary(session: GodotIQSession, detail: str = "normal") -> dict:
    """Build project summary at the requested detail level.

    Args:
        session: Loaded GodotIQSession.
        detail: One of "brief", "normal", "full".

    Returns:
        Dictionary with project summary data.
    """
    if session._index is None:
        return {"error": "Project index not loaded."}

    detail = detail.lower()
    result = _build_brief(session)

    if detail in ("normal", "full"):
        result.update(_build_normal(session))

    if detail == "full":
        result.update(_build_full(session))

    return apply_output_limit(result, detail)


def _build_gd_context(session: GodotIQSession, res_path: str, gd: GdScript) -> dict:
    """Build context dict for a .gd script file."""
    idx = session._index

    # Check if this script is an autoload
    is_autoload = res_path in idx.autoloads.values()

    # Line count
    abs_path = session._resolve_res_path(res_path)
    lines = 0
    if abs_path.exists():
        lines = len(abs_path.read_text(encoding="utf-8").splitlines())

    # Public API: non-private functions
    public_funcs = [
        {
            "name": f.name,
            "args": [
                {"name": a[0], "type": a[1], "default": a[2]} for a in f.args
            ],
            "return_type": f.return_type,
            "is_static": f.is_static,
            "line": f.line,
        }
        for f in gd.functions
        if not f.is_private
    ]

    # Public signals
    public_signals = [
        {
            "name": s.name,
            "args": [{"name": a[0], "type": a[1]} for a in s.args],
            "line": s.line,
        }
        for s in gd.signals
    ]

    # Public variables (class-scope only)
    public_vars = [
        {
            "name": v.name,
            "type": v.type_hint,
            "is_const": v.is_const,
            "annotation": v.annotation,
            "line": v.line,
        }
        for v in gd.variables
        if v.scope == "class"
    ]

    # Enums
    public_enums = [
        {
            "name": e.name,
            "values": list(e.values.keys()),
            "line": e.line,
        }
        for e in gd.enums
    ]

    # Signals
    signals_defined = [s.name for s in gd.signals]
    signals_emitted = [e.signal_name for e in gd.signal_emissions]
    signals_connected = [c.signal_source for c in gd.signal_connections]

    # Dependencies
    autoload_deps = list({r.autoload_name for r in gd.autoload_refs})
    preload_deps = [r.path for r in gd.preload_refs]
    used_in_scenes = idx.script_to_scenes.get(res_path, [])

    # Depended by: find scripts that reference this file
    depended_by: list[str] = []
    if is_autoload:
        # Find the autoload name for this script
        for name, path in idx.autoloads.items():
            if path == res_path:
                depended_by = session.get_autoload_dependents(name)
                break
    # Also check preload references across all scripts
    for other_path, other_gd in session._gd_scripts.items():
        if other_path == res_path:
            continue
        for ref in other_gd.preload_refs:
            if ref.path == res_path and other_path not in depended_by:
                depended_by.append(other_path)

    # Safety
    has_serialize = any(
        f.name in ("serialize", "deserialize") for f in gd.functions
    )

    return {
        "file": res_path,
        "type": "gdscript",
        "class_name": gd.class_name,
        "extends": gd.extends,
        "is_autoload": is_autoload,
        "lines": lines,
        "public_api": {
            "functions": public_funcs,
            "signals": public_signals,
            "variables": public_vars,
            "enums": public_enums,
        },
        "signals_defined": signals_defined,
        "signals_emitted": signals_emitted,
        "signals_connected": signals_connected,
        "depends_on": {
            "autoloads": autoload_deps,
            "preloads": preload_deps,
            "scenes": used_in_scenes,
        },
        "depended_by": depended_by,
        "used_in_scenes": used_in_scenes,
        "safety": {
            "is_instance_valid_count": len(gd.is_instance_valid_lines),
            "has_serialize": has_serialize,
        },
    }


def _build_tscn_context(session: GodotIQSession, res_path: str, scene: TscnScene) -> dict:
    """Build context dict for a .tscn scene file."""
    idx = session._index

    # External resources
    ext_resources = [
        {"id": rid, "type": r.type, "path": r.path}
        for rid, r in scene.ext_resources.items()
    ]

    # Instances and scripts
    instances = scene.get_all_instances()
    scripts_used = scene.get_all_scripts()

    # Unique groups across all nodes
    groups: set[str] = set()
    for node in scene.nodes:
        groups.update(node.groups)

    # Connections
    connections = [
        {
            "signal": c.signal,
            "from": c.from_node,
            "to": c.to_node,
            "method": c.method,
        }
        for c in scene.connections
    ]

    # Instanced by: scenes that instance this scene
    instanced_by = idx.scene_instance_of.get(res_path, [])

    return {
        "file": res_path,
        "type": "scene",
        "total_nodes": len(scene.nodes),
        "root_type": scene.root.type if scene.root else "",
        "ext_resources": ext_resources,
        "instances": instances,
        "scripts_used": scripts_used,
        "groups_used": sorted(groups),
        "connections": connections,
        "instanced_by": instanced_by,
    }


def _build_file_context(
    session: GodotIQSession, file_path: str, detail: str = "normal",
) -> dict:
    """Build deep context for a single file.

    Args:
        session: Loaded GodotIQSession.
        file_path: res:// path or relative path from project root.
        detail: "brief", "normal", or "full".

    Returns:
        Dictionary with file context data. Returns error dict if file not found.
    """
    res_path = session.resolve_file_path(file_path)
    if res_path is None:
        return {"error": "File not found", "file": file_path}

    detail = detail.lower() if detail else "normal"

    # Try as GDScript
    gd = session.get_script(res_path)
    if gd is not None:
        full = _build_gd_context(session, res_path, gd)
        if detail == "brief":
            return {
                "file": full["file"],
                "type": full["type"],
                "class_name": full["class_name"],
                "extends": full["extends"],
                "is_autoload": full["is_autoload"],
                "public_api": full["public_api"],
            }
        return apply_output_limit(full, detail)

    # Try as scene
    scene = session.get_scene(res_path)
    if scene is not None:
        full = _build_tscn_context(session, res_path, scene)
        if detail == "brief":
            return {
                "file": full["file"],
                "type": full["type"],
                "total_nodes": full["total_nodes"],
                "root_type": full["root_type"],
                "scripts_used": full["scripts_used"],
            }
        return apply_output_limit(full, detail)

    return {
        "error": "Unsupported file type. Only .gd and .tscn files are supported.",
        "file": res_path,
    }


@mcp.tool(
    name="godotiq_project_summary",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": False,
    },
)
async def godotiq_project_summary(
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Call FIRST in every session. Architecture, autoloads, conventions, file counts. Replaces manual project exploration — one call gives full context. Use detail="brief" for counts only.

    Args:
        detail: "brief" (counts + autoload names), "normal" (+ architecture), "full" (+ scene structure, signal health).
    """
    session = ctx.request_context.lifespan_context["session"]
    if session is None:
        return {"error": "No Godot project loaded. Set GODOTIQ_PROJECT_ROOT or run from project directory."}
    return gate_pro("godotiq_project_summary", _build_project_summary(session, detail))


@mcp.tool(
    name="godotiq_file_context",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": False,
    },
)
async def godotiq_file_context(
    file: str,
    detail: str = "normal",
    ctx: Context = None,
) -> dict:
    """Call BEFORE editing any file. Returns public API, dependencies, signals, who imports this file, scene usage. Without this you risk breaking callers. DO NOT read .gd/.tscn files directly — this gives structured cross-referenced data.

    Args:
        file: Path to file (res:// or relative). Works for .gd scripts and .tscn scenes.
        detail: "brief" (public API only), "normal" (+ deps, signals, safety), "full".
    """
    session = ctx.request_context.lifespan_context["session"]
    if session is None:
        return {"error": "No Godot project loaded"}
    return gate_pro("godotiq_file_context", _build_file_context(session, file, detail=detail))
