"""
AIPTX SQLMap Scanner
====================

Scanner for sqlmap - automatic SQL injection exploitation.
https://sqlmap.org/
"""

import asyncio
import json
import logging
import re
import shutil
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

from ..base import BaseScanner, ScanResult, ScanFinding, ScanSeverity

logger = logging.getLogger(__name__)


@dataclass
class SqlmapConfig:
    """Configuration for sqlmap scanner."""

    # Detection options
    level: int = 1  # 1-5, higher = more tests
    risk: int = 1  # 1-3, higher = more intrusive
    technique: str = "BEUSTQ"  # Boolean, Error, Union, Stacked, Time, Query

    # Request options
    method: str = "GET"
    data: str = ""  # POST data
    headers: Dict[str, str] = field(default_factory=dict)
    cookies: str = ""
    user_agent: str = ""

    # Injection options
    param: Optional[str] = None  # Specific parameter to test
    prefix: Optional[str] = None
    suffix: Optional[str] = None
    tamper: Optional[str] = None  # Tamper scripts (comma-separated)

    # Enumeration
    dbs: bool = True  # Enumerate databases
    tables: bool = False  # Enumerate tables
    columns: bool = False  # Enumerate columns
    dump: bool = False  # Dump data (CAUTION)
    current_user: bool = True
    current_db: bool = True
    banner: bool = True

    # Optimization
    threads: int = 1  # Max 10
    batch: bool = True  # Never ask for user input
    timeout: int = 30

    # Output
    output_dir: Optional[str] = None
    flush_session: bool = False


