"""
Genesis Command Bridge API

Voice-to-Terminal directive relay between Kinan (via AIVA on Telnyx)
and Claude Code command centre.

Architecture:
    Kinan (phone) -> AIVA (Telnyx Voice Assistant) -> Bridge API -> PostgreSQL <- Claude Code (polls)

Run:
    BRIDGE_API_KEY=your-secret-key uvicorn bridge_api:app --host 0.0.0.0 --port 8765

Environment Variables:
    BRIDGE_API_KEY          - API key for authentication (required)
    GENESIS_POSTGRES_HOST   - PostgreSQL host (default: Elestio)
    GENESIS_POSTGRES_PORT   - PostgreSQL port (default: 25432)
    GENESIS_POSTGRES_USER   - PostgreSQL user (default: postgres)
    GENESIS_POSTGRES_PASSWORD - PostgreSQL password
    GENESIS_POSTGRES_DATABASE - PostgreSQL database (default: postgres)
"""

import os
import sys
import json
import logging
from datetime import datetime, timezone
from typing import Optional, List
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, Depends, Header, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import psycopg2
import psycopg2.extras
from psycopg2.pool import ThreadedConnectionPool

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

BRIDGE_API_KEY = os.environ.get("BRIDGE_API_KEY", "genesis-bridge-2026")

# PostgreSQL config - uses Elestio defaults, overridable via env
PG_HOST = os.environ.get("GENESIS_POSTGRES_HOST", "postgresql-genesis-u50607.vm.elestio.app")
PG_PORT = int(os.environ.get("GENESIS_POSTGRES_PORT", "25432"))
PG_USER = os.environ.get("GENESIS_POSTGRES_USER", "postgres")
PG_PASS = os.environ.get("GENESIS_POSTGRES_PASSWORD", "CiBjh6LM7Yuqkq-jo2r7eQDw")
PG_DB = os.environ.get("GENESIS_POSTGRES_DATABASE", "postgres")

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [BRIDGE] %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
log = logging.getLogger("command_bridge")

# ---------------------------------------------------------------------------
# Database Pool
# ---------------------------------------------------------------------------

pool: Optional[ThreadedConnectionPool] = None


def _create_pool() -> ThreadedConnectionPool:
    """Create a fresh connection pool with TCP keepalives to prevent idle disconnect."""
    p = ThreadedConnectionPool(
        minconn=1,
        maxconn=10,
        host=PG_HOST,
        port=PG_PORT,
        user=PG_USER,
        password=PG_PASS,
        database=PG_DB,
        # TCP keepalives prevent Elestio from closing idle connections after ~30min
        keepalives=1,
        keepalives_idle=30,      # probe after 30s idle
        keepalives_interval=10,  # probe every 10s
        keepalives_count=5,      # drop after 5 failed probes
        connect_timeout=10,
    )
    log.info(f"PostgreSQL pool created -> {PG_HOST}:{PG_PORT}/{PG_DB}")
    return p


def get_pool() -> ThreadedConnectionPool:
    global pool
    if pool is None:
        pool = _create_pool()
    return pool


def get_conn():
    """Get a connection from the pool, recreating pool if all connections are stale."""
    global pool
    try:
        conn = get_pool().getconn()
        # Quick liveness check — catches stale SSL connections
        conn.cursor().execute("SELECT 1")
        return conn
    except Exception as e:
        log.warning(f"Stale pool detected ({e}) — recreating pool")
        try:
            if pool:
                pool.closeall()
        except Exception:
            pass
        pool = _create_pool()
        return pool.getconn()


def put_conn(conn):
    """Return a connection to the pool."""
    try:
        get_pool().putconn(conn)
    except Exception:
        # If pool was recreated, just close this orphaned connection
        try:
            conn.close()
        except Exception:
            pass


def ensure_schema():
    """Create schema and table if they don't exist."""
    conn = get_conn()
    try:
        cur = conn.cursor()
        schema_sql = open(
            os.path.join(os.path.dirname(__file__), "schema.sql"), "r"
        ).read()
        cur.execute(schema_sql)
        conn.commit()
        cur.close()
        log.info("Schema genesis_bridge ensured")
    except Exception as e:
        conn.rollback()
        log.error(f"Schema creation failed: {e}")
        raise
    finally:
        put_conn(conn)


