"""Spens CLI entry point.

Usage:
    spens init [OPTIONS]
    spens <environment> <agent> <workspace> [prompt] [OPTIONS]
    spens list
    spens status <session-id> [--spens-dir PATH]
    spens cancel <session-id> [--spens-dir PATH]
    spens attach <session-id> [--spens-dir PATH]
    spens log-viewer [--port PORT] [--no-browser] [--spens-dir PATH] [workspace]
    spens pricing [--refresh] [--vendor] [--model ID] [--offline]

The control subcommands (status / cancel / attach) are dispatched before the
run-session parser, so they need no environment/agent/workspace arguments.
"""

from __future__ import annotations

import argparse
import shutil
import sys
from pathlib import Path

from spens import events
from spens.runner import SpensUsageError, run_session
from spens.sinks import OUTPUT_MODES
from spens.templates import list_agents, list_environments, load_agent

CONTROL_COMMANDS = ("status", "cancel", "attach")


def _check_docker() -> bool:
    return shutil.which("docker") is not None


def _print_list() -> None:
    print("Available environments:")
    for name, base_image in list_environments():
        print(f"  {name:<16} ({base_image})")
    print("\nAvailable agents:")
    for name in list_agents():
        print(f"  {name}")


def _emit_bare_error(message: str, session_id: str = "") -> None:
    """A bare ``error`` event on stdout -- no session files exist.

    Used when ``--output jsonl|background`` validation fails before any
    session state is created, so machine consumers get a coherent terminal
    event instead of a human-readable traceback.
    """
    event = events.Event(
        event="error",
        session_id=session_id,
        timestamp=events.utc_now_iso(),
        data={"message": message},
    )
    print(event.to_line())
    sys.stdout.flush()


def _report_usage_error(exc: SpensUsageError, output_mode: str) -> None:
    """Report a usage error in the mode-appropriate machine/human form."""
    if output_mode in ("jsonl", "background"):
        _emit_bare_error(f"[spens] Error: {exc}")
    else:
        print(f"[spens] Error: {exc}")


def _build_run_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="spens",
        description="Run and orchestrate agents in a safe environment with full audit control.",
        usage=(
            "spens <environment> <agent> <workspace> [prompt]   |   spens list   |   "
            "spens status/cancel/attach <session-id>   |   spens log-viewer   |   spens pricing"
        ),
    )
    parser.add_argument(
        "environment", nargs="?",
        help='Environment template name (e.g. node-20), "list", or "log-viewer"',
    )
    parser.add_argument("agent", nargs="?", default=None, help="Agent template name (e.g. opencode)")
    parser.add_argument("workspace", nargs="?", default=None, help="Workspace path to mount")
    parser.add_argument(
        "prompt", nargs="?", default=None,
        help="Optional prompt to run the agent in yolo (non-interactive) mode. Quote multi-word prompts.",
    )
    parser.add_argument(
        "--rebuild", action="store_true",
        help="Rebuild Docker images from scratch (no cache).",
    )
    parser.add_argument(
        "--port", type=int, default=7331,
        help="Port for log-viewer web server (default: 7331).",
    )
    parser.add_argument(
        "--host", default="127.0.0.1",
        help="Host for log-viewer web server (default: 127.0.0.1).",
    )
    parser.add_argument(
        "--no-browser", action="store_true",
        help="Don't automatically open a browser for log-viewer.",
    )
    parser.add_argument(
        "--spens-dir",
        default=None,
        metavar="PATH",
        help=(
            "Directory where spens stores session state (traces, audit data, "
            "summaries). Default: <workspace>/.spens. For log-viewer: defaults "
            "to searching for .spens near the workspace."
        ),
    )
    parser.add_argument(
        "--accept-changes", action="store_true",
        help="Auto-accept workspace changes after the session (skips the nono rollback review prompt).",
    )
    parser.add_argument(
        "--reject-changes", action="store_true",
        help=(
            "Reject workspace changes after the session: the agent entrypoint "
            "runs unattended and rolls the workspace back to its pre-session "
            "state (inside the container) once the agent exits."
        ),
    )
    parser.add_argument(
        "--output", default="tty", choices=list(OUTPUT_MODES),
        help=(
            "Output mode: 'tty' (default, today's behavior), 'jsonl' (stream "
            "JSONL events to stdout) or 'background' (return immediately, the "
            "session runs detached; see spens status/cancel/attach)."
        ),
    )
    parser.add_argument(
        "--session-id", default=None, metavar="ID",
        help=(
            "Use a specific session id instead of a random one (used for the "
            "sessions folder, Docker object names, etc.). Must match "
            "^[a-z0-9][a-z0-9-]{0,31}$; reusing an existing id is an error."
        ),
    )
    parser.add_argument(
        "--_reuse-session-id", default=None, metavar="ID",
        help=argparse.SUPPRESS,  # internal: background-mode child only
    )
    return parser


