"""Analysis commands — dead-code, circular-imports, unused-deps, dep-graph."""

from __future__ import annotations

import ast
import subprocess
import sys
import tomllib
from collections import defaultdict
from pathlib import Path

from afd import CommandResult, error, success

from botcore.utils.workspace import find_workspace


async def dev_dead_code(path: str | None = None, min_confidence: int = 80) -> CommandResult[dict]:
    """Find dead/unused code using vulture."""
    ws = find_workspace()
    scan_path = path or (str(ws / "src") if ws else "src")

    result = subprocess.run(
        [sys.executable, "-m", "vulture", scan_path, f"--min-confidence={min_confidence}"],
        capture_output=True,
        text=True,
        cwd=ws,
    )

    issues = []
    for line in result.stdout.strip().splitlines():
        if not line:
            continue
        parts = line.split(":", 2)
        if len(parts) >= 3:
            issues.append({
                "file": parts[0],
                "line": int(parts[1]) if parts[1].isdigit() else 0,
                "message": parts[2].strip(),
            })

    result_data: dict = {
        "path": scan_path,
        "issues": issues,
        "count": len(issues),
    }

    if issues:
        unused_funcs = sum(1 for i in issues if "unused function" in i["message"].lower())
        unused_vars = sum(1 for i in issues if "unused variable" in i["message"].lower())
        unused_imports = sum(1 for i in issues if "unused import" in i["message"].lower())

        result_data["summary"] = {
            "unused_functions": unused_funcs,
            "unused_variables": unused_vars,
            "unused_imports": unused_imports,
        }

        return success(
            data=result_data,
            reasoning=(
                f"Found {len(issues)} dead code issues: "
                f"{unused_funcs} functions, {unused_vars} variables, "
                f"{unused_imports} imports"
            ),
        )

    return success(
        data=result_data,
        reasoning=f"No dead code found in {scan_path}",
    )


async def dev_circular_imports(path: str | None = None) -> CommandResult[dict]:
    """Detect circular imports in Python code."""
    ws = find_workspace()
    scan_path = Path(path) if path else (ws / "src" if ws else Path("src"))

    if not scan_path.exists():
        return error(
            "PATH_NOT_FOUND",
            f"Path not found: {scan_path}",
            suggestion="Verify the path exists or omit to use default src/",
        )

    imports: dict[str, set[str]] = defaultdict(set)
    all_modules: set[str] = set()

    for py_file in scan_path.rglob("*.py"):
        if "__pycache__" in str(py_file):
            continue

        try:
            content = py_file.read_text(encoding="utf-8")
            tree = ast.parse(content)
        except Exception:
            continue

        try:
            rel_path = py_file.relative_to(scan_path)
            module_name = str(rel_path.with_suffix("")).replace("/", ".").replace("\\", ".")
            if module_name.endswith(".__init__"):
                module_name = module_name[:-9]
        except ValueError:
            continue

        all_modules.add(module_name)

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    target = alias.name
                    if target != module_name:
                        imports[module_name].add(target)
            elif isinstance(node, ast.ImportFrom):
                if node.module and node.level == 0:
                    target = node.module
                    if target != module_name:
                        imports[module_name].add(target)

    internal_imports: dict[str, set[str]] = defaultdict(set)
    for src, targets in imports.items():
        for target in targets:
            for mod in all_modules:
                if target == mod or target.startswith(mod + ".") or mod.startswith(target + "."):
                    internal_imports[src].add(target)
                    break

    cycles = _find_cycles(dict(internal_imports), all_modules)

    result_data = {
        "path": str(scan_path),
        "modules_scanned": len(all_modules),
        "cycles": cycles,
        "cycle_count": len(cycles),
    }

    if cycles:
        cycle_summary = "; ".join(" → ".join(c) for c in cycles[:5])
        return error(
            "CIRCULAR_IMPORTS",
            f"Found {len(cycles)} circular import(s): {cycle_summary}",
            suggestion="Break cycles by restructuring imports or using lazy imports",
        )

    return success(
        data=result_data,
        reasoning=f"No circular imports found in {len(imports)} modules",
    )


def _find_cycles(graph: dict[str, set[str]], known_modules: set[str]) -> list[list[str]]:
    """Find cycles in the import graph via DFS."""
    cycles: list[list[str]] = []
    visited: set[str] = set()

    def dfs(node: str, path: list[str]) -> None:
        if node in path:
            cycle_start = path.index(node)
            cycle = path[cycle_start:] + [node]
            if len(set(cycle)) > 1 and cycle not in cycles:
                cycles.append(cycle)
            return

        if node in visited:
            return

        visited.add(node)
        path.append(node)

        for neighbor in graph.get(node, []):
            for mod in known_modules:
                if neighbor == mod or mod.startswith(neighbor + "."):
                    dfs(neighbor, path.copy())
                    break

    for node in graph:
        dfs(node, [])

    return cycles


