"""messaging.send resolver — plan internally, public result capability-shaped.

``invocable: true`` means the Core invoke path exists. Slack implements the
capability. Codegen default is ``messaging.send``; ``slack.post_message`` is an
escape hatch for Slack-specific semantics.
"""
from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Any, Optional
from uuid import UUID

from src.domain.connectors.invoke_capabilities import (
    INVOCABLE_FALSE_MEANS,
    INVOKE_CAPABILITIES,
    MESSAGING_SEND_FORBIDDEN_METADATA_KEYS,
    SLACK_MESSAGING_SEND_MAPPING,
    capability_surface,
)

CAPABILITY_ID = "messaging.send"
_SLACK_PROVIDER_ID = re.compile(r"^[CU][A-Z0-9]{8,}$")
_PUBLIC_RESULT_KEYS = frozenset({"capability", "status", "message_id", "thread_ref", "reason_code", "message"})
_SLACK_SHAPED_KEYS = frozenset({"channel", "thread_ts", "blocks", "action", "connection_id", "request_type", "payload"})


def _catalog_invocable() -> bool:
    return bool(capability_surface(CAPABILITY_ID, []).get("invocable"))


@dataclass(frozen=True)
class MessagingConnectionSnapshot:
    connection_id: UUID
    connector_key: str
    status: str
    has_secrets: bool
    permission_mode: str
    config: dict[str, Any] = field(default_factory=dict)
    requires_secrets: bool = True
    client_side_only: bool = False
    action_slug: str = "slack.post_message"


@dataclass(frozen=True)
class MessagingSendPlan:
    capability: str
    connection_id: UUID
    action: str
    provider: str
    resolved_input: dict[str, Any]

    def integration_execute_payload(self) -> dict[str, Any]:
        return {
            "request_type": "integration.execute",
            "connection_id": str(self.connection_id),
            "action": self.action,
            "input": dict(self.resolved_input),
        }


@dataclass(frozen=True)
class MessagingSendResolution:
    ok: bool
    reason_code: str | None = None
    message: str | None = None
    plan: MessagingSendPlan | None = None
    diagnostics: dict[str, Any] = field(default_factory=dict)


def _fail(code: str, message: str, diagnostics: Optional[dict[str, Any]] = None) -> MessagingSendResolution:
    return MessagingSendResolution(
        ok=False, reason_code=code, message=message, diagnostics=diagnostics or {}
    )


@dataclass(frozen=True)
class MessagingSendPublicResult:
    """Caller-visible messaging.send result. Never includes the execution plan."""

    capability: str = CAPABILITY_ID
    status: str = "rejected"
    message_id: str | None = None
    thread_ref: str | None = None
    reason_code: str | None = None
    message: str | None = None

    def as_dict(self) -> dict[str, Any]:
        body: dict[str, Any] = {
            "capability": self.capability,
            "status": self.status,
        }
        if self.message_id is not None:
            body["message_id"] = self.message_id
        if self.thread_ref is not None:
            body["thread_ref"] = self.thread_ref
        if self.reason_code is not None:
            body["reason_code"] = self.reason_code
        if self.message is not None:
            body["message"] = self.message
        return body


def to_public_messaging_send_result(
    resolution: "MessagingSendResolution",
    *,
    thread_ref: Optional[str] = None,
    enqueue: Optional[dict[str, Any]] = None,
) -> MessagingSendPublicResult:
    """Map resolve/dispatch/enqueue onto the capability output schema."""
    echoed = thread_ref.strip() if isinstance(thread_ref, str) and thread_ref.strip() else None
    if not resolution.ok:
        return MessagingSendPublicResult(
            status="rejected",
            thread_ref=echoed,
            reason_code=resolution.reason_code,
            message=resolution.message,
        )
    if enqueue is None:
        return MessagingSendPublicResult(
            status="rejected",
            thread_ref=echoed,
            reason_code="provider_unavailable",
            message="messaging.send requires the integration.execute enqueue path",
        )
    if not enqueue.get("ok"):
        return MessagingSendPublicResult(
            status="rejected",
            thread_ref=echoed,
            reason_code=str(enqueue.get("reason_code") or "provider_unavailable"),
            message=str(enqueue.get("message") or "integration.execute enqueue failed"),
        )
    job_id = str(enqueue.get("job_id") or "").strip() or None
    return MessagingSendPublicResult(
        status="queued",
        message_id=job_id,
        thread_ref=echoed,
    )


def public_result_is_capability_shaped(body: dict[str, Any]) -> bool:
    keys = set(body)
    return keys <= _PUBLIC_RESULT_KEYS and not (keys & _SLACK_SHAPED_KEYS)


