import hashlib
import hmac
import json
from typing import Any

import httpx
import logfire
import redis.asyncio as aioredis
from fastapi import APIRouter, Depends, Request
from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select

from app.config import get_settings
from app.db.session import commit_write, get_bypass_session
from app.dependencies import restate_auth_headers
from app.models.whatsapp import TemplateStatus, WhatsAppAccount, WhatsAppTemplate
from app.realtime.events import DOMAIN_EVENTS_CHANNEL, DomainEvent, EventType

router = APIRouter(prefix="/webhooks/whatsapp", tags=["whatsapp-webhooks"])


_redis_client: aioredis.Redis | None = None


def _verify_signature(raw_body: bytes, signature_header: str) -> bool:
    settings = get_settings()
    if not settings.META_APP_SECRET:
        return False

    expected = hmac.new(
        settings.META_APP_SECRET.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    provided = signature_header.removeprefix("sha256=").strip()
    return hmac.compare_digest(provided, expected)


async def _publish_event(event: DomainEvent) -> None:
    client = _get_redis_client()
    if client is None:
        return
    await client.publish(DOMAIN_EVENTS_CHANNEL, event.model_dump_json())


def _get_redis_client() -> aioredis.Redis | None:
    settings = get_settings()
    if not settings.REDIS_URL:
        return None
    global _redis_client
    if _redis_client is None:
        _redis_client = aioredis.from_url(settings.REDIS_URL, decode_responses=True)
    return _redis_client


def _extract_change_values(body: Any) -> list[dict[str, Any]]:
    if not isinstance(body, dict):
        return []

    values: list[dict[str, Any]] = []
    for entry in body.get("entry") or []:
        if not isinstance(entry, dict):
            continue
        for change in entry.get("changes") or []:
            if not isinstance(change, dict):
                continue
            value = change.get("value")
            if isinstance(value, dict):
                values.append(value)
    return values


def _extract_template_events(value: dict[str, Any]) -> list[dict[str, Any]]:
    raw_event = value.get("message_template_status_update")
    if isinstance(raw_event, dict):
        return [raw_event]
    if isinstance(raw_event, list):
        return [item for item in raw_event if isinstance(item, dict)]
    return []


def _extract_rejection_reason(event: dict[str, Any]) -> str | None:
    direct_reason = event.get("rejection_reason")
    if isinstance(direct_reason, str) and direct_reason.strip():
        return direct_reason

    rejection_info = event.get("rejected_reason")
    if isinstance(rejection_info, str) and rejection_info.strip():
        return rejection_info

    reasons = event.get("reasons")
    if isinstance(reasons, list):
        for reason in reasons:
            if isinstance(reason, str) and reason.strip():
                return reason
            if isinstance(reason, dict):
                for key in ("reason", "description", "message"):
                    value = reason.get(key)
                    if isinstance(value, str) and value.strip():
                        return value

    return None


def _normalize_template_status(status: Any) -> str | None:
    if not isinstance(status, str) or not status.strip():
        return None

    normalized = status.strip().upper()
    try:
        return TemplateStatus(normalized).value
    except ValueError:
        return normalized


async def _dispatch_inbound_message(
    request: Request,
    account: WhatsAppAccount,
    phone_number_id: str,
    message: dict[str, Any],
    sender: dict[str, Any],
    metadata: dict[str, Any],
) -> None:
    settings = get_settings()
    wamid = message.get("id")
    if not isinstance(wamid, str) or not wamid:
        logfire.warning(
            "whatsapp_inbound_missing_wamid",
            phone_number_id=phone_number_id,
            restaurant_id=account.restaurant_id,
        )
        return

    payload = {
        "restaurant_id": account.restaurant_id,
        "whatsapp_account_id": account.id,
        "phone_number_id": phone_number_id,
        "metadata": metadata,
        "message": message,
        "sender": sender,
    }
    # Fire-and-forget: Restate persists the invocation durably and returns
    # 202 immediately. Keeps Meta's webhook delivery under its ~20s budget
    # regardless of handler latency (LLM + RAG + outbound send), and pushes
    # all retry/idempotency semantics into Restate where they belong.
    url = f"{settings.RESTATE_INGRESS_URL.rstrip('/')}/WhatsAppInboundHandler/{wamid}/process/send"
    headers = restate_auth_headers(settings)

    app_client = getattr(request.app.state, "http_client", None)
    if isinstance(app_client, httpx.AsyncClient):
        response = await app_client.post(url, json=payload, headers=headers)
        response.raise_for_status()
        return

    async with httpx.AsyncClient(timeout=10.0) as client:
        response = await client.post(url, json=payload, headers=headers)
        response.raise_for_status()


async def _handle_template_status_event(
    session: AsyncSession,
    event: dict[str, Any],
) -> None:
    meta_template_id = event.get("message_template_id") or event.get("meta_template_id")
    if not isinstance(meta_template_id, str) or not meta_template_id:
        logfire.warning("whatsapp_template_status_missing_id", event=event)
        return

    # Bypass session (owner pool) — Meta delivers events without a user JWT.
    # meta_template_id is globally unique across our tenants, so we resolve
    # the template by id and pick up restaurant_id from the row.
    result = await session.execute(
        select(WhatsAppTemplate).where(WhatsAppTemplate.meta_template_id == meta_template_id)
    )
    template = result.scalar_one_or_none()
    if template is None:
        logfire.warning(
            "whatsapp_template_status_unknown_template",
            meta_template_id=meta_template_id,
        )
        return

    updated = False
    status = _normalize_template_status(event.get("event") or event.get("status"))
    if status and template.status != status:
        template.status = status
        updated = True

    rejection_reason = _extract_rejection_reason(event)
    if template.rejection_reason != rejection_reason:
        template.rejection_reason = rejection_reason
        updated = True

    if not updated:
        return

    session.add(template)
    await commit_write(session)

    domain_event = DomainEvent(
        type=EventType.template_status_changed,
        restaurant_id=template.restaurant_id,
        entity_id=template.id,
        payload={
            "template_id": template.id,
            "meta_template_id": meta_template_id,
            "status": template.status,
            "rejection_reason": template.rejection_reason,
        },
    )
    await _publish_event(domain_event)


@router.get("")
async def verify_webhook(
    request: Request,
) -> Response:
    hub_mode = request.query_params.get("hub.mode")
    hub_verify_token = request.query_params.get("hub.verify_token")
    hub_challenge = request.query_params.get("hub.challenge")

    if not hub_mode or not hub_verify_token or hub_challenge is None:
        return Response(status_code=400)

    if hub_verify_token != get_settings().META_WEBHOOK_VERIFY_TOKEN:
        return Response(status_code=403)

    if hub_mode == "subscribe":
        return Response(
            content=hub_challenge,
            media_type="text/plain",
            status_code=200,
        )

    return Response(status_code=400)


@router.post("")
async def receive_webhook(
    request: Request,
    session: AsyncSession = Depends(get_bypass_session),
) -> Response:
    raw_body = await request.body()
    signature_header = request.headers.get("X-Hub-Signature-256", "")
    if not raw_body or not signature_header:
        return Response(status_code=401)

    if not _verify_signature(raw_body, signature_header):
        return Response(status_code=401)

    try:
        body = json.loads(raw_body.decode("utf-8"))
    except json.JSONDecodeError as exc:
        logfire.error("whatsapp_webhook_invalid_json", error=str(exc))
        return Response(status_code=200)

    for value in _extract_change_values(body):
        raw_metadata = value.get("metadata")
        metadata: dict[str, Any] = raw_metadata if isinstance(raw_metadata, dict) else {}
        phone_number_id = metadata.get("phone_number_id")
        account: WhatsAppAccount | None = None
        if isinstance(phone_number_id, str) and phone_number_id:
            # Bypass session (owner pool) — Meta delivers events without a user
            # JWT. phone_number_id is unique across our tenants (Meta-enforced),
            # so we resolve the account directly and read restaurant_id off it.
            result = await session.execute(
                select(WhatsAppAccount).where(WhatsAppAccount.phone_number_id == phone_number_id)
            )
            account = result.scalar_one_or_none()
            if account is None:
                logfire.warning(
                    "whatsapp_webhook_unknown_phone_number_id",
                    phone_number_id=phone_number_id,
                )
        messages = value.get("messages")
        contacts = value.get("contacts")
        if isinstance(messages, list) and messages:
            message = messages[0]
            sender = contacts[0] if isinstance(contacts, list) and contacts else {}
            if isinstance(message, dict) and isinstance(sender, dict):
                if account is None or not isinstance(phone_number_id, str) or not phone_number_id:
                    continue
                try:
                    await _dispatch_inbound_message(
                        request,
                        account,
                        phone_number_id=phone_number_id,
                        message=message,
                        sender=sender,
                        metadata=metadata,
                    )
                except Exception as exc:  # noqa: BLE001
                    logfire.error(
                        "whatsapp_inbound_dispatch_failed",
                        phone_number_id=phone_number_id,
                        error=str(exc),
                    )

        statuses = value.get("statuses")
        if isinstance(statuses, list):
            for status in statuses:
                if not isinstance(status, dict):
                    continue
                logfire.info(
                    "whatsapp_message_status_ignored",
                    phone_number_id=phone_number_id,
                    message_id=status.get("id"),
                    status=status.get("status"),
                )

        for event in _extract_template_events(value):
            try:
                await _handle_template_status_event(session, event)
            except Exception as exc:  # noqa: BLE001
                logfire.error(
                    "whatsapp_template_status_processing_failed",
                    phone_number_id=phone_number_id,
                    error=str(exc),
                    event=event,
                )

    return Response(status_code=200)
