"""Webhook event bus and delivery engine.

This module provides the core event emission and delivery system for peon-mcp.
Events are emitted when state transitions occur (task completed, tests passed, etc.)
and delivered asynchronously to configured webhook endpoints.

Discord channel delivery is supported via a special URL scheme:
  ``discord://channel/<channel_id>``  — send to a guild channel
  ``discord://user/<user_id>``        — send as a DM

When a webhook config URL uses one of these schemes, the engine looks up the
project's Discord bot config, builds a rich embed, and sends via the Discord
REST API instead of an HTTP POST.
"""

import asyncio
import hashlib
import hmac
import json
import logging
from datetime import datetime, timezone

import aiosqlite
import httpx

logger = logging.getLogger(__name__)

_DISCORD_SCHEME = "discord://"


class EventType:
    """Supported event type constants."""

    # Task lifecycle
    TASK_COMPLETED = "task.completed"
    TASK_FAILED = "task.failed"
    TASK_TIMEOUT = "task.timeout"
    TASK_CREATED = "task.created"

    # Review events
    REVIEW_SESSION_COMPLETED = "review.session_completed"
    REVIEW_CONVERGENCE_FOUND = "review.convergence_found"

    # Test events
    TEST_PASSED = "test.passed"
    TEST_FAILED = "test.failed"
    TEST_MAX_RETRIES_EXHAUSTED = "test.max_retries_exhausted"

    # Feature events
    FEATURE_COMPLETED = "feature.completed"

    ALL = [
        TASK_COMPLETED,
        TASK_FAILED,
        TASK_TIMEOUT,
        TASK_CREATED,
        REVIEW_SESSION_COMPLETED,
        REVIEW_CONVERGENCE_FOUND,
        TEST_PASSED,
        TEST_FAILED,
        TEST_MAX_RETRIES_EXHAUSTED,
        FEATURE_COMPLETED,
    ]


def _parse_discord_url(url: str) -> tuple[str, str] | None:
    """Parse a Discord delivery URL into (kind, target_id).

    Returns:
        A tuple of ('channel', channel_id) or ('user', user_id),
        or ``None`` if the URL is not a Discord delivery URL.
    """
    if not url.startswith(_DISCORD_SCHEME):
        return None
    path = url[len(_DISCORD_SCHEME):]
    parts = path.split("/", 1)
    if len(parts) != 2 or not parts[1]:
        return None
    kind, target_id = parts
    if kind in ("channel", "user"):
        return kind, target_id
    return None


def _build_payload(event_type: str, project_id: str, data: dict) -> dict:
    """Build the standard event payload envelope.

    Args:
        event_type: The event type string (e.g., 'task.completed')
        project_id: The project identifier
        data: Event-specific payload fields

    Returns:
        Standard payload dict ready for JSON serialization
    """
    return {
        "event": event_type,
        "project_id": project_id,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "data": data,
    }


async def emit_event(
    db: aiosqlite.Connection,
    project_id: str,
    event_type: str,
    payload: dict,
) -> None:
    """Emit an event and dispatch fire-and-forget webhook and Discord deliveries.

    Queries webhook_configs for all enabled webhooks subscribed to this event type.
    For each matching webhook, inserts an event_log row and dispatches delivery
    as an asyncio background task (non-blocking).

    Also dispatches Discord channel deliveries via emit_discord_event() for any
    discord_channel_mappings subscribed to this event type.

    Args:
        db: Active aiosqlite database connection
        project_id: The project that emitted the event
        event_type: The event type string (use EventType constants)
        payload: Event-specific data dict (will be wrapped in standard envelope)
    """
    full_payload = _build_payload(event_type, project_id, payload)
    payload_json = json.dumps(full_payload)

    try:
        rows = await db.execute_fetchall(
            "SELECT * FROM webhook_configs WHERE project_id = ? AND enabled = 1",
            (project_id,),
        )
    except Exception as e:
        logger.error("Failed to query webhook_configs for project %s: %s", project_id, e)
        rows = []

    for config in rows:
        # Check whether this webhook is subscribed to the event type
        raw_events = config["events"] or "[]"
        try:
            subscribed = json.loads(raw_events)
        except (json.JSONDecodeError, TypeError):
            # Fallback: treat as comma-separated string
            subscribed = [e.strip() for e in raw_events.split(",") if e.strip()]

        if event_type not in subscribed:
            continue

        # Insert a webhook_event_logs row for this delivery attempt
        try:
            cursor = await db.execute(
                """INSERT INTO webhook_event_logs
                   (project_id, event_type, payload, webhook_config_id, status, delivery_type)
                   VALUES (?, ?, ?, ?, 'pending', 'webhook')""",
                (project_id, event_type, payload_json, config["id"]),
            )
            event_log_id = cursor.lastrowid
            await db.commit()
        except Exception as e:
            logger.error(
                "Failed to insert event_log for webhook %s: %s", config["id"], e
            )
            continue

        url = config["url"]
        discord_target = _parse_discord_url(url)

        if discord_target is not None:
            # Discord delivery path via webhook config URL scheme
            kind, target_id = discord_target
            asyncio.create_task(
                deliver_discord(
                    db=db,
                    event_log_id=event_log_id,
                    project_id=project_id,
                    event_type=event_type,
                    data=payload,
                    kind=kind,
                    target_id=target_id,
                ),
                name=f"discord-delivery-{event_log_id}",
            )
        else:
            # Standard HTTP POST delivery
            asyncio.create_task(
                deliver_webhook(
                    db=db,
                    event_log_id=event_log_id,
                    url=url,
                    payload=full_payload,
                    secret=config["secret"] or "",
                ),
                name=f"webhook-delivery-{event_log_id}",
            )

    # Also deliver to Discord channels subscribed directly via channel mappings
    await emit_discord_event(db, project_id, event_type, payload, payload_json)


