"""
FlowTracer Service

Détecte les appels API et endpoints dans le code source de manière agnostique.
Supporte JavaScript/TypeScript, Dart/Flutter, Python et plusieurs frameworks.
"""

import os
import re
from dataclasses import dataclass
from dataclasses import field
from typing import Dict
from typing import List
from typing import Optional
from typing import Set

from ..models.user_story_models import APICall
from ..models.user_story_models import APIEndpoint

# === Patterns de détection par langage ===

# JavaScript/TypeScript - Appels API
JS_API_CALL_PATTERNS = {
    # Fetch API
    "fetch": r"fetch\s*\(\s*[`'\"]([^`'\"]+)[`'\"]",
    "fetch_template": r"fetch\s*\(\s*`([^`]+)`",
    # Axios
    "axios_get": r"axios\.get\s*\(\s*[`'\"]([^`'\"]+)[`'\"]",
    "axios_post": r"axios\.post\s*\(\s*[`'\"]([^`'\"]+)[`'\"]",
    "axios_put": r"axios\.put\s*\(\s*[`'\"]([^`'\"]+)[`'\"]",
    "axios_patch": r"axios\.patch\s*\(\s*[`'\"]([^`'\"]+)[`'\"]",
    "axios_delete": r"axios\.delete\s*\(\s*[`'\"]([^`'\"]+)[`'\"]",
    "axios_request": r"axios\s*\(\s*\{[^}]*url:\s*[`'\"]([^`'\"]+)[`'\"]",
    # jQuery
    "jquery_ajax": r"\$\.ajax\s*\(\s*\{[^}]*url:\s*['\"]([^'\"]+)['\"]",
    "jquery_get": r"\$\.get\s*\(\s*['\"]([^'\"]+)['\"]",
    "jquery_post": r"\$\.post\s*\(\s*['\"]([^'\"]+)['\"]",
    # SWR / React Query
    "use_swr": r"useSWR\s*\(\s*[`'\"]([^`'\"]+)[`'\"]",
    "use_query": r"useQuery\s*\([^)]*[`'\"]([^`'\"]+)[`'\"]",
    # tRPC (pattern différent)
    "trpc": r"trpc\.(\w+)\.(\w+)\.(query|mutate|mutation)",
    # Ky
    "ky_get": r"ky\.get\s*\(\s*[`'\"]([^`'\"]+)[`'\"]",
    "ky_post": r"ky\.post\s*\(\s*[`'\"]([^`'\"]+)[`'\"]",
}

# Dart/Flutter - Appels API
DART_API_CALL_PATTERNS = {
    # http package
    "http_get": r"http\.get\s*\(\s*Uri\.parse\s*\(\s*['\"]([^'\"]+)['\"]",
    "http_post": r"http\.post\s*\(\s*Uri\.parse\s*\(\s*['\"]([^'\"]+)['\"]",
    "http_put": r"http\.put\s*\(\s*Uri\.parse\s*\(\s*['\"]([^'\"]+)['\"]",
    "http_delete": r"http\.delete\s*\(\s*Uri\.parse\s*\(\s*['\"]([^'\"]+)['\"]",
    # Dio
    "dio_get": r"dio\.get\s*\(\s*['\"]([^'\"]+)['\"]",
    "dio_post": r"dio\.post\s*\(\s*['\"]([^'\"]+)['\"]",
    "dio_put": r"dio\.put\s*\(\s*['\"]([^'\"]+)['\"]",
    "dio_delete": r"dio\.delete\s*\(\s*['\"]([^'\"]+)['\"]",
    "dio_request": r"dio\.request\s*\(\s*['\"]([^'\"]+)['\"]",
    # Retrofit annotations
    "retrofit_get": r"@GET\s*\(\s*['\"]([^'\"]+)['\"]",
    "retrofit_post": r"@POST\s*\(\s*['\"]([^'\"]+)['\"]",
    "retrofit_put": r"@PUT\s*\(\s*['\"]([^'\"]+)['\"]",
    "retrofit_delete": r"@DELETE\s*\(\s*['\"]([^'\"]+)['\"]",
    # Chopper
    "chopper_get": r"@Get\s*\(\s*path:\s*['\"]([^'\"]+)['\"]",
    "chopper_post": r"@Post\s*\(\s*path:\s*['\"]([^'\"]+)['\"]",
}

