#!/usr/bin/env python3
"""
Order Samurai CLI & Installer Tool
Provides samurai install, samurai doctor, and samurai uninstall capabilities.
"""

import os
import sys
import json
import shutil
import datetime
import argparse
from pathlib import Path

SAMURAI_VERSION = "1.0.0"

def get_paths():
    home = Path.home()
    samurai_root = Path(os.environ.get("SAMURAI_ROOT", Path(__file__).resolve().parent.parent))
    samurai_home = Path(os.environ.get("SAMURAI_HOME", home / ".samurai"))
    samurai_settings = samurai_home / "settings.json"
    claude_hooks = home / ".claude" / "hooks"
    claude_settings = claude_hooks / "settings.json"
    backups_dir = samurai_home / "backups"
    
    return {
        "root": samurai_root,
        "home": samurai_home,
        "samurai_settings": samurai_settings,
        "claude_hooks": claude_hooks,
        "claude_settings": claude_settings,
        "backups": backups_dir,
        "state": samurai_root / "state",
        "taxonomy": samurai_root / "state" / "kill_chain_taxonomy.json"
    }

def _drop_samurai_matcher(matchers: list, command: str) -> list:
    """Drop any matcher entry this installer previously added for `command`, so a
    re-run of `samurai install` stays idempotent instead of appending a duplicate
    hook every time. Non-matching entries (including malformed ones) are preserved
    untouched -- this only removes samurai's own prior entries."""
    kept = []
    for m in matchers:
        if (isinstance(m, dict) and isinstance(m.get("hooks"), list)
                and any(isinstance(h, dict) and h.get("command") == command for h in m["hooks"])):
            continue
        kept.append(m)
    return kept


def _register_hooks_in_file(settings_path: Path, guard_script: str, scrubber_script: str, backups_dir: Path,
                             backup_label: str):
    if not settings_path.parent.exists():
        settings_path.parent.mkdir(parents=True, exist_ok=True)
    if settings_path.exists():
        ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        # Keyed by backup_label, not settings_path.name: samurai_settings and claude_settings
        # both end in "settings.json", so a name-only key made their backups indistinguishable
        # in the shared backups_dir -- cmd_uninstall's restore step could then copy an
        # unrelated samurai_settings backup straight over the user's real claude_settings.
        backup_file = backups_dir / f"{backup_label}.bak.{ts}"
        shutil.copy2(settings_path, backup_file)
        print(f"  [✓] Backed up prior settings to {backup_file}")
    
    existing_settings = {}
    if settings_path.exists():
        try:
            with open(settings_path, "r") as f:
                existing_settings = json.load(f)
        except Exception:
            existing_settings = {}

    hooks = existing_settings.get("hooks", {})
    pre_matchers = hooks.get("PreToolUse", [])
    post_matchers = hooks.get("PostToolUse", [])

    pre_command = f"python3 {guard_script}"
    post_command = f"python3 {scrubber_script}"

    # Claude Code's real settings.json hooks shape is hooks.<Event> = [matcher, ...], each
    # matcher {"matcher": ..., "hooks": [{"type": "command", "command": str}]} -- see
    # execution/verify_claude_hook_contract.py::collect_hook_commands(). A flat
    # {"name", "command", "async"} object (the old shape here) is invisible to that parser,
    # so the installed hook would never actually run.
    pre_matchers = _drop_samurai_matcher(pre_matchers, pre_command)
    post_matchers = _drop_samurai_matcher(post_matchers, post_command)

    pre_matchers.append({"matcher": "*", "hooks": [{"type": "command", "command": pre_command}]})
    post_matchers.append({"matcher": "*", "hooks": [{"type": "command", "command": post_command}]})

    hooks["PreToolUse"] = pre_matchers
    hooks["PostToolUse"] = post_matchers
    existing_settings["hooks"] = hooks

    with open(settings_path, "w") as f:
        json.dump(existing_settings, f, indent=2)
    print(f"  [✓] Registered hooks in {settings_path}")

