"""
Module d'analyse des endpoints API pour Web Sentinel.
Détecte les configurations de sécurité des APIs REST/GraphQL.
"""

from __future__ import annotations

import json
import logging
import re
from typing import Dict, Iterable, List, Optional, Set
from urllib.parse import urljoin

import requests

from ..localization.helpers import create_translated_finding
from ..model import ScanRequest

LOGGER = logging.getLogger("web_sentinel.checks.api_security")

# Endpoints API communs à tester
API_ENDPOINTS = [
    '/api',
    '/api/v1',
    '/api/v2',
    '/rest',
    '/graphql',
    '/webhook',
    '/oauth',
    '/auth',
    '/login',
    '/api/health',
    '/api/status',
    '/api/docs',
    '/api/swagger',
    '/openapi.json',
    '/swagger.json',
]

# Headers de sécurité API spécifiques
API_SECURITY_HEADERS = {
    'Content-Security-Policy': 'csp',
    'X-Content-Type-Options': 'content_type',
    'X-Frame-Options': 'frame_options',
    'X-XSS-Protection': 'xss_protection',
    'Referrer-Policy': 'referrer_policy',
    'Permissions-Policy': 'permissions_policy',
    'Access-Control-Allow-Origin': 'cors_origin',
    'Access-Control-Allow-Methods': 'cors_methods',
    'Access-Control-Allow-Headers': 'cors_headers',
    'Access-Control-Max-Age': 'cors_max_age',
    'X-API-Key': 'api_key',
    'Authorization': 'authorization',
    'X-Rate-Limit-Limit': 'rate_limit',
    'X-Rate-Limit-Remaining': 'rate_limit_remaining',
}


def evaluate_api_security(request: ScanRequest) -> Iterable[Finding]:
    """
    Analyse la sécurité des endpoints API et leurs configurations.
    """
    findings: List[Finding] = []
    
    # Découverte des endpoints API
    api_endpoints = _discover_api_endpoints(request)
    
    for endpoint in api_endpoints:
        findings.extend(_analyze_api_endpoint(request, endpoint))
    
    # Tests spécifiques aux APIs
    findings.extend(_test_api_versioning(request, api_endpoints))
    findings.extend(_test_rate_limiting(request, api_endpoints))
    findings.extend(_test_authentication_bypass(request, api_endpoints))
    
    return findings


def _discover_api_endpoints(request: ScanRequest) -> List[str]:
    """
    Découvre les endpoints API disponibles.
    """
    base_url = f"https://{request.domain}"
    discovered_endpoints = []
    
    for endpoint in API_ENDPOINTS:
        url = urljoin(base_url, endpoint)
        try:
            response = requests.get(
                url,
                headers={"User-Agent": request.user_agent},
                timeout=request.timeout,
                allow_redirects=False
            )
            
            # Considère comme API si retourne JSON ou a des headers API
            content_type = response.headers.get('Content-Type', '').lower()
            if (response.status_code in [200, 401, 403] and 
                ('json' in content_type or 'api' in response.headers.get('Server', '').lower())):
                discovered_endpoints.append(endpoint)
                
        except requests.RequestException:
            continue
    
    return discovered_endpoints


def _analyze_api_endpoint(request: ScanRequest, endpoint: str) -> List[Finding]:
    """
    Analyse un endpoint API spécifique.
    """
    findings: List[Finding] = []
    base_url = f"https://{request.domain}"
    url = urljoin(base_url, endpoint)
    
    try:
        # Test GET pour analyser les headers
        response = requests.get(
            url,
            headers={"User-Agent": request.user_agent},
            timeout=request.timeout
        )
        
        # Analyse des headers de sécurité API
        findings.extend(_check_api_security_headers(response, endpoint))
        
        # Analyse du contenu de la réponse
        if response.status_code == 200:
            findings.extend(_analyze_api_response_content(response, endpoint))
        
        # Test des méthodes HTTP
        findings.extend(_test_http_methods(request, url, endpoint))
        
    except requests.RequestException as exc:
        findings.append(
            create_translated_finding(
                check="api-security",
                i18n_key="api.endpoint_unreachable",
                severity="medium",
                i18n_params={"endpoint": endpoint, "error": str(exc), "url": url}
            )
        )
    
    return findings