async def emit_discord_event(
    db: aiosqlite.Connection,
    project_id: str,
    event_type: str,
    payload: dict,
    payload_json: str | None = None,
) -> None:
    """Dispatch fire-and-forget Discord deliveries to subscribed channel mappings.

    Looks up all enabled Discord configs for the project, then for each config
    finds channel mappings whose subscribed_events include the event type (or
    whose subscribed_events is empty, meaning subscribe to all events). Formats
    the payload as a rich embed and sends to each channel asynchronously.

    Args:
        db: Active aiosqlite database connection
        project_id: The project that emitted the event
        event_type: The event type string (use EventType constants)
        payload: Event-specific data dict (will be wrapped in standard envelope)
        payload_json: Pre-serialized full payload JSON (optional optimisation)
    """
    try:
        config_rows = await db.execute_fetchall(
            "SELECT * FROM discord_configs WHERE project_id = ? AND enabled = 1",
            (project_id,),
        )
    except Exception as e:
        logger.error("emit_discord_event: failed to query discord_configs: %s", e)
        return

    if not config_rows:
        return

    if payload_json is None:
        payload_json = json.dumps(_build_payload(event_type, project_id, payload))

    for config in config_rows:
        config_dict = dict(config)
        config_id = config_dict["id"]

        try:
            channel_rows = await db.execute_fetchall(
                "SELECT * FROM discord_channel_mappings WHERE discord_config_id = ? AND enabled = 1",
                (config_id,),
            )
        except Exception as e:
            logger.error(
                "emit_discord_event: failed to query channels for config %s: %s", config_id, e
            )
            continue

        for channel in channel_rows:
            channel_dict = dict(channel)
            channel_id = channel_dict["channel_id"]

            # Check subscription: empty list means subscribe to all events
            raw_subs = channel_dict.get("subscribed_events") or "[]"
            try:
                subscribed = json.loads(raw_subs)
            except (json.JSONDecodeError, TypeError):
                subscribed = []

            if subscribed and event_type not in subscribed:
                continue

            # Insert an event_log row for this Discord delivery
            try:
                cursor = await db.execute(
                    """INSERT INTO webhook_event_logs
                       (project_id, event_type, payload, webhook_config_id, status, delivery_type)
                       VALUES (?, ?, ?, NULL, 'pending', 'discord')""",
                    (project_id, event_type, payload_json),
                )
                event_log_id = cursor.lastrowid
                await db.commit()
            except Exception as e:
                logger.error(
                    "emit_discord_event: failed to insert event_log for channel %s: %s",
                    channel_id,
                    e,
                )
                continue

            asyncio.create_task(
                deliver_discord(
                    db=db,
                    event_log_id=event_log_id,
                    project_id=project_id,
                    event_type=event_type,
                    data=payload,
                    kind="channel",
                    target_id=channel_id,
                    gateway=config_dict,
                ),
                name=f"discord-channel-{event_log_id}",
            )


