"""
Terminal Agent API - Interactive infrastructure pentesting via AI chat + Docker sandbox.

Provides session-based terminal interaction with AI-guided command execution,
exploitation path tracking, and VPN status monitoring.
"""

import asyncio
import logging
import re
import time
import uuid
from datetime import datetime, timezone
from typing import Dict, List, Optional

from fastapi import APIRouter, HTTPException, UploadFile, File, Form
from pydantic import BaseModel

from core.llm_manager import LLMManager
from core.sandbox_manager import get_sandbox

logger = logging.getLogger(__name__)

router = APIRouter()

# ---------------------------------------------------------------------------
# In-memory session store
# ---------------------------------------------------------------------------
terminal_sessions: Dict[str, Dict] = {}

# Map session_id -> KaliSandbox instance (per-session container)
session_sandboxes: Dict[str, object] = {}

# ---------------------------------------------------------------------------
# Pre-built templates
# ---------------------------------------------------------------------------
TEMPLATES = {
    "network_scanner": {
        "name": "Network Scanner",
        "description": "Host discovery, port scanning, and service detection",
        "system_prompt": (
            "You are an expert network reconnaissance specialist. You guide the "
            "operator through systematic host discovery, port scanning, and service "
            "fingerprinting. Always suggest nmap flags appropriate for the situation, "
            "explain output, and recommend next steps based on discovered services. "
            "Prioritize stealth when asked and suggest timing/fragmentation options."
        ),
        "initial_commands": [
            "nmap -sn {target}",
            "nmap -sV -sC -O -p- {target}",
            "nmap -sU --top-ports 50 {target}",
        ],
    },
    "lateral_movement": {
        "name": "Lateral Movement",
        "description": "Pass-the-hash, SMB/WinRM pivoting, and SSH tunneling",
        "system_prompt": (
            "You are a lateral movement specialist. You help the operator pivot "
            "through compromised networks using techniques such as pass-the-hash, "
            "SMB relay, WinRM sessions, SSH tunneling, and SOCKS proxying. Always "
            "verify credentials before attempting pivots, suggest cleanup steps, "
            "and track which hosts have been compromised."
        ),
        "initial_commands": [
            "crackmapexec smb {target} -u '' -p ''",
            "crackmapexec smb {target} --shares -u '' -p ''",
            "ssh -D 1080 -N -f user@{target}",
        ],
    },
    "privilege_escalation": {
        "name": "Privilege Escalation",
        "description": "SUID binaries, kernel exploits, cron jobs, and writable paths",
        "system_prompt": (
            "You are a privilege escalation expert for Linux and Windows systems. "
            "Guide the operator through enumeration of SUID/SGID binaries, kernel "
            "version checks, misconfigured cron jobs, writable PATH directories, "
            "sudo misconfigurations, and capability abuse. Suggest automated tools "
            "like linpeas/winpeas when appropriate and explain each finding."
        ),
        "initial_commands": [
            "id && whoami && uname -a",
            "find / -perm -4000 -type f 2>/dev/null",
            "cat /etc/crontab && ls -la /etc/cron.*",
            "echo $PATH | tr ':' '\\n' | xargs -I {} ls -ld {}",
        ],
    },
    "vpn_recon": {
        "name": "VPN Reconnaissance",
        "description": "VPN connection management and internal network discovery",
        "system_prompt": (
            "You are a VPN and internal network reconnaissance specialist. You "
            "help the operator connect to target VPNs, verify tunnel status, "
            "discover internal subnets, and enumerate services behind the VPN. "
            "Always confirm connectivity before proceeding with scans and suggest "
            "appropriate scope for internal reconnaissance."
        ),
        "initial_commands": [
            "openvpn --config client.ovpn --daemon",
            "ip addr show tun0",
            "ip route | grep tun",
            "nmap -sn 10.0.0.0/24",
        ],
    },
}

# ---------------------------------------------------------------------------
# Pydantic request / response models
# ---------------------------------------------------------------------------

class CreateSessionRequest(BaseModel):
    template_id: Optional[str] = None
    target: Optional[str] = ""
    name: Optional[str] = ""


class MessageRequest(BaseModel):
    message: str


class ExecuteCommandRequest(BaseModel):
    command: str
    execution_method: str = "sandbox"  # "sandbox" or "direct"


class ExploitationStepRequest(BaseModel):
    description: str
    command: Optional[str] = ""
    result: Optional[str] = ""
    step_type: str = "recon"  # recon | exploit | pivot | escalate | action


class SessionSummary(BaseModel):
    session_id: str
    name: str
    target: str
    template_id: Optional[str]
    status: str
    created_at: str
    messages_count: int
    commands_count: int


