"""automation.workflow.trigger around live Make/n8n webhooks.

Phase 1C: Make and n8n are runtime adapters behind the same capability.
Public invoke is Dashboard/BFF → private Core → existing integration.execute.
"""
from __future__ import annotations

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

from src.domain.connectors.invoke_capabilities import (
    AUTOMATION_WORKFLOW_TRIGGER_FORBIDDEN_METADATA_KEYS,
    AUTOMATION_WORKFLOW_TRIGGER_PROVIDER_INPUT_KEYS,
    INVOCABLE_FALSE_MEANS,
    INVOKE_CAPABILITIES,
    MAKE_AUTOMATION_WORKFLOW_MAPPING,
    N8N_AUTOMATION_WORKFLOW_MAPPING,
    capability_surface,
)

CAPABILITY_ID = "automation.workflow.trigger"
_LOGICAL_WORKFLOW = re.compile(r"^[a-z][a-z0-9-]{0,62}$")
_PUBLIC_RESULT_KEYS = frozenset({
    "capability", "status", "execution_ref", "reason_code", "message",
})
_PROVIDER_SHAPED_KEYS = frozenset({
    "scenario_id",
    "workflow_id",
    "webhook_url",
    "hook_url",
    "zap_id",
    "payload",
    "action",
    "connection_id",
    "request_type",
    "run_id",
})
ACTION_BY_CONNECTOR = {
    "make_com": MAKE_AUTOMATION_WORKFLOW_MAPPING["provider_action"],
    "n8n": N8N_AUTOMATION_WORKFLOW_MAPPING["provider_action"],
}
RUNTIME_ADAPTERS = frozenset(ACTION_BY_CONNECTOR)
MAPPED_ADAPTERS = RUNTIME_ADAPTERS


@dataclass(frozen=True)
class AutomationWorkflowConnectionSnapshot:
    connection_id: UUID
    connector_key: str
    status: str
    has_secrets: bool
    permission_mode: str
    config: dict[str, Any]
    requires_secrets: bool = False
    client_side_only: bool = False


@dataclass
class AutomationWorkflowPlan:
    capability: str
    connection_id: UUID
    connector_key: str
    action: str
    workflow: str
    resolved_input: dict[str, Any]


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

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


@dataclass
class AutomationWorkflowResolution:
    ok: bool
    reason_code: str | None = None
    message: str | None = None
    plan: AutomationWorkflowPlan | None = None
    diagnostics: dict[str, Any] = field(default_factory=dict)

    def as_dict(self) -> dict[str, Any]:
        if not self.ok:
            return {
                "capability": CAPABILITY_ID,
                "status": "rejected",
                "reason_code": self.reason_code,
                "message": self.message,
            }
        return {"capability": CAPABILITY_ID, "status": "queued"}


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


def normalize_logical_workflow(value: str) -> str | None:
    raw = value.strip()
    if not _LOGICAL_WORKFLOW.match(raw):
        return None
    return raw


def map_automation_input_to_provider_payload(input_obj: dict[str, Any]) -> dict[str, Any]:
    """Capability input → existing Make/n8n action `{payload: object}`."""
    return {"payload": dict(input_obj)}


def connection_workflow_alias(config: dict[str, Any]) -> str | None:
    raw = config.get("workflow")
    if isinstance(raw, str) and raw.strip():
        return normalize_logical_workflow(raw)
    return None


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


def _forbidden_provider_fields(payload: dict[str, Any]) -> list[str]:
    return sorted(key for key in payload if key in AUTOMATION_WORKFLOW_TRIGGER_PROVIDER_INPUT_KEYS)


def _eligible(row: AutomationWorkflowConnectionSnapshot, adapters: frozenset[str]) -> bool:
    return row.connector_key in adapters and not row.client_side_only