async def deliver_webhook(
    db: aiosqlite.Connection,
    event_log_id: int,
    url: str,
    payload: dict,
    secret: str,
) -> None:
    """Deliver a webhook payload to the target URL.

    POSTs the JSON payload with Content-Type: application/json. If a secret is
    provided, computes an HMAC-SHA256 signature and includes it as the
    X-Webhook-Signature header (format: 'sha256=<hex_digest>').

    Updates the event_log row with the final status, response code, and any
    error message. Never raises — all errors are caught and logged.

    Args:
        db: Active aiosqlite database connection
        event_log_id: The event_log.id row to update with delivery results
        url: Target webhook endpoint URL
        payload: The event payload dict (will be JSON-serialized for delivery)
        secret: HMAC signing secret; empty string disables signing
    """
    payload_json = json.dumps(payload)
    headers = {"Content-Type": "application/json"}

    if secret:
        sig = hmac.new(
            secret.encode(),
            payload_json.encode(),
            hashlib.sha256,
        ).hexdigest()
        headers["X-Webhook-Signature"] = f"sha256={sig}"

    response_code: int | None = None
    error = ""
    status = "failed"

    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            response = await client.post(
                url,
                content=payload_json,
                headers=headers,
            )
            response_code = response.status_code
            if 200 <= response_code < 300:
                status = "success"
            else:
                error = f"HTTP {response_code}: {response.text[:200]}"
    except httpx.TimeoutException:
        error = "Request timed out after 10 seconds"
        logger.warning("Webhook delivery timed out for event_log %d (url=%s)", event_log_id, url)
    except Exception as e:
        error = str(e)[:500]
        logger.warning("Webhook delivery failed for event_log %d: %s", event_log_id, error)

    try:
        await db.execute(
            """UPDATE webhook_event_logs
               SET status = ?, response_code = ?, error = ?
               WHERE id = ?""",
            (status, response_code, error, event_log_id),
        )
        await db.commit()
    except Exception as e:
        logger.error(
            "Failed to update event_log %d after delivery (status=%s): %s",
            event_log_id,
            status,
            e,
        )


async def deliver_discord(
    db: aiosqlite.Connection,
    event_log_id: int,
    project_id: str,
    event_type: str,
    data: dict,
    kind: str,
    target_id: str,
    gateway: dict | None = None,
) -> None:
    """Deliver a Discord embed to a channel or DM.

    If *gateway* is provided it is used directly; otherwise the project's
    first enabled Discord config is looked up from the database.

    Builds a rich embed via :func:`peon_mcp.discord.embeds.build_embed`,
    and sends via :func:`peon_mcp.discord.sender.send_to_channel` or
    :func:`peon_mcp.discord.sender.send_to_user`.

    Updates the event_log row with the final status.  Never raises.

    Args:
        db: Active aiosqlite database connection
        event_log_id: The event_log.id row to update with delivery results
        project_id: The project that owns the Discord bot config
        event_type: The event type string (e.g. 'task.completed')
        data: Event-specific data dict (passed to the embed builder)
        kind: 'channel' or 'user'
        target_id: Discord channel or user snowflake ID
        gateway: Pre-fetched discord_configs row dict (optional; avoids DB lookup)
    """
    from peon_mcp.discord.embeds import build_embed
    from peon_mcp.discord.sender import send_to_channel, send_to_user

    status = "failed"
    error = ""

    try:
        if gateway is None:
            # Find the enabled Discord config for this project
            config_rows = await db.execute_fetchall(
                "SELECT * FROM discord_configs WHERE project_id = ? AND enabled = 1 LIMIT 1",
                (project_id,),
            )
            if not config_rows:
                error = f"No enabled Discord config found for project '{project_id}'"
                logger.warning("deliver_discord: %s", error)
                gateway = None
            else:
                gateway = dict(config_rows[0])

        if gateway is not None:
            embed = build_embed(event_type, project_id, data)

            if kind == "channel":
                ok = await send_to_channel(gateway, target_id, embed)
            else:
                ok = await send_to_user(gateway, target_id, embed)

            if ok:
                status = "success"
            else:
                error = f"Discord delivery returned failure for {kind} {target_id}"
    except Exception as exc:
        error = str(exc)[:500]
        logger.warning(
            "deliver_discord: unexpected error for event_log %d: %s", event_log_id, exc
        )

    try:
        await db.execute(
            """UPDATE webhook_event_logs
               SET status = ?, error = ?
               WHERE id = ?""",
            (status, error, event_log_id),
        )
        await db.commit()
    except Exception as exc:
        logger.error(
            "deliver_discord: failed to update event_log %d (status=%s): %s",
            event_log_id,
            status,
            exc,
        )
