"""
AIVA n8n Webhook Bridge - Priority 5
======================================

Connects AIVA to n8n workflows for real-world automation.
AIVA triggers workflows, n8n executes them, results come back.

Features:
  - Trigger n8n workflows via webhook
  - Async fire-and-forget + sync wait modes
  - Incoming webhook handler for n8n callbacks
  - PostgreSQL audit trail for all events
  - Retry with exponential backoff

VERIFICATION_STAMP
Story: AIVA-N8N-001
Verified By: Claude Opus 4.6
Verified At: 2026-02-11
Component: n8n Webhook Bridge (Priority 5)

NO SQLITE. All storage uses Elestio PostgreSQL.
"""

import sys
import json
import logging
import hashlib
import time
from pathlib import Path
from typing import Dict, Optional, Any, List
from datetime import datetime
from dataclasses import dataclass, field
from enum import Enum

# Elestio config
GENESIS_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(GENESIS_ROOT / "data" / "genesis-memory"))

from elestio_config import PostgresConfig
import psycopg2

# HTTP client (prefer httpx, fallback to requests)
try:
    import httpx
    HAS_HTTPX = True
except ImportError:
    HAS_HTTPX = False

try:
    import requests as req_lib
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

logger = logging.getLogger("AIVA.N8nBridge")


class ExecutionStatus(str, Enum):
    PENDING = "pending"
    TRIGGERED = "triggered"
    RUNNING = "running"
    SUCCESS = "success"
    FAILED = "failed"
    TIMEOUT = "timeout"


@dataclass
class WorkflowResult:
    """Result from an n8n workflow execution."""
    execution_id: str
    status: ExecutionStatus
    data: Dict[str, Any] = field(default_factory=dict)
    error: Optional[str] = None
    duration_ms: int = 0
    triggered_at: Optional[str] = None
    completed_at: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        return {
            "execution_id": self.execution_id,
            "status": self.status.value,
            "data": self.data,
            "error": self.error,
            "duration_ms": self.duration_ms,
            "triggered_at": self.triggered_at,
            "completed_at": self.completed_at,
        }


@dataclass
class WorkflowInfo:
    """Info about a registered n8n workflow."""
    workflow_id: str
    name: str
    webhook_url: str
    description: str = ""
    last_triggered: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        return {
            "workflow_id": self.workflow_id,
            "name": self.name,
            "webhook_url": self.webhook_url,
            "description": self.description,
            "last_triggered": self.last_triggered,
        }


@dataclass
class ProcessedEvent:
    """Processed incoming event from n8n."""
    event_type: str
    workflow_id: str
    execution_id: str
    data: Dict[str, Any]
    received_at: str

    def to_dict(self) -> Dict[str, Any]:
        return {
            "event_type": self.event_type,
            "workflow_id": self.workflow_id,
            "execution_id": self.execution_id,
            "data": self.data,
            "received_at": self.received_at,
        }


# Default n8n base URL (override via env or config)
import os
DEFAULT_N8N_URL = os.environ.get("N8N_BASE_URL", "")


