"""
ASGI integration for FastAPI / Starlette.

Two modes:

1. **Auto** (zero-config) — ``ASGIIntegration`` monkey-patches
   ``Starlette.__call__`` so every FastAPI / Starlette app is
   automatically instrumented.  Enabled by default when ``starlette``
   is installed.

2. **Manual** — ``middleware_asgi(app)`` wraps a single ASGI app.
   Useful if auto-discovery is disabled or for non-Starlette ASGI apps.
"""

import time
import traceback

from overcast_sdk.integrations import Integration, DidNotEnable
from overcast_sdk.utils import capture_internal, _originals


# ── Shared request-handling logic ─────────────────────────────────────────────

async def _handle_asgi_request(app, scope, receive, send):
    """Instrument a single ASGI HTTP request (shared by both modes)."""
    from overcast_sdk.client import get_client
    from overcast_sdk.scope import (
        Scope, push_isolation_scope, pop_isolation_scope, get_current_scope,
    )

    client = get_client()
    if not client or scope.get("type") != "http":
        await app(scope, receive, send)
        return

    token = push_isolation_scope(Scope())
    start = time.monotonic()
    status_code = 500

    async def _send_wrapper(message):
        nonlocal status_code
        if message["type"] == "http.response.start":
            status_code = message.get("status", 500)
        await send(message)

    exc_to_raise = None
    try:
        await app(scope, receive, _send_wrapper)
    except Exception as exc:
        exc_to_raise = exc
        duration = time.monotonic() - start
        with capture_internal:
            tb = "".join(traceback.format_exception(
                type(exc), exc, exc.__traceback__,
            ))
            client.enqueue("ERROR", f"ASGI error: {exc}", {
                "type": "asgi_error",
                "path": scope.get("path", ""),
                "method": scope.get("method", ""),
                "duration_s": round(duration, 3),
                "error": {
                    "type": type(exc).__name__,
                    "message": str(exc),
                    "traceback": tb,
                },
            })
    finally:
        duration = time.monotonic() - start
        method = scope.get("method", "")
        path = scope.get("path", "")

        with capture_internal:
            if client:
                iso_scope = get_current_scope()
                iso_scope.add_breadcrumb({
                    "type": "http",
                    "category": "asgi",
                    "message": f"{method} {path} {status_code}",
                    "data": {
                        "method": method,
                        "path": path,
                        "status_code": status_code,
                        "duration_ms": round(duration * 1000),
                    },
                    "level": "error" if status_code >= 500 else "info",
                })

                level = (
                    "ERROR" if status_code >= 500
                    else "WARNING" if status_code >= 400
                    else "INFO"
                )
                meta = {
                    "type": "http_incoming",
                    "method": method,
                    "path": path,
                    "status_code": status_code,
                    "duration_s": round(duration, 3),
                    "query_string": scope.get("query_string", b"").decode(
                        "utf-8", errors="ignore"
                    ),
                }
                client.enqueue(
                    level,
                    f"{method} {path} {status_code} ({duration:.3f}s)",
                    meta,
                )

        pop_isolation_scope(token)

    if exc_to_raise is not None:
        raise exc_to_raise


# ═══════════════════════════════════════════════════════════════════════════════
# Auto-discovery integration (monkey-patches Starlette.__call__)
# ═══════════════════════════════════════════════════════════════════════════════

_PATCHED_ATTR = "_overcast_auto_patched"


class ASGIIntegration(Integration):
    identifier = "asgi"

    @staticmethod
    def setup_once():
        try:
            from starlette.applications import Starlette
        except ImportError:
            raise DidNotEnable("starlette is not installed")

        _originals["starlette_call"] = Starlette.__call__

        async def _patched_call(self, scope, receive, send):
            if scope.get("type") != "http":
                return await _originals["starlette_call"](
                    self, scope, receive, send
                )
            # Mark scope so manual middleware can detect us
            scope["_overcast_auto"] = True
            await _handle_asgi_request(
                lambda sc, rc, sd: _originals["starlette_call"](
                    self, sc, rc, sd
                ),
                scope, receive, send,
            )

        Starlette.__call__ = _patched_call


# ═══════════════════════════════════════════════════════════════════════════════
# Manual middleware (kept for backwards-compat / non-Starlette ASGI apps)
# ═══════════════════════════════════════════════════════════════════════════════

def middleware_asgi(app):
    """Wrap an ASGI *app* with Overcast request tracking.

    If the auto ``ASGIIntegration`` is already active for Starlette
    apps, this detects it and avoids double-instrumentation.

    Usage::

        from overcast_sdk import middleware_asgi
        app = FastAPI()
        app = middleware_asgi(app)
    """

    class OvercastASGIMiddleware:
        def __init__(self, app):
            self.app = app

        async def __call__(self, scope, receive, send):
            # Skip if auto-integration already handled this request
            if scope.get("_overcast_auto"):
                return await self.app(scope, receive, send)

            if scope.get("type") != "http":
                return await self.app(scope, receive, send)

            await _handle_asgi_request(self.app, scope, receive, send)

    return OvercastASGIMiddleware(app)
