"""Integration management endpoints — list, detail, config CRUD, lifecycle, logs."""

import asyncio
import logging
import os
import shlex
import signal
import subprocess
from datetime import datetime, timezone
from pathlib import Path

from fastapi import APIRouter, Body, Depends, HTTPException, Request
from starlette.websockets import WebSocket, WebSocketDisconnect

from strawpot.config import get_strawpot_home
from strawpot.context import parse_frontmatter

from strawpot_gui.db import get_db_conn
from strawpot_gui.routers.logs import read_log_delta, read_log_tail
from strawpot_gui.routers.registry import run_strawhub
from strawpot_gui.sse import watch_dir

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/integrations", tags=["integrations"])

MANIFEST = "INTEGRATION.md"


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


def parse_integration_manifest(manifest_path: Path) -> tuple[dict, str]:
    """Parse an INTEGRATION.md file and return (frontmatter, body)."""
    text = manifest_path.read_text(encoding="utf-8")
    parsed = parse_frontmatter(text)
    return parsed.get("frontmatter", {}), parsed.get("body", "")


def scan_integrations(base_dir: Path) -> list[dict]:
    """Scan ``base_dir/integrations/*/INTEGRATION.md`` for installed integrations."""
    scan_path = base_dir / "integrations"
    if not scan_path.is_dir():
        return []
    items: list[dict] = []
    for entry in sorted(scan_path.iterdir()):
        if not entry.is_dir() or entry.name.startswith("."):
            continue
        manifest_path = entry / MANIFEST
        if not manifest_path.is_file():
            continue
        try:
            fm, _body = parse_integration_manifest(manifest_path)
        except Exception:
            fm = {}
        strawpot_meta = fm.get("metadata", {}).get("strawpot", {})
        items.append({
            "name": fm.get("name", entry.name),
            "description": fm.get("description", ""),
            "entry_point": strawpot_meta.get("entry_point", ""),
            "auto_start": strawpot_meta.get("auto_start", False),
            "env_schema": strawpot_meta.get("env", {}),
            "health_check": strawpot_meta.get("health_check"),
            "path": str(entry),
        })
    return items


def _get_db_state(conn, name: str) -> dict | None:
    """Read runtime state from the integrations DB table."""
    row = conn.execute(
        "SELECT status, pid, auto_start, last_error, started_at, created_at "
        "FROM integrations WHERE name = ?",
        (name,),
    ).fetchone()
    if row is None:
        return None
    return dict(row)


def _get_db_config(conn, name: str) -> dict[str, str]:
    """Read saved config values from integration_config table."""
    rows = conn.execute(
        "SELECT key, value FROM integration_config WHERE integration_name = ?",
        (name,),
    ).fetchall()
    return {r["key"]: r["value"] for r in rows}


def _ensure_db_row(conn, name: str) -> None:
    """Insert a row into integrations if it doesn't exist yet."""
    conn.execute(
        "INSERT OR IGNORE INTO integrations (name) VALUES (?)",
        (name,),
    )


def _merge_integration(manifest: dict, db_state: dict | None, db_config: dict) -> dict:
    """Merge filesystem manifest with DB runtime state."""
    result = {**manifest}
    if db_state:
        result["status"] = db_state["status"]
        result["pid"] = db_state["pid"]
        result["last_error"] = db_state["last_error"]
        result["started_at"] = db_state["started_at"]
    else:
        result["status"] = "stopped"
        result["pid"] = None
        result["last_error"] = None
        result["started_at"] = None
    result["config_values"] = db_config
    return result


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@router.get("")
def list_integrations(conn=Depends(get_db_conn)):
    """List all installed integrations with their runtime state."""
    home = get_strawpot_home()
    manifests = scan_integrations(home)
    results = []
    for manifest in manifests:
        name = manifest["name"]
        db_state = _get_db_state(conn, name)
        db_config = _get_db_config(conn, name)
        results.append(_merge_integration(manifest, db_state, db_config))
    return results


