"""CLI entry point for Staso tooling.

Usage:
    staso setup  --target codex --api-key ak_... [options]
    staso sync   [--target codex]         # sync hooks for all or one agent
    staso --version                      # version + latest available hint
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import tomllib
import warnings
from dataclasses import dataclass
from pathlib import Path


# -- agent registry ------------------------------------------------------------


@dataclass(frozen=True)
class AgentConfig:
    """Configuration for a CLI AI agent's hook integration."""

    name: str  # CLI flag value (e.g. "claude-code")
    module: str  # Python module under staso.* (e.g. "claude_code")
    display_name: str
    hook_events: tuple[str, ...]
    events_with_matcher: tuple[str, ...]
    global_settings: str  # path relative to $HOME
    project_settings: str  # path relative to cwd
    embed_env_in_command: bool = False  # prefix env vars in hook command


CLAUDE_CODE = AgentConfig(
    name="claude-code",
    module="claude_code",
    display_name="Claude Code",
    hook_events=(
        "SessionStart",
        "UserPromptSubmit",
        "PreToolUse",
        "PostToolUse",
        "PostToolUseFailure",
        "Stop",
        "StopFailure",
        "SubagentStart",
        "SubagentStop",
        "SessionEnd",
    ),
    events_with_matcher=("PreToolUse", "PostToolUse", "PostToolUseFailure"),
    global_settings=".claude/settings.json",
    project_settings=".claude/settings.local.json",
)

CODEX = AgentConfig(
    name="codex",
    module="codex",
    display_name="Codex",
    hook_events=(
        "SessionStart",
        "UserPromptSubmit",
        "PreToolUse",
        "PostToolUse",
        "Stop",
    ),
    events_with_matcher=("PreToolUse", "PostToolUse"),
    global_settings=".codex/hooks.json",
    project_settings=".codex/hooks.json",
    embed_env_in_command=True,
)

_AGENTS: dict[str, AgentConfig] = {
    "claude-code": CLAUDE_CODE,
    "codex": CODEX,
}


def get_agent(name: str) -> AgentConfig | None:
    return _AGENTS.get(name)


# -- kept for backward compat (imported by _common/repair.py) -----------------

_HOOK_EVENTS = CLAUDE_CODE.hook_events
_HOOK_EVENTS_WITH_MATCHER = CLAUDE_CODE.events_with_matcher


# -- hooks config builder -----------------------------------------------------


def _hook_entry(command: str, with_matcher: bool = False) -> dict:
    hook: dict = {"type": "command", "command": command}
    if with_matcher:
        return {"matcher": ".*", "hooks": [hook]}
    return {"hooks": [hook]}


def _build_hooks_config(command: str, agent: AgentConfig | None = None) -> dict:
    cfg = agent or CLAUDE_CODE
    hooks: dict = {}
    for event in cfg.hook_events:
        hooks[event] = [_hook_entry(command, with_matcher=event in cfg.events_with_matcher)]
    return hooks


def _merge_settings(existing: dict, new_hooks: dict, new_env: dict) -> dict:
    """Merge hook and env entries into existing settings, preserving unrelated keys."""
    result = dict(existing)

    existing_hooks = result.get("hooks", {})
    for event, entries in new_hooks.items():
        current = [
            e for e in existing_hooks.get(event, [])
            if not _is_staso_hook(e)
        ]
        current.extend(entries)
        existing_hooks[event] = current
    result["hooks"] = existing_hooks

    existing_env = result.get("env", {})
    existing_env.update(new_env)
    result["env"] = existing_env

    return result


def _is_staso_hook(entry: dict) -> bool:
    """Return True if this hook entry was written by Staso setup."""
    if "hooks" in entry:
        return any("staso." in h.get("command", "") for h in entry.get("hooks", []))
    return "staso." in entry.get("command", "")


# -- version with upgrade hint ------------------------------------------------


def _version_string() -> str:
    from staso._version import __version__

    line = f"staso {__version__}"
    latest = _check_latest_version()
    if latest and latest != __version__:
        line += f"  (latest: {latest} -- pip install --upgrade staso)"
    return line