# Python - Appels API
PYTHON_API_CALL_PATTERNS = {
    # requests
    "requests_get": r"requests\.get\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
    "requests_post": r"requests\.post\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
    "requests_put": r"requests\.put\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
    "requests_patch": r"requests\.patch\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
    "requests_delete": r"requests\.delete\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
    # httpx
    "httpx_get": r"(?:httpx|client)\.get\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
    "httpx_post": r"(?:httpx|client)\.post\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
    # aiohttp
    "aiohttp_get": r"session\.get\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
    "aiohttp_post": r"session\.post\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
    # urllib
    "urllib_request": r"urllib\.request\.urlopen\s*\(\s*[f]?['\"]([^'\"]+)['\"]",
}

# === Patterns de détection des endpoints ===

# Express.js
EXPRESS_ENDPOINT_PATTERNS = {
    "app_method": r"app\.(get|post|put|patch|delete|all)\s*\(\s*['\"]([^'\"]+)['\"]",
    "router_method": r"router\.(get|post|put|patch|delete|all)\s*\(\s*['\"]([^'\"]+)['\"]",
    "use_route": r"app\.use\s*\(\s*['\"]([^'\"]+)['\"]",
}

# Next.js API Routes
NEXTJS_ENDPOINT_PATTERNS = {
    # Route handlers (app/api/**/route.ts)
    "route_handler": r"export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(",
    # Server Actions
    "server_action": r"['\"]use server['\"]",
}

# FastAPI
FASTAPI_ENDPOINT_PATTERNS = {
    "app_decorator": r"@app\.(get|post|put|patch|delete|head|options)\s*\(\s*['\"]([^'\"]+)['\"]",
    "router_decorator": r"@router\.(get|post|put|patch|delete)\s*\(\s*['\"]([^'\"]+)['\"]",
    "api_route": r"@[\w_]+\.api_route\s*\(\s*['\"]([^'\"]+)['\"]",
}

# Flask
FLASK_ENDPOINT_PATTERNS = {
    "route_decorator": r"@app\.route\s*\(\s*['\"]([^'\"]+)['\"]",
    "blueprint_route": r"@\w+\.route\s*\(\s*['\"]([^'\"]+)['\"]",
    "methods": r"methods\s*=\s*\[([^\]]+)\]",
}

# Django
DJANGO_ENDPOINT_PATTERNS = {
    "path": r"path\s*\(\s*['\"]([^'\"]+)['\"]",
    "re_path": r"re_path\s*\(\s*[r]?['\"]([^'\"]+)['\"]",
    "url": r"url\s*\(\s*[r]?['\"]([^'\"]+)['\"]",
}

# NestJS
NESTJS_ENDPOINT_PATTERNS = {
    "get_decorator": r"@Get\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
    "post_decorator": r"@Post\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
    "put_decorator": r"@Put\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
    "patch_decorator": r"@Patch\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
    "delete_decorator": r"@Delete\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
    "controller": r"@Controller\s*\(\s*['\"]([^'\"]+)['\"]",
}


@dataclass
class FlowTracerResult:
    """Résultat du traçage de flux"""

    api_calls: List[APICall] = field(default_factory=list)
    api_endpoints: List[APIEndpoint] = field(default_factory=list)
    call_to_endpoint_map: Dict[str, str] = field(default_factory=dict)
    orphan_endpoints: List[APIEndpoint] = field(default_factory=list)
    unmatched_calls: List[APICall] = field(default_factory=list)