@router.get("/{name}")
def get_integration(name: str, conn=Depends(get_db_conn)):
    """Get detail for a single integration."""
    home = get_strawpot_home()
    integration_dir = home / "integrations" / name
    manifest_path = integration_dir / MANIFEST
    if not manifest_path.is_file():
        raise HTTPException(404, f"Integration not found: {name}")

    fm, body = parse_integration_manifest(manifest_path)
    strawpot_meta = fm.get("metadata", {}).get("strawpot", {})
    manifest = {
        "name": fm.get("name", name),
        "description": fm.get("description", ""),
        "entry_point": strawpot_meta.get("entry_point", ""),
        "auto_start": strawpot_meta.get("auto_start", False),
        "env_schema": strawpot_meta.get("env", {}),
        "health_check": strawpot_meta.get("health_check"),
        "path": str(integration_dir),
        "body": body,
        "frontmatter": fm,
    }

    db_state = _get_db_state(conn, name)
    db_config = _get_db_config(conn, name)
    return _merge_integration(manifest, db_state, db_config)


@router.get("/{name}/config")
def get_integration_config(name: str, conn=Depends(get_db_conn)):
    """Get config schema and saved values for an integration."""
    home = get_strawpot_home()
    manifest_path = home / "integrations" / name / MANIFEST
    if not manifest_path.is_file():
        raise HTTPException(404, f"Integration not found: {name}")

    fm, _ = parse_integration_manifest(manifest_path)
    env_schema = fm.get("metadata", {}).get("strawpot", {}).get("env", {})
    config_values = _get_db_config(conn, name)

    return {
        "env_schema": env_schema,
        "config_values": config_values,
    }


@router.put("/{name}/config")
def put_integration_config(
    name: str, data: dict = Body(...), conn=Depends(get_db_conn)
):
    """Save config values for an integration."""
    home = get_strawpot_home()
    manifest_path = home / "integrations" / name / MANIFEST
    if not manifest_path.is_file():
        raise HTTPException(404, f"Integration not found: {name}")

    config_values = data.get("config_values", {})
    if not isinstance(config_values, dict):
        raise HTTPException(400, "config_values must be a dict")

    _ensure_db_row(conn, name)

    # Delete existing config and re-insert
    conn.execute(
        "DELETE FROM integration_config WHERE integration_name = ?", (name,)
    )
    for key, value in config_values.items():
        conn.execute(
            "INSERT INTO integration_config (integration_name, key, value) "
            "VALUES (?, ?, ?)",
            (name, key, str(value) if value is not None else None),
        )

    return {"ok": True}


@router.delete("/{name}/config")
def delete_integration_config(name: str, conn=Depends(get_db_conn)):
    """Clear all saved config for an integration."""
    conn.execute(
        "DELETE FROM integration_config WHERE integration_name = ?", (name,)
    )
    conn.execute("DELETE FROM integrations WHERE name = ?", (name,))
    return {"ok": True}


# ---------------------------------------------------------------------------
# Install / Uninstall via Strawhub
# ---------------------------------------------------------------------------


@router.post("/install")
def install_integration(data: dict = Body(...)):
    """Install an integration from Strawhub by name."""
    name = data.get("name", "").strip()
    if not name:
        raise HTTPException(400, "'name' is required")
    return run_strawhub("install", "integration", "-y", name, "--global")


@router.delete("/{name}")
def uninstall_integration(name: str, conn=Depends(get_db_conn)):
    """Stop (if running) and uninstall an integration."""
    # Stop the adapter process if it's running
    db_state = _get_db_state(conn, name)
    if db_state and db_state["status"] == "running" and db_state["pid"]:
        if _is_process_alive(db_state["pid"]):
            try:
                os.kill(db_state["pid"], signal.SIGTERM)
            except (ProcessLookupError, OSError):
                pass
        conn.execute(
            "UPDATE integrations SET status = 'stopped', pid = NULL WHERE name = ?",
            (name,),
        )

    # Remove DB state
    conn.execute("DELETE FROM integration_config WHERE integration_name = ?", (name,))
    conn.execute("DELETE FROM integrations WHERE name = ?", (name,))

    return run_strawhub("uninstall", "integration", name, "--global")


