"""
workflow_executor.py — Executes user guardrails and workflows when a chain is scored.

Called by the /api/{domain}/score endpoint after scoring completes.

Execution model:
  1. Guardrails: synchronous check — domain + threshold match → fire action
     - alert:    POST to webhook / Slack (non-blocking, best-effort)
     - block:    raise HTTPException(403) BEFORE returning score
     - fallback: add `fallback_required: true` to score response

  2. Workflows: async, non-blocking, BFS through the node graph
     - trigger:    always fires (entry point)
     - threshold:  gates downstream nodes — only pass if risk >= threshold
     - slack:      POST to Slack webhook
     - webhook:    POST to any HTTP URL
     - human:      marks chain for human review (stored in DB / logged)
     - fallback:   signals fallback to caller

All HTTP calls are fire-and-forget (asyncio.create_task) so they never delay
the response to the caller.
"""

from __future__ import annotations

import asyncio
import fnmatch
import json
import logging
import time
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .store import ChainStore

logger = logging.getLogger("workflow_executor")


# ── Guardrail execution ────────────────────────────────────────────────────────

class GuardrailResult:
    def __init__(self):
        self.block: bool = False
        self.fallback_required: bool = False
        self.alerts_fired: int = 0
        self.matched: bool = False  # True if any guardrail condition was triggered


async def _post_json(url: str, payload: dict, label: str) -> None:
    """Fire-and-forget HTTP POST. Never raises."""
    try:
        import httpx
        async with httpx.AsyncClient(timeout=5.0) as client:
            resp = await client.post(url, json=payload)
            logger.info(f"{label} → {resp.status_code}")
    except Exception as e:
        logger.warning(f"{label} failed: {e}")


async def execute_guardrails(
    user_id: int,
    domain: str,
    risk_score: float,
    question: str,
    chain_id: int,
    store: "ChainStore",
) -> GuardrailResult:
    """
    Check all enabled guardrails for this user/domain.
    Returns a GuardrailResult describing what actions fired.
    Does NOT raise — blocking is signalled via result.block.
    """
    result = GuardrailResult()
    guardrails = store.get_guardrails(user_id)

    for g in guardrails:
        if not g.get("enabled"):
            continue
        # Domain match: empty domain = apply to all; glob/wildcard patterns supported
        # e.g. "prod-*" matches "prod-chat", "prod-api"
        if g["domain"] and not fnmatch.fnmatch(domain, g["domain"]):
            continue
        if risk_score < g["threshold"]:
            continue

        action = g.get("action", "alert")
        result.matched = True
        logger.info(f"Guardrail fired: user={user_id} domain={domain} action={action} risk={risk_score:.3f}")

        payload = {
            "event": "guardrail_triggered",
            "domain": domain,
            "risk_score": round(risk_score, 4),
            "action": action,
            "chain_id": chain_id,
            "question": question[:200],
            "timestamp": time.time(),
        }

        if action == "block":
            result.block = True

        elif action == "fallback":
            result.fallback_required = True

        # Log to automation_log for audit trail
        try:
            store.log_automation(
                user_id=user_id,
                trigger_type="guardrail",
                domain=domain,
                risk_score=risk_score,
                chain_id=chain_id,
                guardrail_id=g.get("id"),
                action=action,
                result="fired",
            )
        except Exception as _e:
            logger.debug(f"automation_log insert failed: {_e}")

        # Always fire notifications regardless of action
        tasks = []
        if g.get("webhook_url"):
            tasks.append(_post_json(g["webhook_url"], payload, "guardrail webhook"))
        if g.get("slack_webhook"):
            slack_payload = {
                "text": (
                    f":rotating_light: *LLM Guard alert* — risk `{risk_score:.0%}` on domain `{domain}`\n"
                    f"Action: *{action}* | Chain ID: {chain_id}"
                ),
                "attachments": [{
                    "color": "#e53e3e",
                    "fields": [
                        {"title": "Question", "value": question[:120], "short": False},
                        {"title": "Risk score", "value": f"{risk_score:.0%}", "short": True},
                        {"title": "Action", "value": action, "short": True},
                    ],
                }],
            }
            tasks.append(_post_json(g["slack_webhook"], slack_payload, "guardrail slack"))

        for t in tasks:
            asyncio.create_task(t)
            result.alerts_fired += 1

    return result


# ── Workflow execution ─────────────────────────────────────────────────────────