def cmd_install(args):
    paths = get_paths()
    print(f"⚔️  Installing Order Samurai v{SAMURAI_VERSION}...")

    paths["home"].mkdir(parents=True, exist_ok=True)
    paths["backups"].mkdir(parents=True, exist_ok=True)

    guard_script = str(paths["root"] / "hooks" / "prompt_injection_guard.py")
    scrubber_script = str(paths["root"] / "hooks" / "secret_scrubber_realtime.py")

    # Install into ~/.samurai/settings.json (agent-agnostic primary)
    _register_hooks_in_file(paths["samurai_settings"], guard_script, scrubber_script, paths["backups"],
                             "samurai_settings")

    # Also install into ~/.claude/hooks/settings.json if .claude directory exists
    if (Path.home() / ".claude").is_dir() or paths["claude_settings"].exists():
        _register_hooks_in_file(paths["claude_settings"], guard_script, scrubber_script, paths["backups"],
                                 "claude_settings")

    print(f"⚔️  Installation complete! Run 'samurai doctor' to verify system health.")
    return 0

def cmd_doctor(args):
    paths = get_paths()
    print(f"🩺 Order Samurai Doctor v{SAMURAI_VERSION}")
    print("----------------------------------------")

    total = 5
    passed = 0

    def check(name, ok, detail=""):
        nonlocal passed
        status = "[✅ PASS]" if ok else "[❌ FAIL]"
        if ok:
            passed += 1
        print(f"{status} {name}: {detail}")

    # Check 1: Python Version
    py_ok = sys.version_info >= (3, 10)
    check("Python Version", py_ok, f"Python {sys.version.split()[0]} (>= 3.10 required)")

    # Check 2: Taxonomy File
    tax_ok = paths["taxonomy"].exists()
    if tax_ok:
        try:
            with open(paths["taxonomy"]) as f:
                data = json.load(f)
                tax_ok = len(data.get("chains", [])) == 14
        except Exception:
            tax_ok = False
    check("Kill Chain Taxonomy", tax_ok, f"{paths['taxonomy']} (14 chains)")

    # Check 3: Fail-Closed Posture
    fail_open = os.environ.get("BUSHIDO_FAIL_OPEN", "false").lower() == "true"
    check("Security Posture", not fail_open, f"BUSHIDO_FAIL_OPEN={fail_open} (Fail-closed enforced)")

    # Check 4: Hook Registration (Check generic samurai_settings or claude_settings)
    guard_command = f"python3 {paths['root'] / 'hooks' / 'prompt_injection_guard.py'}"
    registered = False
    for settings_path in [paths["samurai_settings"], paths["claude_settings"]]:
        if settings_path.exists():
            try:
                with open(settings_path) as f:
                    cfg = json.load(f)
                    pre = cfg.get("hooks", {}).get("PreToolUse", [])
                    if any(isinstance(m, dict) and isinstance(m.get("hooks"), list)
                           and any(isinstance(h, dict) and h.get("command") == guard_command for h in m["hooks"])
                           for m in pre):
                        registered = True
                        break
            except Exception:
                pass
    check("Claude Code Hook Registration (Agnostic)", registered, "PreToolUse prompt_injection_guard registered")

    # Check 5: Path Authority. agentica_core lives in two different places
    # depending on layout (same distinction emit_event.py/secret_scrub.py/
    # bushido_check.py resolve for their own imports):
    #   live tree:      Governance/Order Samurai/bin/samurai -> paths["root"]
    #                    is ".../Governance/Order Samurai", and agentica_core
    #                    is a SIBLING under Governance/, i.e. paths["root"].parent
    #   public export:  <root>/bin/samurai (pack flattened to the root) ->
    #                    agentica_core is a direct child of paths["root"]
    # Checking only the child case reported Path Authority FAIL on every real
    # dev checkout, since that layout is a sibling, not a child.
    agentica_core_dir = paths["root"] / "agentica_core"
    if not agentica_core_dir.is_dir():
        agentica_core_dir = paths["root"].parent / "agentica_core"
    root_ok = paths["root"].exists() and agentica_core_dir.is_dir()
    check("Path Authority", root_ok, f"Root: {paths['root']} (agentica_core: {agentica_core_dir})")

    print("----------------------------------------")
    print(f"Summary: {passed}/{total} checks passed.")
    return 0 if passed == total else 1