class SqlmapScanner(BaseScanner):
    """
    Scanner for sqlmap - SQL injection exploitation.

    Detects and exploits:
    - Boolean-based blind SQL injection
    - Error-based SQL injection
    - UNION query-based SQL injection
    - Stacked queries SQL injection
    - Time-based blind SQL injection

    Example:
        scanner = SqlmapScanner()
        result = await scanner.scan("https://example.com/page?id=1")

        for finding in result.findings:
            print(f"Injection: {finding.title}")
            print(f"  Type: {finding.description}")
    """

    def __init__(self, config: Optional[SqlmapConfig] = None):
        self.config = config or SqlmapConfig()
        self._process: Optional[asyncio.subprocess.Process] = None
        self._running = False

    def is_available(self) -> bool:
        """Check if sqlmap is installed."""
        return shutil.which("sqlmap") is not None

    async def scan(
        self,
        target: str,
        request_file: Optional[str] = None,
        **kwargs
    ) -> ScanResult:
        """
        Run sqlmap scan.

        Args:
            target: URL with injectable parameter (e.g., https://example.com/page?id=1)
            request_file: Optional file with raw HTTP request
            **kwargs: Additional options

        Returns:
            ScanResult with SQL injection findings
        """
        result = ScanResult(scanner="sqlmap", target=target)
        result.start_time = datetime.utcnow()
        self._running = True

        try:
            cmd = self._build_command(target, request_file)
            logger.debug(f"Running: {' '.join(cmd)}")

            process = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            self._process = process

            stdout, stderr = await asyncio.wait_for(
                process.communicate(),
                timeout=kwargs.get("timeout", 900)  # 15 min default for sqlmap
            )

            result.raw_output = stdout.decode("utf-8", errors="replace")
            result.findings = self.parse_output(result.raw_output)
            result.status = "completed"

            # Check for errors in stderr
            stderr_text = stderr.decode("utf-8", errors="replace")
            if "critical" in stderr_text.lower():
                result.errors.append(stderr_text[:500])

        except asyncio.TimeoutError:
            result.status = "failed"
            result.errors.append("Scan timed out")
        except Exception as e:
            result.status = "failed"
            result.errors.append(str(e))
            logger.error(f"sqlmap scan failed: {e}")
        finally:
            self._running = False
            result.end_time = datetime.utcnow()
            if result.start_time:
                result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    def _build_command(self, target: str, request_file: Optional[str] = None) -> List[str]:
        """Build sqlmap command."""
        cmd = ["sqlmap"]

        # Input
        if request_file:
            cmd.extend(["-r", request_file])
        else:
            cmd.extend(["-u", target])

        # Detection
        cmd.extend(["--level", str(self.config.level)])
        cmd.extend(["--risk", str(self.config.risk)])

        if self.config.technique != "BEUSTQ":
            cmd.extend(["--technique", self.config.technique])

        # Request options
        if self.config.method != "GET" and not request_file:
            cmd.extend(["--method", self.config.method])

        if self.config.data:
            cmd.extend(["--data", self.config.data])

        for key, value in self.config.headers.items():
            cmd.extend(["--header", f"{key}: {value}"])

        if self.config.cookies:
            cmd.extend(["--cookie", self.config.cookies])

        if self.config.user_agent:
            cmd.extend(["--user-agent", self.config.user_agent])

        # Injection options
        if self.config.param:
            cmd.extend(["-p", self.config.param])

        if self.config.prefix:
            cmd.extend(["--prefix", self.config.prefix])

        if self.config.suffix:
            cmd.extend(["--suffix", self.config.suffix])

        if self.config.tamper:
            cmd.extend(["--tamper", self.config.tamper])

        # Enumeration
        if self.config.banner:
            cmd.append("--banner")
        if self.config.current_user:
            cmd.append("--current-user")
        if self.config.current_db:
            cmd.append("--current-db")
        if self.config.dbs:
            cmd.append("--dbs")
        if self.config.tables:
            cmd.append("--tables")
        if self.config.columns:
            cmd.append("--columns")
        if self.config.dump:
            cmd.append("--dump")

        # Optimization
        cmd.extend(["--threads", str(self.config.threads)])
        cmd.extend(["--timeout", str(self.config.timeout)])

        if self.config.batch:
            cmd.append("--batch")

        if self.config.flush_session:
            cmd.append("--flush-session")

        # Output
        if self.config.output_dir:
            cmd.extend(["--output-dir", self.config.output_dir])

        # Additional flags for automation
        cmd.append("--answers=Y")  # Answer yes to prompts

        return cmd

    def parse_output(self, output: str) -> List[ScanFinding]:
        """Parse sqlmap output."""
        findings = []

        # Detect injection patterns
        injection_patterns = [
            (r"Parameter:\s*([^\n]+)", "parameter"),
            (r"Type:\s*([^\n]+)", "injection_type"),
            (r"Title:\s*([^\n]+)", "title"),
            (r"Payload:\s*([^\n]+)", "payload"),
        ]

        # Check for vulnerable indicators
        if "is vulnerable" in output.lower() or "sqlmap identified" in output.lower():
            # Extract vulnerable parameters
            vuln_sections = re.split(r"Parameter:", output)[1:] if "Parameter:" in output else []

            for section in vuln_sections:
                finding = self._parse_injection_section(section)
                if finding:
                    findings.append(finding)

        # Extract database information
        db_info = self._extract_db_info(output)
        if db_info:
            findings.append(db_info)

        # Extract user information
        user_info = self._extract_user_info(output)
        if user_info:
            findings.append(user_info)

        # If no structured findings but vulnerable, create generic finding
        if not findings and ("is vulnerable" in output.lower() or "[INFO]" in output):
            findings.append(self._create_generic_finding(output))

        return findings

    def _parse_injection_section(self, section: str) -> Optional[ScanFinding]:
        """Parse a single injection section."""
        lines = section.strip().split("\n")
        if not lines:
            return None

        param = lines[0].strip()
        injection_type = ""
        title = ""
        payload = ""

        for line in lines:
            if line.strip().startswith("Type:"):
                injection_type = line.split(":", 1)[1].strip()
            elif line.strip().startswith("Title:"):
                title = line.split(":", 1)[1].strip()
            elif line.strip().startswith("Payload:"):
                payload = line.split(":", 1)[1].strip()

        if not param:
            return None

        # Determine severity based on injection type
        severity = ScanSeverity.HIGH
        tags = ["sqli"]

        if "stacked" in injection_type.lower():
            severity = ScanSeverity.CRITICAL
            tags.append("stacked")
        elif "union" in injection_type.lower():
            severity = ScanSeverity.CRITICAL
            tags.append("union")
        elif "error" in injection_type.lower():
            tags.append("error-based")
        elif "boolean" in injection_type.lower():
            tags.append("boolean-blind")
        elif "time" in injection_type.lower():
            tags.append("time-blind")

        return ScanFinding(
            title=f"SQL Injection: {param}",
            severity=severity,
            description=f"Type: {injection_type}" if injection_type else f"Title: {title}",
            evidence=f"Payload: {payload[:300]}" if payload else "",
            cwe="CWE-89",
            scanner="sqlmap",
            tags=tags,
        )

    def _extract_db_info(self, output: str) -> Optional[ScanFinding]:
        """Extract database information from output."""
        db_match = re.search(r"available databases.*?:\n(.*?)(?:\n\n|\[|$)", output, re.DOTALL | re.IGNORECASE)

        if db_match:
            dbs = [db.strip().strip("[]* ") for db in db_match.group(1).split("\n") if db.strip()]
            if dbs:
                return ScanFinding(
                    title="Database Enumeration",
                    severity=ScanSeverity.HIGH,
                    description=f"Databases found: {', '.join(dbs[:10])}",
                    cwe="CWE-89",
                    scanner="sqlmap",
                    tags=["sqli", "enumeration", "database"],
                )

        return None

    def _extract_user_info(self, output: str) -> Optional[ScanFinding]:
        """Extract user information from output."""
        user_match = re.search(r"current user.*?[:\s]+['\"]?([^'\"\n]+)", output, re.IGNORECASE)
        db_match = re.search(r"current database.*?[:\s]+['\"]?([^'\"\n]+)", output, re.IGNORECASE)

        if user_match or db_match:
            user = user_match.group(1).strip() if user_match else "unknown"
            db = db_match.group(1).strip() if db_match else "unknown"

            return ScanFinding(
                title="Database Credentials Exposed",
                severity=ScanSeverity.HIGH,
                description=f"User: {user}, Database: {db}",
                cwe="CWE-89",
                scanner="sqlmap",
                tags=["sqli", "enumeration", "credentials"],
            )

        return None

    def _create_generic_finding(self, output: str) -> ScanFinding:
        """Create generic finding when detailed parsing fails."""
        # Try to extract any useful info
        dbms_match = re.search(r"back-end DBMS:\s*([^\n]+)", output, re.IGNORECASE)
        dbms = dbms_match.group(1).strip() if dbms_match else "Unknown"

        return ScanFinding(
            title="SQL Injection Detected",
            severity=ScanSeverity.HIGH,
            description=f"DBMS: {dbms}",
            cwe="CWE-89",
            scanner="sqlmap",
            tags=["sqli"],
        )

    async def stop(self) -> bool:
        """Stop running scan."""
        if self._process and self._running:
            self._process.terminate()
            try:
                await asyncio.wait_for(self._process.wait(), timeout=5)
            except asyncio.TimeoutError:
                self._process.kill()
            self._running = False
            return True
        return False
