"""Detect project technology stack from file system markers."""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

# Common framework indicators in package.json dependencies
_JS_FRAMEWORKS: dict[str, str] = {
    "next": "Next.js",
    "react": "React",
    "vue": "Vue",
    "nuxt": "Nuxt",
    "svelte": "Svelte",
    "@sveltejs/kit": "SvelteKit",
    "express": "Express",
    "fastify": "Fastify",
    "hono": "Hono",
    "@nestjs/core": "NestJS",
    "gatsby": "Gatsby",
    "remix": "Remix",
    "astro": "Astro",
    "angular": "Angular",
    "@angular/core": "Angular",
    "electron": "Electron",
}

_JS_TEST_FRAMEWORKS: dict[str, str] = {
    "jest": "Jest",
    "vitest": "Vitest",
    "mocha": "Mocha",
    "@playwright/test": "Playwright",
    "cypress": "Cypress",
    "bun": "Bun Test",
}

_JS_BUILD_TOOLS: dict[str, str] = {
    "vite": "Vite",
    "webpack": "Webpack",
    "esbuild": "esbuild",
    "rollup": "Rollup",
    "turbo": "Turborepo",
    "tsup": "tsup",
}

_PYTHON_FRAMEWORKS: dict[str, str] = {
    "django": "Django",
    "flask": "Flask",
    "fastapi": "FastAPI",
    "starlette": "Starlette",
    "tornado": "Tornado",
    "sanic": "Sanic",
    "aiohttp": "aiohttp",
    "litestar": "Litestar",
}

_PYTHON_TEST_FRAMEWORKS: dict[str, str] = {
    "pytest": "pytest",
    "unittest": "unittest",
    "nose2": "nose2",
}

# Directories to exclude from structure detection
_SKIP_DIRS = {
    "node_modules",
    "__pycache__",
    "venv",
    "dist",
    "build",
    "target",
    "coverage",
    "cdk.out",
}


def _detect_from_package_json(
    base: Path,
) -> dict[str, Any]:
    """Extract metadata from package.json."""
    pkg_file = base / "package.json"
    if not pkg_file.is_file():
        return {}

    try:
        data = json.loads(pkg_file.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}

    result: dict[str, Any] = {}
    result["name"] = data.get("name", "")

    # Determine language
    has_ts = (base / "tsconfig.json").is_file()
    result["languages"] = ["TypeScript" if has_ts else "JavaScript"]

    # Merge all deps for detection
    all_deps: dict[str, str] = {
        **data.get("dependencies", {}),
        **data.get("devDependencies", {}),
    }

    # Detect frameworks
    result["frameworks"] = [name for key, name in _JS_FRAMEWORKS.items() if key in all_deps]

    # Detect test frameworks
    result["test_frameworks"] = [name for key, name in _JS_TEST_FRAMEWORKS.items() if key in all_deps]

    # Detect build tools
    result["build_tools"] = [name for key, name in _JS_BUILD_TOOLS.items() if key in all_deps]

    # Package manager detection
    scripts = data.get("scripts", {})
    if (base / "bun.lockb").is_file() or (base / "bun.lock").is_file():
        result["package_manager"] = "bun"
    elif (base / "pnpm-lock.yaml").is_file():
        result["package_manager"] = "pnpm"
    elif (base / "yarn.lock").is_file():
        result["package_manager"] = "yarn"
    elif (base / "package-lock.json").is_file():
        result["package_manager"] = "npm"

    # Dev commands from scripts
    result["dev_commands"] = {}
    for cmd_name in ("dev", "start", "build", "test", "lint"):
        if cmd_name in scripts:
            result["dev_commands"][cmd_name] = scripts[cmd_name]

    return result


