#!/usr/bin/env python3
"""slash_sleep.py — OpenClaw slash command equivalent of SkillOpt's /sleep.

Use from the main session as a /sleep command:
  /sleep status    — show current state + last 5 nights
  /sleep run       — trigger one cycle (all categories) right now
  /sleep run research-cron  — one cycle, single category
  /sleep adopt [night]      — adopt the most recent (or specified) staged proposal
  /sleep reject [night]     — discard the most recent (or specified) staging dir
  /sleep dry-run   — report-only cycle
  /sleep cost      — estimate per-night cost for current config

This script is a thin shell over run_sleep.py. It can be invoked either
manually from the main session or by an OpenClaw command handler.
"""
from __future__ import annotations

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

SKILL_DIR = Path("/home/ethanclaw/.openclaw/workspace/skills/skillopt-sleep")
STATE_DIR = Path(os.path.expanduser("~/.skillopt-sleep"))  # default
STAGING_ROOT = STATE_DIR

def _resolve_state_dir():
    """Find the actual state dir.

    Priority: scan in order:
      1. ~/.skillopt-sleep/                 (default)
      2. /home/ethanclaw/.openclaw/workspace/.skillopt-sleep/  (when staging is there)
      3. /home/ethanclaw/.openclaw/.skillopt-sleep/            (parent of overridden claude_home)
    Pick the first one that has a state.json OR staging dir.
    """
    candidates = [
        Path(os.path.expanduser("~/.skillopt-sleep")),
        Path("/home/ethanclaw/.openclaw/workspace/.skillopt-sleep"),
        Path("/home/ethanclaw/.openclaw/.skillopt-sleep"),
    ]
    # Prefer the one with state.json
    for c in candidates:
        if (c / "state.json").exists():
            return c
    # Then the one with staging
    for c in candidates:
        if (c / "staging").exists():
            return c
    return candidates[0]

TESTS_DIR = SKILL_DIR / "tests"


def status() -> int:
    state_dir = _resolve_state_dir()
    state_file = state_dir / "state.json"
    staging_dir = state_dir / "staging"
    print(f"=== SkillOpt-Sleep status ===")
    print(f"  state dir: {state_dir}")
    print(f"  staging dir: {staging_dir}")
    if staging_dir.exists():
        stages = sorted(staging_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True)
        print(f"  staging entries: {len(stages)}")
        for s in stages[:3]:
            print(f"    {s.name}")
    if not state_file.exists():
        print("  no state.json — run a cycle first (state is written at end of each non-dry-run)")
        return 0

    with open(state_file) as f:
        state = json.load(f)

    nights = state.get("history") or state.get("nights", [])
    print(f"  total nights: {len(nights)}")
    print(f"  accepted: {sum(1 for n in nights if n.get('accepted'))}")
    print(f"  rejected: {sum(1 for n in nights if not n.get('accepted'))}")
    if nights:
        last = nights[-1]
        print(f"  last night: {last.get('night')}")
        print(f"    accepted: {last.get('accepted')}")
        print(f"    baseline: {last.get('baseline'):.3f}  ->  candidate: {last.get('candidate'):.3f}")
        print(f"    staging: {last.get('staging') or '(none)'}")
    return 0


def run_category(category: str, *, dry_run: bool = False) -> int:
    cat_to_file = {
        "research-cron": "research-cron-tasks.json",
        "devops": "devops-tasks.json",
        "wiki": "wiki-tasks.json",
    }
    tasks_file = TESTS_DIR / cat_to_file.get(category, f"{category}-tasks.json")
    if not tasks_file.exists():
        print(f"ERROR: no tasks file for category '{category}': {tasks_file}")
        return 1

    cmd = [sys.executable, str(SKILL_DIR / "run_sleep.py")]
    if dry_run:
        cmd.append("--dry-run")
    cmd.extend(["--tasks", str(tasks_file)])

    print(f"=== /sleep run {category}{' (dry-run)' if dry_run else ''} ===")
    print(f"  cmd: {' '.join(cmd)}")
    rc = os.system(" ".join(f'"{c}"' for c in cmd))
    return rc