def _unique_connection_gate(
    row: AutomationWorkflowConnectionSnapshot,
    enabled_capabilities: set[str] | None,
    approval_present: bool,
) -> AutomationWorkflowResolution | None:
    if row.status != "active":
        return _fail("connection_inactive", "Bound automation connection is not active")
    if row.requires_secrets and not row.has_secrets:
        return _fail("credentials_missing", "Bound automation connection is missing credentials")
    if enabled_capabilities is not None and "integrations.execute" not in enabled_capabilities:
        return _fail("capability_not_bound", "integrations.execute is not enabled")
    if row.permission_mode == "deny":
        return _fail("capability_not_bound", "Connector permission denies this action")
    if row.permission_mode == "ask" and not approval_present:
        return _fail("capability_not_bound", "Connector permission requires approval")
    return None


def _plan_for(
    row: AutomationWorkflowConnectionSnapshot,
    workflow: str,
    input_obj: dict[str, Any],
    diagnostics: dict[str, Any],
) -> AutomationWorkflowResolution:
    action = ACTION_BY_CONNECTOR[row.connector_key]
    return AutomationWorkflowResolution(
        ok=True,
        plan=AutomationWorkflowPlan(
            capability=CAPABILITY_ID,
            connection_id=row.connection_id,
            connector_key=row.connector_key,
            action=action,
            workflow=workflow,
            resolved_input=map_automation_input_to_provider_payload(input_obj),
        ),
        diagnostics=diagnostics,
    )


def to_public_automation_workflow_result(
    resolution: AutomationWorkflowResolution,
    enqueue: dict[str, Any] | None = None,
) -> dict[str, Any]:
    if not resolution.ok:
        body = resolution.as_dict()
        return {k: v for k, v in body.items() if k in _PUBLIC_RESULT_KEYS}
    if enqueue is None:
        return {
            "capability": CAPABILITY_ID,
            "status": "rejected",
            "reason_code": "provider_unavailable",
            "message": "automation.workflow.trigger requires the integration.execute enqueue path",
        }
    if not enqueue.get("ok"):
        return {
            "capability": CAPABILITY_ID,
            "status": "rejected",
            "reason_code": enqueue.get("reason_code") or "provider_unavailable",
            "message": enqueue.get("message"),
        }
    execution_ref = str(enqueue.get("job_id") or "") or None
    body: dict[str, Any] = {"capability": CAPABILITY_ID, "status": "queued"}
    if execution_ref:
        body["execution_ref"] = execution_ref
    return body


