"""
Terminal Relay Server — Voice-to-Terminal Bridge

Receives voice-transcribed commands from the MCP Voice Bridge (CF Worker)
and executes them against the Genesis codebase via terminal agents.

Architecture:
  Phone → Telnyx STT → MCP (CF Worker) → cloudflared → THIS SERVER → claude/gemini/bash → response

Smart routing:
  - "terminal" mode: claude -p (full terminal agent, uses API credits)
  - "gemini" mode: gemini CLI (free via Ultra subscription)
  - "bash" mode: direct subprocess (free, simple commands)
  - "read" mode: read file contents (free)
  - "auto" mode: smart routing based on task complexity

Run: uvicorn scripts.terminal_relay:app --host 0.0.0.0 --port 8765
Expose: cloudflared tunnel --url http://localhost:8765
"""

import asyncio
import json
import os
import time
import re
import subprocess
from typing import Optional

import httpx
from fastapi import FastAPI, HTTPException, Header, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

# ─── Config ───────────────────────────────────────────────

GENESIS_DIR = "/mnt/e/genesis-system"
CLAUDE_CLI = "/home/authentic88/.nvm/versions/node/v22.22.0/bin/claude"
GEMINI_CLI = "/home/authentic88/.nvm/versions/node/v22.22.0/bin/gemini"
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyALfbAdHfJ6aRnqNyiTRmKmGVoena1JsdU")
GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"
RELAY_TOKEN = os.environ.get("RELAY_TOKEN", "subaiva-prod-token-2026-genesis")
MAX_BASH_TIMEOUT = 30  # seconds
MAX_CLAUDE_TIMEOUT = 120  # seconds
MAX_GEMINI_TIMEOUT = 60  # seconds (API is faster than CLI)
MAX_RESPONSE_CHARS = 5000  # Truncate long responses for voice

app = FastAPI(title="Genesis Terminal Relay", version="1.0.0")

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


# ─── Models ───────────────────────────────────────────────

class ExecuteRequest(BaseModel):
    command: str
    mode: str = "auto"  # auto, terminal, gemini, bash, read
    context: Optional[str] = None  # Optional conversation context
    max_tokens: Optional[int] = 500  # Max response length hint


class ExecuteResponse(BaseModel):
    response: str
    mode_used: str
    duration_ms: int
    truncated: bool = False


# ─── Auth ─────────────────────────────────────────────────

def verify_auth(authorization: Optional[str]) -> bool:
    if not authorization:
        return False
    if authorization.startswith("Bearer "):
        token = authorization[7:]
    else:
        token = authorization
    return token == RELAY_TOKEN


# ─── Smart Router ─────────────────────────────────────────

# Patterns that indicate the command needs full terminal agent
TERMINAL_PATTERNS = [
    r"\b(edit|modify|change|update|fix|write|create|delete|remove)\b.*\b(file|code|function|class|config)\b",
    r"\b(commit|push|pull|merge|branch|deploy)\b",
    r"\b(install|pip|npm|apt|brew)\b",
    r"\b(run test|pytest|jest|check|lint|build)\b",
    r"\b(refactor|implement|debug|trace)\b",
]

# Patterns that can be handled with direct bash
BASH_PATTERNS = [
    r"^(ls|pwd|cat|head|tail|wc|du|df|free|uptime|whoami|date|hostname)\b",
    r"^(echo|mkdir|touch|cp|mv|rm|chmod|chown|find|grep|sort|uniq|tr|sed|awk|xargs)\b",
    r"^(bash|sh|python3?|node|curl|wget)\b",
    r"^(git |git$)",
    r"^(ps |top|htop|docker |systemctl |journalctl )\b",
    r"^(cd |source |export |\.|which |type |file )\b",
    r"\b(list files|show files|directory)\b",
    r"\b(disk space|memory usage|cpu usage)\b",
    r"\|",  # any piped command is bash
    r"[>]{1,2}\s",  # any redirect (> or >>) is bash
    r"&&",  # chained commands are bash
    r"/mnt/[a-z]/",  # explicit paths are bash
]

# Patterns for file reads
READ_PATTERNS = [
    r"\bread\b.*\b(file|content|memory\.md|claude\.md)\b",
    r"\bshow me\b.*\b(file|contents?)\b",
    r"\bwhat('s| is) in\b.*\b(file|folder)\b",
]