def _check_api_security_headers(response: requests.Response, endpoint: str) -> List[Finding]:
    """
    Vérifie les headers de sécurité spécifiques aux APIs.
    """
    findings: List[Finding] = []
    
    # Vérification CORS
    cors_origin = response.headers.get('Access-Control-Allow-Origin')
    if cors_origin == '*':
        findings.append(
            create_translated_finding(
                check="api-security",
                i18n_key="api.permissive_cors",
                severity="high",
                i18n_params={"endpoint": endpoint}
            )
        )
    
    # Vérification de l'authentification requise
    if response.status_code == 200 and 'Authorization' not in response.request.headers:
        content_type = response.headers.get('Content-Type', '').lower()
        if 'json' in content_type:
            findings.append(
                create_translated_finding(
                    check="api-security",
                    i18n_key="api.unauthenticated_access",
                    severity="medium",
                    i18n_params={"endpoint": endpoint, "status": response.status_code}
                )
            )
    
    # Vérification du rate limiting
    if not any(header.startswith('X-Rate-Limit') for header in response.headers):
        findings.append(
            create_translated_finding(
                check="api-security",
                i18n_key="api.no_rate_limiting",
                severity="medium",
                i18n_params={"endpoint": endpoint}
            )
        )
    
    return findings


def _analyze_api_response_content(response: requests.Response, endpoint: str) -> List[Finding]:
    """
    Analyse le contenu de la réponse API pour détecter des fuites d'information.
    """
    findings: List[Finding] = []
    
    try:
        if 'json' in response.headers.get('Content-Type', '').lower():
            data = response.json()
            
            # Détection de données sensibles dans la réponse
            sensitive_patterns = {
                'api_key': r'api[_-]?key',
                'secret': r'secret',
                'password': r'password',
                'token': r'token',
                'private_key': r'private[_-]?key',
                'database': r'db[_-]?(host|user|pass)',
            }
            
            content_str = json.dumps(data).lower()
            for pattern_name, pattern in sensitive_patterns.items():
                if re.search(pattern, content_str):
                    readable = pattern_name.replace('_', ' ')
                    findings.append(
                        create_translated_finding(
                            check="api-security",
                            i18n_key="api.sensitive_data_exposure",
                            severity="high",
                            i18n_params={
                                "endpoint": endpoint,
                                "pattern_name": readable,
                                "pattern": pattern
                            }
                        )
                    )
            
            # Vérification de la structure de réponse d'erreur
            if 'error' in data or 'message' in data:
                error_info = str(data.get('error', '')) + str(data.get('message', ''))
                if any(keyword in error_info.lower() for keyword in ['stack', 'trace', 'debug', 'internal']):
                    findings.append(
                        create_translated_finding(
                            check="api-security",
                            i18n_key="api.verbose_errors",
                            severity="medium",
                            i18n_params={"endpoint": endpoint}
                        )
                    )
                    
    except (json.JSONDecodeError, ValueError):
        # Pas de JSON valide, ignore
        pass
    
    return findings


def _test_http_methods(request: ScanRequest, url: str, endpoint: str) -> List[Finding]:
    """
    Teste les méthodes HTTP supportées par l'endpoint.
    """
    findings: List[Finding] = []
    
    # Test de la méthode OPTIONS pour découvrir les méthodes autorisées
    try:
        options_response = requests.options(
            url,
            headers={"User-Agent": request.user_agent},
            timeout=request.timeout
        )
        
        allowed_methods = options_response.headers.get('Allow', '').upper().split(', ')
        dangerous_methods = ['PUT', 'DELETE', 'PATCH', 'TRACE']
        
        found_dangerous = [method for method in dangerous_methods if method in allowed_methods]
        if found_dangerous:
            findings.append(
                create_translated_finding(
                    check="api-security",
                    i18n_key="api.dangerous_methods",
                    severity="medium",
                    i18n_params={
                        "endpoint": endpoint,
                        "methods": ", ".join(found_dangerous),
                        "allowed_methods": ", ".join(allowed_methods)
                    }
                )
            )
        
    except requests.RequestException:
        pass
    
    return findings