def _check_latest_version() -> str | None:
    """Check PyPI for the latest staso version. Returns None on failure."""
    try:
        import httpx
        resp = httpx.get("https://pypi.org/pypi/staso/json", timeout=2.0)
        if resp.status_code == 200:
            return resp.json().get("info", {}).get("version")
    except Exception:
        pass
    return None


# -- setup command -------------------------------------------------------------


def _ensure_codex_hooks_enabled(scope: str) -> None:
    """Enable codex_hooks feature flag in Codex config.toml."""
    if scope == "global":
        config_path = Path.home() / ".codex" / "config.toml"
    else:
        config_path = Path(".codex") / "config.toml"

    config_path.parent.mkdir(parents=True, exist_ok=True)

    existing: dict = {}
    if config_path.exists():
        try:
            existing = tomllib.loads(config_path.read_text(encoding="utf-8"))
        except (tomllib.TOMLDecodeError, OSError):
            existing = {}

    features = existing.get("features", {})
    if features.get("codex_hooks") is True:
        return

    # Rebuild TOML preserving existing keys
    features["codex_hooks"] = True
    existing["features"] = features
    config_path.write_text(_dict_to_toml(existing), encoding="utf-8")


def _dict_to_toml(data: dict) -> str:
    """Minimal TOML serializer for flat/one-level-nested dicts."""
    lines: list[str] = []
    # Top-level scalars first
    for key, value in data.items():
        if not isinstance(value, dict):
            lines.append(f"{key} = {_toml_value(value)}")
    # Then tables
    for key, value in data.items():
        if isinstance(value, dict):
            lines.append(f"\n[{key}]")
            for k, v in value.items():
                lines.append(f"{k} = {_toml_value(v)}")
    lines.append("")
    return "\n".join(lines)


def _toml_value(v: object) -> str:
    if isinstance(v, bool):
        return "true" if v else "false"
    if isinstance(v, str):
        return f'"{v}"'
    if isinstance(v, (int, float)):
        return str(v)
    return f'"{v}"'


def _resolve_settings_path(agent: AgentConfig, scope: str) -> Path:
    if scope == "global":
        return Path.home() / agent.global_settings
    return Path(agent.project_settings)