def _provider_shaped_destination(value: str) -> bool:
    return bool(_SLACK_PROVIDER_ID.match(value.strip()))


def _planned_action_for_connector(connector_key: str) -> str | None:
    mapping = SLACK_MESSAGING_SEND_MAPPING
    if connector_key == "slack" and mapping.get("provider_action"):
        return str(mapping["provider_action"])
    return None


def resolve_messaging_send(
    *,
    payload: dict[str, Any],
    connections: list[MessagingConnectionSnapshot],
    enabled_capabilities: Optional[set[str]] = None,
    provider_hint: Optional[str] = None,
    binding_connection_id: Optional[UUID] = None,
    destination_map: Optional[dict[str, str]] = None,
    approval_present: bool = False,
    enforce_invocable: bool = True,
) -> MessagingSendResolution:
    """Deterministic plan for messaging.send. Does not call Slack."""
    diagnostics: dict[str, Any] = {"rejected": []}

    # 1. Capability exists
    if CAPABILITY_ID not in INVOKE_CAPABILITIES:
        return _fail("capability_not_bound", "Unknown capability messaging.send")

    # 2. Capability is invocable (catalog).
    if enforce_invocable and not _catalog_invocable():
        return _fail("capability_not_invocable", INVOCABLE_FALSE_MEANS)

    # 3. Validate capability input
    text = payload.get("text")
    if not isinstance(text, str) or not text.strip():
        return _fail("text_required", "messaging.send requires text")
    destination = payload.get("destination")
    if not isinstance(destination, str) or not destination.strip():
        return _fail("destination_invalid", "messaging.send requires a logical destination")
    destination = destination.strip()
    if _provider_shaped_destination(destination):
        return _fail(
            "destination_invalid",
            "destination must be a logical name; raw Slack ids use slack.post_message",
        )
    metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
    leaked = sorted(set(metadata) & set(MESSAGING_SEND_FORBIDDEN_METADATA_KEYS))
    if leaked:
        diagnostics["stripped_metadata_keys"] = leaked

    # 4–5. Discover planned actions and project connections
    planned = [
        row for row in connections
        if _planned_action_for_connector(row.connector_key) and not row.client_side_only
    ]

    # Explicit binding is uniquely intended even if currently ineligible.
    if binding_connection_id is not None:
        intended = next((row for row in connections if row.connection_id == binding_connection_id), None)
        if intended is None:
            return _fail(
                "capability_not_bound",
                "Explicit messaging.send binding does not match a project connection",
                diagnostics,
            )
        unique_fail = _unique_connection_gate(intended, enabled_capabilities, approval_present)
        if unique_fail:
            return unique_fail
        return _plan_for(intended, payload, destination, destination_map, text, diagnostics)

    # 6. Filter eligibility (rejected reasons stay in diagnostics only)
    eligible: list[MessagingConnectionSnapshot] = []
    for row in planned:
        reason = _ineligibility_reason(row, enabled_capabilities, approval_present)
        if reason:
            diagnostics["rejected"].append(
                {"connection_id": str(row.connection_id), "connector_key": row.connector_key, "reason": reason}
            )
            continue
        eligible.append(row)

    # 7. Resolve connection ambiguity (provider_hint disambiguates; does not bind)
    hint = (provider_hint or "").strip().lower()
    if hint:
        eligible = [row for row in eligible if row.connector_key == hint]

    if not eligible:
        return _fail(
            "capability_not_bound",
            "No eligible messaging.send connection. Not a fallback to email.send.",
            diagnostics,
        )
    if len(eligible) > 1:
        return _fail(
            "binding_ambiguous",
            "Multiple eligible messaging.send connections; set a project binding or a unique provider_hint",
            {
                **diagnostics,
                "eligible": [
                    {"connection_id": str(row.connection_id), "connector_key": row.connector_key}
                    for row in eligible
                ],
            },
        )

    chosen = eligible[0]
    return _plan_for(chosen, payload, destination, destination_map, text, diagnostics)


def _unique_connection_gate(
    row: MessagingConnectionSnapshot,
    enabled_capabilities: Optional[set[str]],
    approval_present: bool,
) -> MessagingSendResolution | None:
    if row.status != "active":
        return _fail("connection_inactive", "Bound messaging.send connection is not active")
    if row.requires_secrets and not row.has_secrets:
        return _fail("credentials_missing", "Bound messaging.send connection is missing credentials")
    reason = _ineligibility_reason(row, enabled_capabilities, approval_present)
    if reason in {"connection_inactive"}:
        return _fail("connection_inactive", "Bound messaging.send connection is not active")
    if reason == "credentials_missing":
        return _fail("credentials_missing", "Bound messaging.send connection is missing credentials")
    if reason:
        return _fail(
            "capability_not_bound",
            "Bound messaging.send connection is not eligible",
            {"rejected": [{"connection_id": str(row.connection_id), "reason": reason}]},
        )
    return None


