"""aiohttp patching for AutoDebug capture.

This module captures aiohttp ClientSession requests, providing async HTTP
capture equivalent to what we do for requests/httpx sync clients.
"""

from __future__ import annotations

import base64
import hashlib
import json
from typing import Any, Callable, Dict, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from autodebug_probe.recorder import Recorder

# Original functions
_original_request: Optional[Callable] = None

# Global recorder reference
_recorder: Optional["Recorder"] = None


def _get_recorder() -> "Recorder":
    """Get the global recorder instance."""
    if _recorder is None:
        from autodebug_probe.recorder import get_recorder
        return get_recorder()
    return _recorder


def _encode_body(body: bytes) -> tuple:
    """Encode body for recording, handling binary data."""
    try:
        decoded_text = body.decode("utf-8")
        return decoded_text, "utf-8"
    except UnicodeDecodeError:
        return base64.b64encode(body).decode("ascii"), "base64"


def _body_hash(body: bytes, content_type: Optional[str]) -> str:
    """Generate hash for request/response body."""
    if not body:
        return hashlib.sha256(b"").hexdigest()[:16]

    # For JSON, normalize before hashing
    if content_type and "application/json" in content_type:
        try:
            parsed = json.loads(body)
            normalized = json.dumps(parsed, sort_keys=True, separators=(",", ":"))
            return hashlib.sha256(normalized.encode()).hexdigest()[:16]
        except (json.JSONDecodeError, UnicodeDecodeError):
            pass

    return hashlib.sha256(body).hexdigest()[:16]


def _canonical_url(url: str) -> str:
    """Canonicalize URL for matching."""
    from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse

    parsed = urlparse(url)
    # Sort query parameters
    query_params = sorted(parse_qsl(parsed.query))
    canonical_query = urlencode(query_params)

    return urlunparse((
        parsed.scheme,
        parsed.netloc,
        parsed.path,
        parsed.params,
        canonical_query,
        ""  # Remove fragment
    ))


def _normalize_headers(headers: Dict[str, str]) -> tuple:
    """Normalize headers for recording."""
    # Headers to ignore (non-deterministic)
    IGNORED = {
        "date", "server", "set-cookie", "x-request-id",
        "traceparent", "tracestate", "x-amzn-requestid",
    }

    headers_canon = {}
    headers_raw = {}
    headers_ignored = []

    for key, value in headers.items():
        key_lower = key.lower()
        if key_lower in IGNORED:
            headers_ignored.append(key_lower)
        else:
            headers_canon[key_lower] = value
            headers_raw[key_lower] = value

    return headers_canon, headers_raw, headers_ignored


class _ResponseWithBody:
    """Wrapper that provides response with pre-read body."""

    def __init__(self, response, body: bytes):
        self._response = response
        self._body = body
        self._body_read = False

    def __getattr__(self, name):
        return getattr(self._response, name)

    async def __aenter__(self):
        """Support async context manager protocol (async with session.get(...) as resp)."""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Support async context manager protocol."""
        if hasattr(self._response, 'release'):
            await self._response.release()
        return None

    async def read(self) -> bytes:
        """Return pre-read body."""
        return self._body

    async def text(self, encoding: Optional[str] = None) -> str:
        """Return body as text."""
        enc = encoding or self._response.get_encoding() or "utf-8"
        return self._body.decode(enc, errors="replace")

    async def json(self, **kwargs) -> Any:
        """Return body as JSON."""
        return json.loads(self._body)


async def _patched_request(self, method: str, url, **kwargs):
    """Patched ClientSession._request that captures HTTP calls."""
    recorder = _get_recorder()

    # Extract request body
    req_body = b""
    content_type = None

    if "data" in kwargs:
        data = kwargs["data"]
        if isinstance(data, bytes):
            req_body = data
        elif isinstance(data, str):
            req_body = data.encode("utf-8")
        elif isinstance(data, dict):
            req_body = json.dumps(data).encode("utf-8")

    if "json" in kwargs:
        req_body = json.dumps(kwargs["json"]).encode("utf-8")
        content_type = "application/json"

    # Get content type from headers
    headers = kwargs.get("headers", {})
    if isinstance(headers, dict):
        content_type = content_type or headers.get("Content-Type") or headers.get("content-type")

    # Make the actual request
    response = await _original_request(self, method, url, **kwargs)

    # Read the response body
    response_body = await response.read()

    # Encode response body
    response_body_payload, response_body_encoding = _encode_body(response_body)

    # Normalize headers
    resp_headers = dict(response.headers)
    headers_canon, headers_raw, headers_ignored = _normalize_headers(resp_headers)

    # Record the request/response
    payload = {
        "method": method.upper(),
        "url_canonical": _canonical_url(str(url)),
        "body_hash": _body_hash(req_body, content_type),
        "status": response.status,
        "response_body_bytes": response_body_payload,
        "response_body_encoding": response_body_encoding,
        "response_body_hash": _body_hash(response_body, headers_raw.get("content-type")),
        "headers_canon": headers_canon,
        "headers_raw": headers_raw,
        "headers_ignored": headers_ignored,
        "is_async": True,
        "client": "aiohttp",
    }

    recorder.record_http(payload)

    # Return wrapped response with body already read
    return _ResponseWithBody(response, response_body)


def install() -> None:
    """Install aiohttp capture patches."""
    global _original_request

    try:
        import aiohttp
    except ImportError:
        return

    if _original_request is not None:
        return  # Already patched

    _original_request = aiohttp.ClientSession._request
    aiohttp.ClientSession._request = _patched_request


def uninstall() -> None:
    """Restore original aiohttp functions."""
    global _original_request

    try:
        import aiohttp
    except ImportError:
        return

    if _original_request is not None:
        aiohttp.ClientSession._request = _original_request
        _original_request = None


def get_aiohttp_info() -> Dict[str, Any]:
    """Get aiohttp detection info for manifest."""
    try:
        import aiohttp
        return {
            "detected": True,
            "version": aiohttp.__version__,
        }
    except ImportError:
        return {"detected": False}