def _test_api_versioning(request: ScanRequest, api_endpoints: List[str]) -> List[Finding]:
    """
    Teste la gestion des versions d'API.
    """
    findings: List[Finding] = []
    base_url = f"https://{request.domain}"
    
    # Recherche de multiples versions d'API
    version_patterns = ['/v1/', '/v2/', '/v3/', '/api/v1', '/api/v2', '/api/v3']
    accessible_versions = []
    
    for pattern in version_patterns:
        url = urljoin(base_url, pattern)
        try:
            response = requests.get(
                url,
                headers={"User-Agent": request.user_agent},
                timeout=request.timeout
            )
            if response.status_code in [200, 401, 403]:
                accessible_versions.append(pattern)
        except requests.RequestException:
            continue
    
    if len(accessible_versions) > 1:
        findings.append(
            create_translated_finding(
                check="api-security",
                i18n_key="api.multiple_versions",
                severity="medium",
                i18n_params={
                    "versions": ", ".join(accessible_versions),
                    "accessible_versions": ", ".join(accessible_versions)
                }
            )
        )
    
    return findings


def _test_rate_limiting(request: ScanRequest, api_endpoints: List[str]) -> List[Finding]:
    """
    Teste l'efficacité du rate limiting.
    """
    findings: List[Finding] = []
    
    if not request.allow_invasive:
        return findings  # Rate limiting test nécessite le mode invasif
    
    base_url = f"https://{request.domain}"
    
    # Test simple avec quelques requêtes rapides
    for endpoint in api_endpoints[:1]:  # Test seulement le premier endpoint trouvé
        url = urljoin(base_url, endpoint)
        
        try:
            # Envoie 5 requêtes rapides
            responses = []
            for _ in range(5):
                response = requests.get(
                    url,
                    headers={"User-Agent": request.user_agent},
                    timeout=request.timeout
                )
                responses.append(response.status_code)
            
            # Vérifie si aucune requête n'a été bloquée
            if all(status != 429 for status in responses):
                findings.append(
                    create_translated_finding(
                        check="api-security",
                        i18n_key="api.no_rate_limiting_detected",
                        severity="medium",
                        i18n_params={
                            "endpoint": endpoint,
                            "responses": ", ".join(str(status) for status in responses)
                        }
                    )
                )
                break  # Un test suffit
                
        except requests.RequestException:
            continue
    
    return findings


def _test_authentication_bypass(request: ScanRequest, api_endpoints: List[str]) -> List[Finding]:
    """
    Teste les tentatives de contournement d'authentification.
    """
    findings: List[Finding] = []
    
    if not request.allow_invasive:
        return findings
    
    base_url = f"https://{request.domain}"
    
    # Headers de contournement communs
    bypass_headers = [
        {"X-Forwarded-For": "127.0.0.1"},
        {"X-Real-IP": "127.0.0.1"},
        {"X-Originating-IP": "127.0.0.1"},
        {"X-Remote-IP": "127.0.0.1"},
        {"X-Remote-Addr": "127.0.0.1"},
    ]
    
    for endpoint in api_endpoints[:2]:  # Test les 2 premiers endpoints
        url = urljoin(base_url, endpoint)
        
        # Requête normale pour référence
        try:
            normal_response = requests.get(
                url,
                headers={"User-Agent": request.user_agent},
                timeout=request.timeout
            )
            normal_status = normal_response.status_code
            
            # Test avec headers de contournement
            for bypass_header in bypass_headers:
                headers = {"User-Agent": request.user_agent}
                headers.update(bypass_header)
                
                bypass_response = requests.get(
                    url,
                    headers=headers,
                    timeout=request.timeout
                )
                
                # Si le status change de 401/403 vers 200, potentiel contournement
                if (normal_status in [401, 403] and 
                    bypass_response.status_code == 200):
                    header_name = list(bypass_header.keys())[0]
                    findings.append(
                        create_translated_finding(
                            check="api-security",
                            i18n_key="api.auth_bypass_detected",
                            severity="high",
                            i18n_params={
                                "endpoint": endpoint,
                                "header": header_name,
                                "normal_status": normal_status,
                                "bypass_status": bypass_response.status_code
                            }
                        )
                    )
                    break  # Un contournement détecté suffit pour cet endpoint
                    
        except requests.RequestException:
            continue
    
    return findings
