import os
import re
import shutil
import subprocess
import time
from pathlib import Path
from typing import Any, Dict, List, Tuple


def lk_path() -> str | None:
    return shutil.which("lk")


def _default_template_workdir() -> Path:
    env_dir = os.getenv("LIVEKIT_AGENT_TEMPLATE_WORKDIR")
    if env_dir and str(env_dir).strip():
        return Path(str(env_dir).strip())
    # repo layout: <repo>/backend/agent_runtime
    return Path(__file__).resolve().parents[2] / "agent_runtime"


def _deploy_root_dir() -> Path:
    env_dir = os.getenv("LIVEKIT_AGENT_DEPLOY_ROOT")
    if env_dir and str(env_dir).strip():
        return Path(str(env_dir).strip())
    return _default_template_workdir() / ".lk_deploy"


def _safe_slug(name: str) -> str:
    s = re.sub(r"[^a-zA-Z0-9._-]+", "-", (name or "").strip())
    s = re.sub(r"-{2,}", "-", s).strip("-")
    return s[:64] if s else "bot"


def prepare_workdir_for_bot(bot_name: str) -> Path:
    """
    Create a per-bot workdir for `lk agent create`.

    We copy a template agent directory so each bot can become its own LiveKit Cloud deployment
    without clobbering other deployments.
    """
    template = _default_template_workdir()
    if not template.exists():
        raise FileNotFoundError(f"Agent template workdir not found: {template}")

    root = _deploy_root_dir()
    root.mkdir(parents=True, exist_ok=True)

    slug = _safe_slug(bot_name)
    stamp = time.strftime("%Y%m%d-%H%M%S")
    target = root / f"{slug}-{stamp}"

    def _ignore(_dir: str, names: List[str]) -> set[str]:
        ignore = {
            ".git",
            ".venv",
            "__pycache__",
            ".pytest_cache",
            ".lk_deploy",
            "node_modules",
        }
        return {n for n in names if n in ignore}

    shutil.copytree(template, target, dirs_exist_ok=False, ignore=_ignore)

    # LiveKit Cloud builds use the working-dir as the Docker build context.
    # The repo's Dockerfile assumes repo-root context, so write a cloud-safe Dockerfile here.
    dockerfile = target / "Dockerfile"
    dockerfile.write_text(
        "\n".join(
            [
                "# syntax=docker/dockerfile:1",
                "FROM python:3.12-slim-bookworm",
                "",
                "ENV PYTHONUNBUFFERED=1",
                "ENV PYTHONDONTWRITEBYTECODE=1",
                "",
                "WORKDIR /app",
                "",
                "RUN apt-get update && apt-get install -y --no-install-recommends \\",
                "    build-essential \\",
                "    ffmpeg \\",
                "  && rm -rf /var/lib/apt/lists/*",
                "",
                "# Install dependencies for the agent runtime (this directory is the build context).",
                "COPY requirements.txt /app/requirements.txt",
                "RUN python -m pip install --no-cache-dir -U pip \\",
                " && python -m pip install --no-cache-dir -r /app/requirements.txt",
                "",
                "COPY . /app/agent_runtime",
                "WORKDIR /app/agent_runtime",
                "",
                "RUN python src/agent.py download-files || true",
                "",
                'CMD ["python", "src/agent.py", "dev"]',
                "",
            ]
        ),
        encoding="utf-8",
    )

    # Ensure requirements.txt exists at the context root (lk detection also reads this file).
    req = target / "requirements.txt"
    if not req.exists():
        # Fall back to the repo template requirement name if present.
        alt = target / "requirements.in"
        if alt.exists():
            req.write_text(alt.read_text(encoding="utf-8"), encoding="utf-8")
    return target


def build_lk_args(
    action: str,
    workdir: Path,
    *,
    region: str | None = None,
    config: str | None = None,
    agent_id: str | None = None,
    secrets: Dict[str, Any] | None = None,
    secrets_file: str | None = None,
    silent: bool = True,
) -> Tuple[List[str], List[str]]:
    argv: List[str] = ["lk", "agent", action]
    redacted: List[str] = ["lk", "agent", action]

    if region:
        argv += ["--region", region]
        redacted += ["--region", region]

    if config:
        argv += ["--config", config]
        redacted += ["--config", config]

    if agent_id:
        argv += ["--id", agent_id]
        redacted += ["--id", agent_id]

    if secrets_file:
        argv += ["--secrets-file", secrets_file]
        redacted += ["--secrets-file", secrets_file]

    if secrets:
        for k, v in secrets.items():
            kv = f"{k}={'' if v is None else v}"
            argv += ["--secrets", kv]
            redacted += ["--secrets", f"{k}=***"]

    if silent:
        argv += ["--silent"]
        redacted += ["--silent"]

    argv.append(str(workdir))
    redacted.append(str(workdir))
    return argv, redacted


