#!/usr/bin/env python3
"""
Host-side Eyesis automation daemon.

Purpose:
- Run this daemon once in the user's desktop session.
- Codex (sandboxed) submits JSON requests via files in a shared queue dir.
- Daemon executes a restricted command set on the host (GUI/session-visible context).

Usage:
  scripts/eyesis_host_daemon.py serve [--queue-dir DIR] [--poll-ms 200]
  scripts/eyesis_host_daemon.py submit --action {ctl,mcp} -- <args...> [--queue-dir DIR] [--wait-sec 120]
  scripts/eyesis_host_daemon.py read --id REQUEST_ID [--queue-dir DIR]

Queue layout (default): attic/session-logs/eyesis-host-daemon/
  requests/*.json
  processing/*.json
  responses/*.json
  archive/*.json
"""

from __future__ import annotations

import argparse
import datetime as dt
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import time
import uuid

REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_QUEUE = REPO_ROOT / "attic" / "session-logs" / "eyesis-host-daemon"
CTL_SCRIPT = REPO_ROOT / "scripts" / "eyesis_mcp_ctl.sh"
MCP_SCRIPT = REPO_ROOT / "scripts" / "mcp_http.sh"

ALLOWED_CTL = {"start", "stop", "restart", "status", "wait"}
ALLOWED_MCP = {"status", "dialog", "button", "set", "submit", "interrupt", "confirm-stop"}


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


def ensure_dirs(base: Path) -> dict[str, Path]:
    dirs = {
        "base": base,
        "requests": base / "requests",
        "processing": base / "processing",
        "responses": base / "responses",
        "archive": base / "archive",
    }
    for p in dirs.values():
        p.mkdir(parents=True, exist_ok=True)
    return dirs


def atomic_write_json(path: Path, payload: dict) -> None:
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    os.replace(tmp, path)


def validate_request(req: dict) -> tuple[bool, str]:
    action = req.get("action")
    args = req.get("args")
    if action not in {"ctl", "mcp"}:
        return False, "action must be 'ctl' or 'mcp'"
    if not isinstance(args, list) or not all(isinstance(x, str) for x in args):
        return False, "args must be a list of strings"
    if not args:
        return False, "args must not be empty"

    first = args[0]
    if action == "ctl" and first not in ALLOWED_CTL:
        return False, f"unsupported ctl subcommand: {first}"
    if action == "mcp" and first not in ALLOWED_MCP:
        return False, f"unsupported mcp subcommand: {first}"
    return True, ""


def build_command(action: str, args: list[str]) -> list[str]:
    if action == "ctl":
        return [str(CTL_SCRIPT), *args]
    return [str(MCP_SCRIPT), *args]


def process_one(req_path: Path, dirs: dict[str, Path]) -> None:
    proc_path = dirs["processing"] / req_path.name
    os.replace(req_path, proc_path)

    req_id = req_path.stem
    started = now_iso()
    result: dict = {
        "id": req_id,
        "started_at": started,
    }

    try:
        req = json.loads(proc_path.read_text(encoding="utf-8"))
        result["request"] = req
        ok, err = validate_request(req)
        if not ok:
            result.update({"ok": False, "exit_code": 2, "error": err, "stdout": "", "stderr": ""})
        else:
            timeout = req.get("timeout_sec", 300)
            if not isinstance(timeout, int) or timeout < 1 or timeout > 3600:
                timeout = 300
            cmd = build_command(req["action"], req["args"])
            cp = subprocess.run(
                cmd,
                cwd=str(REPO_ROOT),
                text=True,
                capture_output=True,
                timeout=timeout,
                check=False,
            )
            result.update(
                {
                    "ok": cp.returncode == 0,
                    "exit_code": cp.returncode,
                    "stdout": cp.stdout,
                    "stderr": cp.stderr,
                    "command": cmd,
                }
            )
    except subprocess.TimeoutExpired as e:
        result.update(
            {
                "ok": False,
                "exit_code": 124,
                "error": f"timeout after {e.timeout}s",
                "stdout": e.stdout or "",
                "stderr": e.stderr or "",
            }
        )
    except Exception as e:
        result.update(
            {
                "ok": False,
                "exit_code": 1,
                "error": f"daemon exception: {e}",
                "stdout": "",
                "stderr": "",
            }
        )
    finally:
        result["finished_at"] = now_iso()

    resp_path = dirs["responses"] / f"{req_id}.json"
    atomic_write_json(resp_path, result)
    shutil.move(str(proc_path), str(dirs["archive"] / proc_path.name))