def _deregister_hooks_in_file(settings_path: Path, guard_command: str, scrubber_command: str) -> None:
    if not settings_path.exists():
        return
    try:
        with open(settings_path, "r") as f:
            cfg = json.load(f)
        hooks = cfg.get("hooks", {})
        if "PreToolUse" in hooks:
            hooks["PreToolUse"] = _drop_samurai_matcher(hooks["PreToolUse"], guard_command)
        if "PostToolUse" in hooks:
            hooks["PostToolUse"] = _drop_samurai_matcher(hooks["PostToolUse"], scrubber_command)
        cfg["hooks"] = hooks
        with open(settings_path, "w") as f:
            json.dump(cfg, f, indent=2)
        print(f"  [✓] Deregistered Order Samurai hooks from {settings_path}.")
    except Exception as e:
        print(f"  [!] Error deregistering hooks from {settings_path}: {e}")


def cmd_uninstall(args):
    paths = get_paths()
    print(f"⚔️  Uninstalling Order Samurai v{SAMURAI_VERSION}...")

    # 1. Remove hooks from settings.json -- BOTH samurai_settings (the agent-agnostic
    # primary cmd_install always writes to) and claude_settings (installed
    # conditionally). This must run independent of --keep-data: --keep-data only
    # decides whether ~/.samurai/state survives, not whether its hooks stay wired.
    guard_command = f"python3 {paths['root'] / 'hooks' / 'prompt_injection_guard.py'}"
    scrubber_command = f"python3 {paths['root'] / 'hooks' / 'secret_scrubber_realtime.py'}"
    _deregister_hooks_in_file(paths["samurai_settings"], guard_command, scrubber_command)
    _deregister_hooks_in_file(paths["claude_settings"], guard_command, scrubber_command)

    # 2. Restore prior backup if available (claude_settings backups only -- see
    # _register_hooks_in_file's backup_label: a samurai_settings backup is unrelated content
    # and must never be restored onto claude_settings).
    backups = sorted(paths["backups"].glob("claude_settings.bak.*"))
    if backups:
        latest_backup = backups[-1]
        try:
            shutil.copy2(latest_backup, paths["claude_settings"])
            print(f"  [✓] Restored prior settings from {latest_backup.name}")
        except Exception as e:
            print(f"  [!] Warning restoring backup: {e}")

    # 3. Handle zero-residue data cleanup
    if not args.keep_data and paths["home"].exists():
        shutil.rmtree(paths["home"])
        print("  [✓] Removed ~/.samurai state (Zero-residue audit PASS).")
    elif args.keep_data:
        print("  [i] Kept ~/.samurai data (--keep-data specified).")

    print("⚔️  Order Samurai uninstalled cleanly.")
    return 0

def main():
    parser = argparse.ArgumentParser(description="Order Samurai Governance CLI")
    subparsers = parser.add_subparsers(dest="command")

    # install
    subparsers.add_parser("install", help="Install & register Order Samurai hooks")

    # doctor
    subparsers.add_parser("doctor", help="Run diagnostic health checks")

    # uninstall
    un_parser = subparsers.add_parser("uninstall", help="Uninstall Order Samurai hooks and restore settings")
    un_parser.add_argument("--keep-data", action="store_true", help="Preserve ~/.samurai state directory")

    args = parser.parse_args()
    if not args.command or args.command == "doctor":
        sys.exit(cmd_doctor(args))
    elif args.command == "install":
        sys.exit(cmd_install(args))
    elif args.command == "uninstall":
        sys.exit(cmd_uninstall(args))
    else:
        parser.print_help()
        sys.exit(1)

if __name__ == "__main__":
    main()