# ---------------------------------------------------------------------------
# Lifecycle endpoints
# ---------------------------------------------------------------------------


def _is_process_alive(pid: int) -> bool:
    """Check if a process with the given PID is still alive."""
    try:
        os.kill(pid, 0)
        return True
    except (ProcessLookupError, OSError):
        return False


def _build_env(name: str, config_values: dict, request: Request) -> dict:
    """Build environment variables for the adapter subprocess."""
    env = os.environ.copy()

    # Add common PATH entries
    home = Path.home()
    extra_paths = [
        str(home / ".local" / "bin"),
        "/usr/local/bin",
        "/opt/homebrew/bin",
    ]
    existing = env.get("PATH", "")
    env["PATH"] = ":".join(extra_paths) + ":" + existing

    # Derive API URL from the running server
    server_host = os.environ.get("STRAWPOT_GUI_HOST", "127.0.0.1")
    server_port = os.environ.get("STRAWPOT_GUI_PORT", "52532")
    env["STRAWPOT_API_URL"] = f"http://{server_host}:{server_port}"

    # Pass saved env values directly (keys are already env var names)
    for key, value in config_values.items():
        if value is not None:
            env[key] = str(value)

    return env


def _resolve_manifest(name: str) -> tuple[Path, dict]:
    """Resolve integration directory and parsed manifest. Raises HTTPException(404)."""
    home = get_strawpot_home()
    integration_dir = home / "integrations" / name
    manifest_path = integration_dir / MANIFEST
    if not manifest_path.is_file():
        raise HTTPException(404, f"Integration not found: {name}")
    fm, _ = parse_integration_manifest(manifest_path)
    return integration_dir, fm


@router.post("/{name}/start")
def start_integration(name: str, request: Request, conn=Depends(get_db_conn)):
    """Start an integration adapter subprocess."""
    integration_dir, fm = _resolve_manifest(name)
    strawpot_meta = fm.get("metadata", {}).get("strawpot", {})
    entry_point = strawpot_meta.get("entry_point", "")
    if not entry_point:
        raise HTTPException(422, f"Integration '{name}' has no entry_point")

    _ensure_db_row(conn, name)

    # Check if already running
    db_state = _get_db_state(conn, name)
    if db_state and db_state["status"] == "running" and db_state["pid"]:
        if _is_process_alive(db_state["pid"]):
            raise HTTPException(409, f"Integration '{name}' is already running (pid {db_state['pid']})")
        # PID stale — clean up below

    config_values = _get_db_config(conn, name)
    env = _build_env(name, config_values, request)

    # Log file for adapter output
    log_path = integration_dir / ".log"

    try:
        log_file = open(log_path, "a", encoding="utf-8")
        cmd = shlex.split(entry_point)
        proc = subprocess.Popen(
            cmd,
            cwd=str(integration_dir),
            start_new_session=True,
            stdout=log_file,
            stderr=log_file,
            stdin=subprocess.DEVNULL,
            env=env,
        )
    except OSError as exc:
        conn.execute(
            "UPDATE integrations SET status = 'error', last_error = ?, pid = NULL "
            "WHERE name = ?",
            (str(exc), name),
        )
        raise HTTPException(500, f"Failed to start integration: {exc}")

    now = datetime.now(timezone.utc).isoformat()
    conn.execute(
        "UPDATE integrations SET status = 'running', pid = ?, last_error = NULL, "
        "started_at = ? WHERE name = ?",
        (proc.pid, now, name),
    )
    logger.info("Started integration '%s' (pid %d)", name, proc.pid)
    return {"name": name, "status": "running", "pid": proc.pid}