def classify_command(command: str) -> str:
    """Smart-route a voice command to the best execution mode."""
    cmd_lower = command.lower().strip()

    # Check for explicit bash commands FIRST (highest priority)
    for pattern in BASH_PATTERNS:
        if re.search(pattern, cmd_lower):
            return "bash"

    # Check for file read requests
    for pattern in READ_PATTERNS:
        if re.search(pattern, cmd_lower):
            return "read"

    # Check for codebase-aware questions (need context injection)
    for pattern in TERMINAL_PATTERNS:
        if re.search(pattern, cmd_lower):
            return "gemini_ctx"  # Gemini with codebase context

    # Check if question is about Genesis/AIVA/system state
    # Only match if the command looks like a natural language question,
    # NOT if it looks like a shell command (contains special chars)
    shell_indicators = ['|', '>', '<', '&&', '||', ';', '$(', '`', '/mnt/', '/home/', '/tmp/']
    looks_like_shell = any(ind in command for ind in shell_indicators)

    if not looks_like_shell:
        system_keywords = [
            "genesis", "aiva", "subaiva", "mission", "priority", "status",
            "memory", "mrr", "revenue", "customer", "george", "telnyx",
            "voice", "agent", "knowledge", "deploy", "production",
            "what are we", "what is the", "current state", "progress",
        ]
        if any(kw in cmd_lower for kw in system_keywords):
            return "gemini_ctx"

    # Default: use gemini (free, fast, no context)
    return "gemini"


# ─── Context Injection ───────────────────────────────────

# Core files to inject for system-aware responses
CONTEXT_FILES = {
    "memory": os.path.join(GENESIS_DIR, "MEMORY.md"),
    "handoff": os.path.join(GENESIS_DIR, "hive/session_recovery/LATEST.md"),
}

def gather_codebase_context(command: str) -> str:
    """Read relevant codebase files to inject as context for AI responses."""
    context_parts = []
    cmd_lower = command.lower()

    # Always include MEMORY.md for system state
    try:
        with open(CONTEXT_FILES["memory"], "r") as f:
            content = f.read()[:3000]  # First 3000 chars
        context_parts.append(f"=== GENESIS MEMORY (current state) ===\n{content}")
    except Exception:
        pass

    # Include handoff if asking about session/progress
    if any(kw in cmd_lower for kw in ["session", "progress", "handoff", "last"]):
        try:
            with open(CONTEXT_FILES["handoff"], "r") as f:
                content = f.read()[:2000]
            context_parts.append(f"=== LATEST SESSION HANDOFF ===\n{content}")
        except Exception:
            pass

    # For git/deploy questions, run a quick bash command
    if any(kw in cmd_lower for kw in ["commit", "branch", "deploy", "git"]):
        try:
            result = subprocess.run(
                "git log --oneline -5", shell=True, capture_output=True,
                text=True, timeout=5, cwd=GENESIS_DIR,
            )
            if result.stdout:
                context_parts.append(f"=== RECENT COMMITS ===\n{result.stdout.strip()}")
        except Exception:
            pass

    return "\n\n".join(context_parts) if context_parts else ""


# ─── Execution Backends ──────────────────────────────────

def truncate_for_voice(text: str, max_chars: int = MAX_RESPONSE_CHARS) -> tuple[str, bool]:
    """Truncate response for voice delivery."""
    if len(text) <= max_chars:
        return text, False
    # Find a good cut point (end of sentence)
    cut = text[:max_chars].rfind(". ")
    if cut > max_chars // 2:
        return text[: cut + 1], True
    return text[:max_chars] + "...", True