async def _execute_workflow(
    workflow: dict,
    domain: str,
    risk_score: float,
    question: str,
    chain_id: int,
    store: "ChainStore | None" = None,
    user_id: int = 0,
) -> None:
    """Execute a single workflow graph. Fire-and-forget; never raises."""
    try:
        nodes: list[dict] = workflow.get("nodes") or []
        edges: list[dict] = workflow.get("edges") or []

        if not nodes:
            return

        # Build adjacency: source_id → list[target_id]
        adj: dict[str, list[str]] = {}
        incoming: set[str] = set()
        for edge in edges:
            src = edge.get("source", "")
            tgt = edge.get("target", "")
            if src and tgt:
                adj.setdefault(src, []).append(tgt)
                incoming.add(tgt)

        node_by_id: dict[str, dict] = {n["id"]: n for n in nodes}

        # Start from root nodes (no incoming edges)
        roots = [n for n in nodes if n["id"] not in incoming]
        if not roots:
            roots = nodes[:1]  # fallback: start from first node

        # BFS execution with a pass/block context
        queue: list[tuple[dict, bool]] = [(n, True) for n in roots]
        visited: set[str] = set()
        context_pass = True  # starts True; threshold nodes can set False

        while queue:
            node, passing = queue.pop(0)
            nid = node["id"]
            if nid in visited:
                continue
            visited.add(nid)

            data = node.get("data", {})
            # node type is stored in data.type or the node's own type field
            ntype = data.get("nodeType") or data.get("type") or node.get("type", "")
            config = data.get("config") or {}

            logger.debug(f"Workflow {workflow.get('id')} node {nid} type={ntype} passing={passing}")

            if ntype == "trigger":
                # Always passes; no action needed
                pass

            elif ntype == "threshold":
                t = float(config.get("threshold", 0.65))
                context_pass = risk_score >= t
                if not context_pass:
                    logger.info(f"Workflow {workflow.get('id')}: threshold {t} not met (risk={risk_score:.3f}), blocking downstream")

            elif ntype in ("slack", "webhook", "human", "fallback"):
                if not passing:
                    continue  # blocked by upstream threshold

                if ntype == "slack":
                    webhook_url = config.get("webhook_url", "")
                    if webhook_url:
                        slack_payload = {
                            "text": (
                                f":robot_face: *Workflow alert* — `{workflow.get('name', 'workflow')}` triggered\n"
                                f"Domain: `{domain}` | Risk: `{risk_score:.0%}`"
                            ),
                            "attachments": [{
                                "color": "#4f46e5",
                                "fields": [
                                    {"title": "Question", "value": question[:120], "short": False},
                                    {"title": "Risk", "value": f"{risk_score:.0%}", "short": True},
                                    {"title": "Chain ID", "value": str(chain_id), "short": True},
                                ],
                            }],
                        }
                        await _post_json(webhook_url, slack_payload, f"workflow {workflow.get('id')} slack")
                        if store and user_id:
                            try:
                                store.log_automation(
                                    user_id=user_id, trigger_type="workflow",
                                    domain=domain, risk_score=risk_score,
                                    chain_id=chain_id, workflow_id=workflow.get("id"),
                                    action="slack", result="sent",
                                )
                            except Exception:
                                pass

                elif ntype == "webhook":
                    url = config.get("url", "")
                    if url:
                        await _post_json(url, {
                            "workflow_id": workflow.get("id"),
                            "workflow_name": workflow.get("name"),
                            "domain": domain,
                            "risk_score": risk_score,
                            "chain_id": chain_id,
                            "question": question[:200],
                        }, f"workflow {workflow.get('id')} webhook")
                        if store and user_id:
                            try:
                                store.log_automation(
                                    user_id=user_id, trigger_type="workflow",
                                    domain=domain, risk_score=risk_score,
                                    chain_id=chain_id, workflow_id=workflow.get("id"),
                                    action="webhook", result=url[:100],
                                )
                            except Exception:
                                pass

                elif ntype == "human":
                    logger.info(f"Workflow {workflow.get('id')}: chain {chain_id} flagged for human review")
                    if store and user_id:
                        try:
                            store.log_automation(
                                user_id=user_id, trigger_type="workflow",
                                domain=domain, risk_score=risk_score,
                                chain_id=chain_id, workflow_id=workflow.get("id"),
                                action="human_review", result="queued",
                            )
                        except Exception:
                            pass

                elif ntype == "fallback":
                    logger.info(f"Workflow {workflow.get('id')}: fallback triggered for chain {chain_id}")
                    if store and user_id:
                        try:
                            store.log_automation(
                                user_id=user_id, trigger_type="workflow",
                                domain=domain, risk_score=risk_score,
                                chain_id=chain_id, workflow_id=workflow.get("id"),
                                action="fallback", result="triggered",
                            )
                        except Exception:
                            pass

            # Enqueue children
            for child_id in adj.get(nid, []):
                child = node_by_id.get(child_id)
                if child:
                    queue.append((child, context_pass))

    except Exception as e:
        logger.error(f"Workflow {workflow.get('id', '?')} execution error: {e}", exc_info=True)


async def execute_workflows(
    user_id: int,
    domain: str,
    risk_score: float,
    question: str,
    chain_id: int,
    store: "ChainStore",
) -> None:
    """
    Fire all enabled workflows for this user.
    Non-blocking: each workflow runs in its own task.
    """
    workflows = store.get_workflows(user_id)
    for w in workflows:
        if not w.get("enabled"):
            continue
        asyncio.create_task(
            _execute_workflow(w, domain, risk_score, question, chain_id, store=store, user_id=user_id)
        )


# ── Unified entry point called by server.py ────────────────────────────────────

async def run_automations(
    user_id: int,
    domain: str,
    risk_score: float,
    question: str,
    chain_id: int,
    store: "ChainStore",
) -> dict:
    """
    Run guardrails + workflows for a completed score.

    Returns a dict of extra fields to merge into the score response:
      {
        "guardrail_action": "block" | "fallback" | "alert" | None,
        "fallback_required": bool,
      }
    """
    result = await execute_guardrails(user_id, domain, risk_score, question, chain_id, store)
    await execute_workflows(user_id, domain, risk_score, question, chain_id, store)

    return {
        "guardrail_action":  "block"    if result.block else
                             "fallback" if result.fallback_required else
                             "alert"    if result.matched else None,
        "fallback_required": result.fallback_required,
    }