@router.post("/{name}/stop")
def stop_integration(name: str, conn=Depends(get_db_conn)):
    """Stop a running integration adapter by sending SIGTERM."""
    _resolve_manifest(name)  # verify exists
    _ensure_db_row(conn, name)

    db_state = _get_db_state(conn, name)
    if not db_state or db_state["status"] != "running" or not db_state["pid"]:
        raise HTTPException(409, f"Integration '{name}' is not running")

    pid = db_state["pid"]

    if not _is_process_alive(pid):
        conn.execute(
            "UPDATE integrations SET status = 'stopped', pid = NULL WHERE name = ?",
            (name,),
        )
        return {"name": name, "status": "stopped"}

    try:
        os.kill(pid, signal.SIGTERM)
    except (ProcessLookupError, OSError):
        pass  # race: died between check and kill

    conn.execute(
        "UPDATE integrations SET status = 'stopped', pid = NULL WHERE name = ?",
        (name,),
    )
    logger.info("Stopped integration '%s' (pid %d)", name, pid)
    return {"name": name, "status": "stopped"}


@router.get("/{name}/status")
def get_integration_status(name: str, conn=Depends(get_db_conn)):
    """Check live status of an integration (process alive + optional health check)."""
    _resolve_manifest(name)
    db_state = _get_db_state(conn, name)
    if not db_state:
        return {"name": name, "status": "stopped", "pid": None}

    pid = db_state["pid"]
    if pid and not _is_process_alive(pid):
        # Process died — update DB
        conn.execute(
            "UPDATE integrations SET status = 'error', "
            "last_error = 'Process exited unexpectedly' WHERE name = ?",
            (name,),
        )
        return {
            "name": name,
            "status": "error",
            "pid": pid,
            "last_error": "Process exited unexpectedly",
        }

    return {
        "name": name,
        "status": db_state["status"],
        "pid": pid,
        "last_error": db_state["last_error"],
        "started_at": db_state["started_at"],
    }


# ---------------------------------------------------------------------------
# Auto-start support
# ---------------------------------------------------------------------------


def mark_orphaned_integrations_stopped(db_path: str) -> None:
    """Mark integrations whose PID is dead as stopped. Called at startup."""
    from strawpot_gui.db import get_db

    with get_db(db_path) as conn:
        rows = conn.execute(
            "SELECT name, pid FROM integrations WHERE status = 'running' AND pid IS NOT NULL"
        ).fetchall()
        for row in rows:
            if not _is_process_alive(row["pid"]):
                conn.execute(
                    "UPDATE integrations SET status = 'stopped', pid = NULL WHERE name = ?",
                    (row["name"],),
                )
                logger.info(
                    "Marked orphaned integration '%s' (pid %d) as stopped",
                    row["name"], row["pid"],
                )


def auto_start_integrations(db_path: str) -> None:
    """Start all integrations with auto_start enabled. Called during app lifespan."""
    from strawpot_gui.db import get_db

    home = get_strawpot_home()
    manifests = scan_integrations(home)

    for manifest in manifests:
        if not manifest.get("auto_start"):
            continue
        name = manifest["name"]
        entry_point = manifest.get("entry_point", "")
        if not entry_point:
            continue

        integration_dir = Path(manifest["path"])
        with get_db(db_path) as conn:
            _ensure_db_row(conn, name)
            db_state = _get_db_state(conn, name)
            if db_state and db_state["status"] == "running" and db_state["pid"]:
                if _is_process_alive(db_state["pid"]):
                    continue  # already running

            config_values = _get_db_config(conn, name)

        # Build env without request context
        env = os.environ.copy()
        home_dir = Path.home()
        extra_paths = [
            str(home_dir / ".local" / "bin"),
            "/usr/local/bin",
            "/opt/homebrew/bin",
        ]
        env["PATH"] = ":".join(extra_paths) + ":" + env.get("PATH", "")
        server_host = os.environ.get("STRAWPOT_GUI_HOST", "127.0.0.1")
        server_port = os.environ.get("STRAWPOT_GUI_PORT", "52532")
        env["STRAWPOT_API_URL"] = f"http://{server_host}:{server_port}"
        for key, value in config_values.items():
            if value is not None:
                env[key] = str(value)

        log_path = integration_dir / ".log"
        try:
            log_file = open(log_path, "a", encoding="utf-8")
            cmd = shlex.split(entry_point)
            proc = subprocess.Popen(
                cmd,
                cwd=str(integration_dir),
                start_new_session=True,
                stdout=log_file,
                stderr=log_file,
                stdin=subprocess.DEVNULL,
                env=env,
            )
        except OSError as exc:
            logger.warning("Failed to auto-start integration '%s': %s", name, exc)
            with get_db(db_path) as conn:
                conn.execute(
                    "UPDATE integrations SET status = 'error', last_error = ? "
                    "WHERE name = ?",
                    (str(exc), name),
                )
            continue

        now = datetime.now(timezone.utc).isoformat()
        with get_db(db_path) as conn:
            conn.execute(
                "UPDATE integrations SET status = 'running', pid = ?, "
                "last_error = NULL, started_at = ? WHERE name = ?",
                (proc.pid, now, name),
            )
        logger.info("Auto-started integration '%s' (pid %d)", name, proc.pid)