def run_all(*, dry_run: bool = False) -> int:
    rc = 0
    for cat in ("research-cron", "devops", "wiki"):
        r = run_category(cat, dry_run=dry_run)
        if r != 0:
            rc = r
    return rc


def adopt(night: str = None) -> int:
    state_dir = _resolve_state_dir()
    state_file = state_dir / "state.json"
    if not state_file.exists():
        print("ERROR: no state to adopt from")
        return 1
    with open(state_file) as f:
        state = json.load(f)
    nights = state.get("history") or state.get("nights", [])
    if not nights:
        print("ERROR: no nights recorded")
        return 1

    target = None
    if night:
        target = next((n for n in nights if str(n.get("night")) == night), None)
        if not target:
            print(f"ERROR: night '{night}' not found")
            return 1
    else:
        # most recent accepted
        candidates = [n for n in nights if n.get("accepted") and n.get("staging")]
        if not candidates:
            print("ERROR: no accepted nights with staging to adopt")
            return 1
        target = candidates[-1]

    staging = target["staging"]
    if not os.path.isdir(staging):
        print(f"ERROR: staging dir missing: {staging}")
        return 1

    print(f"=== /sleep adopt night {target['night']} ===")
    print(f"  staging: {staging}")
    print(f"  baseline: {target.get('baseline'):.3f}  candidate: {target.get('candidate'):.3f}")

    # Read proposed skill from staging
    manifest = Path(staging) / "manifest.json"
    if manifest.exists():
        with open(manifest) as f:
            m = json.load(f)
        proposed = m.get("proposed_skill")
        if proposed and Path(proposed).exists():
            live = STATE_DIR / "live_skill.md"
            backup = STATE_DIR / f"live_skill.md.bak-{target['night']}"
            if live.exists():
                shutil.copy2(live, backup)
                print(f"  backed up current live skill → {backup}")
            shutil.copy2(proposed, live)
            print(f"  adopted proposed skill → {live}")
            print()
            print("✅ Adoption complete. Next cycle will use the new skill.")
            return 0

    print("ERROR: no proposed_skill in manifest")
    return 1


def reject(night: str = None) -> int:
    state_dir = _resolve_state_dir()
    state_file = state_dir / "state.json"
    if not state_file.exists():
        print("ERROR: no state")
        return 1
    with open(state_file) as f:
        state = json.load(f)
    nights = state.get("history") or state.get("nights", [])
    target = None
    if night:
        target = next((n for n in nights if str(n.get("night")) == night), None)
    else:
        candidates = [n for n in reversed(nights) if n.get("staging")]
        target = candidates[0] if candidates else None

    if not target or not target.get("staging"):
        print("ERROR: nothing to reject")
        return 1

    staging = target["staging"]
    if os.path.isdir(staging):
        shutil.rmtree(staging)
        print(f"🗑️  Removed staging: {staging}")
    # remove from state
    state["history"] = [n for n in nights if n.get("night") != target["night"]]
    with open(state_file, "w") as f:
        json.dump(state, f, indent=2)
    print("✅ Rejected. State updated.")
    return 0


def schedule_cmd(hour: int, minute: int) -> int:
    """Install a nightly cron entry via the shared SkillOpt-Sleep scheduler.

    Note: this schedules the shared engine (``python -m skillopt_sleep run``),
    not the OpenClaw-specific ``run_sleep.py``.  Use ``run_sleep_cron.sh`` if
    you need the OpenClaw-native backend and category task files instead.
    """
    try:
        from skillopt_sleep.scheduler import schedule
    except ImportError:
        print("ERROR: skillopt_sleep.scheduler not available — is SkillOpt-Sleep installed?")
        return 1
    project = str(SKILL_DIR)
    ok, msg = schedule(project, hour=hour, minute=minute)
    print(msg)
    return 0 if ok else 1