def cmd_setup(args: argparse.Namespace) -> int:
    """Write hook configuration for a CLI agent to the chosen settings file."""
    agent_name = getattr(args, "target", None) or "claude-code"
    agent = get_agent(agent_name)
    if agent is None:
        available = ", ".join(sorted(_AGENTS.keys()))
        print(f"error: unknown agent '{agent_name}'. Available: {available}", file=sys.stderr)
        return 1

    api_key = args.api_key or os.environ.get("STASO_API_KEY", "")
    if not api_key:
        print(
            "error: --api-key is required (or set STASO_API_KEY env var)",
            file=sys.stderr,
        )
        return 1

    python = sys.executable
    env_vars: dict[str, str] = {"STASO_API_KEY": api_key}
    if args.base_url:
        env_vars["STASO_BASE_URL"] = args.base_url
    if args.workspace:
        env_vars["STASO_WORKSPACE_SLUG"] = args.workspace
    if args.environment:
        env_vars["STASO_ENVIRONMENT"] = args.environment
    if args.agent_id:
        env_vars["STASO_AGENT_ID"] = args.agent_id
    if getattr(args, "auto_sync", False):
        env_vars["STASO_AUTO_SYNC"] = "1"

    if agent.embed_env_in_command:
        env_prefix = " ".join(f"{k}={v}" for k, v in env_vars.items())
        command = f"{env_prefix} {python} -m staso.{agent.module}"
        settings_env: dict[str, str] = {}
    else:
        command = f"{python} -m staso.{agent.module}"
        settings_env = env_vars

    hooks = _build_hooks_config(command, agent)

    settings_path = _resolve_settings_path(agent, args.scope)

    settings_path.parent.mkdir(parents=True, exist_ok=True)
    existing: dict = {}
    if settings_path.exists():
        try:
            existing = json.loads(settings_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            print(
                f"warning: could not read existing {settings_path}, starting fresh",
                file=sys.stderr,
            )

    merged = _merge_settings(existing, hooks, settings_env)
    settings_path.write_text(json.dumps(merged, indent=2), encoding="utf-8")

    # Agent-specific post-setup
    if agent.name == "codex":
        _ensure_codex_hooks_enabled(args.scope)

    scope_label = f"~/{agent.global_settings}" if args.scope == "global" else agent.project_settings
    print(f"Staso {agent.display_name} hooks configured in {scope_label}")
    print(f"  Python: {python}")
    print(f"  API key: {api_key[:8]}...")
    if args.base_url:
        print(f"  Backend: {args.base_url}")
    if getattr(args, "auto_sync", False):
        print(f"  Auto-sync: enabled")
    print()
    print(f"Start a {agent.display_name} conversation to begin tracing.")
    return 0


# kept for backward compat
def cmd_setup_claude_code(args: argparse.Namespace) -> int:
    """Deprecated: use `staso setup --target claude-code` instead."""
    args.target = "claude-code"
    return cmd_setup(args)


# -- sync command --------------------------------------------------------------


def _friendly_path(path: Path) -> str:
    """Shorten a path for display — replace $HOME with ~."""
    home = str(Path.home())
    s = str(path)
    return s.replace(home, "~") if s.startswith(home) else s


def _sync_agent(agent_name: str, cwd: str) -> None:
    """Sync a single agent's hook config. Prints status."""
    from staso._common.repair import (
        _find_settings_with_staso_hooks,
        _settings_candidates,
        repair_hooks,
        sync_python_path,
    )

    agent = get_agent(agent_name)
    if agent is None:
        return

    candidates = _settings_candidates(agent, cwd)
    settings_path, _ = _find_settings_with_staso_hooks(candidates)
    if settings_path is None:
        return

    label = f"{agent.display_name} ({_friendly_path(settings_path)})"

    path_updated = sync_python_path(agent_name, cwd)
    hooks_synced = repair_hooks(agent_name, cwd)

    if path_updated:
        print(f"  {label}: Python path updated to {sys.executable}")
    if hooks_synced:
        print(f"  {label}: hook events synced.")
    if not path_updated and not hooks_synced:
        print(f"  {label}: already up to date.")


def _toggle_auto_sync(agent_name: str, cwd: str, enable: bool) -> None:
    """Enable or disable STASO_AUTO_SYNC in an agent's hook config."""
    from staso._common.repair import (
        _find_settings_with_staso_hooks,
        _settings_candidates,
    )

    agent = get_agent(agent_name)
    if agent is None:
        return

    candidates = _settings_candidates(agent, cwd)
    settings_path, existing = _find_settings_with_staso_hooks(candidates)
    if settings_path is None:
        print(f"No staso hooks found for {agent.display_name}.")
        return

    label = _friendly_path(settings_path)

    if agent.embed_env_in_command:
        # For Codex: add/remove STASO_AUTO_SYNC=1 in command strings
        hooks = existing.get("hooks", {})
        changed = False
        for event_entries in hooks.values():
            for entry in event_entries:
                if not _is_staso_hook(entry):
                    continue
                for h in entry.get("hooks", [entry]):
                    cmd = h.get("command", "")
                    has_it = "STASO_AUTO_SYNC=1" in cmd
                    if enable and not has_it:
                        h["command"] = f"STASO_AUTO_SYNC=1 {cmd}"
                        changed = True
                    elif not enable and has_it:
                        h["command"] = cmd.replace("STASO_AUTO_SYNC=1 ", "")
                        changed = True
        if changed:
            settings_path.write_text(json.dumps(existing, indent=2), encoding="utf-8")
    else:
        # For Claude Code: set in env section
        env = existing.get("env", {})
        if enable:
            env["STASO_AUTO_SYNC"] = "1"
        else:
            env.pop("STASO_AUTO_SYNC", None)
        existing["env"] = env
        settings_path.write_text(json.dumps(existing, indent=2), encoding="utf-8")

    state = "enabled" if enable else "disabled"
    print(f"  {agent.display_name} ({label}): auto-sync {state}.")


def cmd_sync(args: argparse.Namespace) -> int:
    """Sync hook configuration with the current package version."""
    from staso._common.repair import _find_settings_with_staso_hooks, _settings_candidates

    cwd = os.getcwd()
    target = getattr(args, "target", None)
    enable_auto = getattr(args, "enable_auto_sync", False)
    disable_auto = getattr(args, "disable_auto_sync", False)

    # Handle auto-sync toggle
    if enable_auto or disable_auto:
        targets = [target] if target else list(_AGENTS.keys())
        for name in targets:
            agent = get_agent(name)
            if agent is None:
                continue
            candidates = _settings_candidates(agent, cwd)
            settings_path, _ = _find_settings_with_staso_hooks(candidates)
            if settings_path is not None:
                _toggle_auto_sync(name, cwd, enable_auto)
        return 0

    # Normal sync
    if target:
        agent = get_agent(target)
        if agent is None:
            available = ", ".join(sorted(_AGENTS.keys()))
            print(f"error: unknown agent '{target}'. Available: {available}", file=sys.stderr)
            return 1
        candidates = _settings_candidates(agent, cwd)
        settings_path, _ = _find_settings_with_staso_hooks(candidates)
        if settings_path is None:
            print(f"No staso hooks found for {agent.display_name}. Run 'staso setup --target {target}' first.")
            return 0
        _sync_agent(target, cwd)
    else:
        found = False
        for name, agent in _AGENTS.items():
            candidates = _settings_candidates(agent, cwd)
            settings_path, _ = _find_settings_with_staso_hooks(candidates)
            if settings_path is not None:
                found = True
                _sync_agent(name, cwd)
        if not found:
            print("No staso hook configs found. Run 'staso setup' first.")

    return 0


# -- parser --------------------------------------------------------------------


def _add_setup_args(parser: argparse.ArgumentParser) -> None:
    """Add common setup arguments to a parser."""
    parser.add_argument("--api-key", metavar="KEY", help="Staso API key (ak_...)")
    parser.add_argument("--base-url", metavar="URL", help="Staso backend URL")
    parser.add_argument("--workspace", metavar="SLUG", help="Workspace slug")
    parser.add_argument(
        "--environment",
        metavar="ENV",
        default="production",
        help="Environment name (default: production)",
    )
    parser.add_argument("--agent-id", metavar="ID", help="Agent ID registered in Staso")
    parser.add_argument(
        "--scope",
        choices=["global", "project"],
        default="global",
        help="Write to global or project settings",
    )
    parser.add_argument(
        "--auto-sync",
        action="store_true",
        help="Auto-sync hook config on each session start",
    )


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="staso",
        description="Staso CLI",
    )
    parser.add_argument("--version", action="version", version=_version_string())
    sub = parser.add_subparsers(dest="command")

    # Primary: staso setup --target <name>
    setup = sub.add_parser(
        "setup",
        help="Configure hooks for a CLI AI agent to send traces to Staso",
    )
    setup.add_argument(
        "--target",
        choices=sorted(_AGENTS.keys()),
        default="claude-code",
        help="Which CLI tool to configure (default: claude-code)",
    )
    _add_setup_args(setup)

    # Sync: staso sync [--target <name>]
    sync = sub.add_parser(
        "sync",
        help="Sync hook config — add missing events, fix python path",
    )
    sync.add_argument(
        "--target",
        choices=sorted(_AGENTS.keys()),
        default=None,
        help="Which CLI tool to sync (default: all configured agents)",
    )
    auto_group = sync.add_mutually_exclusive_group()
    auto_group.add_argument(
        "--enable-auto-sync",
        action="store_true",
        help="Enable auto-sync on each session start",
    )
    auto_group.add_argument(
        "--disable-auto-sync",
        action="store_true",
        help="Disable auto-sync",
    )

    # Deprecated alias: staso setup-claude-code
    setup_cc = sub.add_parser(
        "setup-claude-code",
        help="(deprecated) Use 'staso setup --target claude-code' instead",
    )
    _add_setup_args(setup_cc)

    return parser


def main() -> None:
    parser = _build_parser()
    args = parser.parse_args()

    if args.command == "setup":
        sys.exit(cmd_setup(args))
    elif args.command == "sync":
        sys.exit(cmd_sync(args))
    elif args.command == "setup-claude-code":
        warnings.warn(
            "setup-claude-code is deprecated, use: staso setup --target claude-code",
            DeprecationWarning,
            stacklevel=1,
        )
        sys.exit(cmd_setup_claude_code(args))
    else:
        parser.print_help()
        sys.exit(0)