async def execute_bash(command: str) -> str:
    """Execute a bash command directly on the Genesis system."""
    # Safety: block dangerous commands
    dangerous = ["rm -rf", "mkfs", "dd if=", "> /dev/", ":(){ :|:& };:"]
    cmd_lower = command.lower()
    for d in dangerous:
        if d in cmd_lower:
            return f"Blocked: dangerous command pattern detected ({d})"

    # Translate voice to bash if needed
    voice_to_bash = {
        "list files": "ls -la",
        "show files": "ls -la",
        "disk space": "df -h",
        "memory usage": "free -h",
        "current directory": "pwd",
        "git status": "git status",
        "show branches": "git branch -a",
        "recent commits": "git log --oneline -10",
    }
    for voice, bash in voice_to_bash.items():
        if voice in cmd_lower:
            command = bash
            break

    proc = await asyncio.create_subprocess_shell(
        command,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        cwd=GENESIS_DIR,
    )
    try:
        stdout, stderr = await asyncio.wait_for(
            proc.communicate(), timeout=MAX_BASH_TIMEOUT
        )
        output = stdout.decode("utf-8", errors="replace")
        if stderr:
            output += "\n" + stderr.decode("utf-8", errors="replace")
        return output.strip() or "(no output)"
    except asyncio.TimeoutError:
        proc.kill()
        return "Command timed out after 30 seconds."


async def execute_read(command: str) -> str:
    """Read a file from the Genesis codebase."""
    # Extract file path from voice command
    # Try to find a path-like pattern
    path_match = re.search(r"(/[\w/.-]+|\b[\w/.-]+\.(py|ts|js|md|json|txt|yaml|toml|sh))\b", command)
    if path_match:
        filepath = path_match.group(1)
    else:
        # Common files by keyword
        keyword_files = {
            "memory": "MEMORY.md",
            "status": "MEMORY.md",
            "mission": "MEMORY.md",
            "handoff": "hive/session_recovery/LATEST.md",
            "rules": ".claude/rules/GLOBAL_GENESIS_RULES.md",
        }
        filepath = None
        for kw, fp in keyword_files.items():
            if kw in command.lower():
                filepath = fp
                break
        if not filepath:
            return "I couldn't identify which file you want me to read. Please specify a filename."

    # Resolve relative paths
    if not filepath.startswith("/"):
        filepath = os.path.join(GENESIS_DIR, filepath)

    if not os.path.exists(filepath):
        return f"File not found: {filepath}"

    try:
        with open(filepath, "r", encoding="utf-8", errors="replace") as f:
            content = f.read()
        if len(content) > MAX_RESPONSE_CHARS:
            content = content[:MAX_RESPONSE_CHARS] + "\n... (truncated)"
        return content
    except Exception as e:
        return f"Error reading file: {e}"