class MessageResponse(BaseModel):
    role: str
    response: str
    timestamp: str
    suggested_commands: List[str]


class CommandResult(BaseModel):
    command: str
    exit_code: int
    stdout: str
    stderr: str
    duration: float
    execution_method: str
    timestamp: str


class VPNStatus(BaseModel):
    connected: bool
    ip: Optional[str] = None
    interface: Optional[str] = None
    container_name: Optional[str] = None


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _build_session(
    session_id: str,
    name: str,
    target: str,
    template_id: Optional[str],
) -> Dict:
    return {
        "session_id": session_id,
        "name": name,
        "target": target,
        "template_id": template_id,
        "status": "active",
        "created_at": _now_iso(),
        "messages": [],
        "command_history": [],
        "exploitation_path": [],
        "vpn_status": {"connected": False, "ip": None},
        "container_name": None,
        "vpn_config_uploaded": False,
    }


def _get_session(session_id: str) -> Dict:
    session = terminal_sessions.get(session_id)
    if not session:
        raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
    return session


def _build_context_string(
    messages: List[Dict],
    commands: List[Dict],
    exploitation: List[Dict],
) -> str:
    parts: List[str] = []

    if messages:
        parts.append("=== Recent Conversation ===")
        for msg in messages:
            role = msg.get("role", "unknown").upper()
            parts.append(f"[{role}] {msg.get('content', '')}")

    if commands:
        parts.append("\n=== Recent Command Results ===")
        for cmd in commands:
            parts.append(
                f"$ {cmd['command']}\n"
                f"Exit code: {cmd['exit_code']}\n"
                f"Stdout: {cmd['stdout'][:500]}\n"
                f"Stderr: {cmd['stderr'][:300]}"
            )

    if exploitation:
        parts.append("\n=== Exploitation Path ===")
        for i, step in enumerate(exploitation, 1):
            parts.append(
                f"Step {i} [{step['step_type']}]: {step['description']}"
            )
            if step.get("command"):
                parts.append(f"  Command: {step['command']}")
            if step.get("result"):
                parts.append(f"  Result: {step['result'][:300]}")

    return "\n".join(parts)


def _extract_suggested_commands(text: str) -> List[str]:
    """Extract commands from backtick-fenced code blocks."""
    blocks = re.findall(r"```(?:bash|sh|shell)?\n?(.*?)```", text, re.DOTALL)
    commands: List[str] = []
    for block in blocks:
        for line in block.strip().splitlines():
            stripped = line.strip()
            if stripped and not stripped.startswith("#"):
                commands.append(stripped)
    return commands


# ---------------------------------------------------------------------------
# Template endpoints
# ---------------------------------------------------------------------------

@router.get("/templates")
async def list_templates():
    """List all available session templates."""
    result = []
    for tid, tmpl in TEMPLATES.items():
        result.append({
            "id": tid,
            "name": tmpl["name"],
            "description": tmpl["description"],
            "initial_commands": tmpl["initial_commands"],
        })
    return result


# ---------------------------------------------------------------------------
# Session CRUD
# ---------------------------------------------------------------------------

@router.post("/session")
async def create_session(req: CreateSessionRequest):
    """Create a new terminal session, optionally from a template."""
    session_id = str(uuid.uuid4())
    target = req.target or ""
    template_id = req.template_id

    if template_id and template_id not in TEMPLATES:
        raise HTTPException(status_code=400, detail=f"Unknown template: {template_id}")

    name = req.name or (
        TEMPLATES[template_id]["name"] if template_id else f"Session {session_id[:8]}"
    )

    session = _build_session(session_id, name, target, template_id)

    # Provision a per-session Kali container (best-effort)
    try:
        from core.container_pool import get_pool
        pool = get_pool()
        sandbox = await pool.get_or_create(f"terminal-{session_id}", enable_vpn=True)
        session_sandboxes[session_id] = sandbox
        session["container_name"] = sandbox.container_name
    except Exception as exc:
        logger.warning(f"Failed to provision Kali container for terminal session: {exc}")

    # Seed initial system message from template
    if template_id:
        tmpl = TEMPLATES[template_id]
        session["messages"].append({
            "role": "system",
            "content": tmpl["system_prompt"],
            "timestamp": _now_iso(),
            "metadata": {"template": template_id},
        })
        # Provide initial suggested commands with target interpolated
        initial_cmds = [
            cmd.replace("{target}", target) for cmd in tmpl["initial_commands"]
        ]
        session["messages"].append({
            "role": "assistant",
            "content": (
                f"Session initialised with the **{tmpl['name']}** template.\n\n"
                f"Target: `{target or '(not set)'}`\n\n"
                "Suggested starting commands:\n"
                + "\n".join(f"```\n{c}\n```" for c in initial_cmds)
            ),
            "timestamp": _now_iso(),
            "suggested_commands": initial_cmds,
        })

    terminal_sessions[session_id] = session
    return session


