"""API Mode Server for mcpaudit — HTTP REST API for integration.

Provides an HTTP server for integrating mcpaudit with other tools.
Built on stdlib http.server with JSON request/response handling.

Endpoints:
  GET  /health   — Server health check
  GET  /version  — mcpaudit version info
  GET  /rules    — List available rules
  GET  /compliance — List compliance frameworks
  POST /scan     — Run a scan on provided code

CLI: mcpaudit serve [--host 127.0.0.1] [--port 8088]

Zero external dependencies. Python 3.11+ stdlib only.
"""

from __future__ import annotations

import io
import json
import os
import tempfile
import threading
import time
from dataclasses import dataclass, field
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path
from typing import Any
from urllib.parse import urlparse, parse_qs

from . import __version__


__all__ = [
    "ApiResponse",
    "McpauditHandler",
    "McpauditServer",
    "ScanRequest",
    "ScanResponse",
    "create_api_server",
    "handle_scan_request",
    "parse_scan_request",
    "format_error_response",
    "format_success_response",
    "get_routes",
]


# ---------------------------------------------------------------------------
# Data Models
# ---------------------------------------------------------------------------

@dataclass
class ScanRequest:
    """Represents a scan request."""

    code: str = ""
    language: str = "python"
    filename: str = "input.py"
    severity_threshold: str = ""
    rules: list[str] = field(default_factory=list)
    format: str = "json"

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ScanRequest:
        return cls(
            code=data.get("code", ""),
            language=data.get("language", "python"),
            filename=data.get("filename", "input.py"),
            severity_threshold=data.get("severity_threshold", ""),
            rules=data.get("rules", []),
            format=data.get("format", "json"),
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "code": self.code,
            "language": self.language,
            "filename": self.filename,
            "severity_threshold": self.severity_threshold,
            "rules": self.rules,
            "format": self.format,
        }

    def validate(self) -> list[str]:
        """Validate the request, return list of errors."""
        errors = []
        if not self.code:
            errors.append("'code' field is required")
        if self.language not in ("python", "typescript", "javascript", "go"):
            errors.append(
                f"Unsupported language '{self.language}'. "
                f"Valid: python, typescript, javascript, go"
            )
        if self.severity_threshold:
            valid_sevs = ("critical", "high", "medium", "low", "info")
            if self.severity_threshold.lower() not in valid_sevs:
                errors.append(
                    f"Invalid severity_threshold '{self.severity_threshold}'. "
                    f"Valid: {', '.join(valid_sevs)}"
                )
        return errors


@dataclass
class ScanResponse:
    """Represents a scan response."""

    success: bool = True
    findings: list[dict[str, Any]] = field(default_factory=list)
    summary: dict[str, Any] = field(default_factory=dict)
    scan_time_ms: float = 0.0
    error: str = ""

    def to_dict(self) -> dict[str, Any]:
        result: dict[str, Any] = {
            "success": self.success,
            "scan_time_ms": round(self.scan_time_ms, 2),
        }
        if self.error:
            result["error"] = self.error
        else:
            result["findings"] = self.findings
            result["summary"] = self.summary
            result["findings_count"] = len(self.findings)
        return result


@dataclass
class ApiResponse:
    """Generic API response wrapper."""

    status: int = 200
    data: dict[str, Any] = field(default_factory=dict)
    error: str = ""

    def to_json(self) -> str:
        if self.error:
            return json.dumps({
                "success": False,
                "error": self.error,
                "status": self.status,
            })
        return json.dumps({
            "success": True,
            "data": self.data,
            "status": self.status,
        })


# ---------------------------------------------------------------------------
# Scan Handler
# ---------------------------------------------------------------------------

def parse_scan_request(body: bytes | str) -> ScanRequest:
    """Parse a scan request from JSON bytes/string."""
    if isinstance(body, bytes):
        body = body.decode("utf-8")
    try:
        data = json.loads(body)
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON: {e}")
    if not isinstance(data, dict):
        raise ValueError("Request body must be a JSON object")
    return ScanRequest.from_dict(data)