def main() -> None:
    argv = sys.argv[1:]

    if argv and argv[0] == "init":
        from spens.init import run_init

        sys.exit(run_init(argv[1:]))

    if argv and argv[0] == "pricing":
        from spens.pricing import run_pricing_cli

        sys.exit(run_pricing_cli(argv[1:]))

    if argv and argv[0] in CONTROL_COMMANDS:
        from spens.control import run_control_command

        sys.exit(run_control_command(argv[0], argv[1:]))

    parser = _build_run_parser()
    args = parser.parse_args(argv)

    if not args.environment:
        parser.print_help()
        return

    if args.environment == "list":
        _print_list()
        return

    if args.environment == "log-viewer":
        from spens.viewer import start_viewer

        workspace = Path(args.workspace) if args.workspace else None
        spens_dir = Path(args.spens_dir) if args.spens_dir else None
        start_viewer(
            host=args.host,
            port=args.port,
            workspace=workspace,
            open_browser=not args.no_browser,
            spens_dir=spens_dir,
        )
        return

    if not args.agent or not args.workspace:
        parser.error(
            "agent and workspace are required when environment is not 'list', "
            "'log-viewer' or a control command"
        )

    if not _check_docker():
        print("[spens] Error: docker not found on PATH. Please install Docker and try again.")
        sys.exit(1)

    reuse_session_id = args._reuse_session_id
    session_id = reuse_session_id or args.session_id

    # Non-interactive-mode validation BEFORE creating any session state: a
    # prompt (yolo mode) with a yolo-capable agent and exactly one change
    # decision.  Needs the agent template, but no session files are touched.
    if args.output in ("jsonl", "background"):
        try:
            agent = load_agent(args.agent, Path(args.workspace).resolve())
        except Exception as exc:  # noqa: BLE001 -- template errors are usage errors here
            _emit_bare_error(f"[spens] Error: {exc}")
            sys.exit(1)
        try:
            from spens.runner import validate_non_interactive

            validate_non_interactive(
                args.output, args.prompt, agent,
                args.accept_changes, args.reject_changes,
            )
        except SpensUsageError as exc:
            _report_usage_error(exc, args.output)
            sys.exit(1)

    try:
        if args.output == "background":
            from spens.background import launch_background_session

            sid = launch_background_session(
                args.environment, args.agent, args.workspace,
                prompt=args.prompt,
                accept_changes=args.accept_changes,
                reject_changes=args.reject_changes,
                spens_dir=args.spens_dir,
                session_id=session_id,
                no_cache=args.rebuild,
            )
            # Machine-readable: the session id, and nothing else, on stdout.
            print(sid)
            sys.exit(0)

        exit_code = run_session(
            args.environment, args.agent, Path(args.workspace),
            no_cache=args.rebuild,
            prompt=args.prompt,
            accept_changes=args.accept_changes,
            reject_changes=args.reject_changes,
            spens_dir=args.spens_dir,
            output_mode=args.output,
            session_id=session_id,
            reuse_session_id=bool(reuse_session_id),
        )
        sys.exit(exit_code)
    except SpensUsageError as exc:
        _report_usage_error(exc, args.output)
        sys.exit(1)