def _ineligibility_reason(
    row: MessagingConnectionSnapshot,
    enabled_capabilities: Optional[set[str]],
    approval_present: bool,
) -> str | None:
    if not _planned_action_for_connector(row.connector_key):
        return "adapter_not_planned"
    if row.client_side_only:
        return "client_side_only"
    if row.status != "active":
        return "connection_inactive"
    if row.requires_secrets and not row.has_secrets:
        return "credentials_missing"
    required = {"integrations.execute"}
    if required - (enabled_capabilities or set()):
        return "entitlement_missing"
    mode = (row.permission_mode or "deny").lower()
    if mode == "deny":
        return "permission_denied"
    if mode == "ask" and not approval_present:
        return "approval_required"
    return None


def _plan_for(
    row: MessagingConnectionSnapshot,
    payload: dict[str, Any],
    destination: str,
    destination_map: Optional[dict[str, str]],
    text: str,
    diagnostics: dict[str, Any],
) -> MessagingSendResolution:
    # 8. Resolve logical destination after a unique connection exists
    channel = _lookup_destination(destination, row, destination_map)
    if channel is None:
        return _fail(
            "destination_unbound",
            f"Logical destination {destination!r} is not mapped on the selected connection",
            diagnostics,
        )

    action = _planned_action_for_connector(row.connector_key) or row.action_slug
    resolved: dict[str, Any] = {"channel": channel, "text": text}
    thread_ref = payload.get("thread_ref")
    if isinstance(thread_ref, str) and thread_ref.strip():
        resolved["thread_ts"] = thread_ref.strip()

    # 9. Execution plan — caller may dispatch via integration.execute
    plan = MessagingSendPlan(
        capability=CAPABILITY_ID,
        connection_id=row.connection_id,
        action=action,
        provider=row.connector_key,
        resolved_input=resolved,
    )
    return MessagingSendResolution(ok=True, plan=plan, diagnostics=diagnostics)


def _lookup_destination(
    destination: str,
    row: MessagingConnectionSnapshot,
    overlay: Optional[dict[str, str]],
) -> str | None:
    for mapping in (overlay, row.config.get("destinations"), row.config.get("destination_map")):
        if isinstance(mapping, dict) and destination in mapping:
            value = mapping[destination]
            if isinstance(value, str) and value.strip():
                return value.strip()
    if destination == "default":
        default_channel = row.config.get("default_channel_id")
        if isinstance(default_channel, str) and default_channel.strip():
            return default_channel.strip()
    return None


@dataclass(frozen=True)
class DispatchAuthorization:
    """integration.execute authorization — independent of resolver eligibility."""

    allowed: bool
    reason_code: str | None = None
    message: str | None = None


def dispatch_messaging_send_plan(
    plan: MessagingSendPlan,
    authorization: DispatchAuthorization,
    *,
    diagnostics: Optional[dict[str, Any]] = None,
) -> MessagingSendResolution:
    """Re-check execution authorization. Does not enqueue Cloud work."""
    notes = dict(diagnostics or {})
    notes["payload"] = plan.integration_execute_payload()
    if not authorization.allowed:
        notes["authorization"] = "rejected"
        return _fail(
            authorization.reason_code or "connector_action_denied",
            authorization.message or "integration.execute rejected the messaging.send plan",
            notes,
        )
    notes["authorization"] = "allowed"
    return MessagingSendResolution(ok=True, plan=plan, diagnostics=notes)


def invoke_messaging_send(
    *,
    payload: dict[str, Any],
    connections: list[MessagingConnectionSnapshot],
    authorize: DispatchAuthorization | None = None,
    enabled_capabilities: Optional[set[str]] = None,
    provider_hint: Optional[str] = None,
    binding_connection_id: Optional[UUID] = None,
    destination_map: Optional[dict[str, str]] = None,
    approval_present: bool = False,
    enforce_invocable: bool = True,
) -> MessagingSendResolution:
    """Resolve then optionally re-authorize. Catalog invocable remains the public gate."""
    resolution = resolve_messaging_send(
        payload=payload,
        connections=connections,
        enabled_capabilities=enabled_capabilities,
        provider_hint=provider_hint,
        binding_connection_id=binding_connection_id,
        destination_map=destination_map,
        approval_present=approval_present,
        enforce_invocable=enforce_invocable,
    )
    if not resolution.ok or resolution.plan is None or authorize is None:
        return resolution
    return dispatch_messaging_send_plan(
        resolution.plan, authorize, diagnostics=resolution.diagnostics
    )