# ---------------------------------------------------------------------------
# FastAPI App
# ---------------------------------------------------------------------------

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Startup/shutdown lifecycle."""
    log.info("Command Bridge starting up...")
    ensure_schema()
    log.info("Command Bridge ONLINE - Kinan <-> Claude relay active")
    yield
    if pool:
        pool.closeall()
        log.info("Command Bridge shut down, pool closed")


app = FastAPI(
    title="Genesis Command Bridge",
    description="Voice-to-Terminal directive relay: Kinan -> AIVA -> Claude Code",
    version="1.0.0",
    lifespan=lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# ---------------------------------------------------------------------------
# Auth
# ---------------------------------------------------------------------------


async def verify_api_key(x_bridge_key: str = Header(default=None)):
    """Simple API key authentication."""
    if x_bridge_key != BRIDGE_API_KEY:
        raise HTTPException(status_code=401, detail="Invalid or missing X-Bridge-Key")
    return True


# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------


class DirectiveRequest(BaseModel):
    """Directive from Kinan (via AIVA) to Claude."""
    message: str = Field(..., description="The directive from Kinan")
    priority: str = Field(default="normal", description="low/normal/high/urgent")
    source: str = Field(default="aiva_voice", description="Origin of the directive")
    metadata: dict = Field(default_factory=dict, description="Extra context")


class StatusRequest(BaseModel):
    """Status update from Claude to Kinan."""
    message: str = Field(..., description="Status update text")
    priority: str = Field(default="normal", description="low/normal/high/urgent")
    metadata: dict = Field(default_factory=dict, description="Extra context")


class DirectiveResponse(BaseModel):
    """Response after queuing a directive."""
    status: str
    id: int
    message: str


class QueuedMessage(BaseModel):
    """A message from the queue."""
    id: int
    message: str
    priority: str
    created_at: str
    source: Optional[str] = None
    metadata: dict = {}


class StatusResponse(BaseModel):
    """Response with pending messages."""
    updates: List[QueuedMessage]
    count: int


class ExecuteRequest(BaseModel):
    """Mark a directive as executed with optional response."""
    response: Optional[str] = Field(None, description="Execution result summary")


# ---------------------------------------------------------------------------
# Telnyx Webhook Endpoint (for AIVA assistant tool calls)
# ---------------------------------------------------------------------------


@app.post("/bridge/telnyx-webhook")
async def telnyx_webhook(request: Request):
    """
    Handle Telnyx Voice Assistant tool webhook calls.

    Telnyx sends the tool call details here. We parse the function name
    and parameters, execute the appropriate action, and return the result
    for AIVA to speak back to Kinan.
    """
    body = await request.json()
    log.info(f"Telnyx webhook received: {json.dumps(body, indent=2)[:500]}")

    # Extract function call details from Telnyx payload
    # Telnyx sends tool calls in their assistant webhook format
    try:
        # Handle different Telnyx webhook structures
        function_name = None
        arguments = {}

        # Try standard Telnyx assistant tool call format
        if "function_name" in body:
            function_name = body["function_name"]
            arguments = body.get("arguments", body.get("function_arguments", {}))
        elif "data" in body:
            data = body["data"]
            if "function_name" in data:
                function_name = data["function_name"]
                arguments = data.get("arguments", data.get("function_arguments", {}))
            elif "payload" in data:
                payload = data["payload"]
                function_name = payload.get("function_name")
                arguments = payload.get("arguments", payload.get("function_arguments", {}))

        # Parse string arguments if needed
        if isinstance(arguments, str):
            try:
                arguments = json.loads(arguments)
            except json.JSONDecodeError:
                arguments = {"directive": arguments}

        if function_name == "relay_directive_to_claude":
            directive = arguments.get("directive", "")
            priority = arguments.get("priority", "normal")

            if not directive:
                return {"result": "No directive provided. Please tell me what to relay to Claude."}

            # Insert into command queue
            conn = get_conn()
            try:
                cur = conn.cursor()
                cur.execute(
                    """INSERT INTO genesis_bridge.command_queue
                       (direction, message, priority, source, metadata)
                       VALUES ('kinan_to_claude', %s, %s, 'aiva_voice', %s)
                       RETURNING id""",
                    (directive, priority, json.dumps({"via": "telnyx_webhook"})),
                )
                cmd_id = cur.fetchone()[0]
                conn.commit()
                cur.close()
                log.info(f"DIRECTIVE #{cmd_id} queued [{priority}]: {directive[:100]}")
                return {
                    "result": f"Got it. I've relayed that to Claude as directive #{cmd_id} with {priority} priority. Claude will pick it up shortly."
                }
            except Exception as e:
                conn.rollback()
                log.error(f"Failed to queue directive: {e}")
                return {"result": f"Sorry, I couldn't relay that to Claude. Database error: {str(e)[:100]}"}
            finally:
                put_conn(conn)

        elif function_name == "check_claude_status":
            # Fetch pending status updates from Claude
            conn = get_conn()
            try:
                cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
                cur.execute(
                    """UPDATE genesis_bridge.command_queue
                       SET status = 'read', read_at = NOW()
                       WHERE direction = 'claude_to_kinan' AND status = 'pending'
                       RETURNING id, message, priority, created_at, metadata"""
                )
                rows = cur.fetchall()
                conn.commit()
                cur.close()

                if not rows:
                    return {"result": "No new updates from Claude at the moment. Everything is running as expected."}

                # Format updates for AIVA to read aloud
                updates = []
                for row in rows:
                    p = f" ({row['priority']} priority)" if row["priority"] != "normal" else ""
                    updates.append(f"Update {row['id']}{p}: {row['message']}")

                result_text = f"I have {len(rows)} update{'s' if len(rows) > 1 else ''} from Claude. " + " ... ".join(updates)
                log.info(f"Delivered {len(rows)} status updates to Kinan via AIVA")
                return {"result": result_text}
            except Exception as e:
                conn.rollback()
                log.error(f"Failed to fetch status: {e}")
                return {"result": f"Sorry, I couldn't check Claude's status right now. Error: {str(e)[:100]}"}
            finally:
                put_conn(conn)

        elif function_name == "log_conversation_summary":
            summary = arguments.get("summary", "")
            outcome = arguments.get("outcome", "completed")
            caller_mood = arguments.get("caller_mood", "neutral")

            # Determine caller from metadata if available
            caller_number = (
                body.get("from") or body.get("caller_number")
                or arguments.get("caller_number", "")
            )
            duration = arguments.get("duration_seconds", 0)

            conn = get_conn()
            try:
                cur = conn.cursor()
                cur.execute(
                    """INSERT INTO aiva_rlm.aiva_interactions
                       (call_id, caller_number, transcript, call_duration_seconds, outcome, outcome_label)
                       VALUES (gen_random_uuid()::text, %s, %s, %s, %s, %s)""",
                    (
                        str(caller_number)[:50],
                        str(summary)[:5000],
                        int(duration) if str(duration).isdigit() else 0,
                        str(outcome)[:50],
                        "positive" if caller_mood == "positive" else
                        "negative" if caller_mood == "frustrated" else "neutral",
                    ),
                )
                conn.commit()
                cur.close()
                log.info(f"Call logged to aiva_rlm.aiva_interactions: outcome={outcome}, mood={caller_mood}")
                return {"result": "Logged. Thank you, AIVA. Your growth data has been recorded."}
            except Exception as e:
                conn.rollback()
                log.error(f"Failed to log conversation: {e}")
                return {"result": "Note logged locally. Database sync will retry."}
            finally:
                put_conn(conn)

        else:
            log.warning(f"Unknown function: {function_name}")
            return {"result": f"I don't recognize the function '{function_name}'. I can relay directives or check status."}

    except Exception as e:
        log.error(f"Webhook processing error: {e}")
        return {"result": f"Something went wrong processing that request: {str(e)[:100]}"}


# ---------------------------------------------------------------------------
# REST API Endpoints (for direct access)
# ---------------------------------------------------------------------------


@app.post("/bridge/directive", response_model=DirectiveResponse, dependencies=[Depends(verify_api_key)])
async def post_directive(req: DirectiveRequest):
    """
    Queue a directive from Kinan to Claude.
    Called by AIVA via Telnyx tool webhook or directly.
    """
    if req.priority not in ("low", "normal", "high", "urgent"):
        raise HTTPException(status_code=400, detail="Invalid priority")

    conn = get_conn()
    try:
        cur = conn.cursor()
        cur.execute(
            """INSERT INTO genesis_bridge.command_queue
               (direction, message, priority, source, metadata)
               VALUES ('kinan_to_claude', %s, %s, %s, %s)
               RETURNING id""",
            (req.message, req.priority, req.source, json.dumps(req.metadata)),
        )
        cmd_id = cur.fetchone()[0]
        conn.commit()
        cur.close()
        log.info(f"DIRECTIVE #{cmd_id} queued [{req.priority}]: {req.message[:100]}")
        return DirectiveResponse(status="queued", id=cmd_id, message=req.message)
    except Exception as e:
        conn.rollback()
        log.error(f"Failed to queue directive: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    finally:
        put_conn(conn)


@app.get("/bridge/directives", response_model=StatusResponse, dependencies=[Depends(verify_api_key)])
async def get_directives():
    """
    Claude polls this to get new directives from Kinan.
    Returns pending kinan_to_claude messages and marks them as 'read'.
    """
    conn = get_conn()
    try:
        cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
        cur.execute(
            """UPDATE genesis_bridge.command_queue
               SET status = 'read', read_at = NOW()
               WHERE direction = 'kinan_to_claude' AND status = 'pending'
               RETURNING id, message, priority, created_at, source, metadata"""
        )
        rows = cur.fetchall()
        conn.commit()
        cur.close()

        updates = []
        for row in rows:
            updates.append(
                QueuedMessage(
                    id=row["id"],
                    message=row["message"],
                    priority=row["priority"],
                    created_at=row["created_at"].isoformat(),
                    source=row.get("source"),
                    metadata=row.get("metadata", {}),
                )
            )

        if updates:
            log.info(f"Delivered {len(updates)} directives to Claude")

        return StatusResponse(updates=updates, count=len(updates))
    except Exception as e:
        conn.rollback()
        log.error(f"Failed to fetch directives: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    finally:
        put_conn(conn)


@app.post("/bridge/status", response_model=DirectiveResponse, dependencies=[Depends(verify_api_key)])
async def post_status(req: StatusRequest):
    """
    Claude posts status updates for Kinan.
    AIVA will read these when Kinan asks for updates.
    """
    conn = get_conn()
    try:
        cur = conn.cursor()
        cur.execute(
            """INSERT INTO genesis_bridge.command_queue
               (direction, message, priority, source, metadata)
               VALUES ('claude_to_kinan', %s, %s, 'claude_terminal', %s)
               RETURNING id""",
            (req.message, req.priority, json.dumps(req.metadata)),
        )
        cmd_id = cur.fetchone()[0]
        conn.commit()
        cur.close()
        log.info(f"STATUS #{cmd_id} posted [{req.priority}]: {req.message[:100]}")
        return DirectiveResponse(status="queued", id=cmd_id, message=req.message)
    except Exception as e:
        conn.rollback()
        log.error(f"Failed to post status: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    finally:
        put_conn(conn)


@app.get("/bridge/status", response_model=StatusResponse, dependencies=[Depends(verify_api_key)])
async def get_status():
    """
    AIVA polls this to get status updates from Claude for Kinan.
    Returns pending claude_to_kinan messages and marks them as 'read'.
    """
    conn = get_conn()
    try:
        cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
        cur.execute(
            """UPDATE genesis_bridge.command_queue
               SET status = 'read', read_at = NOW()
               WHERE direction = 'claude_to_kinan' AND status = 'pending'
               RETURNING id, message, priority, created_at, source, metadata"""
        )
        rows = cur.fetchall()
        conn.commit()
        cur.close()

        updates = []
        for row in rows:
            updates.append(
                QueuedMessage(
                    id=row["id"],
                    message=row["message"],
                    priority=row["priority"],
                    created_at=row["created_at"].isoformat(),
                    source=row.get("source"),
                    metadata=row.get("metadata", {}),
                )
            )

        if updates:
            log.info(f"Delivered {len(updates)} status updates for Kinan")

        return StatusResponse(updates=updates, count=len(updates))
    except Exception as e:
        conn.rollback()
        log.error(f"Failed to fetch status: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    finally:
        put_conn(conn)


@app.put("/bridge/directive/{directive_id}/execute", dependencies=[Depends(verify_api_key)])
async def mark_executed(directive_id: int, req: ExecuteRequest = None):
    """Mark a directive as executed, with optional response text."""
    conn = get_conn()
    try:
        cur = conn.cursor()
        response_text = req.response if req else None
        cur.execute(
            """UPDATE genesis_bridge.command_queue
               SET status = 'executed', executed_at = NOW(), response = %s
               WHERE id = %s AND direction = 'kinan_to_claude'
               RETURNING id""",
            (response_text, directive_id),
        )
        row = cur.fetchone()
        conn.commit()
        cur.close()

        if not row:
            raise HTTPException(status_code=404, detail="Directive not found")

        log.info(f"Directive #{directive_id} marked as executed")
        return {"status": "executed", "id": directive_id}
    except HTTPException:
        raise
    except Exception as e:
        conn.rollback()
        log.error(f"Failed to mark executed: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    finally:
        put_conn(conn)


@app.get("/bridge/history", dependencies=[Depends(verify_api_key)])
async def get_history(limit: int = 20, direction: Optional[str] = None):
    """Get recent command history."""
    conn = get_conn()
    try:
        cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
        if direction:
            cur.execute(
                """SELECT id, direction, message, priority, status, source,
                          created_at, read_at, executed_at, response, metadata
                   FROM genesis_bridge.command_queue
                   WHERE direction = %s
                   ORDER BY created_at DESC LIMIT %s""",
                (direction, limit),
            )
        else:
            cur.execute(
                """SELECT id, direction, message, priority, status, source,
                          created_at, read_at, executed_at, response, metadata
                   FROM genesis_bridge.command_queue
                   ORDER BY created_at DESC LIMIT %s""",
                (limit,),
            )
        rows = cur.fetchall()
        cur.close()

        # Convert datetime objects to strings
        for row in rows:
            for key in ("created_at", "read_at", "executed_at"):
                if row[key]:
                    row[key] = row[key].isoformat()

        return {"history": rows, "count": len(rows)}
    except Exception as e:
        log.error(f"Failed to fetch history: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    finally:
        put_conn(conn)


@app.post("/bridge/telnyx-transcript")
async def telnyx_transcript(request: Request):
    """
    Receive Telnyx post-call transcript and log to aiva_rlm.aiva_interactions.
    Configure in Telnyx: AI Assistant → transcription.webhook_url → this endpoint.

    Accepts any Telnyx transcript webhook format and extracts what it can.
    Returns 200 immediately so Telnyx doesn't retry.
    """
    try:
        body = await request.json()
    except Exception:
        body = {}

    log.info(f"Telnyx transcript webhook received: {str(body)[:300]}")

    try:
        # Extract fields from various Telnyx transcript payload shapes
        data = body.get("data", body)
        payload = data.get("payload", data)

        call_id = (
            payload.get("call_control_id")
            or payload.get("call_id")
            or payload.get("id")
            or body.get("call_control_id")
            or body.get("call_id")
            or f"unknown-{datetime.now(timezone.utc).timestamp()}"
        )

        caller_number = (
            payload.get("from")
            or payload.get("caller_number")
            or payload.get("caller_id")
            or body.get("from")
            or body.get("caller_number")
            or ""
        )

        # Transcript can be a string or list of {role, content} turns
        raw_transcript = (
            payload.get("transcript")
            or payload.get("transcription")
            or body.get("transcript")
            or body.get("transcription")
            or ""
        )
        if isinstance(raw_transcript, list):
            transcript = "\n".join(
                f"{t.get('role','?').upper()}: {t.get('content', t.get('text',''))}"
                for t in raw_transcript
            )
        else:
            transcript = str(raw_transcript)

        duration = (
            payload.get("duration_seconds")
            or payload.get("duration")
            or body.get("duration_seconds")
            or 0
        )
        try:
            duration = int(float(duration))
        except (ValueError, TypeError):
            duration = 0

        outcome = payload.get("outcome") or body.get("outcome") or "completed"

        # Insert into aiva_rlm.aiva_interactions
        conn = get_conn()
        try:
            cur = conn.cursor()
            cur.execute("""
                INSERT INTO aiva_rlm.aiva_interactions
                    (call_id, caller_number, transcript, call_duration_seconds, outcome, outcome_label)
                VALUES (%s, %s, %s, %s, %s, %s)
                ON CONFLICT DO NOTHING
            """, (
                str(call_id)[:255],
                str(caller_number)[:50],
                transcript[:50000],
                duration,
                str(outcome)[:50],
                "positive" if outcome in ("completed", "resolved", "success") else "neutral",
            ))
            conn.commit()
            cur.close()
            log.info(f"Logged call {call_id} ({duration}s) to aiva_rlm.aiva_interactions")
        except Exception as e:
            conn.rollback()
            log.error(f"Failed to log transcript to aiva_rlm: {e}")
        finally:
            put_conn(conn)

    except Exception as e:
        log.error(f"Transcript webhook processing error: {e}")

    # Always return 200 so Telnyx doesn't retry
    return {"status": "received"}


@app.get("/bridge/health")
async def health():
    """Health check - no auth required."""
    try:
        conn = get_conn()
        cur = conn.cursor()
        cur.execute("SELECT 1")
        cur.close()
        put_conn(conn)

        return {
            "status": "healthy",
            "service": "genesis-command-bridge",
            "database": "connected",
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }
    except Exception as e:
        return {
            "status": "unhealthy",
            "service": "genesis-command-bridge",
            "database": f"error: {str(e)[:100]}",
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    import uvicorn

    port = int(os.environ.get("BRIDGE_PORT", "8765"))
    log.info(f"Starting Command Bridge on port {port}")
    uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
