"""
Codebase Architecture Analyzer — generates a structural mindmap
of any repository by analyzing imports, exports, routes, and
cross-boundary connections.

Works with: Python, JS/TS/JSX/TSX, C#, Go, Rust, Java, Ruby, PHP
"""

import re
from collections import defaultdict
from typing import Optional

# ═══════════════════════════════════════════════════
#  Language-specific parsers
# ═══════════════════════════════════════════════════

def _parse_python(path: str, content: str) -> dict:
    """Extract imports, definitions, and routes from Python files."""
    imports = []
    definitions = []
    routes = []
    decorators = []

    for line in content.split("\n"):
        stripped = line.strip()

        # Imports
        m = re.match(r'^from\s+([\w.]+)\s+import\s+(.+)', stripped)
        if m:
            module = m.group(1)
            names = [n.strip().split(" as ")[0].strip() for n in m.group(2).split(",")]
            imports.append({"module": module, "names": names})
            continue

        m = re.match(r'^import\s+([\w.]+)', stripped)
        if m:
            imports.append({"module": m.group(1), "names": []})
            continue

        # Decorators (accumulate for next def/class)
        m = re.match(r'^@(\w+)\.(get|post|put|delete|patch|websocket)\s*\(\s*["\']([^"\']*)', stripped)
        if m:
            method = m.group(2).upper()
            route_path = m.group(3)
            decorators.append({"method": method, "path": route_path})
            continue

        m = re.match(r'^@(\w+)\.route\s*\(\s*["\']([^"\']*)', stripped)
        if m:
            decorators.append({"method": "ROUTE", "path": m.group(2)})
            continue

        # Function defs
        m = re.match(r'^(?:async\s+)?def\s+(\w+)\s*\(', stripped)
        if m:
            name = m.group(1)
            if not name.startswith("_") or name.startswith("__"):
                defn = {"type": "function", "name": name}
                definitions.append(defn)
                if decorators:
                    for d in decorators:
                        routes.append({
                            "method": d["method"],
                            "path": d["path"],
                            "handler": name,
                            "file": path,
                        })
            decorators = []
            continue

        # Class defs
        m = re.match(r'^class\s+(\w+)\s*[\(:]', stripped)
        if m:
            definitions.append({"type": "class", "name": m.group(1)})
            decorators = []
            continue

        # Clear decorators on non-matching lines
        if stripped and not stripped.startswith("@") and not stripped.startswith("#"):
            decorators = []

    return {"imports": imports, "definitions": definitions, "routes": routes}


def _parse_javascript(path: str, content: str) -> dict:
    """Extract imports, exports, routes, and API calls from JS/TS files."""
    imports = []
    exports = []
    api_calls = []
    routes = []

    for line in content.split("\n"):
        stripped = line.strip()

        # ES6 imports
        m = re.match(r'^import\s+(?:{([^}]+)}|(\w+))\s+from\s+["\']([^"\']+)', stripped)
        if m:
            names = []
            if m.group(1):
                names = [n.strip().split(" as ")[0].strip() for n in m.group(1).split(",")]
            elif m.group(2):
                names = [m.group(2)]
            imports.append({"module": m.group(3), "names": names})
            continue

        # import default + named
        m = re.match(r'^import\s+(\w+)\s*,\s*{([^}]+)}\s+from\s+["\']([^"\']+)', stripped)
        if m:
            names = [m.group(1)] + [n.strip().split(" as ")[0].strip() for n in m.group(2).split(",")]
            imports.append({"module": m.group(3), "names": names})
            continue

        # import * as
        m = re.match(r'^import\s+\*\s+as\s+(\w+)\s+from\s+["\']([^"\']+)', stripped)
        if m:
            imports.append({"module": m.group(2), "names": [f"* as {m.group(1)}"]})
            continue

        # require
        m = re.search(r'require\s*\(\s*["\']([^"\']+)', stripped)
        if m and not stripped.startswith("//"):
            imports.append({"module": m.group(1), "names": []})

        # Exports
        m = re.match(r'^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)', stripped)
        if m:
            exports.append({"type": "function", "name": m.group(1)})
            continue

        m = re.match(r'^export\s+(?:default\s+)?(?:const|let|var)\s+(\w+)', stripped)
        if m:
            exports.append({"type": "const", "name": m.group(1)})
            continue

        m = re.match(r'^export\s+default\s+(?:class\s+)?(\w+)', stripped)
        if m:
            exports.append({"type": "default", "name": m.group(1)})
            continue

        # API calls (fetch)
        for fm in re.finditer(r'fetch\s*\(\s*[`"\']([^`"\']*(?:/api/[^`"\']*)?)[`"\']', stripped):
            url = fm.group(1)
            if "/api/" in url or url.startswith("/"):
                api_calls.append(url)

        for fm in re.finditer(r'fetch\s*\(\s*`\$\{[^}]*\}(/[^`]*)`', stripped):
            api_calls.append(fm.group(1))

        # Express routes
        m = re.match(r'(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["\']([^"\']+)', stripped)
        if m:
            routes.append({
                "method": m.group(1).upper(),
                "path": m.group(2),
                "file": path,
            })

    return {"imports": imports, "exports": exports, "api_calls": api_calls, "routes": routes}