class FlowTracer:
    """
    Service de traçage des flux API

    Détecte les appels API dans le frontend et les endpoints dans le backend,
    puis établit les correspondances entre eux.
    """

    def __init__(self, root_dir: str, all_files: Optional[List[str]] = None):
        self.root_dir = root_dir
        self.all_files = all_files or []
        self.api_calls: List[APICall] = []
        self.api_endpoints: List[APIEndpoint] = []
        self._file_cache: Dict[str, str] = {}

    def _read_file(self, file_path: str) -> Optional[str]:
        """Lit un fichier et le met en cache"""
        if file_path in self._file_cache:
            return self._file_cache[file_path]

        try:
            full_path = os.path.join(self.root_dir, file_path)
            with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
                self._file_cache[file_path] = content
                return content
        except Exception:
            return None

    def _detect_language(self, file_path: str) -> str:
        """Détecte le langage d'un fichier basé sur son extension"""
        ext = os.path.splitext(file_path)[1].lower()

        language_map = {
            ".js": "javascript",
            ".jsx": "javascript",
            ".ts": "typescript",
            ".tsx": "typescript",
            ".mjs": "javascript",
            ".cjs": "javascript",
            ".dart": "dart",
            ".py": "python",
            ".vue": "javascript",
            ".svelte": "javascript",
        }

        return language_map.get(ext, "unknown")

    def _detect_framework(self, file_path: str, content: str) -> str:
        """Détecte le framework utilisé dans un fichier"""
        # Next.js API routes
        if "/app/api/" in file_path and file_path.endswith("route.ts"):
            return "nextjs"
        if "/pages/api/" in file_path:
            return "nextjs"

        # Express
        if "express" in content.lower() and ("app." in content or "router." in content):
            return "express"

        # NestJS
        if "@Controller" in content or "@Injectable" in content:
            return "nestjs"

        # FastAPI
        if "@app." in content and "FastAPI" in content:
            return "fastapi"
        if "@router." in content and "APIRouter" in content:
            return "fastapi"

        # Flask
        if "@app.route" in content or "Flask(" in content:
            return "flask"

        # Django
        if "urlpatterns" in content or "django" in content.lower():
            return "django"

        return "unknown"

    def _extract_method_from_pattern_name(self, pattern_name: str) -> str:
        """Extrait la méthode HTTP du nom du pattern"""
        method_keywords = ["get", "post", "put", "patch", "delete", "head", "options"]
        pattern_lower = pattern_name.lower()

        for method in method_keywords:
            if method in pattern_lower:
                return method.upper()

        return "GET"  # Défaut

    def _has_error_handling(self, content: str, line_number: int) -> bool:
        """Vérifie si l'appel API a une gestion d'erreur"""
        lines = content.split("\n")
        start = max(0, line_number - 5)
        end = min(len(lines), line_number + 10)
        context = "\n".join(lines[start:end])

        # Patterns de gestion d'erreur
        error_patterns = [
            r"\.catch\s*\(",
            r"try\s*\{",
            r"\.then\s*\([^)]*\)\s*\.catch",
            r"catch\s*\(",
            r"except\s*:",
            r"except\s+\w+",
            r"onError\s*:",
            r"\.catchError\s*\(",
        ]

        for pattern in error_patterns:
            if re.search(pattern, context):
                return True

        return False

    def _has_loading_state(self, content: str, line_number: int) -> bool:
        """Vérifie si l'appel API a un état de chargement"""
        lines = content.split("\n")
        start = max(0, line_number - 15)
        end = min(len(lines), line_number + 5)
        context = "\n".join(lines[start:end])

        # Patterns d'état de chargement
        loading_patterns = [
            r"isLoading",
            r"loading",
            r"setLoading",
            r"useState.*loading",
            r"pending",
            r"isFetching",
            r"isValidating",
        ]

        for pattern in loading_patterns:
            if re.search(pattern, context, re.IGNORECASE):
                return True

        return False

    def detect_api_calls_in_file(
        self, file_path: str, content: Optional[str] = None
    ) -> List[APICall]:
        """Détecte tous les appels API dans un fichier"""
        if content is None:
            content = self._read_file(file_path)
            if content is None:
                return []

        language = self._detect_language(file_path)
        calls: List[APICall] = []

        # Sélectionner les patterns selon le langage
        if language in ["javascript", "typescript"]:
            patterns = JS_API_CALL_PATTERNS
        elif language == "dart":
            patterns = DART_API_CALL_PATTERNS
        elif language == "python":
            patterns = PYTHON_API_CALL_PATTERNS
        else:
            return []

        for pattern_name, pattern in patterns.items():
            for match in re.finditer(pattern, content, re.MULTILINE):
                # Trouver le numéro de ligne
                line_number = content[: match.start()].count("\n") + 1

                # Extraire l'URL
                url = match.group(1) if match.lastindex >= 1 else ""

                # Déterminer la méthode HTTP
                method = self._extract_method_from_pattern_name(pattern_name)

                # Déterminer la librairie
                library = pattern_name.split("_")[0]

                call = APICall(
                    file_path=file_path,
                    line_number=line_number,
                    method=method,
                    url_pattern=url,
                    library=library,
                    has_error_handling=self._has_error_handling(content, line_number),
                    has_loading_state=self._has_loading_state(content, line_number),
                )
                calls.append(call)

        return calls

    def detect_endpoints_in_file(
        self, file_path: str, content: Optional[str] = None
    ) -> List[APIEndpoint]:
        """Détecte tous les endpoints API dans un fichier"""
        if content is None:
            content = self._read_file(file_path)
            if content is None:
                return []

        framework = self._detect_framework(file_path, content)
        endpoints: List[APIEndpoint] = []

        # Sélectionner les patterns selon le framework
        if framework == "express":
            patterns = EXPRESS_ENDPOINT_PATTERNS
        elif framework == "nextjs":
            endpoints.extend(self._detect_nextjs_endpoints(file_path, content))
            return endpoints
        elif framework == "fastapi":
            patterns = FASTAPI_ENDPOINT_PATTERNS
        elif framework == "flask":
            patterns = FLASK_ENDPOINT_PATTERNS
        elif framework == "django":
            patterns = DJANGO_ENDPOINT_PATTERNS
        elif framework == "nestjs":
            endpoints.extend(self._detect_nestjs_endpoints(file_path, content))
            return endpoints
        else:
            # Essayer tous les patterns de backend
            for fw_patterns in [
                EXPRESS_ENDPOINT_PATTERNS,
                FASTAPI_ENDPOINT_PATTERNS,
                FLASK_ENDPOINT_PATTERNS,
            ]:
                for pattern_name, pattern in fw_patterns.items():
                    for match in re.finditer(pattern, content, re.MULTILINE):
                        line_number = content[: match.start()].count("\n") + 1

                        if match.lastindex and match.lastindex >= 2:
                            method = match.group(1).upper()
                            path = match.group(2)
                        elif match.lastindex and match.lastindex >= 1:
                            path = match.group(1)
                            method = self._extract_method_from_pattern_name(pattern_name)
                        else:
                            continue

                        endpoint = APIEndpoint(
                            file_path=file_path,
                            line_number=line_number,
                            method=method,
                            path=path,
                            framework="unknown",
                        )
                        endpoints.append(endpoint)
            return endpoints

        for pattern_name, pattern in patterns.items():
            for match in re.finditer(pattern, content, re.MULTILINE):
                line_number = content[: match.start()].count("\n") + 1

                # Extraire la méthode et le chemin
                if match.lastindex and match.lastindex >= 2:
                    method = match.group(1).upper()
                    path = match.group(2)
                elif match.lastindex and match.lastindex >= 1:
                    path = match.group(1)
                    method = self._extract_method_from_pattern_name(pattern_name)
                else:
                    continue

                # Détecter si l'endpoint est protégé
                is_protected = self._is_endpoint_protected(content, line_number)

                endpoint = APIEndpoint(
                    file_path=file_path,
                    line_number=line_number,
                    method=method,
                    path=path,
                    framework=framework,
                    is_protected=is_protected,
                )
                endpoints.append(endpoint)

        return endpoints

    def _detect_nextjs_endpoints(self, file_path: str, content: str) -> List[APIEndpoint]:
        """Détecte les endpoints Next.js (route handlers)"""
        endpoints: List[APIEndpoint] = []

        # Extraire le chemin de l'API depuis le chemin du fichier
        # Ex: app/api/users/route.ts -> /api/users
        api_path = ""
        if "/app/api/" in file_path:
            parts = file_path.split("/app/api/")
            if len(parts) > 1:
                path_part = parts[1].replace("/route.ts", "").replace("/route.js", "")
                api_path = "/api/" + path_part
        elif "/pages/api/" in file_path:
            parts = file_path.split("/pages/api/")
            if len(parts) > 1:
                path_part = parts[1].replace(".ts", "").replace(".js", "")
                api_path = "/api/" + path_part

        # Détecter les handlers exportés (GET, POST, etc.)
        handler_pattern = (
            r"export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\("
        )
        for match in re.finditer(handler_pattern, content):
            line_number = content[: match.start()].count("\n") + 1
            method = match.group(1)

            endpoint = APIEndpoint(
                file_path=file_path,
                line_number=line_number,
                method=method,
                path=api_path or "/api/unknown",
                handler_name=method,
                framework="nextjs",
            )
            endpoints.append(endpoint)

        return endpoints

    def _detect_nestjs_endpoints(self, file_path: str, content: str) -> List[APIEndpoint]:
        """Détecte les endpoints NestJS"""
        endpoints: List[APIEndpoint] = []

        # Trouver le préfixe du controller
        controller_prefix = ""
        controller_match = re.search(r"@Controller\s*\(\s*['\"]([^'\"]+)['\"]", content)
        if controller_match:
            controller_prefix = "/" + controller_match.group(1).strip("/")

        # Patterns pour les décorateurs de méthodes
        method_patterns = {
            "GET": r"@Get\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
            "POST": r"@Post\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
            "PUT": r"@Put\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
            "PATCH": r"@Patch\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
            "DELETE": r"@Delete\s*\(\s*['\"]?([^'\"]*)['\"]?\s*\)",
        }

        for method, pattern in method_patterns.items():
            for match in re.finditer(pattern, content):
                line_number = content[: match.start()].count("\n") + 1
                route_path = match.group(1) if match.group(1) else ""

                full_path = controller_prefix
                if route_path:
                    full_path = controller_prefix + "/" + route_path.strip("/")

                endpoint = APIEndpoint(
                    file_path=file_path,
                    line_number=line_number,
                    method=method,
                    path=full_path or "/",
                    framework="nestjs",
                )
                endpoints.append(endpoint)

        return endpoints

    def _is_endpoint_protected(self, content: str, line_number: int) -> bool:
        """Vérifie si un endpoint est protégé par authentification"""
        lines = content.split("\n")
        start = max(0, line_number - 10)
        end = min(len(lines), line_number + 2)
        context = "\n".join(lines[start:end])

        # Patterns d'authentification
        auth_patterns = [
            r"@UseGuards",
            r"@Auth",
            r"@Authenticated",
            r"authenticate",
            r"requireAuth",
            r"isAuthenticated",
            r"passport\.",
            r"jwt\.",
            r"auth\(",
            r"protect",
            r"@login_required",
            r"@jwt_required",
            r"Depends\(.*auth",
        ]

        for pattern in auth_patterns:
            if re.search(pattern, context, re.IGNORECASE):
                return True

        return False

    def trace_all_files(self) -> FlowTracerResult:
        """Trace tous les fichiers du projet"""
        result = FlowTracerResult()

        for file_path in self.all_files:
            # Détecter les appels API
            calls = self.detect_api_calls_in_file(file_path)
            result.api_calls.extend(calls)

            # Détecter les endpoints
            endpoints = self.detect_endpoints_in_file(file_path)
            result.api_endpoints.extend(endpoints)

        # Stocker les résultats
        self.api_calls = result.api_calls
        self.api_endpoints = result.api_endpoints

        # Faire les correspondances
        result = self.match_calls_to_endpoints(result)

        return result

    def match_calls_to_endpoints(self, result: FlowTracerResult) -> FlowTracerResult:
        """Fait correspondre les appels API aux endpoints"""
        matched_endpoints: Set[str] = set()

        for call in result.api_calls:
            matched = False
            url = call.url_pattern

            # Normaliser l'URL
            url_normalized = self._normalize_url(url)

            for endpoint in result.api_endpoints:
                endpoint_normalized = self._normalize_url(endpoint.path)

                # Vérifier la correspondance
                if self._urls_match(url_normalized, endpoint_normalized):
                    if call.method == endpoint.method or call.method == "GET":
                        call.matched_endpoint = endpoint.path
                        result.call_to_endpoint_map[f"{call.file_path}:{call.line_number}"] = (
                            endpoint.path
                        )
                        matched_endpoints.add(f"{endpoint.file_path}:{endpoint.line_number}")
                        matched = True
                        break

            if not matched:
                result.unmatched_calls.append(call)

        # Trouver les endpoints orphelins
        for endpoint in result.api_endpoints:
            key = f"{endpoint.file_path}:{endpoint.line_number}"
            if key not in matched_endpoints:
                result.orphan_endpoints.append(endpoint)

        return result

    def _normalize_url(self, url: str) -> str:
        """Normalise une URL pour la comparaison"""
        # Supprimer le protocole et le domaine si présent
        url = re.sub(r"^https?://[^/]+", "", url)

        # Supprimer les paramètres de query
        url = url.split("?")[0]

        # Normaliser les slashes
        url = "/" + url.strip("/")

        # Remplacer les variables par des placeholders
        # ${var}, :var, {var}
        url = re.sub(r"\$\{[^}]+\}", ":param", url)
        url = re.sub(r"\{[^}]+\}", ":param", url)
        url = re.sub(r":[^/]+", ":param", url)

        return url.lower()

    def _urls_match(self, call_url: str, endpoint_url: str) -> bool:
        """Vérifie si deux URLs correspondent"""
        # Correspondance exacte
        if call_url == endpoint_url:
            return True

        # Correspondance avec paramètres
        call_parts = call_url.split("/")
        endpoint_parts = endpoint_url.split("/")

        if len(call_parts) != len(endpoint_parts):
            return False

        for cp, ep in zip(call_parts, endpoint_parts):
            if cp == ep:
                continue
            if cp == ":param" or ep == ":param":
                continue
            return False

        return True

    def find_orphan_endpoints(self) -> List[APIEndpoint]:
        """Trouve les endpoints qui ne sont jamais appelés"""
        result = self.trace_all_files()
        return result.orphan_endpoints

    def find_unmatched_calls(self) -> List[APICall]:
        """Trouve les appels API sans endpoint correspondant"""
        result = self.trace_all_files()
        return result.unmatched_calls
