#!/usr/bin/env python3
"""
deploy_support_workflows.py — Add Sam's support-school (apoyo escolar) tools to the
LIVE kq-chatwoot-agent WITHOUT recreating it, plus the non-tool confirmation workflow.

Idempotent. Run from this directory with the n8n API key in the environment:

  cd /home/edd/Proyectos/Edd-OS/projects/kings-and-queens/whatsapp-agent/workflows
  set -a; . /home/edd/Proyectos/Edd-OS/server/.env; set +a
  N8N_BASE_URL=http://127.0.0.1:5678 python3 deploy_support_workflows.py

Secrets (KQ_TOOL_TOKEN, CHATWOOT_ADMIN_API_TOKEN) are referenced as n8n $env
expressions; they are never embedded in the workflow JSON or this source.
"""

import copy
import json
import os
import sys
import urllib.error
import urllib.request

# ── PocketBase target (internal/public base URL where the hooks live) ─────────
PB_PUBLIC_URL = os.environ.get("KQ_PB_PUBLIC_URL", "https://db.kingsandqueens.com.ar").rstrip("/")

AGENT_WORKFLOW_NAME = "kq-chatwoot-agent"
AGENT_NODE_NAME = "Sam - AI Agent"
CONFIRMATION_WORKFLOW_NAME = "kq-support-confirmation"
CONFIRMATION_WEBHOOK_PATH = "kq-support-confirmed"
CHATWOOT_ACCOUNT_ID = "4"

SUPPORT_MARKER_START = "## KQ_SUPPORT_START"
SUPPORT_MARKER_END = "## KQ_SUPPORT_END"
SUPPORT_PROMPT_BLOCK = "\n".join([
    SUPPORT_MARKER_START,
    "## Apoyo escolar de inglés",
    "- Usá herramientas para horarios, cupos, seña y reservas. Nunca inventes disponibilidad.",
    "- Podés informar únicamente el importe de seña devuelto por hold_support_slot.",
    "- Para otros cursos sigue prohibido dar precios sin derivar a Ale.",
    "- Pedí nombre, primario/secundario, grado o año, tema y preferencia horaria.",
    "- Mostrá como máximo las 3 opciones devueltas por search_support_slots.",
    "- No confirmes pago: el comprobante queda en revisión de Ale.",
    SUPPORT_MARKER_END,
])

# name → endpoint, AI-provided fields, whether the conversation id is injected
# from the parent workflow context, and the AI-facing description.
TOOL_SPECS = {
    "search_support_slots": {
        "endpoint": "/api/kq/support/search",
        "ai_fields": ["preferred_days", "preferred_time"],
        "needs_conversation": False,
        "description": (
            "Busca hasta 3 horarios de apoyo escolar de inglés disponibles. "
            "Devuelve id, fecha, hora, cupos restantes y seña. Nunca inventes disponibilidad. "
            "Campos opcionales: preferred_days, preferred_time (HH:MM)."
        ),
    },
    "hold_support_slot": {
        "endpoint": "/api/kq/support/hold",
        "ai_fields": ["slot_id", "student_name", "contact_phone", "guardian_name",
                      "education_level", "school_year", "topic"],
        "needs_conversation": True,
        "description": (
            "Reserva (hold de 12 h) un horario de apoyo escolar. Devuelve la seña, alias e "
            "instrucciones de transferencia. Campos: slot_id, student_name, contact_phone, "
            "guardian_name, education_level (primary|secondary), school_year, topic."
        ),
    },
    "attach_support_receipt": {
        "endpoint": "/api/kq/support/receipt",
        "ai_fields": ["receipt_url"],
        "needs_conversation": True,
        "description": (
            "Adjunta el comprobante de transferencia a la reserva activa y la deja en revisión "
            "de Ale. Campos: receipt_url (URL del comprobante recibido en el chat)."
        ),
    },
    "cancel_support_booking": {
        "endpoint": "/api/kq/support/cancel",
        "ai_fields": ["booking_code", "contact_phone"],
        "needs_conversation": False,
        "description": (
            "Cancela una reserva de apoyo. Con más de 24 h genera crédito; con menos, se pierde "
            "la seña. Campos: booking_code, contact_phone."
        ),
    },
    "reschedule_support_booking": {
        "endpoint": "/api/kq/support/reschedule",
        "ai_fields": ["booking_code", "contact_phone", "slot_id"],
        "needs_conversation": False,
        "description": (
            "Reprograma un crédito de apoyo a otro horario (una sola vez). "
            "Campos: booking_code, contact_phone, slot_id (horario destino)."
        ),
    },
}


def workflow_settings():
    return {"executionOrder": "v1"}