def _parse_csharp(path: str, content: str) -> dict:
    """Extract basic structure from C# files."""
    imports = []
    definitions = []
    routes = []

    for line in content.split("\n"):
        stripped = line.strip()

        m = re.match(r'^using\s+([\w.]+)\s*;', stripped)
        if m:
            imports.append({"module": m.group(1), "names": []})
            continue

        m = re.match(r'^(?:public|private|protected|internal|static|\s)*class\s+(\w+)', stripped)
        if m:
            definitions.append({"type": "class", "name": m.group(1)})
            continue

        m = re.match(r'^(?:public|private|protected|internal|static|\s)*interface\s+(\w+)', stripped)
        if m:
            definitions.append({"type": "interface", "name": m.group(1)})
            continue

        # ASP.NET routes
        m = re.search(r'\[Http(Get|Post|Put|Delete|Patch)\s*\(\s*"([^"]*)"', stripped)
        if m:
            routes.append({"method": m.group(1).upper(), "path": m.group(2), "file": path})
            continue

        m = re.search(r'\[Route\s*\(\s*"([^"]*)"', stripped)
        if m:
            routes.append({"method": "ROUTE", "path": m.group(1), "file": path})

    return {"imports": imports, "definitions": definitions, "routes": routes}


def _parse_generic(path: str, content: str) -> dict:
    """Fallback: extract function/class patterns from any language."""
    definitions = []
    imports = []

    for line in content.split("\n"):
        stripped = line.strip()

        # Go imports
        m = re.match(r'^import\s+"([^"]+)"', stripped)
        if m:
            imports.append({"module": m.group(1), "names": []})

        # Go/Rust function defs
        m = re.match(r'^(?:pub\s+)?fn\s+(\w+)', stripped)
        if m:
            definitions.append({"type": "function", "name": m.group(1)})
            continue

        m = re.match(r'^func\s+(?:\([^)]+\)\s+)?(\w+)', stripped)
        if m:
            definitions.append({"type": "function", "name": m.group(1)})
            continue

        # Go/Rust struct/type
        m = re.match(r'^(?:pub\s+)?(?:type|struct)\s+(\w+)', stripped)
        if m:
            definitions.append({"type": "struct", "name": m.group(1)})

    return {"imports": imports, "definitions": definitions}


# ═══════════════════════════════════════════════════
#  File parser dispatcher
# ═══════════════════════════════════════════════════