async def dev_unused_deps() -> CommandResult[dict]:
    """Find unused dependencies in pyproject.toml."""
    import re

    ws = find_workspace()
    if not ws:
        return error(
            "NO_WORKSPACE",
            "Could not find workspace root",
            suggestion="Run from within a Git repository",
        )

    pyproject = ws / "pyproject.toml"
    if not pyproject.exists():
        return error(
            "NO_PYPROJECT",
            "No pyproject.toml found",
            suggestion="Initialize project with 'hatch new' or create pyproject.toml",
        )

    data = tomllib.loads(pyproject.read_text(encoding="utf-8"))

    declared_deps: set[str] = set()
    for dep in data.get("project", {}).get("dependencies", []):
        match = re.match(r"([a-zA-Z0-9_-]+)", dep)
        if match:
            declared_deps.add(match.group(1).lower().replace("-", "_"))

    src_path = ws / "src"
    if not src_path.exists():
        src_path = ws

    used_imports = _scan_imports(src_path)
    potentially_unused = declared_deps - used_imports
    runtime_deps = {"pip", "setuptools", "wheel", "hatchling", "hatch"}
    potentially_unused -= runtime_deps

    result_data = {
        "declared": sorted(declared_deps),
        "used": sorted(used_imports),
        "potentially_unused": sorted(potentially_unused),
    }

    if potentially_unused:
        deps_list = ", ".join(sorted(potentially_unused))
        return success(
            data=result_data,
            reasoning=f"{len(potentially_unused)} potentially unused deps: {deps_list}",
        )

    return success(
        data=result_data,
        reasoning=f"All {len(declared_deps)} declared dependencies appear to be used",
    )


# Known stdlib modules (subset) for filtering
_STDLIB_MODULES = {
    "os", "sys", "re", "json", "pathlib", "typing", "asyncio", "io",
    "subprocess", "collections", "datetime", "time", "functools",
    "itertools", "contextlib", "logging", "unittest", "dataclasses",
    "abc", "copy", "tempfile", "shutil", "glob", "hashlib", "base64",
    "urllib", "http", "email", "html", "xml", "sqlite3", "csv",
    "pickle", "struct", "codecs", "locale", "gettext", "argparse",
    "configparser", "traceback", "warnings", "inspect", "importlib",
    "pkgutil", "platform", "socket", "ssl", "select", "threading",
    "multiprocessing", "queue", "concurrent", "contextvars", "enum",
    "numbers", "decimal", "fractions", "random", "statistics", "math",
    "cmath", "operator", "string", "textwrap", "difflib", "secrets",
    "uuid", "weakref", "types", "pprint", "reprlib", "graphlib",
    "heapq", "bisect", "array", "ast", "dis", "compileall", "venv",
    "zipfile", "tarfile", "gzip", "bz2", "lzma", "zlib", "signal",
    "tomllib",
}


def _scan_imports(src_path: Path) -> set[str]:
    """Scan Python files for imported third-party packages."""
    used_imports: set[str] = set()

    for py_file in src_path.rglob("*.py"):
        if "__pycache__" in str(py_file):
            continue

        try:
            content = py_file.read_text(encoding="utf-8")
            tree = ast.parse(content)
        except Exception:
            continue

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    pkg = alias.name.split(".")[0].lower().replace("-", "_")
                    if pkg not in _STDLIB_MODULES:
                        used_imports.add(pkg)
            elif isinstance(node, ast.ImportFrom):
                if node.module and node.level == 0:
                    pkg = node.module.split(".")[0].lower().replace("-", "_")
                    if pkg not in _STDLIB_MODULES:
                        used_imports.add(pkg)

    return used_imports


async def dev_dep_graph(
    path: str | None = None,
    output: str = "json",
) -> CommandResult[dict]:
    """Generate dependency graph of Python modules."""
    ws = find_workspace()
    scan_path = Path(path) if path else (ws / "src" if ws else Path("src"))

    if not scan_path.exists():
        return error(
            "PATH_NOT_FOUND",
            f"Path not found: {scan_path}",
            suggestion="Verify the path exists or omit to use default src/",
        )

    graph: dict[str, list[str]] = defaultdict(list)
    modules: set[str] = set()

    for py_file in scan_path.rglob("*.py"):
        if "__pycache__" in str(py_file):
            continue

        try:
            content = py_file.read_text(encoding="utf-8")
            tree = ast.parse(content)
        except Exception:
            continue

        try:
            rel_path = py_file.relative_to(scan_path)
            module_name = str(rel_path.with_suffix("")).replace("/", ".").replace("\\", ".")
            if module_name.endswith(".__init__"):
                module_name = module_name[:-9]
        except ValueError:
            continue

        modules.add(module_name)

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    target = alias.name
                    if any(target.startswith(m.split(".")[0]) for m in modules if m):
                        graph[module_name].append(target)
            elif isinstance(node, ast.ImportFrom):
                if node.module and node.level == 0:
                    target = node.module
                    if any(target.startswith(m.split(".")[0]) for m in modules if m):
                        graph[module_name].append(target)

    graph = {k: sorted(set(v)) for k, v in graph.items()}

    result_data: dict = {
        "path": str(scan_path),
        "modules": sorted(modules),
        "module_count": len(modules),
        "edges": sum(len(v) for v in graph.values()),
        "graph": dict(graph),
    }

    if output == "dot":
        dot_lines = ["digraph dependencies {", "  rankdir=LR;"]
        for src, targets in graph.items():
            for target in targets:
                dot_lines.append(f'  "{src}" -> "{target}";')
        dot_lines.append("}")
        result_data["dot"] = "\n".join(dot_lines)

    edges = result_data["edges"]
    return success(
        data=result_data,
        reasoning=f"Generated dependency graph: {len(modules)} modules, {edges} edges",
    )