def handle_scan_request(request: ScanRequest) -> ScanResponse:
    """Handle a scan request and return results.

    Parameters
    ----------
    request : ScanRequest
        The scan request with code and options.

    Returns
    -------
    ScanResponse
        Scan results with findings and summary.
    """
    errors = request.validate()
    if errors:
        return ScanResponse(
            success=False,
            error="; ".join(errors),
        )

    start = time.monotonic()

    try:
        # Import scanner lazily to avoid circular imports
        from .scanner import MCPSecurityScanner

        # Write code to a temp file for scanning
        ext_map = {
            "python": ".py",
            "typescript": ".ts",
            "javascript": ".js",
            "go": ".go",
        }
        ext = ext_map.get(request.language, ".py")

        with tempfile.NamedTemporaryFile(
            mode="w",
            suffix=ext,
            prefix="mcpaudit_api_",
            delete=False,
        ) as f:
            f.write(request.code)
            tmp_path = f.name

        try:
            scanner = MCPSecurityScanner()
            result = scanner.scan_file(Path(tmp_path))

            findings = []
            for finding in result.findings:
                fd = finding.to_dict()
                # Replace temp path with original filename
                fd["file"] = request.filename
                # Filter by severity threshold if specified
                if request.severity_threshold:
                    sev_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]
                    threshold_idx = sev_order.index(
                        request.severity_threshold.upper()
                    )
                    finding_idx = sev_order.index(fd["severity"])
                    if finding_idx > threshold_idx:
                        continue
                # Filter by rules if specified
                if request.rules and fd["rule_id"] not in request.rules:
                    continue
                findings.append(fd)

            elapsed = (time.monotonic() - start) * 1000

            severity_counts = {}
            for fd in findings:
                sev = fd["severity"]
                severity_counts[sev] = severity_counts.get(sev, 0) + 1

            summary = {
                "total_findings": len(findings),
                "severity_counts": severity_counts,
                "lines_scanned": result.lines_scanned,
                "grade": result.grade,
                "language": request.language,
                "filename": request.filename,
            }

            return ScanResponse(
                success=True,
                findings=findings,
                summary=summary,
                scan_time_ms=elapsed,
            )
        finally:
            try:
                os.unlink(tmp_path)
            except OSError:
                pass

    except Exception as exc:
        elapsed = (time.monotonic() - start) * 1000
        return ScanResponse(
            success=False,
            error=str(exc),
            scan_time_ms=elapsed,
        )


# ---------------------------------------------------------------------------
# Response Helpers
# ---------------------------------------------------------------------------

def format_error_response(status: int, message: str) -> ApiResponse:
    """Create an error API response."""
    return ApiResponse(status=status, error=message)


def format_success_response(data: dict[str, Any]) -> ApiResponse:
    """Create a success API response."""
    return ApiResponse(status=200, data=data)


def get_routes() -> dict[str, str]:
    """Get all available API routes."""
    return {
        "GET /health": "Server health check",
        "GET /version": "mcpaudit version information",
        "GET /rules": "List available scanning rules",
        "GET /compliance": "List compliance frameworks",
        "POST /scan": "Run a security scan on provided code",
    }


# ---------------------------------------------------------------------------
# HTTP Handler
# ---------------------------------------------------------------------------

