"""
Stack Detector — Detects the technology stack from project config files.

Reads package.json, pyproject.toml, Cargo.toml, go.mod, requirements.txt,
and other config files to build a comprehensive picture of the tech stack.
"""

import json
import os
import re
from pathlib import Path
from typing import Optional


class StackDetector:
    """Detects technology stack from project configuration files."""

    def __init__(self, project_root: str):
        self.root = Path(project_root)

    def detect(self) -> dict:
        """
        Detect the full tech stack.
        Returns a dict with languages, frameworks, tools, and key dependencies.
        """
        result = {
            "languages": [],
            "frameworks": [],
            "tools": [],
            "runtime": {},
            "build_tools": [],
            "test_frameworks": [],
            "databases": [],
            "key_dependencies": {},
            "package_managers": [],
            "detected_files": [],
        }

        # Run all detectors
        self._detect_python(result)
        self._detect_nodejs(result)
        self._detect_rust(result)
        self._detect_golang(result)
        self._detect_java(result)
        self._detect_docker(result)
        self._detect_ci(result)
        self._detect_misc(result)

        # Deduplicate
        for key in ["languages", "frameworks", "tools", "build_tools", "test_frameworks",
                    "databases", "package_managers"]:
            result[key] = list(dict.fromkeys(result[key]))

        return result

    def _read_file(self, path: str) -> Optional[str]:
        """Safely read a file."""
        try:
            full = self.root / path
            if full.exists():
                return full.read_text(encoding="utf-8", errors="ignore")
        except Exception:
            pass
        return None

    def _detect_python(self, result: dict):
        """Detect Python stack."""
        has_python = False

        # Check pyproject.toml
        content = self._read_file("pyproject.toml")
        if content:
            has_python = True
            result["detected_files"].append("pyproject.toml")
            result["package_managers"].append("pip")

            # Detect build system
            if "hatchling" in content:
                result["build_tools"].append("hatch")
            if "poetry" in content.lower():
                result["build_tools"].append("poetry")
                result["package_managers"].append("poetry")
            if "flit" in content:
                result["build_tools"].append("flit")
            if "setuptools" in content:
                result["build_tools"].append("setuptools")

            # Extract dependencies
            self._extract_python_deps(content, result)

        # Check requirements.txt
        content = self._read_file("requirements.txt")
        if content:
            has_python = True
            result["detected_files"].append("requirements.txt")
            if "pip" not in result["package_managers"]:
                result["package_managers"].append("pip")
            self._extract_requirements(content, result)

        # Check setup.py
        if (self.root / "setup.py").exists():
            has_python = True
            result["detected_files"].append("setup.py")

        # Check conda environment
        for conda_file in ["environment.yml", "environment.yaml", "conda.yml"]:
            content = self._read_file(conda_file)
            if content:
                has_python = True
                result["detected_files"].append(conda_file)
                result["package_managers"].append("conda")

        # Check for Python version
        content = self._read_file(".python-version")
        if content:
            result["runtime"]["python"] = content.strip()

        if has_python:
            result["languages"].append("Python")

    def _extract_python_deps(self, content: str, result: dict):
        """Extract key Python dependencies from pyproject.toml content."""
        key_frameworks = {
            "fastapi": ("FastAPI", "frameworks"),
            "flask": ("Flask", "frameworks"),
            "django": ("Django", "frameworks"),
            "starlette": ("Starlette", "frameworks"),
            "aiohttp": ("aiohttp", "frameworks"),
            "tornado": ("Tornado", "frameworks"),
            "sqlalchemy": ("SQLAlchemy", "databases"),
            "alembic": ("Alembic", "databases"),
            "django-db": ("Django ORM", "databases"),
            "pydantic": ("Pydantic", "tools"),
            "pytest": ("pytest", "test_frameworks"),
            "unittest": ("unittest", "test_frameworks"),
            "hypothesis": ("Hypothesis", "test_frameworks"),
            "celery": ("Celery", "tools"),
            "redis": ("Redis", "databases"),
            "mongodb": ("MongoDB", "databases"),
            "psycopg": ("PostgreSQL", "databases"),
            "mysql": ("MySQL", "databases"),
            "anthropic": ("Anthropic SDK", "tools"),
            "openai": ("OpenAI SDK", "tools"),
            "langchain": ("LangChain", "frameworks"),
            "numpy": ("NumPy", "tools"),
            "pandas": ("Pandas", "tools"),
            "torch": ("PyTorch", "frameworks"),
            "tensorflow": ("TensorFlow", "frameworks"),
            "scikit-learn": ("scikit-learn", "tools"),
            "click": ("Click", "tools"),
            "typer": ("Typer", "tools"),
            "rich": ("Rich", "tools"),
            "httpx": ("HTTPX", "tools"),
            "requests": ("Requests", "tools"),
        }
        content_lower = content.lower()
        for key, (name, category) in key_frameworks.items():
            if key in content_lower:
                result[category].append(name)

    def _extract_requirements(self, content: str, result: dict):
        """Extract deps from requirements.txt."""
        self._extract_python_deps(content, result)

    def _detect_nodejs(self, result: dict):
        """Detect Node.js/JavaScript stack."""
        content = self._read_file("package.json")
        if not content:
            return

        try:
            pkg = json.loads(content)
        except json.JSONDecodeError:
            return

        result["languages"].append("JavaScript/TypeScript")
        result["detected_files"].append("package.json")

        # Package manager
        if (self.root / "yarn.lock").exists():
            result["package_managers"].append("yarn")
        elif (self.root / "pnpm-lock.yaml").exists():
            result["package_managers"].append("pnpm")
        elif (self.root / "bun.lockb").exists():
            result["package_managers"].append("bun")
        else:
            result["package_managers"].append("npm")

        # TypeScript
        if (self.root / "tsconfig.json").exists():
            result["languages"].append("TypeScript")
            result["detected_files"].append("tsconfig.json")

        # Runtime version
        engines = pkg.get("engines", {})
        if "node" in engines:
            result["runtime"]["node"] = engines["node"]

        # Extract frameworks from dependencies
        all_deps = {}
        all_deps.update(pkg.get("dependencies", {}))
        all_deps.update(pkg.get("devDependencies", {}))

        framework_map = {
            "next": "Next.js",
            "react": "React",
            "vue": "Vue.js",
            "svelte": "@sveltejs",
            "angular": "@angular",
            "express": "Express.js",
            "fastify": "Fastify",
            "hono": "Hono",
            "nestjs": "@nestjs",
            "remix": "@remix-run",
            "nuxt": "Nuxt",
            "astro": "Astro",
            "electron": "Electron",
            "jest": "Jest",
            "vitest": "Vitest",
            "mocha": "Mocha",
            "cypress": "Cypress",
            "playwright": "Playwright",
            "vite": "Vite",
            "webpack": "Webpack",
            "tailwindcss": "Tailwind CSS",
            "prisma": "Prisma",
            "drizzle": "Drizzle ORM",
            "mongoose": "Mongoose",
            "sequelize": "Sequelize",
            "graphql": "GraphQL",
            "trpc": "tRPC",
            "zod": "Zod",
            "axios": "Axios",
        }

        for dep_key, name in framework_map.items():
            for pkg_name in all_deps:
                if dep_key in pkg_name.lower():
                    if name in ["Jest", "Vitest", "Mocha", "Cypress", "Playwright"]:
                        result["test_frameworks"].append(name)
                    elif name in ["Prisma", "Drizzle ORM", "Mongoose", "Sequelize"]:
                        result["databases"].append(name)
                    elif name in ["Webpack", "Vite"]:
                        result["build_tools"].append(name)
                    else:
                        result["frameworks"].append(name)
                    break

        # Scripts as hints
        scripts = pkg.get("scripts", {})
        if "test" in scripts:
            test_cmd = scripts["test"].lower()
            for test_tool in ["jest", "vitest", "mocha", "jasmine"]:
                if test_tool in test_cmd:
                    result["test_frameworks"].append(test_tool.capitalize())

    def _detect_rust(self, result: dict):
        """Detect Rust stack."""
        content = self._read_file("Cargo.toml")
        if not content:
            return

        result["languages"].append("Rust")
        result["detected_files"].append("Cargo.toml")
        result["package_managers"].append("cargo")
        result["build_tools"].append("cargo")

        framework_map = {
            "actix-web": "Actix-web",
            "axum": "Axum",
            "rocket": "Rocket",
            "warp": "Warp",
            "tokio": "Tokio (async runtime)",
            "serde": "Serde",
            "sqlx": "SQLx",
            "diesel": "Diesel",
            "clap": "Clap (CLI)",
            "tauri": "Tauri",
        }

        for key, name in framework_map.items():
            if key in content:
                result["frameworks"].append(name)

    def _detect_golang(self, result: dict):
        """Detect Go stack."""
        content = self._read_file("go.mod")
        if not content:
            return

        result["languages"].append("Go")
        result["detected_files"].append("go.mod")
        result["package_managers"].append("go modules")
        result["build_tools"].append("go")

        # Extract Go version
        match = re.search(r"^go\s+(\S+)", content, re.MULTILINE)
        if match:
            result["runtime"]["go"] = match.group(1)

        framework_map = {
            "gin-gonic/gin": "Gin",
            "labstack/echo": "Echo",
            "gofiber/fiber": "Fiber",
            "gorilla/mux": "Gorilla Mux",
            "go-chi/chi": "Chi",
            "gorm.io": "GORM",
            "sqlx": "sqlx",
            "cobra": "Cobra (CLI)",
        }

        for key, name in framework_map.items():
            if key in content:
                result["frameworks"].append(name)

    def _detect_java(self, result: dict):
        """Detect Java/Kotlin/Gradle stack."""
        if (self.root / "pom.xml").exists():
            result["languages"].append("Java")
            result["detected_files"].append("pom.xml")
            result["build_tools"].append("Maven")

        if (self.root / "build.gradle").exists() or (self.root / "build.gradle.kts").exists():
            result["languages"].append("Java/Kotlin")
            detected = "build.gradle.kts" if (self.root / "build.gradle.kts").exists() else "build.gradle"
            result["detected_files"].append(detected)
            result["build_tools"].append("Gradle")
            if "build.gradle.kts" in detected:
                result["languages"].append("Kotlin")

    def _detect_docker(self, result: dict):
        """Detect Docker usage."""
        if (self.root / "Dockerfile").exists():
            result["tools"].append("Docker")
            result["detected_files"].append("Dockerfile")
        if (self.root / "docker-compose.yml").exists() or (self.root / "docker-compose.yaml").exists():
            result["tools"].append("Docker Compose")

    def _detect_ci(self, result: dict):
        """Detect CI/CD configuration."""
        gh_actions = self.root / ".github" / "workflows"
        if gh_actions.exists() and any(gh_actions.iterdir()):
            result["tools"].append("GitHub Actions")

        if (self.root / ".gitlab-ci.yml").exists():
            result["tools"].append("GitLab CI")
        if (self.root / "Jenkinsfile").exists():
            result["tools"].append("Jenkins")
        if (self.root / ".travis.yml").exists():
            result["tools"].append("Travis CI")
        if (self.root / "azure-pipelines.yml").exists():
            result["tools"].append("Azure Pipelines")

    def _detect_misc(self, result: dict):
        """Detect miscellaneous tools."""
        if (self.root / ".pre-commit-config.yaml").exists():
            result["tools"].append("pre-commit")
        if (self.root / ".eslintrc.js").exists() or (self.root / ".eslintrc.json").exists():
            result["tools"].append("ESLint")
        if (self.root / "prettier.config.js").exists() or (self.root / ".prettierrc").exists():
            result["tools"].append("Prettier")
        if (self.root / "Makefile").exists():
            result["build_tools"].append("Make")
        if (self.root / ".env.example").exists() or (self.root / ".env.sample").exists():
            result["tools"].append("dotenv")

    def to_summary(self) -> str:
        """Return a human-readable summary of the detected stack."""
        stack = self.detect()
        lines = []

        if stack["languages"]:
            lines.append(f"Languages: {', '.join(stack['languages'])}")
        if stack["frameworks"]:
            lines.append(f"Frameworks: {', '.join(stack['frameworks'])}")
        if stack["test_frameworks"]:
            lines.append(f"Testing: {', '.join(stack['test_frameworks'])}")
        if stack["databases"]:
            lines.append(f"Databases/ORM: {', '.join(stack['databases'])}")
        if stack["build_tools"]:
            lines.append(f"Build: {', '.join(stack['build_tools'])}")
        if stack["package_managers"]:
            lines.append(f"Package managers: {', '.join(stack['package_managers'])}")
        if stack["tools"]:
            lines.append(f"Tools: {', '.join(stack['tools'])}")
        if stack["runtime"]:
            runtime_str = ", ".join(f"{k} {v}" for k, v in stack["runtime"].items())
            lines.append(f"Runtime: {runtime_str}")

        return "\n".join(lines) if lines else "Stack: unknown"