def build_tool_workflow(tool_name, pb_base):
    """Sub-workflow: Execute Workflow Trigger → HTTP Request to PocketBase.

    The HTTP node forwards the tool arguments ($json) and authenticates with the
    server-side KQ_TOOL_TOKEN via an n8n environment expression.
    """
    spec = TOOL_SPECS[tool_name]
    url = pb_base.rstrip("/") + spec["endpoint"]
    return {
        "name": f"kq-{tool_name.replace('_', '-')}",
        "nodes": [
            {"id": "n1", "name": "When Called by Tool",
             "type": "n8n-nodes-base.executeWorkflowTrigger", "typeVersion": 1,
             "position": [240, 300], "parameters": {}},
            {"id": "n2", "name": "Call PocketBase",
             "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4,
             "position": [460, 300],
             "parameters": {
                 "method": "POST",
                 "url": url,
                 "sendHeaders": True,
                 "headerParameters": {"parameters": [
                     {"name": "X-KQ-Tool-Token", "value": "={{ $env.KQ_TOOL_TOKEN }}"},
                     {"name": "Content-Type", "value": "application/json"},
                 ]},
                 "sendBody": True,
                 "specifyBody": "json",
                 "jsonBody": "={{ JSON.stringify($json) }}",
                 "options": {},
             }},
        ],
        "connections": {
            "When Called by Tool": {"main": [[{"node": "Call PocketBase", "type": "main", "index": 0}]]},
        },
        "settings": workflow_settings(),
    }


CONFIRMATION_CODE_JS = (
    "const token = $json.headers ? $json.headers['x-kq-tool-token'] : undefined;\n"
    "if (!token || token !== $env.KQ_TOOL_TOKEN) {\n"
    "  throw new Error('unauthorized');\n"
    "}\n"
    "const b = $json.body || {};\n"
    "const message = `\\u2705 Seña confirmada para ${b.student_name}.\\n\\ud83d\\udcc5 ${b.date} de ${b.time_start} a ${b.time_end}.\\nTu lugar quedó reservado.`;\n"
    "return [{ json: { ...b, message } }];\n"
)


def build_confirmation_workflow():
    """Non-tool workflow: webhook → token check + message → post into Chatwoot.

    It is NOT attached to the AI Agent and sends no reminders. Delivery uses Sam's
    inbox/number because it posts into the existing Chatwoot conversation.
    """
    chatwoot_url = (
        f"=http://chatwoot-web:3000/api/v1/accounts/{CHATWOOT_ACCOUNT_ID}"
        "/conversations/{{ $json.conversation_id }}/messages"
    )
    return {
        "name": CONFIRMATION_WORKFLOW_NAME,
        "nodes": [
            {"id": "c1", "name": "Confirmation Webhook",
             "type": "n8n-nodes-base.webhook", "typeVersion": 1,
             "position": [240, 300],
             "parameters": {
                 "httpMethod": "POST",
                 "path": CONFIRMATION_WEBHOOK_PATH,
                 "responseMode": "lastNode",
                 "options": {},
             }},
            {"id": "c2", "name": "Verify token + build message",
             "type": "n8n-nodes-base.code", "typeVersion": 2,
             "position": [460, 300],
             "parameters": {"jsCode": CONFIRMATION_CODE_JS}},
            {"id": "c3", "name": "Post to Chatwoot",
             "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4,
             "position": [680, 300],
             "parameters": {
                 "method": "POST",
                 "url": chatwoot_url,
                 "sendHeaders": True,
                 "headerParameters": {"parameters": [
                     {"name": "api_access_token", "value": "={{ $env.CHATWOOT_ADMIN_API_TOKEN }}"},
                     {"name": "Content-Type", "value": "application/json"},
                 ]},
                 "sendBody": True,
                 "specifyBody": "json",
                 "jsonBody": "={{ JSON.stringify({ content: $json.message, message_type: 'outgoing' }) }}",
                 "options": {},
             }},
        ],
        "connections": {
            "Confirmation Webhook": {"main": [[{"node": "Verify token + build message", "type": "main", "index": 0}]]},
            "Verify token + build message": {"main": [[{"node": "Post to Chatwoot", "type": "main", "index": 0}]]},
        },
        "settings": workflow_settings(),
    }


def agent_tool_node(tool_name, workflow_id, index, spec):
    field_values = []
    if spec.get("needs_conversation"):
        field_values.append({
            "name": "conversation_id",
            "type": "string",
            "stringValue": "={{ $('Procesar mensaje').first().json.conversation_id }}",
        })
    return {
        "id": f"kqs_{tool_name}",
        "name": tool_name,
        "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
        "typeVersion": 1.2,
        "position": [1500 + index * 60, 640],
        "parameters": {
            "name": tool_name,
            "description": spec["description"],
            "workflowId": {"__rl": True, "value": workflow_id, "mode": "id"},
            "fields": {"values": field_values},
        },
    }