_AGENT_DEPLOYMENT_ID_RE = re.compile(r"\b((?:CA|sb)_[a-zA-Z0-9]+)\b")


def extract_agent_deployment_id(stdout: str, stderr: str) -> str | None:
    """
    Best-effort extraction of a LiveKit Cloud agent deployment id.

    LiveKit Cloud agent deployments currently look like:
    - CA_... (agent deployment id in the Cloud dashboard)
    - sb_... (older/other identifiers in some outputs)
    """
    txt = "\n".join([stdout or "", stderr or ""])
    m = _AGENT_DEPLOYMENT_ID_RE.search(txt)
    return m.group(1) if m else None


def run_lk_agent(
    action: str,
    workdir: Path,
    *,
    region: str | None = None,
    config: str | None = None,
    agent_id: str | None = None,
    secrets: Dict[str, Any] | None = None,
    secrets_file: str | None = None,
    silent: bool = True,
    timeout_s: int = 1800,
) -> Dict[str, Any]:
    if action not in ("create", "deploy"):
        raise ValueError("action must be 'create' or 'deploy'")

    if not lk_path():
        return {
            "ok": False,
            "exit_code": None,
            "message": "LiveKit CLI (`lk`) is not installed on this server.",
        }

    workdir = Path(workdir)
    if not workdir.exists():
        return {"ok": False, "exit_code": None, "message": f"workdir does not exist: {workdir}"}

    # `lk agent create` can generate livekit.toml; deploy usually requires it.
    if action != "create" and not (workdir / "livekit.toml").exists():
        return {
            "ok": False,
            "exit_code": None,
            "message": "workdir must contain livekit.toml for deploy.",
        }

    argv, redacted = build_lk_args(
        action,
        workdir,
        region=region,
        config=config,
        agent_id=agent_id,
        secrets=secrets,
        secrets_file=secrets_file,
        silent=silent,
    )

    # Workaround for interactive confirmation prompts during `lk agent create`.
    # In CI/non-interactive environments, we always accept defaults.
    stdin = None
    if action == "create":
        stdin = "Y\n"

    try:
        proc = subprocess.run(
            argv,
            cwd=str(workdir),
            capture_output=True,
            text=True,
            input=stdin,
            timeout=max(10, min(int(timeout_s), 3600)),
        )
    except subprocess.TimeoutExpired:
        return {"ok": False, "exit_code": None, "command": redacted, "message": "timeout"}
    except Exception as e:
        return {"ok": False, "exit_code": None, "command": redacted, "message": str(e)}

    dep_id = extract_agent_deployment_id(proc.stdout or "", proc.stderr or "")
    return {
        "ok": proc.returncode == 0,
        "exit_code": proc.returncode,
        "command": redacted,
        "stdout": proc.stdout or "",
        "stderr": proc.stderr or "",
        "agent_id": dep_id,
    }


def list_cloud_agent_deployments() -> list[str]:
    """
    Best-effort list of LiveKit Cloud agent deployment IDs (CA_...) using credentials
    from environment (LIVEKIT_URL/LIVEKIT_API_KEY/LIVEKIT_API_SECRET).

    Returns a list of ids in the order `lk` prints them (typically newest first).
    """
    if not lk_path():
        return []

    url = os.getenv("LIVEKIT_URL") or ""
    api_key = os.getenv("LIVEKIT_API_KEY") or ""
    api_secret = os.getenv("LIVEKIT_API_SECRET") or ""
    if not (url and api_key and api_secret):
        return []

    try:
        proc = subprocess.run(
            [
                "lk",
                "agent",
                "list",
                "--url",
                url,
                "--api-key",
                api_key,
                "--api-secret",
                api_secret,
            ],
            capture_output=True,
            text=True,
            timeout=30,
        )
    except Exception:
        return []

    txt = "\n".join([proc.stdout or "", proc.stderr or ""])
    ids = []
    for m in re.finditer(r"\bCA_[a-zA-Z0-9]+\b", txt):
        ids.append(m.group(0))
    # de-dupe preserving order
    seen = set()
    out: list[str] = []
    for x in ids:
        if x in seen:
            continue
        seen.add(x)
        out.append(x)
    return out