@router.get("/sessions")
async def list_sessions():
    """Return lightweight summaries of every session."""
    summaries = []
    for sid, s in terminal_sessions.items():
        summaries.append(
            SessionSummary(
                session_id=sid,
                name=s["name"],
                target=s["target"],
                template_id=s["template_id"],
                status=s["status"],
                created_at=s["created_at"],
                messages_count=len(s["messages"]),
                commands_count=len(s["command_history"]),
            ).model_dump()
        )
    return summaries


@router.get("/sessions/{session_id}")
async def get_session(session_id: str):
    """Return the full session including messages, commands, and exploitation path."""
    return _get_session(session_id)


@router.delete("/sessions/{session_id}")
async def delete_session(session_id: str):
    """Delete a terminal session and its Kali container."""
    if session_id not in terminal_sessions:
        raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

    # Destroy associated Kali container
    sandbox = session_sandboxes.pop(session_id, None)
    if sandbox:
        try:
            from core.container_pool import get_pool
            pool = get_pool()
            await pool.destroy(f"terminal-{session_id}")
        except Exception as exc:
            logger.warning(f"Failed to destroy container for session {session_id}: {exc}")

    del terminal_sessions[session_id]
    return {"status": "deleted", "session_id": session_id}


# ---------------------------------------------------------------------------
# AI message interaction
# ---------------------------------------------------------------------------

@router.post("/sessions/{session_id}/message")
async def send_message(session_id: str, req: MessageRequest):
    """Send a user prompt to the AI and receive a response with suggested commands."""
    session = _get_session(session_id)
    user_message = req.message.strip()
    if not user_message:
        raise HTTPException(status_code=400, detail="Message content cannot be empty")

    # Record user message
    session["messages"].append({
        "role": "user",
        "content": user_message,
        "timestamp": _now_iso(),
        "metadata": {},
    })

    # Determine system prompt
    template_id = session.get("template_id")
    if template_id and template_id in TEMPLATES:
        system_prompt = TEMPLATES[template_id]["system_prompt"]
    else:
        system_prompt = (
            "You are an expert infrastructure penetration tester. Help the "
            "operator plan and execute attacks against the target. Suggest "
            "concrete commands, explain their purpose, and interpret output. "
            "Always wrap commands in fenced code blocks so they can be extracted."
        )

    # Build context window
    context_messages = session["messages"][-20:]
    context_cmds = session["command_history"][-10:]
    exploitation = session["exploitation_path"]
    context = _build_context_string(context_messages, context_cmds, exploitation)

    # Call LLM
    try:
        llm = LLMManager()
        prompt = f"{context}\n\nUser: {user_message}"
        response = await llm.generate(prompt, system_prompt)
    except Exception as exc:
        raise HTTPException(status_code=502, detail=f"LLM call failed: {exc}")

    suggested_commands = _extract_suggested_commands(response)

    # Record assistant response
    session["messages"].append({
        "role": "assistant",
        "content": response,
        "timestamp": _now_iso(),
        "suggested_commands": suggested_commands,
    })

    return MessageResponse(
        role="assistant",
        response=response,
        timestamp=session["messages"][-1]["timestamp"],
        suggested_commands=suggested_commands,
    ).model_dump()


# ---------------------------------------------------------------------------
# Command execution
# ---------------------------------------------------------------------------

