import json
import re
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import List, Optional, Dict
from pydantic import BaseModel
from .utils import get_logger

logger = get_logger("detector")

class Stack(BaseModel):
    language: Optional[str] = None
    language_version: Optional[str] = None
    project_version: Optional[str] = "0.1.0"
    base_dir: str = "" # Directory where the main code is
    tests: Optional[str] = None
    container: bool = False
    infrastructure: Optional[str] = None
    cloud: Optional[str] = None
    has_api: bool = False
    api_spec_path: Optional[str] = None
    scripts: Dict[str, str] = {}
    security_tools: List[str] = []

def detect_stack(found_files: List[str], base_path: Path = Path(".")) -> Stack:
    """Detects the stack based on found files and their content with versioning."""
    stack = Stack()
    logger.info(f"Analyzing stack in {base_path}")

    # --- .NET Detection ---
    csproj_files = list(base_path.glob("**/*.csproj"))
    if csproj_files or any(f.endswith(".sln") for f in found_files):
        stack.language = "C#"
        stack.tests = "dotnet test"
        if csproj_files:
            try:
                # Find the base_dir for .NET (folder containing the .csproj)
                rel_path = csproj_files[0].parent.relative_to(base_path)
                stack.base_dir = str(rel_path) if str(rel_path) != "." else ""
                
                tree = ET.parse(csproj_files[0])
                root = tree.getroot()
                version = root.find(".//Version")
                if version is not None:
                    stack.project_version = version.text
                
                target_framework = root.find(".//TargetFramework")
                if target_framework is not None:
                    raw_tf = target_framework.text
                    version_match = re.search(r"(\d+\.\d+)", raw_tf)
                    stack.language_version = version_match.group(1) if version_match else raw_tf
            except Exception as e:
                logger.warning(f"Failed to parse .csproj: {e}")

    # --- Node.js Detection ---
    elif list(base_path.glob("**/package.json")):
        stack.language = "Node.js"
        try:
            pkg_file = list(base_path.glob("**/package.json"))[0]
            rel_path = pkg_file.parent.relative_to(base_path)
            stack.base_dir = str(rel_path) if str(rel_path) != "." else ""
            
            with open(pkg_file, "r") as f:
                pkg_data = json.load(f)
                stack.project_version = pkg_data.get("version", "0.1.0")
                engines = pkg_data.get("engines", {})
                if "node" in engines:
                    stack.language_version = engines.get("node").strip()

                stack.scripts = pkg_data.get("scripts", {})
                all_deps = {**pkg_data.get("dependencies", {}), **pkg_data.get("devDependencies", {})}
                if "jest" in all_deps: stack.tests = "Jest"
                elif "mocha" in all_deps: stack.tests = "Mocha"
                if "snyk" in all_deps: stack.security_tools.append("Snyk")
        except Exception as e:
            logger.error(f"Failed to parse package.json: {e}")

    # --- Python / MicroPython Detection ---
    elif any(list(base_path.glob(f"**/{f}")) for f in ["requirements.txt", "setup.py", "main.py", "boot.py"]):
        stack.language = "Python"
        
        # Determine base_dir
        for f in ["requirements.txt", "setup.py"]:
            found = list(base_path.glob(f"**/{f}"))
            if found:
                rel_path = found[0].parent.relative_to(base_path)
                stack.base_dir = str(rel_path) if str(rel_path) != "." else ""
                break

        setup_files = list(base_path.glob("**/setup.py"))
        req_files = list(base_path.glob("**/requirements.txt"))
        
        if setup_files:
            try:
                content = setup_files[0].read_text()
                version_match = re.search(r"version\s*=\s*['\"]([^'\"]+)['\"]", content)
                if version_match:
                    stack.project_version = version_match.group(1)
            except Exception: pass
            
        try:
            if req_files:
                content = req_files[0].read_text().lower()
                if "pytest" in content: stack.tests = "Pytest"
                if "bandit" in content: stack.security_tools.append("Bandit")
        except Exception: pass

    # --- Docker Detection ---
    if "Dockerfile" in found_files:
        stack.container = True
        try:
            # Find the root Dockerfile
            df = base_path / "Dockerfile"
            if df.exists():
                content = df.read_text()
                match = re.search(r"^FROM\s+[\w\./-]+:([^\s\n\r]+)", content, re.MULTILINE)
                if match:
                    # Clean version string (e.g., 3.11-slim -> 3.11)
                    v = match.group(1).strip()
                    clean_v = re.search(r"^(\d+\.\d+)", v)
                    stack.language_version = clean_v.group(1) if clean_v else v
        except Exception: pass

    # --- Infrastructure Detection ---
    if "terraform" in found_files or any(f.endswith(".tf") for f in found_files):
        stack.infrastructure = "Terraform"
        for tf_file in base_path.glob("**/*.tf"):
            try:
                content = tf_file.read_text()
                if 'provider "aws"' in content: stack.cloud = "AWS"; break
                elif 'provider "google"' in content: stack.cloud = "GCP"; break
                elif 'provider "azurerm"' in content: stack.cloud = "Azure"; break
            except Exception: pass

    # --- Makefile ---
    if "Makefile" in found_files and not stack.language:
        stack.language = "C/C++ or Makefile based"
        stack.infrastructure = "Makefile"

    # --- Bash / Shell Script ---
    sh_files = list(base_path.glob("**/*.sh"))
    if sh_files and not stack.language:
        stack.language = "Bash"
        if (base_path / "test.sh").exists():
            stack.tests = "./test.sh"
        elif (base_path / "tests.sh").exists():
            stack.tests = "./tests.sh"
        else:
            stack.tests = "shellcheck" # Default linter if no test script

    # --- API ---
    api_specs = list(base_path.glob("**/swagger.json")) + \
                list(base_path.glob("**/openapi.json")) + \
                list(base_path.glob("**/openapi.yaml")) + \
                list(base_path.glob("**/openapi.yml"))
    if api_specs:
        stack.has_api = True
        stack.api_spec_path = str(api_specs[0].relative_to(base_path))

    logger.info(f"Detected Stack: {stack.model_dump()}")
    return stack