async def execute_claude(command: str, context: Optional[str] = None) -> str:
    """Execute via claude -p (full terminal agent with Genesis codebase access)."""
    prompt = command
    if context:
        prompt = f"Context from voice call: {context}\n\nTask: {command}"

    # Add voice-friendly instruction
    prompt += "\n\nIMPORTANT: Keep your response concise and spoken-word friendly. No markdown formatting, no code blocks, no URLs. Respond as if speaking on a phone call. 2-4 sentences max unless the task requires more."

    # CRITICAL: unset CLAUDECODE to allow nested session
    clean_env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}
    clean_env["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1"

    proc = await asyncio.create_subprocess_exec(
        CLAUDE_CLI, "-p", prompt,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        cwd=GENESIS_DIR,
        env=clean_env,
    )
    try:
        stdout, stderr = await asyncio.wait_for(
            proc.communicate(), timeout=MAX_CLAUDE_TIMEOUT
        )
        output = stdout.decode("utf-8", errors="replace").strip()
        if not output and stderr:
            output = stderr.decode("utf-8", errors="replace").strip()
        return output or "Claude completed the task but returned no output."
    except asyncio.TimeoutError:
        proc.kill()
        return "The terminal agent timed out. The task may be too complex for a voice command. Try breaking it into smaller steps."


async def execute_gemini(command: str, context: Optional[str] = None) -> str:
    """Execute via Gemini API (free, fast, good for general questions and codebase queries)."""
    prompt = command
    if context:
        prompt = f"Context: {context}\n\nQuestion: {command}"

    system_instruction = (
        "You are AIVA, the Genesis AI voice assistant. "
        "You are responding to a voice call. Keep responses concise and spoken-word friendly. "
        "No markdown, no code blocks, no URLs. 2-4 sentences unless more detail is needed. "
        "Speak naturally as if in a phone conversation."
    )

    payload = {
        "system_instruction": {"parts": [{"text": system_instruction}]},
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {
            "maxOutputTokens": 500,
            "temperature": 0.7,
        },
    }

    try:
        async with httpx.AsyncClient(timeout=MAX_GEMINI_TIMEOUT) as client:
            resp = await client.post(
                f"{GEMINI_API_URL}?key={GEMINI_API_KEY}",
                json=payload,
            )
            if resp.status_code == 200:
                data = resp.json()
                candidates = data.get("candidates", [])
                if candidates:
                    parts = candidates[0].get("content", {}).get("parts", [])
                    text = " ".join(p.get("text", "") for p in parts).strip()
                    if text:
                        return text
                return "Gemini returned an empty response."
            elif resp.status_code == 429:
                # Rate limited — wait and retry once
                await asyncio.sleep(2)
                resp2 = await client.post(
                    f"{GEMINI_API_URL}?key={GEMINI_API_KEY}",
                    json=payload,
                )
                if resp2.status_code == 200:
                    data = resp2.json()
                    candidates = data.get("candidates", [])
                    if candidates:
                        parts = candidates[0].get("content", {}).get("parts", [])
                        text = " ".join(p.get("text", "") for p in parts).strip()
                        if text:
                            return text
                return "Gemini is temporarily rate limited. Please try again in a moment."
            else:
                error_text = resp.text[:200]
                return f"Gemini API error ({resp.status_code}): {error_text}"
    except httpx.TimeoutException:
        return "Gemini timed out. Try a simpler question."
    except Exception as e:
        return f"Gemini error: {str(e)[:200]}"


# ─── Main Endpoint ───────────────────────────────────────

@app.post("/execute", response_model=ExecuteResponse)
async def execute_command(
    body: ExecuteRequest,
    authorization: Optional[str] = Header(None),
):
    """Execute a voice command against the Genesis terminal."""
    if not verify_auth(authorization):
        raise HTTPException(status_code=401, detail="Unauthorized")

    start = time.time()

    # Determine execution mode
    mode = body.mode
    if mode == "auto":
        mode = classify_command(body.command)

    # Execute based on mode
    if mode == "bash":
        raw_response = await execute_bash(body.command)
    elif mode == "read":
        raw_response = await execute_read(body.command)
    elif mode == "terminal":
        raw_response = await execute_claude(body.command, body.context)
    elif mode == "gemini":
        raw_response = await execute_gemini(body.command, body.context)
    elif mode == "gemini_ctx":
        # Gemini with codebase context injection — terminal-like intelligence
        codebase_ctx = gather_codebase_context(body.command)
        full_context = body.context or ""
        if codebase_ctx:
            full_context = f"{codebase_ctx}\n\n{full_context}" if full_context else codebase_ctx
        raw_response = await execute_gemini(body.command, full_context)
    else:
        raise HTTPException(status_code=400, detail=f"Unknown mode: {mode}")

    # Truncate for voice delivery
    response, truncated = truncate_for_voice(raw_response)

    duration_ms = int((time.time() - start) * 1000)

    return ExecuteResponse(
        response=response,
        mode_used=mode,
        duration_ms=duration_ms,
        truncated=truncated,
    )


@app.get("/health")
async def health():
    """Health check endpoint."""
    # Check which backends are available
    claude_ok = os.path.exists(CLAUDE_CLI)
    gemini_ok = bool(GEMINI_API_KEY)
    genesis_ok = os.path.isdir(GENESIS_DIR)

    return {
        "status": "ok",
        "service": "genesis-terminal-relay",
        "version": "1.1.0",
        "backends": {
            "claude": claude_ok,
            "gemini_api": gemini_ok,
            "bash": True,
            "read": True,
        },
        "genesis_dir": genesis_ok,
        "timestamp": time.time(),
    }


# ─── Run ──────────────────────────────────────────────────

if __name__ == "__main__":
    import uvicorn
    print("Starting Genesis Terminal Relay on port 8765...")
    print(f"  Claude CLI: {CLAUDE_CLI} ({'OK' if os.path.exists(CLAUDE_CLI) else 'MISSING'})")
    print(f"  Gemini CLI: {GEMINI_CLI} ({'OK' if os.path.exists(GEMINI_CLI) else 'MISSING'})")
    print(f"  Genesis dir: {GENESIS_DIR}")
    print(f"  Auth token: {RELAY_TOKEN[:8]}...")
    print()
    print("Expose via: cloudflared tunnel --url http://localhost:8765")
    uvicorn.run(app, host="0.0.0.0", port=8765)