def unschedule_cmd(all_projects: bool) -> int:
    """Remove cron entry via the shared SkillOpt-Sleep scheduler."""
    try:
        from skillopt_sleep.scheduler import unschedule
    except ImportError:
        print("ERROR: skillopt_sleep.scheduler not available — is SkillOpt-Sleep installed?")
        return 1
    project = str(SKILL_DIR)
    ok, msg = unschedule(project, all_projects=all_projects)
    print(msg)
    return 0 if ok else 1


def cost() -> int:
    """Estimate per-night cost based on the actual measurement from Phase 2.

    From the real dry-run: 5 devops tasks used 14,427 tokens total.
    That is ~2,885 tokens per task (all 3 phases combined).
    """
    cfg_path = SKILL_DIR / "config.json"
    cfg = {}
    if cfg_path.exists():
        cfg = json.loads(cfg_path.read_text())
    cfg.pop("_comment", None)

    max_tasks = cfg.get("max_tasks_per_night", 12)
    model = cfg.get("model", "deepseek-v4-pro")
    # DeepSeek V4 pricing
    if "pro" in model:
        cost_in = 0.435  # per 1M
        cost_out = 0.87
    elif "flash" in model:
        cost_in = 0.14
        cost_out = 0.28
    else:
        cost_in, cost_out = 0.5, 1.0

    # Measured: ~2,900 tokens per task, 30% output / 70% input
    toks_per_task = 2900
    input_toks = int(toks_per_task * 0.7)
    output_toks = int(toks_per_task * 0.3)

    cost_in_total = (input_toks * max_tasks / 1_000_000) * cost_in
    cost_out_total = (output_toks * max_tasks / 1_000_000) * cost_out
    cost = cost_in_total + cost_out_total

    print(f"=== Cost estimate (per actual measurement) ===")
    print(f"  model: {model}")
    print(f"  max tasks/night: {max_tasks}")
    print(f"  ~tokens/night: {toks_per_task * max_tasks:,}")
    print(f"  cost/night: ${cost:.3f}")
    print(f"  cost/month (30 nights): ${cost*30:.2f}")
    print(f"  cost/year (365 nights): ${cost*365:.2f}")
    return 0


def main():
    ap = argparse.ArgumentParser(description="OpenClaw /sleep command")
    sub = ap.add_subparsers(dest="cmd", required=True)

    sub.add_parser("status", help="show state + last 5 nights")
    p_run = sub.add_parser("run", help="trigger one cycle")
    p_run.add_argument("category", nargs="?", default=None,
                        choices=["research-cron", "devops", "wiki", None])
    p_run.add_argument("--dry-run", action="store_true")
    sub.add_parser("dry-run", help="report-only cycle (all categories)")
    p_adopt = sub.add_parser("adopt", help="adopt most recent accepted staging")
    p_adopt.add_argument("night", nargs="?", default=None)
    p_reject = sub.add_parser("reject", help="discard most recent staging")
    p_reject.add_argument("night", nargs="?", default=None)
    sub.add_parser("cost", help="estimate cost")
    p_schedule = sub.add_parser("schedule", help="install nightly cron entry")
    p_schedule.add_argument("--hour", type=int, default=3, help="hour (0-23)")
    p_schedule.add_argument("--minute", type=int, default=17, help="minute (0-59)")
    p_unschedule = sub.add_parser("unschedule", help="remove cron entry")
    p_unschedule.add_argument("--all", dest="all_projects", action="store_true",
                               help="remove entries for all projects")

    args = ap.parse_args()

    if args.cmd == "status":
        return status()
    if args.cmd == "run":
        if args.category:
            return run_category(args.category, dry_run=args.dry_run)
        return run_all(dry_run=args.dry_run)
    if args.cmd == "dry-run":
        return run_all(dry_run=True)
    if args.cmd == "adopt":
        return adopt(args.night)
    if args.cmd == "reject":
        return reject(args.night)
    if args.cmd == "cost":
        return cost()
    if args.cmd == "schedule":
        return schedule_cmd(args.hour, args.minute)
    if args.cmd == "unschedule":
        return unschedule_cmd(args.all_projects)
    return 1


if __name__ == "__main__":
    sys.exit(main())