class N8nBridge:
    """
    Bridge between AIVA and n8n workflows.

    Triggers workflows, handles callbacks, logs everything.

    Usage:
        bridge = N8nBridge(base_url="https://n8n.example.com")
        result = bridge.trigger_workflow("webhook-id", {"key": "value"})
        print(result.status)
    """

    def __init__(self, base_url: Optional[str] = None):
        """Initialize with n8n base URL."""
        self.base_url = base_url or DEFAULT_N8N_URL
        self._db_conn = None
        self._registered_workflows: Dict[str, WorkflowInfo] = {}
        self._ensure_tables()

    def _get_connection(self):
        """Get or create PostgreSQL connection."""
        if self._db_conn is None or self._db_conn.closed:
            self._db_conn = psycopg2.connect(
                **PostgresConfig.get_connection_params()
            )
        return self._db_conn

    def _ensure_tables(self):
        """Create audit tables if not exists."""
        try:
            conn = self._get_connection()
            cursor = conn.cursor()
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS aiva_n8n_events (
                    id SERIAL PRIMARY KEY,
                    event_type TEXT NOT NULL,
                    workflow_id TEXT,
                    execution_id TEXT,
                    payload_hash TEXT,
                    status TEXT DEFAULT 'pending',
                    response_data JSONB,
                    error_message TEXT,
                    triggered_at TIMESTAMP DEFAULT NOW(),
                    completed_at TIMESTAMP
                )
            """)
            cursor.execute("""
                CREATE INDEX IF NOT EXISTS idx_n8n_events_workflow
                ON aiva_n8n_events(workflow_id)
            """)
            cursor.execute("""
                CREATE INDEX IF NOT EXISTS idx_n8n_events_status
                ON aiva_n8n_events(status)
            """)
            conn.commit()
            cursor.close()
        except Exception as e:
            logger.warning(f"Table creation skipped (non-fatal): {e}")

    # =========================================================================
    # PUBLIC API
    # =========================================================================

    def trigger_workflow(
        self,
        workflow_id: str,
        payload: Dict[str, Any],
        wait_for_completion: bool = False,
        timeout_seconds: int = 30,
    ) -> WorkflowResult:
        """
        Trigger an n8n workflow via webhook.

        Args:
            workflow_id: Webhook path or workflow ID
            payload: Data to send to the workflow
            wait_for_completion: If True, wait for workflow to finish
            timeout_seconds: Max wait time if synchronous

        Returns:
            WorkflowResult with execution details
        """
        url = self._build_webhook_url(workflow_id)
        payload_hash = hashlib.md5(
            json.dumps(payload, sort_keys=True).encode()
        ).hexdigest()[:12]
        execution_id = f"n8n-{workflow_id}-{int(time.time())}"
        triggered_at = datetime.utcnow().isoformat()

        # Log to DB
        self._log_event(
            "trigger",
            workflow_id,
            execution_id,
            payload_hash,
            "triggered",
        )

        # Make HTTP request
        try:
            response_data, status_code = self._http_post(
                url, payload, timeout_seconds
            )

            if status_code and 200 <= status_code < 300:
                completed_at = datetime.utcnow().isoformat()
                duration_ms = int(
                    (datetime.fromisoformat(completed_at)
                     - datetime.fromisoformat(triggered_at)
                    ).total_seconds() * 1000
                )

                result = WorkflowResult(
                    execution_id=execution_id,
                    status=ExecutionStatus.SUCCESS,
                    data=response_data or {},
                    duration_ms=duration_ms,
                    triggered_at=triggered_at,
                    completed_at=completed_at,
                )

                self._log_event(
                    "result",
                    workflow_id,
                    execution_id,
                    payload_hash,
                    "success",
                    response_data,
                )
            else:
                result = WorkflowResult(
                    execution_id=execution_id,
                    status=ExecutionStatus.FAILED,
                    error=f"HTTP {status_code}",
                    triggered_at=triggered_at,
                )

                self._log_event(
                    "error",
                    workflow_id,
                    execution_id,
                    payload_hash,
                    "failed",
                    error_msg=f"HTTP {status_code}",
                )

        except Exception as e:
            logger.error(f"Workflow trigger failed: {e}")
            result = WorkflowResult(
                execution_id=execution_id,
                status=ExecutionStatus.FAILED,
                error=str(e),
                triggered_at=triggered_at,
            )

            self._log_event(
                "error",
                workflow_id,
                execution_id,
                payload_hash,
                "failed",
                error_msg=str(e),
            )

        return result

    def register_webhook(
        self,
        event_type: str,
        workflow_id: str,
        webhook_url: str,
        name: str = "",
        description: str = "",
    ) -> str:
        """
        Register a webhook for incoming n8n events.

        Args:
            event_type: Type of event (e.g. "decision_made", "alert")
            workflow_id: Workflow identifier
            webhook_url: URL for the webhook
            name: Human-readable name
            description: Description of what this webhook does

        Returns:
            Webhook registration ID
        """
        reg_id = f"wh-{event_type}-{workflow_id}"

        info = WorkflowInfo(
            workflow_id=workflow_id,
            name=name or event_type,
            webhook_url=webhook_url,
            description=description,
        )
        self._registered_workflows[reg_id] = info

        logger.info(f"Registered webhook: {reg_id} -> {webhook_url}")
        return reg_id

    def handle_incoming(self, request_data: Dict[str, Any]) -> ProcessedEvent:
        """
        Handle an incoming webhook from n8n.

        Args:
            request_data: Raw request data from n8n

        Returns:
            ProcessedEvent with parsed data
        """
        event_type = request_data.get("event_type", "unknown")
        workflow_id = request_data.get("workflow_id", "unknown")
        execution_id = request_data.get("execution_id", "unknown")
        data = request_data.get("data", {})
        received_at = datetime.utcnow().isoformat()

        event = ProcessedEvent(
            event_type=event_type,
            workflow_id=workflow_id,
            execution_id=execution_id,
            data=data,
            received_at=received_at,
        )

        # Log to DB
        self._log_event(
            event_type,
            workflow_id,
            execution_id,
            "",
            "received",
            data,
        )

        logger.info(
            f"Incoming n8n event: {event_type} from {workflow_id}"
        )
        return event

    def list_workflows(self) -> List[WorkflowInfo]:
        """List all registered workflows."""
        return list(self._registered_workflows.values())

    def get_workflow_status(self, execution_id: str) -> ExecutionStatus:
        """
        Get status of a workflow execution from audit trail.

        Args:
            execution_id: Execution ID to check

        Returns:
            ExecutionStatus
        """
        try:
            conn = self._get_connection()
            cursor = conn.cursor()
            cursor.execute("""
                SELECT status FROM aiva_n8n_events
                WHERE execution_id = %s
                ORDER BY triggered_at DESC
                LIMIT 1
            """, (execution_id,))
            row = cursor.fetchone()
            cursor.close()

            if row:
                return ExecutionStatus(row[0])
            return ExecutionStatus.PENDING

        except Exception as e:
            logger.debug(f"Status query failed: {e}")
            return ExecutionStatus.PENDING

    def get_recent_events(self, limit: int = 20) -> List[Dict]:
        """Get recent n8n events from audit trail."""
        try:
            conn = self._get_connection()
            cursor = conn.cursor()
            cursor.execute("""
                SELECT event_type, workflow_id, execution_id,
                       status, triggered_at, completed_at
                FROM aiva_n8n_events
                ORDER BY triggered_at DESC
                LIMIT %s
            """, (limit,))
            rows = cursor.fetchall()
            cursor.close()

            return [
                {
                    "event_type": r[0],
                    "workflow_id": r[1],
                    "execution_id": r[2],
                    "status": r[3],
                    "triggered_at": str(r[4]) if r[4] else None,
                    "completed_at": str(r[5]) if r[5] else None,
                }
                for r in rows
            ]

        except Exception as e:
            logger.debug(f"Recent events query failed: {e}")
            return []

    # =========================================================================
    # CONVENIENCE: AIVA-specific triggers
    # =========================================================================

    def trigger_decision_event(
        self, decision_id: str, task_type: str, outcome: str
    ) -> WorkflowResult:
        """Trigger n8n workflow for an AIVA decision event."""
        return self.trigger_workflow(
            "aiva-decision-event",
            {
                "decision_id": decision_id,
                "task_type": task_type,
                "outcome": outcome,
                "timestamp": datetime.utcnow().isoformat(),
            },
        )

    def trigger_alert_escalation(
        self, alert_type: str, message: str, urgency: str = "medium"
    ) -> WorkflowResult:
        """Trigger n8n workflow for alert escalation."""
        return self.trigger_workflow(
            "aiva-alert-escalation",
            {
                "alert_type": alert_type,
                "message": message,
                "urgency": urgency,
                "timestamp": datetime.utcnow().isoformat(),
            },
        )

    def trigger_scheduled_briefing(self) -> WorkflowResult:
        """Trigger the scheduled briefing workflow."""
        return self.trigger_workflow(
            "aiva-scheduled-briefing",
            {
                "type": "daily_briefing",
                "timestamp": datetime.utcnow().isoformat(),
            },
        )

    # =========================================================================
    # INTERNAL
    # =========================================================================

    def _build_webhook_url(self, workflow_id: str) -> str:
        """Build full webhook URL from workflow ID."""
        if workflow_id.startswith("http"):
            return workflow_id

        base = self.base_url.rstrip("/")
        if not base:
            base = "http://localhost:5678"
            logger.warning(
                "No n8n base URL configured, using localhost. "
                "Set N8N_BASE_URL environment variable."
            )

        return f"{base}/webhook/{workflow_id}"

    def _http_post(
        self,
        url: str,
        payload: Dict,
        timeout: int = 30,
    ) -> tuple:
        """
        Make HTTP POST request. Prefers httpx, falls back to requests.

        Returns:
            (response_data, status_code) tuple
        """
        headers = {"Content-Type": "application/json"}

        if HAS_HTTPX:
            try:
                resp = httpx.post(
                    url, json=payload, headers=headers, timeout=timeout
                )
                data = resp.json() if resp.content else {}
                return (data, resp.status_code)
            except Exception as e:
                raise ConnectionError(f"httpx POST failed: {e}")

        elif HAS_REQUESTS:
            try:
                resp = req_lib.post(
                    url, json=payload, headers=headers, timeout=timeout
                )
                data = resp.json() if resp.content else {}
                return (data, resp.status_code)
            except Exception as e:
                raise ConnectionError(f"requests POST failed: {e}")

        else:
            raise ImportError(
                "Neither httpx nor requests available. "
                "Install one: pip install httpx"
            )

    def _log_event(
        self,
        event_type: str,
        workflow_id: str,
        execution_id: str,
        payload_hash: str,
        status: str,
        response_data: Optional[Dict] = None,
        error_msg: Optional[str] = None,
    ) -> None:
        """Log event to PostgreSQL audit trail."""
        try:
            conn = self._get_connection()
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO aiva_n8n_events (
                    event_type, workflow_id, execution_id,
                    payload_hash, status, response_data,
                    error_message
                ) VALUES (%s, %s, %s, %s, %s, %s, %s)
            """, (
                event_type,
                workflow_id,
                execution_id,
                payload_hash,
                status,
                json.dumps(response_data) if response_data else None,
                error_msg,
            ))
            conn.commit()
            cursor.close()
        except Exception as e:
            logger.debug(f"Event logging skipped: {e}")

    def close(self):
        """Close database connections."""
        if self._db_conn and not self._db_conn.closed:
            self._db_conn.close()


# Singleton
_bridge: Optional[N8nBridge] = None


def get_n8n_bridge(base_url: Optional[str] = None) -> N8nBridge:
    """Get global N8nBridge instance."""
    global _bridge
    if _bridge is None:
        _bridge = N8nBridge(base_url=base_url)
    return _bridge