def resolve_automation_workflow_trigger(
    *,
    payload: dict[str, Any],
    connections: list[AutomationWorkflowConnectionSnapshot],
    enabled_capabilities: set[str] | None = None,
    provider_hint: str | None = None,
    binding_connection_id: UUID | None = None,
    workflow_map: dict[str, UUID] | None = None,
    approval_present: bool = False,
    enforce_invocable: bool = True,
    runtime_adapters: frozenset[str] | None = None,
) -> AutomationWorkflowResolution:
    """Deterministic plan. Does not call Make/n8n."""
    diagnostics: dict[str, Any] = {"rejected": []}
    adapters = runtime_adapters if runtime_adapters is not None else RUNTIME_ADAPTERS

    if CAPABILITY_ID not in INVOKE_CAPABILITIES:
        return _fail("capability_not_bound", "Unknown capability automation.workflow.trigger")

    if enforce_invocable and not _catalog_invocable():
        return _fail("capability_not_invocable", INVOCABLE_FALSE_MEANS)

    leaked = _forbidden_provider_fields(payload)
    if leaked:
        return _fail(
            "provider_fields_forbidden",
            "automation.workflow.trigger does not accept provider workflow IDs; "
            "use make_com.trigger_scenario or n8n.trigger_workflow",
            {"forbidden_fields": leaked},
        )
    metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
    stripped = sorted(set(metadata) & set(AUTOMATION_WORKFLOW_TRIGGER_FORBIDDEN_METADATA_KEYS))
    if stripped:
        diagnostics["stripped_metadata_keys"] = stripped

    workflow_raw = payload.get("workflow")
    if not isinstance(workflow_raw, str) or not workflow_raw.strip():
        return _fail("workflow_invalid", "automation.workflow.trigger requires workflow")
    workflow = normalize_logical_workflow(workflow_raw)
    if workflow is None:
        return _fail(
            "workflow_invalid",
            "workflow must be a logical name (e.g. lead-intake), not a provider ID or URL",
        )

    input_obj = payload.get("input")
    if not isinstance(input_obj, dict) or isinstance(input_obj, list):
        return _fail("input_required", "automation.workflow.trigger requires an input object")

    candidates = [row for row in connections if _eligible(row, adapters)]
    if provider_hint:
        candidates = [row for row in candidates if row.connector_key == provider_hint]

    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 or not _eligible(intended, adapters):
            return _fail(
                "capability_not_bound",
                "Explicit automation.workflow.trigger 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, workflow, input_obj, diagnostics)

    if workflow_map and workflow in workflow_map:
        mapped_id = workflow_map[workflow]
        mapped = next((row for row in candidates if row.connection_id == mapped_id), None)
        if mapped is None:
            return _fail(
                "workflow_unbound",
                "workflow_map does not match an eligible automation connection",
                diagnostics,
            )
        unique_fail = _unique_connection_gate(mapped, enabled_capabilities, approval_present)
        if unique_fail:
            return unique_fail
        return _plan_for(mapped, workflow, input_obj, diagnostics)

    aliased = [
        row for row in candidates
        if connection_workflow_alias(row.config) == workflow
    ]
    if len(aliased) == 1:
        unique_fail = _unique_connection_gate(aliased[0], enabled_capabilities, approval_present)
        if unique_fail:
            return unique_fail
        return _plan_for(aliased[0], workflow, input_obj, diagnostics)
    if len(aliased) > 1:
        return _fail(
            "binding_ambiguous",
            "Multiple eligible automation connections share this workflow alias",
            diagnostics,
        )

    if not candidates:
        return _fail(
            "capability_not_bound",
            "No eligible automation.workflow.trigger connection",
            diagnostics,
        )
    if len(candidates) > 1:
        return _fail(
            "binding_ambiguous",
            "Multiple eligible automation connections; set a workflow alias or workflow_map",
            diagnostics,
        )

    return _fail(
        "workflow_unbound",
        "Logical workflow is not bound to the eligible connection; "
        "set connection.config.workflow or workflow_map",
        diagnostics,
    )


def dispatch_automation_workflow_plan(
    plan: AutomationWorkflowPlan,
    authorization: DispatchAuthorization,
    *,
    diagnostics: dict[str, Any] | None = None,
) -> AutomationWorkflowResolution:
    """Re-check execution authorization. Does not enqueue Cloud work."""
    notes = dict(diagnostics or {})
    notes["payload"] = dict(plan.resolved_input)
    if not authorization.allowed:
        notes["authorization"] = "rejected"
        return _fail(
            authorization.reason_code or "connector_action_denied",
            authorization.message or "integration.execute rejected the automation plan",
            notes,
        )
    notes["authorization"] = "allowed"
    return AutomationWorkflowResolution(ok=True, plan=plan, diagnostics=notes)


def invoke_automation_workflow_trigger(
    *,
    payload: dict[str, Any],
    connections: list[AutomationWorkflowConnectionSnapshot],
    authorize: DispatchAuthorization | None = None,
    enabled_capabilities: set[str] | None = None,
    provider_hint: str | None = None,
    binding_connection_id: UUID | None = None,
    workflow_map: dict[str, UUID] | None = None,
    approval_present: bool = False,
    enforce_invocable: bool = True,
    runtime_adapters: frozenset[str] | None = None,
) -> AutomationWorkflowResolution:
    resolution = resolve_automation_workflow_trigger(
        payload=payload,
        connections=connections,
        enabled_capabilities=enabled_capabilities,
        provider_hint=provider_hint,
        binding_connection_id=binding_connection_id,
        workflow_map=workflow_map,
        approval_present=approval_present,
        enforce_invocable=enforce_invocable,
        runtime_adapters=runtime_adapters,
    )
    if not resolution.ok or resolution.plan is None or authorize is None:
        return resolution
    return dispatch_automation_workflow_plan(
        resolution.plan, authorize, diagnostics=resolution.diagnostics
    )


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