"""
Phase 63: Auto-Documenter — Agents auto-document code into the knowledge base.

Parses Python files, extracts classes/functions/docstrings, and stores
structured documentation entries on disk.

Storage:
  state/broker/docs/
    {module_name}.json — documentation for a single module
    coverage.json      — cached coverage stats
"""

import ast
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from brynq.server import BROKER_DIR, _write_json_sync

# ── Paths ──────────────────────────────────────────────────────────────────

DOCS_DIR = BROKER_DIR / "docs"
DOCS_DIR.mkdir(parents=True, exist_ok=True)


# ── AST Extraction ─────────────────────────────────────────────────────────

def _extract_from_source(source: str, module_path: str) -> dict:
    """Parse Python source and extract documentation info."""
    try:
        tree = ast.parse(source)
    except SyntaxError:
        return {"error": f"SyntaxError in {module_path}", "classes": [], "functions": []}

    module_doc = ast.get_docstring(tree) or ""
    classes: List[dict] = []
    functions: List[dict] = []

    for node in ast.iter_child_nodes(tree):
        if isinstance(node, ast.ClassDef):
            cls_doc = ast.get_docstring(node) or ""
            methods = []
            for item in ast.iter_child_nodes(node):
                if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    m_doc = ast.get_docstring(item) or ""
                    args = [a.arg for a in item.args.args if a.arg != "self"]
                    methods.append({
                        "name": item.name,
                        "docstring": m_doc,
                        "args": args,
                        "line": item.lineno,
                        "is_private": item.name.startswith("_"),
                    })
            classes.append({
                "name": node.name,
                "docstring": cls_doc,
                "methods": methods,
                "line": node.lineno,
            })

        elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            fn_doc = ast.get_docstring(node) or ""
            args = [a.arg for a in node.args.args if a.arg != "self"]
            functions.append({
                "name": node.name,
                "docstring": fn_doc,
                "args": args,
                "line": node.lineno,
                "is_private": node.name.startswith("_"),
            })

    return {
        "module_docstring": module_doc,
        "classes": classes,
        "functions": functions,
    }


def _module_name_from_path(module_path: str) -> str:
    """Derive a module name from a file path."""
    p = Path(module_path)
    return p.stem


# ── Public API ─────────────────────────────────────────────────────────────

def document_module(module_path: str) -> dict:
    """Parse a Python file and store its documentation.

    Args:
        module_path: Path to a .py file.

    Returns:
        Documentation entry dict.
    """
    p = Path(module_path)
    if not p.exists():
        raise FileNotFoundError(f"Module not found: {module_path}")
    if p.suffix != ".py":
        raise ValueError(f"Not a Python file: {module_path}")

    source = p.read_text("utf-8", errors="replace")
    extracted = _extract_from_source(source, module_path)

    mod_name = _module_name_from_path(module_path)
    now = datetime.now(timezone.utc).isoformat()

    entry = {
        "module_name": mod_name,
        "module_path": str(p.resolve()),
        "module_docstring": extracted["module_docstring"],
        "classes": extracted["classes"],
        "functions": extracted["functions"],
        "documented_at": now,
        "total_classes": len(extracted["classes"]),
        "total_functions": len(extracted["functions"]),
    }

    if "error" in extracted:
        entry["error"] = extracted["error"]

    _write_json_sync(DOCS_DIR / f"{mod_name}.json", entry)
    return entry


def document_all(src_dir: str = "src/brynq") -> dict:
    """Document all Python files in a source directory.

    Returns:
        Summary with counts.
    """
    root = Path(src_dir)
    if not root.is_dir():
        raise ValueError(f"Not a directory: {src_dir}")

    results = {"modules_documented": 0, "errors": 0, "modules": []}
    for py_file in sorted(root.rglob("*.py")):
        if py_file.name.startswith("__"):
            continue
        try:
            entry = document_module(str(py_file))
            results["modules_documented"] += 1
            results["modules"].append(entry["module_name"])
        except Exception:
            results["errors"] += 1

    return results


def get_documentation(module_name: str) -> Optional[dict]:
    """Retrieve stored documentation for a module."""
    path = DOCS_DIR / f"{module_name}.json"
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return None


def get_undocumented() -> List[dict]:
    """Find modules/functions missing docstrings across all documented modules."""
    missing = []
    for f in sorted(DOCS_DIR.glob("*.json")):
        if f.name == "coverage.json":
            continue
        try:
            entry = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        mod = entry.get("module_name", f.stem)

        if not entry.get("module_docstring"):
            missing.append({"module": mod, "type": "module", "name": mod})

        for fn in entry.get("functions", []):
            if not fn.get("docstring"):
                missing.append({"module": mod, "type": "function", "name": fn["name"],
                                "line": fn.get("line")})

        for cls in entry.get("classes", []):
            if not cls.get("docstring"):
                missing.append({"module": mod, "type": "class", "name": cls["name"],
                                "line": cls.get("line")})
            for method in cls.get("methods", []):
                if not method.get("docstring") and not method.get("is_private", False):
                    missing.append({"module": mod, "type": "method",
                                    "name": f"{cls['name']}.{method['name']}",
                                    "line": method.get("line")})

    return missing


def get_doc_coverage() -> dict:
    """Calculate documentation coverage across all stored modules."""
    total_items = 0
    documented_items = 0
    modules_scanned = 0

    for f in sorted(DOCS_DIR.glob("*.json")):
        if f.name == "coverage.json":
            continue
        try:
            entry = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        modules_scanned += 1
        # Module docstring
        total_items += 1
        if entry.get("module_docstring"):
            documented_items += 1

        for fn in entry.get("functions", []):
            total_items += 1
            if fn.get("docstring"):
                documented_items += 1

        for cls in entry.get("classes", []):
            total_items += 1
            if cls.get("docstring"):
                documented_items += 1
            for method in cls.get("methods", []):
                total_items += 1
                if method.get("docstring"):
                    documented_items += 1

    pct = round(documented_items / total_items * 100, 1) if total_items > 0 else 0.0
    result = {
        "modules_scanned": modules_scanned,
        "total_items": total_items,
        "documented_items": documented_items,
        "undocumented_items": total_items - documented_items,
        "coverage_percent": pct,
    }
    _write_json_sync(DOCS_DIR / "coverage.json", result)
    return result
