from __future__ import annotations

from dataclasses import dataclass
from typing import Awaitable, Callable

from asgiref.sync import sync_to_async

from core.models import GatewayCommandEvent
from core.security.command_policy import CommandSecurityContext, evaluate_command_policy

GatewayEmit = Callable[[str], None]
GatewayHandler = Callable[["GatewayCommandContext", GatewayEmit], Awaitable[bool]]
GatewayMatcher = Callable[[str], bool]


@dataclass(slots=True)
class GatewayCommandContext:
    user: object
    source_message: object
    service: str
    channel_identifier: str
    sender_identifier: str
    message_text: str
    message_meta: dict
    payload: dict


@dataclass(slots=True)
class GatewayCommandRoute:
    name: str
    scope_key: str
    matcher: GatewayMatcher
    handler: GatewayHandler


def _first_token(text: str) -> str:
    body = str(text or "").strip()
    if not body:
        return ""
    return str(body.split()[0] or "").strip().lower()


def _derive_unknown_scope(text: str) -> str:
    token = _first_token(text).lstrip(".")
    if not token:
        token = "message"
    return f"gateway.{token}"


async def dispatch_gateway_command(
    *,
    context: GatewayCommandContext,
    routes: list[GatewayCommandRoute],
    emit: GatewayEmit,
) -> bool:
    text = str(context.message_text or "").strip()
    if not text:
        return False

    route = next((row for row in routes if row.matcher(text)), None)
    scope_key = route.scope_key if route is not None else _derive_unknown_scope(text)
    command_name = route.name if route is not None else _first_token(text).lstrip(".")

    event = await sync_to_async(GatewayCommandEvent.objects.create)(
        user=context.user,
        source_message=context.source_message,
        service=str(context.service or "").strip().lower() or "xmpp",
        channel_identifier=str(context.channel_identifier or "").strip(),
        sender_identifier=str(context.sender_identifier or "").strip(),
        scope_key=scope_key,
        command_name=command_name,
        command_text=text,
        status="pending",
        request_meta={
            "payload": dict(context.payload or {}),
            "message_meta": dict(context.message_meta or {}),
        },
    )

    if route is None:
        event.status = "ignored"
        event.error = "unmatched_gateway_command"
        await sync_to_async(event.save)(update_fields=["status", "error", "updated_at"])
        return False

    decision = await sync_to_async(evaluate_command_policy)(
        user=context.user,
        scope_key=scope_key,
        context=CommandSecurityContext(
            service=context.service,
            channel_identifier=context.channel_identifier,
            message_meta=dict(context.message_meta or {}),
            payload=dict(context.payload or {}),
        ),
    )
    if not decision.allowed:
        message = (
            f"blocked by policy: {decision.code}"
            if not decision.reason
            else f"blocked by policy: {decision.reason}"
        )
        emit(message)
        event.status = "blocked"
        event.error = f"{decision.code}:{decision.reason}"
        event.response_meta = {
            "policy_code": decision.code,
            "policy_reason": decision.reason,
        }
        await sync_to_async(event.save)(
            update_fields=["status", "error", "response_meta", "updated_at"]
        )
        return True

    responses: list[str] = []

    def _captured_emit(value: str) -> None:
        row = str(value or "")
        responses.append(row)
        emit(row)

    try:
        handled = await route.handler(context, _captured_emit)
    except Exception as exc:
        event.status = "failed"
        event.error = f"handler_exception:{exc}"
        event.response_meta = {"responses": responses}
        await sync_to_async(event.save)(
            update_fields=["status", "error", "response_meta", "updated_at"]
        )
        return True

    event.status = "ok" if handled else "ignored"
    event.response_meta = {"responses": responses}
    await sync_to_async(event.save)(
        update_fields=["status", "response_meta", "updated_at"]
    )
    return bool(handled)