def _detect_from_pyproject(base: Path) -> dict[str, Any]:
    """Extract metadata from pyproject.toml (simple parsing, no toml dep)."""
    pyproject = base / "pyproject.toml"
    if not pyproject.is_file():
        return {}

    try:
        content = pyproject.read_text(encoding="utf-8")
    except OSError:
        return {}

    result: dict[str, Any] = {}
    result["languages"] = ["Python"]

    # Extract project name
    name_match = re.search(r'^name\s*=\s*"([^"]+)"', content, re.MULTILINE)
    if name_match:
        result["name"] = name_match.group(1)

    # Detect frameworks from dependencies (match exact package name in quotes)
    result["frameworks"] = [
        name for key, name in _PYTHON_FRAMEWORKS.items() if re.search(rf'"{key}[">=<~!, \]]', content, re.IGNORECASE)
    ]

    # Detect test frameworks
    result["test_frameworks"] = [
        name
        for key, name in _PYTHON_TEST_FRAMEWORKS.items()
        if re.search(rf'"{key}[">=<~!, \]]', content, re.IGNORECASE)
    ]

    # Package manager
    if (base / "uv.lock").is_file():
        result["package_manager"] = "uv"
    elif (base / "poetry.lock").is_file():
        result["package_manager"] = "poetry"
    elif (base / "Pipfile.lock").is_file():
        result["package_manager"] = "pipenv"
    else:
        result["package_manager"] = "pip"

    return result


def detect_project(project_dir: Path | None = None) -> dict[str, Any]:
    """Detect project metadata from common config files.

    Returns a dict with keys: ``name``, ``languages``, ``frameworks``,
    ``build_tools``, ``test_frameworks``, ``package_manager``,
    ``dev_commands``, ``directory_structure``.
    """
    base = project_dir or Path.cwd()

    result: dict[str, Any] = {
        "name": base.name,
        "languages": [],
        "frameworks": [],
        "build_tools": [],
        "test_frameworks": [],
        "package_manager": None,
        "dev_commands": {},
        "directory_structure": [],
    }

    # JavaScript / TypeScript
    js_info = _detect_from_package_json(base)
    if js_info:
        result["name"] = js_info.get("name") or result["name"]
        result["languages"].extend(js_info.get("languages", []))
        result["frameworks"].extend(js_info.get("frameworks", []))
        result["build_tools"].extend(js_info.get("build_tools", []))
        result["test_frameworks"].extend(js_info.get("test_frameworks", []))
        if js_info.get("package_manager"):
            result["package_manager"] = js_info["package_manager"]
        result["dev_commands"].update(js_info.get("dev_commands", {}))

    # Python
    py_info = _detect_from_pyproject(base)
    if py_info:
        if not result["name"] or result["name"] == base.name:
            result["name"] = py_info.get("name") or result["name"]
        result["languages"].extend(py_info.get("languages", []))
        result["frameworks"].extend(py_info.get("frameworks", []))
        result["test_frameworks"].extend(py_info.get("test_frameworks", []))
        if not result["package_manager"] and py_info.get("package_manager"):
            result["package_manager"] = py_info["package_manager"]

    # Go
    if (base / "go.mod").is_file():
        result["languages"].append("Go")
        try:
            go_content = (base / "go.mod").read_text(encoding="utf-8")
            mod_match = re.search(r"^module\s+(.+)$", go_content, re.MULTILINE)
            if mod_match and (not result["name"] or result["name"] == base.name):
                result["name"] = mod_match.group(1).split("/")[-1]
        except OSError:
            pass

    # Rust
    if (base / "Cargo.toml").is_file():
        result["languages"].append("Rust")
        try:
            cargo = (base / "Cargo.toml").read_text(encoding="utf-8")
            name_match = re.search(r'^name\s*=\s*"([^"]+)"', cargo, re.MULTILINE)
            if name_match and (not result["name"] or result["name"] == base.name):
                result["name"] = name_match.group(1)
        except OSError:
            pass

    # Directory structure (top-level dirs, excluding common noise)
    try:
        result["directory_structure"] = sorted(
            d.name for d in base.iterdir() if d.is_dir() and d.name not in _SKIP_DIRS and not d.name.startswith(".")
        )
    except OSError:
        pass

    return result