@router.post("/sessions/{session_id}/execute")
async def execute_command(session_id: str, req: ExecuteCommandRequest):
    """Execute a command in the Docker sandbox (fallback: direct shell)."""
    session = _get_session(session_id)
    command = req.command.strip()
    if not command:
        raise HTTPException(status_code=400, detail="Command cannot be empty")

    start = time.time()
    stdout = ""
    stderr = ""
    exit_code = -1
    execution_method = "direct"

    # Use requested execution method
    use_sandbox = req.execution_method == "sandbox"

    if use_sandbox:
        # Prefer session's own Kali container
        sandbox = session_sandboxes.get(session_id)
        if sandbox and sandbox.is_available:
            try:
                result = await sandbox.execute_raw(command)
                stdout = result.stdout
                stderr = result.stderr
                exit_code = result.exit_code
                execution_method = "kali-sandbox"
            except Exception:
                pass

        # Fallback to shared sandbox
        if execution_method == "direct":
            try:
                shared = await get_sandbox()
                if shared and shared.is_available:
                    result = await shared.execute_raw(command)
                    stdout = result.stdout
                    stderr = result.stderr
                    exit_code = result.exit_code
                    execution_method = "sandbox"
            except Exception:
                pass

    # Fallback or direct execution requested
    if execution_method not in ("kali-sandbox", "sandbox"):
        try:
            proc = await asyncio.create_subprocess_shell(
                command,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            raw_stdout, raw_stderr = await asyncio.wait_for(
                proc.communicate(), timeout=120
            )
            stdout = raw_stdout.decode(errors="replace")
            stderr = raw_stderr.decode(errors="replace")
            exit_code = proc.returncode or 0
            execution_method = "direct"
        except asyncio.TimeoutError:
            stderr = "Command timed out after 120 seconds"
            exit_code = 124
        except Exception as exc:
            stderr = str(exc)
            exit_code = 1

    duration = round(time.time() - start, 3)

    cmd_record = {
        "command": command,
        "exit_code": exit_code,
        "stdout": stdout,
        "stderr": stderr,
        "duration": duration,
        "execution_method": execution_method,
        "timestamp": _now_iso(),
    }
    session["command_history"].append(cmd_record)

    # Mirror into messages for AI context continuity
    output_preview = stdout[:2000] if stdout else stderr[:2000]
    session["messages"].append({
        "role": "tool",
        "content": f"$ {command}\n[exit {exit_code}] ({execution_method}, {duration}s)\n{output_preview}",
        "timestamp": cmd_record["timestamp"],
        "metadata": {"exit_code": exit_code, "execution_method": execution_method},
    })

    return CommandResult(**cmd_record).model_dump()


# ---------------------------------------------------------------------------
# Exploitation path
# ---------------------------------------------------------------------------

@router.post("/sessions/{session_id}/exploitation-path")
async def add_exploitation_step(session_id: str, req: ExploitationStepRequest):
    """Add a manual step to the exploitation path timeline."""
    session = _get_session(session_id)

    valid_types = {"recon", "exploit", "pivot", "escalate", "action"}
    if req.step_type not in valid_types:
        raise HTTPException(
            status_code=400,
            detail=f"step_type must be one of {sorted(valid_types)}",
        )

    step = {
        "description": req.description,
        "command": req.command or "",
        "result": req.result or "",
        "timestamp": _now_iso(),
        "step_type": req.step_type,
    }
    session["exploitation_path"].append(step)
    return step


@router.get("/sessions/{session_id}/exploitation-path")
async def get_exploitation_path(session_id: str):
    """Return the full exploitation path timeline."""
    session = _get_session(session_id)
    return session["exploitation_path"]


# ---------------------------------------------------------------------------
# VPN management
# ---------------------------------------------------------------------------

@router.post("/sessions/{session_id}/vpn/upload")
async def upload_vpn_config(
    session_id: str,
    ovpn_file: UploadFile = File(...),
    username: Optional[str] = Form(None),
    password: Optional[str] = Form(None),
):
    """Upload .ovpn config and optionally credentials into the session's container."""
    session = _get_session(session_id)
    sandbox = session_sandboxes.get(session_id)

    if not sandbox or not sandbox.is_available:
        raise HTTPException(
            status_code=503,
            detail="No Kali container available for this session.",
        )

    content = await ovpn_file.read()
    if len(content) > 1_000_000:
        raise HTTPException(status_code=400, detail="File too large (max 1MB)")
    if not (ovpn_file.filename or "").endswith((".ovpn", ".conf")):
        raise HTTPException(status_code=400, detail="File must be .ovpn or .conf")

    # Upload config to container
    dest = "/etc/openvpn/client.ovpn"
    ok = await sandbox.upload_file(content, dest)
    if not ok:
        raise HTTPException(status_code=500, detail="Failed to upload config to container")

    # Write auth file if credentials provided
    if username and password:
        auth_bytes = f"{username}\n{password}\n".encode()
        await sandbox.upload_file(auth_bytes, "/etc/openvpn/auth.txt")
        await sandbox._exec("chmod 600 /etc/openvpn/auth.txt", timeout=5)
        await sandbox._exec(
            "grep -q 'auth-user-pass' /etc/openvpn/client.ovpn || "
            "echo 'auth-user-pass /etc/openvpn/auth.txt' >> /etc/openvpn/client.ovpn",
            timeout=5,
        )
        await sandbox._exec(
            "sed -i 's|auth-user-pass$|auth-user-pass /etc/openvpn/auth.txt|' /etc/openvpn/client.ovpn",
            timeout=5,
        )

    session["vpn_config_uploaded"] = True
    return {
        "status": "uploaded",
        "filename": ovpn_file.filename,
        "credentials_set": bool(username),
    }


@router.post("/sessions/{session_id}/vpn/connect")
async def connect_vpn(session_id: str):
    """Start VPN connection using previously uploaded config."""
    session = _get_session(session_id)
    sandbox = session_sandboxes.get(session_id)

    if not sandbox or not sandbox.is_available:
        raise HTTPException(status_code=503, detail="No Kali container for this session")

    if not session.get("vpn_config_uploaded"):
        raise HTTPException(status_code=400, detail="No VPN config uploaded. Upload .ovpn first.")

    # Create TUN device
    await sandbox._exec(
        "mkdir -p /dev/net && "
        "[ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200; "
        "chmod 600 /dev/net/tun",
        timeout=5,
    )

    # Kill any existing VPN
    await sandbox._exec("pkill -9 openvpn 2>/dev/null", timeout=5)

    # Start OpenVPN
    result = await sandbox._exec(
        "openvpn --config /etc/openvpn/client.ovpn --daemon "
        "--log /var/log/openvpn.log --writepid /var/run/openvpn.pid",
        timeout=15,
    )
    if result.exit_code != 0:
        raise HTTPException(
            status_code=500,
            detail=f"OpenVPN failed to start: {result.stderr or result.stdout}",
        )

    # Wait for tunnel (max 20s)
    for _ in range(20):
        await asyncio.sleep(1)
        check = await sandbox._exec("ip addr show tun0 2>/dev/null", timeout=5)
        if check.exit_code == 0 and "inet " in check.stdout:
            match = re.search(r"inet\s+(\d+\.\d+\.\d+\.\d+)", check.stdout)
            ip = match.group(1) if match else None
            vpn = {"connected": True, "ip": ip}
            session["vpn_status"] = vpn
            return {"status": "connected", "ip": ip}

    # Timeout
    log_result = await sandbox._exec("tail -30 /var/log/openvpn.log 2>/dev/null", timeout=5)
    raise HTTPException(
        status_code=504,
        detail=f"VPN connection timed out (20s). Log:\n{(log_result.stdout or '')[-500:]}",
    )


@router.post("/sessions/{session_id}/vpn/disconnect")
async def disconnect_vpn(session_id: str):
    """Kill VPN connection inside the container."""
    session = _get_session(session_id)
    sandbox = session_sandboxes.get(session_id)

    if not sandbox or not sandbox.is_available:
        raise HTTPException(status_code=503, detail="No Kali container for this session")

    await sandbox._exec(
        "kill $(cat /var/run/openvpn.pid 2>/dev/null) 2>/dev/null; "
        "pkill -9 openvpn 2>/dev/null",
        timeout=10,
    )
    session["vpn_status"] = {"connected": False, "ip": None}
    return {"status": "disconnected"}


# ---------------------------------------------------------------------------
# VPN status
# ---------------------------------------------------------------------------

@router.get("/sessions/{session_id}/vpn-status")
async def get_vpn_status(session_id: str):
    """Check VPN status inside the session's Kali container (fallback: host)."""
    session = _get_session(session_id)

    sandbox = session_sandboxes.get(session_id)

    # Check inside container if available
    if sandbox and sandbox.is_available:
        vpn_data = await sandbox.get_vpn_status()
        session["vpn_status"] = vpn_data
        return VPNStatus(
            connected=vpn_data["connected"],
            ip=vpn_data.get("ip"),
            interface=vpn_data.get("interface"),
            container_name=sandbox.container_name,
        ).model_dump()

    # Fallback: check on host (legacy behavior)
    connected = False
    ip_addr: Optional[str] = None

    try:
        proc = await asyncio.create_subprocess_shell(
            "pgrep -a openvpn",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        raw_stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
        if proc.returncode == 0 and raw_stdout.strip():
            connected = True
    except Exception:
        pass

    if connected:
        try:
            proc = await asyncio.create_subprocess_shell(
                "ip addr show tun0",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            raw_stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
            if proc.returncode == 0:
                match = re.search(
                    r"inet\s+(\d+\.\d+\.\d+\.\d+)", raw_stdout.decode(errors="replace")
                )
                if match:
                    ip_addr = match.group(1)
        except Exception:
            pass

    vpn = {"connected": connected, "ip": ip_addr}
    session["vpn_status"] = vpn
    return VPNStatus(**vpn).model_dump()