# ---------------------------------------------------------------------------
# WebSocket: integration log streaming
# ---------------------------------------------------------------------------


@router.websocket("/{name}/logs/ws")
async def integration_logs_ws(websocket: WebSocket, name: str) -> None:
    """Stream integration adapter logs over WebSocket.

    Protocol (server → client):
      {"type": "log_snapshot", "lines": [...], "offset": N}
      {"type": "log_delta",    "lines": [...], "offset": N}
      {"type": "log_done"}  — integration stopped, stream ends
      {"type": "error", "message": "..."}
    """
    home = get_strawpot_home()
    integration_dir = home / "integrations" / name
    manifest_path = integration_dir / MANIFEST
    if not manifest_path.is_file():
        await websocket.accept()
        await websocket.send_json({"type": "error", "message": f"Integration not found: {name}"})
        await websocket.close(code=4004)
        return

    log_path = str(integration_dir / ".log")

    await websocket.accept()

    # Send initial snapshot
    lines, offset = read_log_tail(log_path)
    await websocket.send_json({
        "type": "log_snapshot",
        "lines": lines,
        "offset": offset,
    })

    # Check if integration is currently running
    db_path: str = websocket.app.state.db_path
    from strawpot_gui.db import get_db
    with get_db(db_path) as conn:
        db_state = _get_db_state(conn, name)

    is_running = (
        db_state is not None
        and db_state["status"] == "running"
        and db_state["pid"]
        and _is_process_alive(db_state["pid"])
    )

    if not is_running:
        await websocket.send_json({"type": "log_done"})
        await websocket.close()
        return

    # Watch for log changes
    stop = asyncio.Event()

    async def log_watcher() -> None:
        nonlocal offset
        try:
            async for changed_files in watch_dir(str(integration_dir), stop):
                if not changed_files:
                    # Timeout — check if still running
                    with get_db(db_path) as conn:
                        state = _get_db_state(conn, name)
                    alive = (
                        state is not None
                        and state["status"] == "running"
                        and state["pid"]
                        and _is_process_alive(state["pid"])
                    )
                    if not alive:
                        # Final read
                        new_lines, offset = read_log_delta(log_path, offset)
                        if new_lines:
                            await websocket.send_json({
                                "type": "log_delta",
                                "lines": new_lines,
                                "offset": offset,
                            })
                        await websocket.send_json({"type": "log_done"})
                        return
                    continue

                if any(f.endswith(".log") for f in changed_files):
                    new_lines, offset = read_log_delta(log_path, offset)
                    if new_lines:
                        await websocket.send_json({
                            "type": "log_delta",
                            "lines": new_lines,
                            "offset": offset,
                        })
        except asyncio.CancelledError:
            pass

    async def receiver() -> None:
        """Drain client messages (keep connection alive)."""
        try:
            while True:
                await websocket.receive_text()
        except WebSocketDisconnect:
            stop.set()

    watcher_task = asyncio.create_task(log_watcher())
    receiver_task = asyncio.create_task(receiver())

    try:
        await asyncio.wait(
            {watcher_task, receiver_task},
            return_when=asyncio.FIRST_COMPLETED,
        )
    finally:
        stop.set()
        for task in [watcher_task, receiver_task]:
            task.cancel()
        await asyncio.gather(watcher_task, receiver_task, return_exceptions=True)
        try:
            await websocket.close()
        except Exception:
            pass