def cmd_serve(args: argparse.Namespace) -> int:
    dirs = ensure_dirs(Path(args.queue_dir).resolve())
    poll_s = max(0.05, args.poll_ms / 1000.0)

    print(f"eyesis-host-daemon: serving {dirs['base']}")
    print(f"eyesis-host-daemon: ctl script={CTL_SCRIPT}")
    print(f"eyesis-host-daemon: mcp script={MCP_SCRIPT}")
    sys.stdout.flush()

    while True:
        reqs = sorted(dirs["requests"].glob("*.json"))
        if not reqs:
            time.sleep(poll_s)
            continue
        for req in reqs:
            process_one(req, dirs)


def submit_request(queue_dir: Path, action: str, req_args: list[str], timeout_sec: int) -> str:
    dirs = ensure_dirs(queue_dir)
    req_id = uuid.uuid4().hex
    payload = {
        "id": req_id,
        "created_at": now_iso(),
        "action": action,
        "args": req_args,
        "timeout_sec": timeout_sec,
        "cwd": str(REPO_ROOT),
    }
    req_path = dirs["requests"] / f"{req_id}.json"
    atomic_write_json(req_path, payload)
    return req_id


def wait_response(queue_dir: Path, req_id: str, wait_sec: int) -> dict | None:
    resp_path = queue_dir / "responses" / f"{req_id}.json"
    deadline = time.time() + max(1, wait_sec)
    while time.time() < deadline:
        if resp_path.exists():
            return json.loads(resp_path.read_text(encoding="utf-8"))
        time.sleep(0.1)
    return None


def cmd_submit(args: argparse.Namespace) -> int:
    queue_dir = Path(args.queue_dir).resolve()
    req_id = submit_request(queue_dir, args.action, args.req_args, args.timeout_sec)
    print(req_id)
    if args.wait_sec <= 0:
        return 0

    resp = wait_response(queue_dir, req_id, args.wait_sec)
    if resp is None:
        print(json.dumps({"ok": False, "id": req_id, "error": "timeout waiting response"}, indent=2))
        return 124

    # print command outputs in a terminal-friendly way
    stdout = resp.get("stdout", "")
    stderr = resp.get("stderr", "")
    if stdout:
        sys.stdout.write(stdout)
        if not stdout.endswith("\n"):
            sys.stdout.write("\n")
    if stderr:
        sys.stderr.write(stderr)
        if not stderr.endswith("\n"):
            sys.stderr.write("\n")
    return int(resp.get("exit_code", 1))


def cmd_read(args: argparse.Namespace) -> int:
    queue_dir = Path(args.queue_dir).resolve()
    resp_path = queue_dir / "responses" / f"{args.id}.json"
    if not resp_path.exists():
        print(f"response not found: {resp_path}", file=sys.stderr)
        return 1
    print(resp_path.read_text(encoding="utf-8"), end="")
    return 0


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(description="Eyesis host daemon")
    sub = p.add_subparsers(dest="cmd", required=True)

    p_serve = sub.add_parser("serve", help="Run daemon loop")
    p_serve.add_argument("--queue-dir", default=str(DEFAULT_QUEUE))
    p_serve.add_argument("--poll-ms", type=int, default=200)
    p_serve.set_defaults(func=cmd_serve)

    p_submit = sub.add_parser("submit", help="Submit one request")
    p_submit.add_argument("--queue-dir", default=str(DEFAULT_QUEUE))
    p_submit.add_argument("--action", choices=["ctl", "mcp"], required=True)
    p_submit.add_argument("--wait-sec", type=int, default=120)
    p_submit.add_argument("--timeout-sec", type=int, default=300)
    p_submit.add_argument("req_args", nargs=argparse.REMAINDER, help="Request args after '--'")
    p_submit.set_defaults(func=cmd_submit)

    p_read = sub.add_parser("read", help="Read response by id")
    p_read.add_argument("--queue-dir", default=str(DEFAULT_QUEUE))
    p_read.add_argument("--id", required=True)
    p_read.set_defaults(func=cmd_read)
    return p


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()

    if getattr(args, "cmd", "") == "submit":
        if args.req_args and args.req_args[0] == "--":
            args.req_args = args.req_args[1:]
        if not args.req_args:
            print("submit requires request args after '--'", file=sys.stderr)
            return 2

    return args.func(args)


if __name__ == "__main__":
    raise SystemExit(main())
