"""
WebhookEmitter - Sends AgenticResult payloads to webhooks.

Handles profile resolution, size limits, format transformation, and retry logic.
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any, Dict, Optional

from urllib.parse import urlparse

from g_gremlin.contracts import AgenticResult
from g_gremlin.retry_utils import RetryConfig, _compute_delay, _sleep
from g_gremlin.webhook_out.envelope import WebhookEnvelope
from g_gremlin.webhook_out.profiles import profile_get
from g_gremlin.webhook_out.transformers import get_transformer


# Maximum payload size before auto-truncation (1.5MB - safe for all iPaaS)
MAX_PAYLOAD_BYTES = 1_500_000

# Allowed URL schemes to prevent SSRF attacks
ALLOWED_SCHEMES = {"https", "http"}


@dataclass
class WebhookDeliveryResult:
    """Result of webhook delivery attempt."""

    success: bool
    delivery_id: str
    status_code: Optional[int] = None
    error: Optional[str] = None
    attempts: int = 1

    def to_dict(self) -> Dict[str, Any]:
        return {
            "success": self.success,
            "delivery_id": self.delivery_id,
            "status_code": self.status_code,
            "error": self.error,
            "attempts": self.attempts,
        }


def _maybe_truncate(
    envelope: WebhookEnvelope,
    force_summary: bool = False,
    force_full: bool = False,
) -> WebhookEnvelope:
    """
    Truncate payload if too large, always preserving envelope fields.

    Args:
        envelope: The envelope to potentially truncate
        force_summary: If True, always truncate to summary
        force_full: If True, never truncate (may exceed size limits)

    Returns:
        Original envelope or truncated version
    """
    if force_full:
        return envelope

    # Serialize once for size check
    serialized = json.dumps(envelope.to_dict()).encode("utf-8")

    if force_summary or len(serialized) > MAX_PAYLOAD_BYTES:
        # Extract summary, preserve all envelope fields
        payload = envelope.payload
        truncated_payload = {
            "$truncated": True,
            "original_bytes": len(serialized),
            "summary": {
                "$schema": payload.get("$schema"),
                "status": payload.get("status"),
                "command": payload.get("command"),
                "correlation_id": payload.get("correlation_id"),
                "dry_run": payload.get("dry_run"),
                "error": payload.get("error"),
                "timestamp": payload.get("timestamp"),
            },
            "message": "Payload exceeded 1.5MB. Use --output to save full result locally.",
        }

        return WebhookEnvelope(
            event_type=envelope.event_type,
            correlation_id=envelope.correlation_id,
            delivery_id=envelope.delivery_id,
            source=envelope.source,
            source_version=envelope.source_version,
            timestamp=envelope.timestamp,
            payload=truncated_payload,
            attempt=envelope.attempt,
            max_attempts=envelope.max_attempts,
        )

    return envelope


class WebhookEmitter:
    """
    Emits AgenticResult payloads to webhook endpoints.

    Supports:
    - Direct URL or @profile-name references
    - Format-specific transformations (flat, wrapped, generic)
    - Configurable retry with exponential backoff
    - Automatic payload truncation for large results
    """

    DEFAULT_TIMEOUT = 10.0  # Fast timeout for webhooks
    DEFAULT_RETRY_CONFIG = RetryConfig(
        max_attempts=3,
        base_delay=1.0,
        max_delay=30.0,
        exponential_base=2.0,
        jitter=True,
        retryable_codes={429, 500, 502, 503, 504},
    )

    def __init__(
        self,
        timeout: Optional[float] = None,
        retry_config: Optional[RetryConfig] = None,
    ):
        self.timeout = timeout or self.DEFAULT_TIMEOUT
        self.retry_config = retry_config or self.DEFAULT_RETRY_CONFIG
        self._httpx = None

    def _get_httpx(self):
        """Lazy import httpx."""
        if self._httpx is None:
            import httpx

            self._httpx = httpx
        return self._httpx

    def _validate_url(self, url: str) -> None:
        """
        Validate URL scheme to prevent SSRF attacks.

        Only https:// and http:// are allowed.
        Rejects file://, ftp://, gopher://, localhost without scheme, etc.
        """
        try:
            parsed = urlparse(url)
            if parsed.scheme.lower() not in ALLOWED_SCHEMES:
                raise ValueError(
                    f"Invalid URL scheme '{parsed.scheme}'. Only https:// and http:// are allowed."
                )
            if not parsed.netloc:
                raise ValueError("Invalid URL: missing host")
        except Exception as e:
            if "Invalid URL" in str(e) or "scheme" in str(e):
                raise
            raise ValueError(f"Invalid URL: {e}")

    def resolve_target(
        self,
        webhook_out: str,
        format_override: Optional[str] = None,
    ) -> tuple[str, Optional[str], Dict[str, str], float, int]:
        """
        Resolve webhook target from URL or @profile reference.

        Returns:
            (url, format, headers, timeout, max_retries)

        Raises:
            ValueError: If profile not found or URL scheme is invalid
        """
        if webhook_out.startswith("@"):
            profile_name = webhook_out[1:]  # Strip @
            profile = profile_get(profile_name)
            if profile is None:
                raise ValueError(f"Webhook profile '{profile_name}' not found")
            # Validate URL from profile
            self._validate_url(profile.url)
            return (
                profile.url,
                format_override or profile.format,
                profile.headers,
                profile.timeout,
                profile.max_retries,
            )

        # Direct URL - validate and use defaults
        self._validate_url(webhook_out)
        return (
            webhook_out,
            format_override,
            {},
            self.timeout,
            self.retry_config.max_attempts,
        )

    def emit(
        self,
        result: AgenticResult,
        webhook_out: str,
        format: Optional[str] = None,
        force_summary: bool = False,
        force_full: bool = False,
        extra_headers: Optional[Dict[str, str]] = None,
    ) -> WebhookDeliveryResult:
        """
        Send AgenticResult to webhook endpoint.

        Args:
            result: The AgenticResult to send
            webhook_out: URL or @profile-name
            format: Format preset (flat|wrapped|generic), overrides profile
            force_summary: Force summary-only payload
            force_full: Force full payload (may exceed size limits)
            extra_headers: Additional headers to include

        Returns:
            WebhookDeliveryResult with delivery status
        """
        httpx = self._get_httpx()

        # Resolve target
        url, resolved_format, profile_headers, timeout, max_retries = self.resolve_target(
            webhook_out, format
        )

        # Build envelope from result
        envelope = WebhookEnvelope.from_agentic_result(result, max_attempts=max_retries)

        # Apply size limits
        envelope = _maybe_truncate(envelope, force_summary=force_summary, force_full=force_full)

        # Transform for format
        transformer = get_transformer(resolved_format)
        body, transform_headers = transformer(envelope)

        # Merge headers (profile < transform < extra)
        final_headers = {**profile_headers, **transform_headers}
        if extra_headers:
            final_headers.update(extra_headers)

        # Update retry config with profile settings
        retry_config = RetryConfig(
            max_attempts=max_retries,
            base_delay=self.retry_config.base_delay,
            max_delay=self.retry_config.max_delay,
            exponential_base=self.retry_config.exponential_base,
            jitter=self.retry_config.jitter,
            retryable_codes=self.retry_config.retryable_codes,
        )

        # Retry loop
        last_error: Optional[str] = None
        last_status: Optional[int] = None

        for attempt in range(1, retry_config.max_attempts + 1):
            envelope.attempt = attempt

            try:
                resp = httpx.post(
                    url,
                    json=body,
                    headers=final_headers,
                    timeout=timeout,
                )
                last_status = resp.status_code

                if 200 <= resp.status_code < 300:
                    return WebhookDeliveryResult(
                        success=True,
                        delivery_id=envelope.delivery_id,
                        status_code=resp.status_code,
                        attempts=attempt,
                    )

                # Check if retryable
                if resp.status_code in retry_config.retryable_codes:
                    last_error = f"HTTP {resp.status_code}"
                    if attempt < retry_config.max_attempts:
                        delay = _compute_delay(retry_config, attempt)
                        _sleep(delay)
                        continue

                # Non-retryable error
                return WebhookDeliveryResult(
                    success=False,
                    delivery_id=envelope.delivery_id,
                    status_code=resp.status_code,
                    error=f"HTTP {resp.status_code}",
                    attempts=attempt,
                )

            except Exception as exc:
                last_error = str(exc)
                if attempt < retry_config.max_attempts:
                    delay = _compute_delay(retry_config, attempt)
                    _sleep(delay)
                    continue

        # All retries exhausted
        return WebhookDeliveryResult(
            success=False,
            delivery_id=envelope.delivery_id,
            status_code=last_status,
            error=last_error or "Max retries exceeded",
            attempts=retry_config.max_attempts,
        )