class McpauditHandler(BaseHTTPRequestHandler):
    """HTTP request handler for mcpaudit API."""

    # Suppress default logging
    def log_message(self, format: str, *args: Any) -> None:
        pass

    def _set_headers(self, status: int = 200, content_type: str = "application/json") -> None:
        self.send_response(status)
        self.send_header("Content-Type", content_type)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()

    def _send_json(self, status: int, data: dict[str, Any]) -> None:
        self._set_headers(status)
        body = json.dumps(data, indent=2).encode("utf-8")
        self.wfile.write(body)

    def _read_body(self) -> bytes:
        length = int(self.headers.get("Content-Length", 0))
        if length > 0:
            return self.rfile.read(length)
        return b""

    def do_OPTIONS(self) -> None:
        """Handle CORS preflight."""
        self._set_headers(204)

    def do_GET(self) -> None:
        """Handle GET requests."""
        parsed = urlparse(self.path)
        path = parsed.path.rstrip("/")

        if path == "/health":
            self._handle_health()
        elif path == "/version":
            self._handle_version()
        elif path == "/rules":
            self._handle_rules()
        elif path == "/compliance":
            self._handle_compliance()
        elif path == "" or path == "/":
            self._handle_index()
        else:
            self._send_json(404, {
                "success": False,
                "error": f"Not found: {self.path}",
            })

    def do_POST(self) -> None:
        """Handle POST requests."""
        parsed = urlparse(self.path)
        path = parsed.path.rstrip("/")

        if path == "/scan":
            self._handle_scan()
        else:
            self._send_json(404, {
                "success": False,
                "error": f"Not found: {self.path}",
            })

    def _handle_index(self) -> None:
        """Root endpoint — list available routes."""
        self._send_json(200, {
            "success": True,
            "data": {
                "name": "mcpaudit API",
                "version": __version__,
                "routes": get_routes(),
            },
        })

    def _handle_health(self) -> None:
        """Health check endpoint."""
        self._send_json(200, {
            "success": True,
            "data": {
                "status": "healthy",
                "version": __version__,
                "uptime_seconds": getattr(self.server, "_uptime", 0),
            },
        })

    def _handle_version(self) -> None:
        """Version info endpoint."""
        self._send_json(200, {
            "success": True,
            "data": {
                "version": __version__,
                "name": "mcpaudit",
                "description": "MCP Security Audit Scanner",
                "python_version": _python_version(),
            },
        })

    def _handle_rules(self) -> None:
        """List available rules."""
        try:
            from .scanner import MCPSecurityScanner
            scanner = MCPSecurityScanner()
            rules = []
            for rule in scanner.rules:
                sev = getattr(rule, "severity", "MEDIUM")
                if hasattr(sev, "value"):
                    sev = sev.value
                rules.append({
                    "rule_id": rule.rule_id,
                    "name": getattr(rule, "name", rule.rule_id),
                    "severity": str(sev),
                    "category": getattr(rule, "category", "general"),
                })
            self._send_json(200, {
                "success": True,
                "data": {
                    "rules": rules,
                    "total": len(rules),
                },
            })
        except Exception as e:
            self._send_json(500, {
                "success": False,
                "error": f"Failed to load rules: {e}",
            })

    def _handle_compliance(self) -> None:
        """List compliance frameworks."""
        try:
            from .compliance_validator import list_frameworks
            frameworks = list_frameworks()
            self._send_json(200, {
                "success": True,
                "data": {
                    "frameworks": frameworks,
                    "total": len(frameworks),
                },
            })
        except Exception as e:
            self._send_json(500, {
                "success": False,
                "error": f"Failed to load frameworks: {e}",
            })

    def _handle_scan(self) -> None:
        """Handle scan request."""
        try:
            body = self._read_body()
            if not body:
                self._send_json(400, {
                    "success": False,
                    "error": "Request body is empty",
                })
                return

            request = parse_scan_request(body)
            response = handle_scan_request(request)
            status = 200 if response.success else 400
            self._send_json(status, response.to_dict())
        except ValueError as e:
            self._send_json(400, {
                "success": False,
                "error": str(e),
            })
        except Exception as e:
            self._send_json(500, {
                "success": False,
                "error": f"Internal error: {e}",
            })


def _python_version() -> str:
    """Get Python version string."""
    import sys
    return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"


# ---------------------------------------------------------------------------
# Server
# ---------------------------------------------------------------------------

class McpauditServer:
    """Wrapper around HTTPServer for mcpaudit API."""

    def __init__(self, host: str = "127.0.0.1", port: int = 8088) -> None:
        self.host = host
        self.port = port
        self._server: HTTPServer | None = None
        self._thread: threading.Thread | None = None
        self._start_time: float = 0.0

    @property
    def is_running(self) -> bool:
        return self._server is not None

    @property
    def address(self) -> str:
        return f"http://{self.host}:{self.port}"

    @property
    def uptime(self) -> float:
        if self._start_time > 0:
            return time.monotonic() - self._start_time
        return 0.0

    def start(self, blocking: bool = False) -> None:
        """Start the API server.

        Parameters
        ----------
        blocking : bool
            If True, block the current thread. If False, run in background.
        """
        self._server = HTTPServer((self.host, self.port), McpauditHandler)
        self._server._uptime = 0  # type: ignore[attr-defined]
        self._start_time = time.monotonic()

        if blocking:
            try:
                self._server.serve_forever()
            except KeyboardInterrupt:
                self.stop()
        else:
            self._thread = threading.Thread(
                target=self._server.serve_forever,
                daemon=True,
            )
            self._thread.start()

    def stop(self) -> None:
        """Stop the API server."""
        if self._server is not None:
            self._server.shutdown()
            self._server.server_close()
            self._server = None
            self._thread = None
            self._start_time = 0.0

    def __enter__(self) -> McpauditServer:
        self.start(blocking=False)
        return self

    def __exit__(self, *args: Any) -> None:
        self.stop()


def create_api_server(
    host: str = "127.0.0.1",
    port: int = 8088,
) -> McpauditServer:
    """Create an mcpaudit API server instance.

    Parameters
    ----------
    host : str
        Host address to bind to.
    port : int
        Port number to listen on.

    Returns
    -------
    McpauditServer
        Server instance (call .start() to begin serving).
    """
    return McpauditServer(host=host, port=port)