def patch_agent_workflow(agent, tool_ids):
    """Pure, additive, idempotent patch of the live agent.

    Appends the prompt block once, adds any missing support tool nodes, and wires
    them as ai_tool connections with indices continuing from the existing maximum.
    Existing nodes, connections, ids, webhook paths, memory and active state are
    preserved.
    """
    agent = copy.deepcopy(agent)
    nodes = agent.setdefault("nodes", [])
    connections = agent.setdefault("connections", {})

    # 1. Append the approved prompt block once.
    for node in nodes:
        if node.get("type") == "@n8n/n8n-nodes-langchain.agent" or node.get("name") == AGENT_NODE_NAME:
            options = node.setdefault("parameters", {}).setdefault("options", {})
            current = options.get("systemMessage", "") or ""
            if SUPPORT_MARKER_START not in current:
                options["systemMessage"] = current.rstrip() + "\n\n" + SUPPORT_PROMPT_BLOCK
            break

    # 2. Compute the next free ai_tool index pointing at the agent node.
    used = []
    for conn in connections.values():
        for branch in conn.get("ai_tool", []):
            for link in branch:
                if link.get("node") == AGENT_NODE_NAME and isinstance(link.get("index"), int):
                    used.append(link["index"])
    next_index = (max(used) + 1) if used else 0

    by_name = {n.get("name"): n for n in nodes}
    for tool_name, spec in TOOL_SPECS.items():
        workflow_id = tool_ids.get(tool_name, "")
        if tool_name in by_name:
            # Update only the workflow reference; never renumber or duplicate.
            params = by_name[tool_name].setdefault("parameters", {})
            params["workflowId"] = {"__rl": True, "value": workflow_id, "mode": "id"}
            continue
        nodes.append(agent_tool_node(tool_name, workflow_id, next_index, spec))
        connections[tool_name] = {
            "ai_tool": [[{"node": AGENT_NODE_NAME, "type": "ai_tool", "index": next_index}]]
        }
        next_index += 1

    return agent


# ── n8n REST API (only used by main(), not by the unit tests) ─────────────────
def _n8n_base():
    return os.environ.get("N8N_BASE_URL", "http://127.0.0.1:5678").rstrip("/") + "/api/v1"


def _api(method, path, data=None):
    key = os.environ.get("N8N_API_KEY")
    if not key:
        raise SystemExit("N8N_API_KEY is required for deployment")
    body = json.dumps(data).encode() if data is not None else None
    req = urllib.request.Request(_n8n_base() + path, data=body, method=method,
                                 headers={"X-N8N-API-KEY": key, "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req) as resp:
            raw = resp.read().decode()
            return json.loads(raw) if raw else {}
    except urllib.error.HTTPError as exc:
        print(f"  n8n API {method} {path} -> {exc.code}: {exc.read().decode()}", file=sys.stderr)
        raise


def _find_by_name(name):
    data = _api("GET", "/workflows?limit=250")
    for wf in data.get("data", []):
        if wf.get("name") == name:
            return wf
    return None


def _upsert(workflow):
    existing = _find_by_name(workflow["name"])
    payload = {
        "name": workflow["name"],
        "nodes": workflow["nodes"],
        "connections": workflow["connections"],
        "settings": workflow.get("settings", workflow_settings()),
    }
    if existing:
        result = _api("PUT", f"/workflows/{existing['id']}", payload)
        return existing["id"], result
    result = _api("POST", "/workflows", payload)
    return result.get("id"), result


def _activate(workflow_id):
    try:
        _api("POST", f"/workflows/{workflow_id}/activate")
    except urllib.error.HTTPError:
        pass


# The n8n public API rejects unknown workflow `settings` keys on write (e.g. the
# newer binaryMode / availableInMCP). Keep only the documented, writable ones.
ALLOWED_SETTINGS = {
    "executionOrder", "errorWorkflow", "callerPolicy", "callerIds", "timezone",
    "saveExecutionProgress", "saveManualExecutions", "saveDataErrorExecution",
    "saveDataSuccessExecution", "executionTimeout",
}


def _clean_settings(settings):
    cleaned = {k: v for k, v in (settings or {}).items() if k in ALLOWED_SETTINGS}
    return cleaned or workflow_settings()


def main():
    print("Upserting support tool workflows...")
    tool_ids = {}
    for tool_name in TOOL_SPECS:
        wf = build_tool_workflow(tool_name, PB_PUBLIC_URL)
        wf_id, _ = _upsert(wf)
        tool_ids[tool_name] = wf_id
        print(f"  {wf['name']} -> {wf_id}")

    print("Upserting confirmation workflow...")
    conf_id, _ = _upsert(build_confirmation_workflow())
    _activate(conf_id)
    print(f"  {CONFIRMATION_WORKFLOW_NAME} -> {conf_id} (active)")

    print("Patching live kq-chatwoot-agent (additive)...")
    agent = _find_by_name(AGENT_WORKFLOW_NAME)
    if not agent:
        raise SystemExit(f"{AGENT_WORKFLOW_NAME} not found; aborting without changes")
    full = _api("GET", f"/workflows/{agent['id']}")
    was_active = bool(full.get("active"))
    patched = patch_agent_workflow(full, tool_ids)
    _api("PUT", f"/workflows/{agent['id']}", {
        "name": patched["name"],
        "nodes": patched["nodes"],
        "connections": patched["connections"],
        "settings": _clean_settings(patched.get("settings")),
    })
    # A public-API PUT can clear the active flag; restore it so Sam keeps answering.
    if was_active:
        refreshed = _api("GET", f"/workflows/{agent['id']}")
        if not refreshed.get("active"):
            _activate(agent["id"])
    print(f"  {AGENT_WORKFLOW_NAME} patched ({len(TOOL_SPECS)} tools wired); active state preserved")


if __name__ == "__main__":
    main()