def _parse_file(path: str, content: str) -> dict:
    """Parse a file based on its extension."""
    lower = path.lower()
    if lower.endswith((".py",)):
        return _parse_python(path, content)
    elif lower.endswith((".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".vue", ".svelte")):
        return _parse_javascript(path, content)
    elif lower.endswith((".cs",)):
        return _parse_csharp(path, content)
    else:
        return _parse_generic(path, content)


# ═══════════════════════════════════════════════════
#  Resolve import paths to actual files
# ═══════════════════════════════════════════════════

def _resolve_import(importing_file: str, module: str, all_paths: set[str]) -> Optional[str]:
    """Try to resolve an import module string to an actual file path."""
    if not module:
        return None

    # Direct path match
    for ext in ["", ".py", ".js", ".ts", ".jsx", ".tsx", ".cs", ".go", ".rs"]:
        candidate = module.replace(".", "/") + ext
        if candidate in all_paths:
            return candidate

    # Relative imports for Python (from .module import ...)
    if module.startswith("."):
        dir_parts = importing_file.rsplit("/", 1)
        base_dir = dir_parts[0] if len(dir_parts) > 1 else ""
        rel = module.lstrip(".")
        for ext in [".py", ""]:
            candidate = f"{base_dir}/{rel.replace('.', '/')}{ext}"
            if candidate in all_paths:
                return candidate
            # __init__.py
            candidate = f"{base_dir}/{rel.replace('.', '/')}/__init__.py"
            if candidate in all_paths:
                return candidate

    # JS relative imports
    if module.startswith("./") or module.startswith("../"):
        dir_parts = importing_file.rsplit("/", 1)
        base_dir = dir_parts[0] if len(dir_parts) > 1 else ""
        if module.startswith("./"):
            rel = module[2:]
        else:
            # Go up directories
            parts = base_dir.split("/")
            ups = module.count("../")
            base = "/".join(parts[:-ups]) if ups < len(parts) else ""
            rel = module.replace("../", "")
            base_dir = base

        for ext in ["", ".js", ".jsx", ".ts", ".tsx", "/index.js", "/index.tsx", "/index.ts"]:
            candidate = f"{base_dir}/{rel}{ext}" if base_dir else f"{rel}{ext}"
            if candidate in all_paths:
                return candidate

    # Backend-style imports (backend.models → backend/models.py)
    for ext in [".py"]:
        candidate = module.replace(".", "/") + ext
        if candidate in all_paths:
            return candidate

    return None


# ═══════════════════════════════════════════════════
#  Main analyzer
# ═══════════════════════════════════════════════════

MAX_MAP_CHARS = 40_000  # ~10K tokens max for the map


def analyze_codebase(files: list[dict]) -> str:
    """
    Analyze a list of {path, content} dicts and produce
    an architecture mindmap string.
    """
    all_paths = {f["path"] for f in files}
    file_data = {}  # path → parsed data
    all_routes = []
    all_api_calls = []
    import_graph = defaultdict(set)       # file → set of files it imports
    imported_by = defaultdict(set)         # file → set of files that import it
    definitions_map = defaultdict(list)    # file → list of definitions
    exports_map = defaultdict(list)        # file → list of exports

    # ── Pass 1: Parse all files ──
    for f in files:
        path = f["path"]
        content = f.get("content", "")
        if not content or content.startswith("["):
            continue

        parsed = _parse_file(path, content)
        file_data[path] = parsed

        # Collect definitions
        for d in parsed.get("definitions", []):
            definitions_map[path].append(d)

        for e in parsed.get("exports", []):
            exports_map[path].append(e)

        # Collect routes
        for r in parsed.get("routes", []):
            r["file"] = path
            all_routes.append(r)

        # Collect API calls
        for call in parsed.get("api_calls", []):
            all_api_calls.append({"file": path, "url": call})

    # ── Pass 2: Resolve imports → build dependency graph ──
    for path, parsed in file_data.items():
        for imp in parsed.get("imports", []):
            target = _resolve_import(path, imp["module"], all_paths)
            if target and target != path:
                import_graph[path].add(target)
                imported_by[target].add(path)

    # ── Pass 3: Detect project structure ──
    lang_counts = defaultdict(int)
    dir_categories = defaultdict(set)

    for path in all_paths:
        ext = ""
        if "." in path.rsplit("/", 1)[-1]:
            ext = "." + path.rsplit(".", 1)[-1].lower()

        lang_map = {
            ".py": "Python", ".js": "JavaScript", ".ts": "TypeScript",
            ".jsx": "React JSX", ".tsx": "React TSX", ".cs": "C#",
            ".java": "Java", ".go": "Go", ".rs": "Rust", ".rb": "Ruby",
            ".php": "PHP", ".vue": "Vue", ".svelte": "Svelte",
            ".html": "HTML", ".css": "CSS", ".scss": "SCSS",
            ".sql": "SQL", ".sh": "Shell",
        }
        if ext in lang_map:
            lang_counts[lang_map[ext]] += 1

        top_dir = path.split("/")[0] if "/" in path else "(root)"
        dir_categories[top_dir].add(path)

    # Detect frameworks
    frameworks = []
    all_content_lower = " ".join(
        f.get("content", "")[:500].lower() for f in files[:50]
    )
    framework_signals = {
        "FastAPI": ["fastapi", "from fastapi"],
        "Flask": ["from flask"],
        "Django": ["from django", "django.conf"],
        "Express": ["express()", "require('express')"],
        "React": ["from 'react'", 'from "react"', "react-dom"],
        "Vue": ["createapp", "vue"],
        "Next.js": ["next/", "getserversideprops"],
        "Unity": ["monobehaviour", "unityengine"],
        "ASP.NET": ["microsoft.aspnetcore", "iactionresult"],
        "SQLAlchemy": ["sqlalchemy", "declarative_base"],
        "Prisma": ["@prisma/client"],
    }
    for fw, signals in framework_signals.items():
        for sig in signals:
            if sig in all_content_lower:
                frameworks.append(fw)
                break

    # ── Pass 4: Identify hot files (most referenced) ──
    hot_files = sorted(imported_by.items(), key=lambda x: -len(x[1]))[:20]

    # ── Pass 5: Cross-boundary flow detection ──
    flows = _detect_cross_boundary_flows(all_routes, all_api_calls, import_graph, definitions_map)

    # ── Pass 6: Build the map ──
    return _format_architecture_map(
        all_paths=all_paths,
        lang_counts=lang_counts,
        frameworks=frameworks,
        dir_categories=dir_categories,
        all_routes=all_routes,
        all_api_calls=all_api_calls,
        import_graph=import_graph,
        imported_by=imported_by,
        definitions_map=definitions_map,
        exports_map=exports_map,
        hot_files=hot_files,
        flows=flows,
    )


def _detect_cross_boundary_flows(
    routes: list, api_calls: list,
    import_graph: dict, definitions_map: dict,
) -> list[str]:
    """Detect end-to-end flows across frontend/backend boundaries."""
    flows = []

    # Match API calls to routes
    route_map = {}
    for r in routes:
        key = r.get("path", "").rstrip("/")
        if key:
            route_map[key] = r

    for call in api_calls:
        url = call["url"]
        # Clean URL patterns
        clean = re.sub(r'\$\{[^}]+\}', '{param}', url)
        clean = clean.rstrip("/")

        matched_route = None
        for route_path, route_info in route_map.items():
            # Simple match — exact or parameterized
            route_clean = re.sub(r'\{[^}]+\}', '{param}', route_path)
            if clean == route_clean or clean.endswith(route_clean):
                matched_route = route_info
                break

        if matched_route:
            flows.append(
                f"  {call['file']} → {matched_route.get('method', '?')} {matched_route.get('path', '?')} → {matched_route.get('file', '?')}:{matched_route.get('handler', '?')}()"
            )

    return flows[:50]  # Cap flows


def _format_architecture_map(
    all_paths, lang_counts, frameworks, dir_categories,
    all_routes, all_api_calls, import_graph, imported_by,
    definitions_map, exports_map, hot_files, flows,
) -> str:
    """Format the analysis into a compact, AI-readable string."""
    lines = []
    lines.append("╔══════════════════════════════════════════╗")
    lines.append("║     CODEBASE ARCHITECTURE MINDMAP        ║")
    lines.append("╚══════════════════════════════════════════╝")
    lines.append("")

    # ── Project Overview ──
    lines.append("PROJECT OVERVIEW:")
    lines.append(f"  Total source files: {len(all_paths)}")
    if lang_counts:
        top_langs = sorted(lang_counts.items(), key=lambda x: -x[1])[:8]
        lines.append(f"  Languages: {', '.join(f'{l} ({c})' for l, c in top_langs)}")
    if frameworks:
        lines.append(f"  Frameworks: {', '.join(sorted(set(frameworks)))}")
    lines.append("")

    # ── Directory Structure ──
    lines.append("DIRECTORY STRUCTURE:")
    for dir_name in sorted(dir_categories.keys()):
        count = len(dir_categories[dir_name])
        lines.append(f"  {dir_name}/ ({count} files)")
    lines.append("")

    # ── API Routes ──
    if all_routes:
        lines.append("API ENDPOINTS:")
        # Group by file
        routes_by_file = defaultdict(list)
        for r in all_routes:
            routes_by_file[r.get("file", "?")].append(r)
        for rfile in sorted(routes_by_file.keys()):
            lines.append(f"  [{rfile}]")
            for r in routes_by_file[rfile]:
                handler = r.get("handler", "")
                handler_str = f" → {handler}()" if handler else ""
                lines.append(f"    {r.get('method', '?'):6s} {r.get('path', '?')}{handler_str}")
        lines.append("")

    # ── Frontend → Backend Connections ──
    if flows:
        lines.append("CROSS-BOUNDARY FLOWS (Frontend → Backend):")
        for flow in flows:
            lines.append(flow)
        lines.append("")

    # ── Hot Files (most imported) ──
    if hot_files:
        lines.append("HOT FILES (most referenced by other files):")
        for path, importers in hot_files:
            if len(importers) >= 2:
                lines.append(f"  {path} ← imported by {len(importers)} files")
        lines.append("")

    # ── Dependency Graph (compact) ──
    if import_graph:
        lines.append("DEPENDENCY GRAPH (file → its dependencies):")
        # Show only files with 2+ deps, sorted by dep count
        sorted_deps = sorted(import_graph.items(), key=lambda x: -len(x[1]))
        shown = 0
        for path, deps in sorted_deps:
            if shown >= 60:
                break
            dep_names = sorted(deps)[:15]
            extra = f" (+{len(deps) - 15} more)" if len(deps) > 15 else ""
            short_deps = [d.rsplit("/", 1)[-1] for d in dep_names]
            lines.append(f"  {path}")
            lines.append(f"    → {', '.join(short_deps)}{extra}")
            shown += 1
        lines.append("")

    # ── Key Definitions ──
    if definitions_map:
        lines.append("KEY DEFINITIONS:")
        for path in sorted(definitions_map.keys()):
            defs = definitions_map[path]
            if not defs:
                continue
            classes = [d["name"] for d in defs if d["type"] == "class"]
            functions = [d["name"] for d in defs if d["type"] == "function"]
            structs = [d["name"] for d in defs if d["type"] in ("struct", "interface")]

            parts = []
            if classes:
                parts.append(f"classes: {', '.join(classes[:10])}")
            if structs:
                parts.append(f"types: {', '.join(structs[:10])}")
            if functions:
                # Only show non-private functions, cap at 8
                pub = [f for f in functions if not f.startswith("_")][:8]
                if pub:
                    parts.append(f"functions: {', '.join(pub)}")

            if parts:
                lines.append(f"  {path}")
                for p in parts:
                    lines.append(f"    {p}")
        lines.append("")

    # ── Cycle/Pattern Detection ──
    cycles = _detect_cycles(import_graph)
    if cycles:
        lines.append("CIRCULAR DEPENDENCIES DETECTED:")
        for cycle in cycles[:10]:
            lines.append(f"  ⟲ {' → '.join(cycle)}")
        lines.append("")

    # ── Shared Definitions ──
    # Find classes/models used across many files
    shared = _find_shared_definitions(definitions_map, imported_by)
    if shared:
        lines.append("WIDELY-USED DEFINITIONS:")
        for name, info in shared[:15]:
            lines.append(f"  {name} (defined in {info['defined_in']}, used by {info['used_by_count']} files)")
        lines.append("")

    result = "\n".join(lines)

    # Truncate if too long
    if len(result) > MAX_MAP_CHARS:
        result = result[:MAX_MAP_CHARS] + "\n\n... [architecture map truncated]"

    return result


def _detect_cycles(import_graph: dict) -> list[list[str]]:
    """Detect import cycles using DFS."""
    cycles = []
    visited = set()
    path_set = set()
    path_list = []

    def dfs(node):
        if len(cycles) >= 10:
            return
        if node in path_set:
            # Found cycle
            idx = path_list.index(node)
            cycle = path_list[idx:] + [node]
            # Use short names
            short = [p.rsplit("/", 1)[-1] for p in cycle]
            cycles.append(short)
            return
        if node in visited:
            return

        visited.add(node)
        path_set.add(node)
        path_list.append(node)

        for dep in import_graph.get(node, []):
            dfs(dep)

        path_set.discard(node)
        path_list.pop()

    for node in import_graph:
        if node not in visited:
            dfs(node)

    return cycles


def _find_shared_definitions(
    definitions_map: dict,
    imported_by: dict,
) -> list[tuple[str, dict]]:
    """Find definitions that are used across many files."""
    results = []

    for path, defs in definitions_map.items():
        importers_count = len(imported_by.get(path, set()))
        if importers_count < 2:
            continue

        for d in defs:
            if d["type"] in ("class", "struct", "interface"):
                results.append((
                    d["name"],
                    {
                        "defined_in": path,
                        "type": d["type"],
                        "used_by_count": importers_count,
                    }
                ))

    results.sort(key=lambda x: -x[1]["used_by_count"])
    return results