#!/bin/bash
# awake — Keep Mac alive while AI coding agents run
# Uses pmset disablesleep 1 (the ONLY way to prevent lid-close sleep)
# Requires: /etc/sudoers.d/pmset for passwordless sudo pmset

set -uo pipefail
# NOTE: no set -e — pgrep returns 1 when no match, which would kill the daemon

# --- Defaults (overridable via ~/.config/awake/config) ---

AGENTS="claude codex aider copilot amp opencode"
POLL_INTERVAL=15
GRACE_SECONDS=300       # 5 minutes
BATTERY_CRITICAL=5      # force sleep even if agents running
BATTERY_WARN=15         # send notification
MODE_DEFAULT="running"

# --- Load config ---

CONFIG_FILE="$HOME/.config/awake/config"
[ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE"

# --- Constants ---

resolve_script_path() {
    local src="${1:-$0}"
    while [ -L "$src" ]; do
        local dir
        dir="$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)"
        src="$(readlink "$src")"
        [[ "$src" != /* ]] && src="$dir/$src"
    done
    local dir
    dir="$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)"
    echo "$dir/$(basename "$src")"
}

SCRIPT_PATH="$(resolve_script_path "${BASH_SOURCE[0]:-$0}")"
SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
LOCAL_BIN_DIR="$HOME/.local/bin"
LOCAL_APP_SRC_DIR="$LOCAL_BIN_DIR/AwakeApp"
LOCAL_APP_SRC_FILE="$LOCAL_APP_SRC_DIR/main.swift"
LOCAL_APP_PRIVATE_HEADER="$LOCAL_APP_SRC_DIR/private-apis.h"
REPO_APP_SRC_FILE="$SCRIPT_DIR/ui/main.swift"
REPO_APP_PRIVATE_HEADER="$SCRIPT_DIR/ui/private-apis.h"
BUNDLED_APP_SRC_FILE="$(CDPATH='' cd -- "$SCRIPT_DIR/.." 2>/dev/null && pwd)/ui/main.swift"
BUNDLED_APP_PRIVATE_HEADER="$(CDPATH='' cd -- "$SCRIPT_DIR/.." 2>/dev/null && pwd)/ui/private-apis.h"
LOCAL_PACKAGE_JSON="$LOCAL_BIN_DIR/awake-package.json"
LAUNCH_AGENT_LABEL="com.awake.daemon"
LAUNCH_AGENT_PATH="$HOME/Library/LaunchAgents/$LAUNCH_AGENT_LABEL.plist"
LEASE_MONITOR_LABEL="com.awake.lease-monitor"
LEASE_MONITOR_PATH="$HOME/Library/LaunchAgents/$LEASE_MONITOR_LABEL.plist"
BASELINE_FILE="$HOME/.config/awake/power-baseline.json"
OVERRIDE_MARKER_FILE="$HOME/.config/awake/power-override-active"
MODE_FILE="$HOME/.config/awake/default-mode"
RULES_DIR="$HOME/.config/awake/rules.d"
INSTALL_METADATA_FILE="$HOME/.config/awake/install-metadata.json"
UPDATE_CACHE_FILE="$HOME/.config/awake/update-cache.json"
PID_FILE="/tmp/awake.pid"
DAEMON_LOCK_DIR="/tmp/awake-daemon.lock"
DAEMON_OWNER_FILE="$DAEMON_LOCK_DIR/pid"
LEASES_DIR="/tmp/awake-leases"
STATE_FILE="/tmp/awake-state"
LAST_ACTIVE_FILE="/tmp/awake-last-active"
CAFFEINE_PID_FILE="/tmp/awake-caffeinate.pid"
FOR_PID_FILE="/tmp/awake-for.pid"
FOR_END_FILE="/tmp/awake-for-end"
FOR_TOKEN_FILE="/tmp/awake-for-token"
DISPLAY_SLEEP_FILE="/tmp/awake-display-sleep"
WHY_FILE="/tmp/awake-why"
BATTERY_GUARD_FILE="/tmp/awake-battery-guard"
LEASE_MONITOR_READY_FILE="/tmp/awake-lease-monitor-ready"
LEASE_MONITOR_HEARTBEAT_FILE="/tmp/awake-lease-monitor-heartbeat"
LOG_PREFIX="[awake]"
PACKAGE_NAME="awake-agent"
GITHUB_RELEASES_URL="https://github.com/nickita-khylkouski/awake/releases/latest"
UPDATE_CACHE_TTL="${AWAKE_UPDATE_CACHE_TTL:-21600}"
UPDATE_APPLY_TIMEOUT="${AWAKE_UPDATE_APPLY_TIMEOUT:-300}"

# --- Helpers ---

log() { echo "$LOG_PREFIX $(date '+%H:%M:%S') $*"; }

now_epoch() { date +%s; }

ensure_runtime_dirs() {
    mkdir -p "$LEASES_DIR" "$RULES_DIR" "$(dirname "$MODE_FILE")"
}

launch_agent_domain() {
    printf 'gui/%s' "$(id -u)"
}

launch_agent_target() {
    printf '%s/%s' "$(launch_agent_domain)" "$LAUNCH_AGENT_LABEL"
}

launch_agent_loaded() {
    [ -f "$LAUNCH_AGENT_PATH" ] || return 1
    /bin/launchctl print "$(launch_agent_target)" >/dev/null 2>&1
}

json_escape() {
    local value="${1:-}"
    value="${value//\\/\\\\}"
    value="${value//\"/\\\"}"
    value="${value//$'\n'/\\n}"
    value="${value//$'\r'/\\r}"
    value="${value//$'\t'/\\t}"
    printf '%s' "$value"
}

json_optional_string() {
    local value="${1:-}"
    if [ -n "$value" ]; then
        printf '"%s"' "$(json_escape "$value")"
    else
        printf 'null'
    fi
}

current_default_mode() {
    local mode
    mode="$(read_file_value "$MODE_FILE" 2>/dev/null || true)"
    case "$mode" in
        running|presenting|agent-safe) echo "$mode" ;;
        *) echo "$MODE_DEFAULT" ;;
    esac
}

set_default_mode() {
    local mode="$1"
    case "$mode" in
        running|presenting|agent-safe) ;;
        *)
            echo "Invalid mode: $mode (use running|presenting|agent-safe)"
            return 1
            ;;
    esac
    mkdir -p "$(dirname "$MODE_FILE")"
    echo "$mode" > "$MODE_FILE"
}

resolve_mode() {
    local mode="${1:-agent-safe}"
    case "$mode" in
        running|presenting)
            echo "$mode"
            ;;
        agent-safe)
            if [ -f "$DISPLAY_SLEEP_FILE" ]; then
                echo "running"
            else
                echo "presenting"
            fi
            ;;
        *)
            echo "presenting"
            ;;
    esac
}

read_file_value() {
    local path="$1"
    [ -f "$path" ] || return 1
    cat "$path" 2>/dev/null
}

package_json_candidate_paths() {
    printf '%s\n' \
        "${AWAKE_PACKAGE_JSON_PATH:-}" \
        "$SCRIPT_DIR/awake-package.json" \
        "$SCRIPT_DIR/package.json" \
        "$LOCAL_PACKAGE_JSON"
}

current_package_version() {
    if [ -n "${AWAKE_PACKAGE_VERSION:-}" ]; then
        printf '%s\n' "$AWAKE_PACKAGE_VERSION"
        return 0
    fi

    local candidate
    for candidate in $(package_json_candidate_paths); do
        [ -n "$candidate" ] || continue
        [ -f "$candidate" ] || continue
        /usr/bin/python3 - "$candidate" <<'PY'
import json, sys
path = sys.argv[1]
try:
    with open(path) as f:
        payload = json.load(f)
    version = payload.get("version")
    if version:
        print(version)
        raise SystemExit(0)
except Exception:
    pass
raise SystemExit(1)
PY
        if [ $? -eq 0 ]; then
            return 0
        fi
    done

    local version_from_metadata
    version_from_metadata="$(install_metadata_field packageVersion 2>/dev/null || true)"
    [ -n "$version_from_metadata" ] || version_from_metadata="$(install_metadata_field currentVersion 2>/dev/null || true)"
    if [ -n "$version_from_metadata" ]; then
        printf '%s\n' "$version_from_metadata"
        return 0
    fi

    printf 'unknown\n'
}

install_metadata_field() {
    local field="$1"
    [ -f "$INSTALL_METADATA_FILE" ] || return 1
    /usr/bin/python3 - "$INSTALL_METADATA_FILE" "$field" <<'PY'
import json, sys
path, field = sys.argv[1], sys.argv[2]
try:
    with open(path) as f:
        payload = json.load(f)
    value = payload.get(field)
except Exception:
    raise SystemExit(1)

if value is None or value == "":
    raise SystemExit(1)
if isinstance(value, bool):
    print("true" if value else "false")
else:
    print(value)
PY
}

current_app_version() {
    local plist="$LOCAL_BIN_DIR/Awake.app/Contents/Info.plist"
    if [ -f "$plist" ]; then
        local version
        version="$(/usr/bin/defaults read "$plist" CFBundleShortVersionString 2>/dev/null || true)"
        [ -n "$version" ] && { printf '%s\n' "$version"; return 0; }
    fi
    current_package_version
}

detect_install_source() {
    if [ -n "${AWAKE_UPDATE_SOURCE_TYPE:-}" ]; then
        local detail="${AWAKE_UPDATE_SOURCE_DETAIL:-${SCRIPT_PATH}}"
        printf '%s|%s\n' "$AWAKE_UPDATE_SOURCE_TYPE" "$detail"
        return 0
    fi

    case "$SCRIPT_PATH" in
        "$LOCAL_BIN_DIR/awake"|"$LOCAL_BIN_DIR/Awake.app/Contents/Resources/bin/awake")
            local existing_type existing_detail
            existing_type="$(install_metadata_field sourceType 2>/dev/null || true)"
            existing_detail="$(install_metadata_field sourcePath 2>/dev/null || true)"
            if [ -n "$existing_type" ]; then
                printf '%s|%s\n' "$existing_type" "${existing_detail:-$SCRIPT_PATH}"
                return 0
            fi
            printf 'local-copy|%s\n' "$SCRIPT_PATH"
            return 0
            ;;
        *"/_npx/"*"/node_modules/awake-agent/"*|*"/npm/_npx/"*"/node_modules/awake-agent/"*)
            printf 'npx|%s\n' "$SCRIPT_PATH"
            return 0
            ;;
        *"/lib/node_modules/awake-agent/"*)
            printf 'npm-global|%s\n' "$SCRIPT_PATH"
            return 0
            ;;
        *"/node_modules/awake-agent/"*)
            local npm_prefix
            npm_prefix="$(npm prefix -g 2>/dev/null || true)"
            if [ -n "$npm_prefix" ] && [[ "$SCRIPT_PATH" == "$npm_prefix"* ]]; then
                printf 'npm-global|%s\n' "$SCRIPT_PATH"
            else
                printf 'local-copy|%s\n' "$SCRIPT_PATH"
            fi
            return 0
            ;;
    esac

    if [ -d "$SCRIPT_DIR/.git" ] || git -C "$SCRIPT_DIR" rev-parse --git-dir >/dev/null 2>&1; then
        printf 'repo|%s\n' "$SCRIPT_DIR"
        return 0
    fi

    printf 'unknown|%s\n' "$SCRIPT_PATH"
}

write_install_metadata() {
    local source_info source_type source_path package_version
    source_info="$(detect_install_source)"
    source_type="${source_info%%|*}"
    source_path="${source_info#*|}"
    package_version="$(current_package_version)"

    mkdir -p "$(dirname "$INSTALL_METADATA_FILE")"
    cat > "$INSTALL_METADATA_FILE" <<EOF
{
  "version": 1,
  "packageName": "$PACKAGE_NAME",
  "packageVersion": "$(json_escape "$package_version")",
  "currentVersion": "$(json_escape "$package_version")",
  "sourceType": "$(json_escape "$source_type")",
  "sourcePath": "$(json_escape "$source_path")",
  "installedAt": $(now_epoch)
}
EOF
}

version_is_newer() {
    local lhs="$1"
    local rhs="$2"
    /usr/bin/python3 - "$lhs" "$rhs" <<'PY'
import re, sys
left, right = sys.argv[1], sys.argv[2]

def parse(raw: str):
    pieces = []
    for chunk in raw.split("."):
        match = re.match(r"(\d+)", chunk)
        pieces.append(int(match.group(1)) if match else 0)
    return pieces

a = parse(left)
b = parse(right)
size = max(len(a), len(b))
a.extend([0] * (size - len(a)))
b.extend([0] * (size - len(b)))
raise SystemExit(0 if a > b else 1)
PY
}

update_command_for_source() {
    local source_type="$1"
    case "$source_type" in
        npx)
            printf 'npx --yes %s@latest install' "$PACKAGE_NAME"
            ;;
        npm-global)
            printf 'npm install -g %s@latest && awake install' "$PACKAGE_NAME"
            ;;
        repo)
            printf 'git pull && awake install'
            ;;
        local-copy|unknown)
            printf 'npx --yes %s@latest install' "$PACKAGE_NAME"
            ;;
        *)
            return 1
            ;;
    esac
}

npm_global_awake_path() {
    local prefix
    prefix="$(npm prefix -g 2>/dev/null || true)"
    if [ -n "$prefix" ] && [ -x "$prefix/bin/awake" ]; then
        printf '%s\n' "$prefix/bin/awake"
        return 0
    fi
    return 1
}

run_with_timeout() {
    local timeout_seconds="$1"
    shift
    /usr/bin/python3 - "$timeout_seconds" "$@" <<'PY'
import os
import signal
import subprocess
import sys

timeout = int(sys.argv[1])
cmd = sys.argv[2:]
proc = subprocess.Popen(cmd, start_new_session=True)
try:
    raise SystemExit(proc.wait(timeout=timeout))
except subprocess.TimeoutExpired:
    try:
        os.killpg(proc.pid, signal.SIGTERM)
    except ProcessLookupError:
        pass
    print(f"[awake] update command timed out after {timeout}s", file=sys.stderr)
    raise SystemExit(124)
PY
}

python_update_tool() {
    /usr/bin/python3 - "$@" <<'PY'
import json
import os
import sys
import time
import urllib.error
import urllib.request

action = sys.argv[1]

def parse_version(raw: str):
    parts = []
    for piece in raw.split("."):
        digits = ""
        for char in piece:
            if char.isdigit():
                digits += char
            else:
                break
        parts.append(int(digits or "0"))
    return parts

def is_newer(latest: str, current: str) -> bool:
    left = parse_version(latest)
    right = parse_version(current)
    size = max(len(left), len(right))
    left.extend([0] * (size - len(left)))
    right.extend([0] * (size - len(right)))
    return left > right

if action == "update-status":
    cache_path, package_name, current_version, app_version, source_type, source_detail, release_url, force_refresh = sys.argv[2:10]
    ttl = int(os.environ.get("AWAKE_UPDATE_CACHE_TTL", "21600"))
    now = int(time.time())
    latest = None
    checked_at = None
    cached = False
    error = None
    source = "cache"

    override = os.environ.get("AWAKE_UPDATE_LATEST_VERSION")
    if override:
        latest = override
        checked_at = now
        source = "override"
    else:
        payload = None
        if os.path.exists(cache_path):
            try:
                with open(cache_path) as f:
                    payload = json.load(f)
            except Exception:
                payload = None

        if payload:
            latest = payload.get("latestVersion")
            checked_at = payload.get("checkedAt")
            error = payload.get("error")
            try:
                checked_at_int = int(checked_at) if checked_at is not None else None
            except (TypeError, ValueError):
                checked_at_int = None
            if latest and checked_at_int and force_refresh != "true" and (now - checked_at_int) < ttl:
                checked_at = checked_at_int
                cached = True
                source = payload.get("source", "cache")

        if not cached:
            registry_url = os.environ.get("AWAKE_UPDATE_REGISTRY_URL", f"https://registry.npmjs.org/{package_name}/latest")
            try:
                with urllib.request.urlopen(registry_url, timeout=6) as response:
                    fetched = json.loads(response.read().decode("utf-8"))
                latest = fetched.get("version")
                checked_at = now
                error = None
                source = "npm"
                os.makedirs(os.path.dirname(cache_path), exist_ok=True)
                with open(cache_path, "w") as f:
                    json.dump({
                        "version": 1,
                        "packageName": package_name,
                        "latestVersion": latest,
                        "checkedAt": checked_at,
                        "source": source,
                        "error": None,
                    }, f, indent=2)
                    f.write("\n")
            except Exception as exc:
                error = str(exc)
                if latest:
                    cached = True
                    source = "stale-cache"
                else:
                    latest = current_version
                    checked_at = now
                    source = "unavailable"

    update_available = bool(
        latest
        and current_version
        and latest != "unknown"
        and current_version != "unknown"
        and is_newer(latest, current_version)
    )
    can_self_update = source_type in {"npx", "npm-global"}

    print(json.dumps({
        "packageName": package_name,
        "currentVersion": current_version,
        "appVersion": app_version,
        "latestVersion": latest,
        "updateAvailable": update_available,
        "installSource": source_type,
        "installSourceDetail": source_detail,
        "canSelfUpdate": can_self_update,
        "checkedAt": checked_at,
        "cached": cached,
        "error": error,
        "releaseURL": release_url,
        "source": source,
    }))
    raise SystemExit(0)

raise SystemExit(f"unknown action: {action}")
PY
}

lease_dir() {
    echo "$LEASES_DIR/$1"
}

lease_field() {
    local id="$1"
    local field="$2"
    read_file_value "$(lease_dir "$id")/$field"
}

lease_exists() {
    [ -d "$(lease_dir "$1")" ]
}

lease_generation_file() {
    printf '%s\n' "$LEASES_DIR/.generation"
}

lease_generation() {
    local generation
    generation="$(read_file_value "$(lease_generation_file)" 2>/dev/null || true)"
    case "$generation" in ''|*[!0-9]*) printf '0\n' ;; *) printf '%s\n' "$generation" ;; esac
}

bump_lease_generation() {
    ensure_runtime_dirs
    local generation next tmp
    generation="$(lease_generation)"
    next=$(( generation + 1 ))
    tmp="$(mktemp "$LEASES_DIR/.generation.tmp.XXXXXX")" || return 1
    printf '%s\n' "$next" > "$tmp"
    mv "$tmp" "$(lease_generation_file)"
}

lease_create_or_update() {
    local id="$1"
    local type="$2"
    local mode="$3"
    local reason="$4"
    local priority="$5"
    local expires_at="${6:-}"
    local source="${7:-}"
    local owner_pid="${8:-}"
    ensure_runtime_dirs
    local dir publish_dir="" started_at
    dir="$(lease_dir "$id")"
    if [ -d "$dir" ]; then
        started_at="$(lease_field "$id" started_at 2>/dev/null || now_epoch)"
    else
        publish_dir="$(mktemp -d "$LEASES_DIR/.${id}.tmp.XXXXXX")" || return 1
        dir="$publish_dir"
        started_at="$(now_epoch)"
    fi
    printf '%s' "$started_at" > "$dir/started_at"
    printf '%s' "$id" > "$dir/id"
    printf '%s' "$type" > "$dir/type"
    printf '%s' "$mode" > "$dir/mode"
    printf '%s' "$reason" > "$dir/reason"
    printf '%s' "$priority" > "$dir/priority"
    printf '%s' "$source" > "$dir/source"
    printf '%s' "${expires_at:-}" > "$dir/expires_at"
    if [ -n "$owner_pid" ]; then
        printf '%s' "$owner_pid" > "$dir/owner_pid"
    else
        rm -f "$dir/owner_pid"
    fi
    printf '%s' "1" > "$dir/ready"
    if [ -n "$publish_dir" ]; then
        mv "$publish_dir" "$(lease_dir "$id")"
    fi
    bump_lease_generation
}

lease_remove() {
    rm -rf "$(lease_dir "$1")"
}

lease_has_expired() {
    local id="$1"
    local expires_at
    expires_at="$(lease_field "$id" expires_at 2>/dev/null || true)"
    [ -n "$expires_at" ] || return 1
    [ "$expires_at" -gt 0 ] 2>/dev/null || return 1
    [ "$(now_epoch)" -ge "$expires_at" ]
}

lease_is_well_formed() {
    local id="$1" dir type mode priority started recorded_id
    dir="$(lease_dir "$id")"
    [ -d "$dir" ] || return 1
    for field in id type mode reason priority started_at source expires_at; do
        [ -f "$dir/$field" ] || return 1
    done
    recorded_id="$(lease_field "$id" id 2>/dev/null || true)"
    type="$(lease_field "$id" type 2>/dev/null || true)"
    mode="$(lease_field "$id" mode 2>/dev/null || true)"
    priority="$(lease_field "$id" priority 2>/dev/null || true)"
    started="$(lease_field "$id" started_at 2>/dev/null || true)"
    [ "$recorded_id" = "$id" ] || return 1
    case "$type" in manual|timer|command|daemon|rule) ;; *) return 1 ;; esac
    case "$mode" in running|presenting|agent-safe) ;; *) return 1 ;; esac
    [[ "$priority" =~ ^-?[0-9]+$ ]] || return 1
    [[ "$started" =~ ^[0-9]+$ ]] || return 1
}

cleanup_expired_leases() {
    ensure_runtime_dirs
    local dir id
    for dir in "$LEASES_DIR"/*; do
        [ -d "$dir" ] || continue
        id="$(basename "$dir")"
        if lease_has_expired "$id"; then
            rm -rf "$dir"
        fi
    done
}

best_lease_id() {
    cleanup_expired_leases
    ensure_runtime_dirs
    local dir id best_id="" best_priority=-9999 best_started=0
    local priority started
    for dir in "$LEASES_DIR"/*; do
        [ -d "$dir" ] || continue
        id="$(basename "$dir")"
        priority="$(lease_field "$id" priority 2>/dev/null || echo 0)"
        started="$(lease_field "$id" started_at 2>/dev/null || echo 0)"
        if [ "$priority" -gt "$best_priority" ] 2>/dev/null || { [ "$priority" -eq "$best_priority" ] 2>/dev/null && [ "$started" -ge "$best_started" ] 2>/dev/null; }; then
            best_id="$id"
            best_priority="$priority"
            best_started="$started"
        fi
    done
    [ -n "$best_id" ] || return 1
    echo "$best_id"
}

effective_mode_value() {
    local id
    id="$(best_lease_id 2>/dev/null || true)"
    [ -n "$id" ] || return 1
    lease_field "$id" mode
}

effective_resolved_mode() {
    local mode
    mode="$(effective_mode_value 2>/dev/null || true)"
    [ -n "$mode" ] || return 1
    resolve_mode "$mode"
}

effective_reason_value() {
    local id
    id="$(best_lease_id 2>/dev/null || true)"
    [ -n "$id" ] || return 1
    lease_field "$id" reason
}

lease_count() {
    ensure_runtime_dirs
    local dir count=0
    for dir in "$LEASES_DIR"/*; do
        [ -d "$dir" ] || continue
        count=$(( count + 1 ))
    done
    echo "$count"
}

rule_lease_count() {
    ensure_runtime_dirs
    local dir count=0
    for dir in "$LEASES_DIR"/rule-*; do
        [ -d "$dir" ] || continue
        count=$(( count + 1 ))
    done
    echo "$count"
}

clear_manual_leases() {
    lease_remove "manual-toggle"
    lease_remove "manual-timer"
    lease_remove "run-command"
}

cancel_timer_session() {
    lease_remove "manual-timer"
    if [ -f "$FOR_PID_FILE" ]; then
        kill "$(cat "$FOR_PID_FILE")" 2>/dev/null || true
    fi
    rm -f "$FOR_PID_FILE" "$FOR_END_FILE" "$FOR_TOKEN_FILE"
}

rule_dir() {
    echo "$RULES_DIR/$1"
}

rule_field() {
    local id="$1"
    local field="$2"
    read_file_value "$(rule_dir "$id")/$field"
}

rule_count() {
    ensure_runtime_dirs
    local dir count=0
    for dir in "$RULES_DIR"/*; do
        [ -d "$dir" ] || continue
        count=$(( count + 1 ))
    done
    echo "$count"
}

pid_is_alive() {
    local pid="${1:-}"
    [ -n "$pid" ] || return 1
    kill -0 "$pid" 2>/dev/null
}

wait_for_pid_exit() {
    local pid="${1:-}"
    local _attempt=0
    [ -n "$pid" ] || return 0
    while [ "$_attempt" -lt 100 ]; do
        pid_is_alive "$pid" || return 0
        sleep 0.1
        _attempt=$(( _attempt + 1 ))
    done
    return 1
}

pid_matches_awake_daemon() {
    local pid="${1:-}"
    pid_is_alive "$pid" || return 1
    local cmd
    cmd="$(ps -p "$pid" -o command= 2>/dev/null)" || return 1
    [[ "$cmd" == *"_daemon"* && "$cmd" == *"$(basename "$SCRIPT_PATH")"* ]]
}

daemon_pid_from_process_list() {
    ps -axo pid=,command= 2>/dev/null | while read -r pid cmdline; do
        [ -n "$pid" ] || continue
        case "$cmdline" in
            *"/awake _daemon"*|*" awake _daemon"*)
                if pid_matches_awake_daemon "$pid"; then
                    echo "$pid"
                    return 0
                fi
                ;;
        esac
    done | head -n 1
}

active_daemon_pid() {
    local pid
    pid="$(read_file_value "$PID_FILE" 2>/dev/null || true)"
    if [ -n "$pid" ] && pid_matches_awake_daemon "$pid"; then
        echo "$pid"
        return 0
    fi
    pid="$(daemon_pid_from_process_list)"
    [ -n "$pid" ] || return 1
    echo "$pid"
}

lease_monitor_target() {
    printf '%s/%s' "$(launch_agent_domain)" "$LEASE_MONITOR_LABEL"
}

lease_monitor_loaded() {
    [ -f "$LEASE_MONITOR_PATH" ] || return 1
    launchctl print "$(lease_monitor_target)" >/dev/null 2>&1
}

monitor_command_path() {
    if [ -x "$LOCAL_BIN_DIR/awake" ] && grep -Fq '_lease-monitor' "$LOCAL_BIN_DIR/awake" 2>/dev/null; then
        printf '%s\n' "$LOCAL_BIN_DIR/awake"
    else
        printf '%s\n' "$SCRIPT_PATH"
    fi
}

pid_matches_awake_lease_monitor() {
    local pid="${1:-}"
    pid_is_alive "$pid" || return 1
    local cmd
    cmd="$(ps -p "$pid" -o command= 2>/dev/null)" || return 1
    [[ "$cmd" == *"_lease-monitor"* && "$cmd" == *"awake"* ]]
}

lease_monitor_ready_pid() {
    local expected_generation="${1:-}" pid heartbeat generation extra now max_age
    pid="$(read_file_value "$LEASE_MONITOR_READY_FILE" 2>/dev/null || true)"
    heartbeat="$(read_file_value "$LEASE_MONITOR_HEARTBEAT_FILE" 2>/dev/null || true)"
    read -r heartbeat generation extra <<< "$heartbeat"
    case "$heartbeat" in ''|*[!0-9]*) return 1 ;; esac
    case "$generation" in ''|*[!0-9]*) return 1 ;; esac
    [ -z "$extra" ] || return 1
    [ -z "$expected_generation" ] || [ "$generation" = "$expected_generation" ] || return 1
    now="$(now_epoch)"
    max_age=$(( POLL_INTERVAL * 3 ))
    [ "$max_age" -lt 45 ] && max_age=45
    [ $(( now - heartbeat )) -le "$max_age" ] || return 1
    pid_matches_awake_lease_monitor "$pid" || return 1
    printf '%s\n' "$pid"
}

lease_monitor_is_healthy() {
    lease_monitor_ready_pid "${1:-}" >/dev/null 2>&1
}

cleanup_stale_daemon_lease() {
    if [ -d "$(lease_dir daemon-agent)" ] && ! active_daemon_pid >/dev/null 2>&1; then
        lease_remove "daemon-agent"
    fi
}

timer_running() {
    local pid
    pid="$(read_file_value "$FOR_PID_FILE")" || return 1
    pid_is_alive "$pid"
}

current_for_token() {
    read_file_value "$FOR_TOKEN_FILE"
}

timer_claim_is_current() {
    local token="$1"
    local pid="$2"
    [ "$(read_file_value "$FOR_PID_FILE" 2>/dev/null)" = "$pid" ] || return 1
    [ "$(current_for_token 2>/dev/null)" = "$token" ]
}

daemon_lock_owned_by() {
    local pid="$1"
    [ -d "$DAEMON_LOCK_DIR" ] || return 1
    [ "$(read_file_value "$DAEMON_OWNER_FILE" 2>/dev/null)" = "$pid" ]
}

daemon_owns_runtime() {
    daemon_lock_owned_by "$$" || return 1
    [ "$(read_file_value "$PID_FILE" 2>/dev/null)" = "$$" ]
}

acquire_daemon_lock() {
    if mkdir "$DAEMON_LOCK_DIR" 2>/dev/null; then
        echo "$$" > "$DAEMON_OWNER_FILE"
        return 0
    fi

    local owner
    owner="$(read_file_value "$DAEMON_OWNER_FILE" 2>/dev/null || true)"
    if pid_matches_awake_daemon "$owner"; then
        return 1
    fi

    rm -rf "$DAEMON_LOCK_DIR" 2>/dev/null || return 1
    mkdir "$DAEMON_LOCK_DIR" 2>/dev/null || return 1
    echo "$$" > "$DAEMON_OWNER_FILE"
}

release_daemon_lock() {
    if daemon_lock_owned_by "$$"; then
        rm -rf "$DAEMON_LOCK_DIR"
    fi
}

python_temp_tool() {
    /usr/bin/python3 - "$@" <<'PY'
import json
import re
import subprocess
import sys
import time
import shutil

args = sys.argv[1:]
action = args[0] if args else "json"

commands = [
    ["sudo", "-n", "powermetrics", "--samplers", "thermal,cpu_power", "-n", "1", "-i", "1000"],
    ["sudo", "-n", "powermetrics", "--samplers", "cpu_power", "-n", "1", "-i", "1000"],
    ["sudo", "-n", "powermetrics", "--samplers", "smc", "-n", "1", "-i", "1000"],
]

payload = {
    "available": False,
    "value": None,
    "unit": "C",
    "sampledAt": int(time.time()),
    "source": "powermetrics",
    "label": "CPU temperature",
    "detail": "CPU via powermetrics",
    "reason": None,
}
smartctl_path = shutil.which("smartctl") or "/opt/homebrew/bin/smartctl"

output = ""
last_error = ""
for cmd in commands:
    try:
        output = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT, timeout=12)
        break
    except subprocess.CalledProcessError as exc:
        last_error = exc.output.strip() or str(exc)
    except Exception as exc:
        last_error = str(exc)
else:
    lowered = last_error.lower()
    if "password is required" in lowered or "superuser" in lowered or "not allowed" in lowered:
        payload["reason"] = "Needs passwordless sudo for /usr/bin/powermetrics"
    else:
        payload["reason"] = last_error or "Unable to read CPU temperature"
    if action == "json":
        print(json.dumps(payload))
    else:
        print(payload["reason"])
    raise SystemExit(0)

temps = []
preferred_patterns = [
    r"CPU(?:\s+\w+)?\s+die temperature:\s*([0-9]+(?:\.[0-9]+)?)\s*C",
    r"CPU(?:\s+[A-Za-z0-9_-]+)? temperature:\s*([0-9]+(?:\.[0-9]+)?)\s*C",
    r"(?:P-CPU|E-CPU)\s+temperature:\s*([0-9]+(?:\.[0-9]+)?)\s*C",
]

for pattern in preferred_patterns:
    for match in re.finditer(pattern, output, re.IGNORECASE):
        temps.append(float(match.group(1)))

if not temps:
    for line in output.splitlines():
        lowered = line.lower()
        if "temperature" not in lowered:
            continue
        if "cpu" not in lowered and "die" not in lowered:
            continue
        match = re.search(r"([0-9]+(?:\.[0-9]+)?)\s*C", line)
        if match:
            temps.append(float(match.group(1)))

if not temps:
    try:
        scan = subprocess.check_output([smartctl_path, "--scan"], text=True, stderr=subprocess.STDOUT, timeout=10)
        candidates = []
        for line in scan.splitlines():
            line = line.strip()
            if not line:
                continue
            if " -d nvme" in line:
                device = line.split(" -d nvme", 1)[0].strip()
                candidates.append(device)
        for device in candidates:
            try:
                smart_proc = subprocess.run(
                    [smartctl_path, "-a", "-d", "nvme", device],
                    text=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    timeout=15,
                    check=False,
                )
                smart = smart_proc.stdout or ""
            except Exception:
                continue
            match = re.search(r"^\s*Temperature:\s*([0-9]+(?:\.[0-9]+)?)\s+Celsius\s*$", smart, re.MULTILINE)
            if match:
                payload["available"] = True
                payload["value"] = round(float(match.group(1)), 1)
                payload["source"] = "nvme-smart"
                payload["label"] = "SSD temperature"
                payload["detail"] = "NVMe SMART fallback"
                payload["reason"] = None
                break
        if not payload["available"]:
            payload["reason"] = "No CPU or NVMe temperature was available"
    except Exception as exc:
        payload["reason"] = f"powermetrics did not return a CPU temperature"
else:
    payload["available"] = True
    payload["value"] = round(max(temps), 1)

if action == "json":
    print(json.dumps(payload))
elif action == "value":
    if payload["available"]:
        print(payload["value"])
    else:
        raise SystemExit(payload["reason"] or "unavailable")
else:
    raise SystemExit(f"unknown temp action: {action}")
PY
}

managed_keys_string() {
    echo "sleep displaysleep disksleep womp powernap lessbright lidwake acwake"
}

advanced_keys_string() {
    echo "ttyskeepawake proximitywake standby autopoweroff hibernatemode"
}

all_settings_keys_string() {
    echo "$(managed_keys_string) $(advanced_keys_string)"
}

copy_if_present() {
    local src="$1"
    local dst="$2"
    [ -f "$src" ] || return 1
    mkdir -p "$(dirname "$dst")"
    if [ "$(cd -- "$(dirname "$src")" && pwd)/$(basename "$src")" != "$(cd -- "$(dirname "$dst")" && pwd 2>/dev/null || true)/$(basename "$dst")" ]; then
        cp "$src" "$dst"
    fi
    chmod +x "$dst" 2>/dev/null || true
}

notify() {
    osascript -e "display notification \"$2\" with title \"$1\"" 2>/dev/null || true
}

has_passwordless_pmset() {
    sudo -n pmset -g >/dev/null 2>&1
}

has_passwordless_powermetrics() {
    sudo -n powermetrics -h >/dev/null 2>&1
}

claude_settings_path() {
    echo "$HOME/.claude/settings.json"
}

codex_config_path() {
    echo "$HOME/.codex/config.toml"
}

claude_detected() {
    [ -f "$(claude_settings_path)" ] || command -v claude >/dev/null 2>&1
}

codex_detected() {
    [ -d "$HOME/.codex" ] || command -v codex >/dev/null 2>&1
}

claude_hook_installed() {
    local settings
    settings="$(claude_settings_path)"
    [ -f "$settings" ] || return 1
    python3 - "$settings" "$LOCAL_BIN_DIR/awake-hook claude" <<'PY'
import json
import sys

path = sys.argv[1]
command = sys.argv[2]
try:
    with open(path) as f:
        cfg = json.load(f)
except Exception:
    raise SystemExit(1)

hooks = cfg.get("hooks", {})
entries = hooks.get("PreToolUse", [])
for entry in entries:
    for hook in entry.get("hooks", []):
        if hook.get("type") == "command" and hook.get("command") == command:
            raise SystemExit(0)
raise SystemExit(1)
PY
}

install_claude_hooks() {
    local settings
    settings="$(claude_settings_path)"
    mkdir -p "$(dirname "$settings")"
    if [ ! -f "$settings" ]; then
        printf '{\n  "hooks": {}\n}\n' > "$settings"
    fi
    python3 - "$settings" "$LOCAL_BIN_DIR/awake-hook claude" <<'PY'
import json
import sys

path = sys.argv[1]
command = sys.argv[2]
with open(path) as f:
    cfg = json.load(f)
hooks = cfg.setdefault("hooks", {})
entries = hooks.setdefault("PreToolUse", [])
if not any(
    hook.get("type") == "command" and hook.get("command") == command
    for entry in entries
    for hook in entry.get("hooks", [])
):
    entries.append({
        "matcher": "",
        "hooks": [{"type": "command", "command": command, "timeout": 3}]
    })
with open(path, "w") as f:
    json.dump(cfg, f, indent=2)
    f.write("\n")
PY
}

codex_notify_installed() {
    local cfg
    cfg="$(codex_config_path)"
    [ -f "$cfg" ] || return 1
    grep -Fq "notify = \"$LOCAL_BIN_DIR/awake-notify\"" "$cfg"
}

install_codex_notify() {
    local cfg
    cfg="$(codex_config_path)"
    mkdir -p "$(dirname "$cfg")"
    touch "$cfg"
    if ! codex_notify_installed; then
        printf '\nnotify = "%s"\n' "$LOCAL_BIN_DIR/awake-notify" >> "$cfg"
    fi
}

setup_status_json() {
    sync_rule_leases
    cleanup_expired_leases
    local winning_lease="" effective_mode="" resolved_mode="" effective_reason=""
    local batt="" charging=false
    winning_lease="$(best_lease_id 2>/dev/null || true)"
    effective_mode="$(effective_mode_value 2>/dev/null || true)"
    resolved_mode="$(effective_resolved_mode 2>/dev/null || true)"
    effective_reason="$(effective_reason_value 2>/dev/null || true)"
    batt="$(get_battery_pct 2>/dev/null || true)"
    is_charging && charging=true

    printf '{'
    printf '"sleepControlConfigured":%s,' "${HAS_PMSET:-false}"
    printf '"temperatureConfigured":%s,' "${HAS_POWERMETRICS:-false}"
    printf '"claudeDetected":%s,' "${HAS_CLAUDE:-false}"
    printf '"claudeConfigured":%s,' "${HAS_CLAUDE_HOOK:-false}"
    printf '"codexDetected":%s,' "${HAS_CODEX:-false}"
    printf '"codexConfigured":%s,' "${HAS_CODEX_NOTIFY:-false}"
    printf '"powerState":"%s",' "$(json_escape "$(current_state)")"
    printf '"daemonRunning":%s,' "$([ -n "$(active_daemon_pid 2>/dev/null || true)" ] && echo true || echo false)"
    printf '"timerActive":%s,' "$(timer_running && echo true || echo false)"
    printf '"leaseCount":%s,' "$(lease_count)"
    printf '"ruleCount":%s,' "$(rule_count)"
    printf '"defaultMode":"%s",' "$(json_escape "$(current_default_mode)")"
    printf '"effectiveLeaseId":%s,' "$(json_optional_string "$winning_lease")"
    printf '"effectiveMode":%s,' "$(json_optional_string "$effective_mode")"
    printf '"effectiveResolvedMode":%s,' "$(json_optional_string "$resolved_mode")"
    printf '"effectiveReason":%s,' "$(json_optional_string "$effective_reason")"
    printf '"whyAwake":"%s",' "$(json_escape "$(why_summary)")"
    printf '"restorePlan":"%s",' "$(json_escape "$(restore_plan_summary)")"
    printf '"batteryPercent":%s,' "${batt:-null}"
    printf '"batteryCharging":%s,' "$charging"
    printf '"leases":%s,' "$(leases_json)"
    printf '"rules":%s,' "$(rules_json)"
    printf '"warnings":%s' "$(warnings_json)"
    printf '}\n'
}

get_battery_pct() {
    local output pct
    output="$(pmset -g batt 2>/dev/null || true)"
    pct="$(printf '%s\n' "$output" | sed -nE '/InternalBattery/s/.*[^0-9]([0-9]{1,3})%.*/\1/p' | head -n 1)"
    if [ -z "$pct" ]; then
        pct="$(printf '%s\n' "$output" | sed -nE '/[Bb]attery/s/.*[^0-9]([0-9]{1,3})%.*/\1/p' | head -n 1)"
    fi
    printf '%s\n' "$pct"
}

is_charging() {
    pmset -g batt 2>/dev/null | grep -Eqi 'InternalBattery.*;[[:space:]]*(charging|charged)(;|$)'
}

clear_battery_guard_state() {
    rm -f "$BATTERY_GUARD_FILE"
}

enforce_battery_guard() {
    local batt stage=""
    batt="$(get_battery_pct 2>/dev/null || true)"

    if [ -z "$batt" ] || is_charging; then
        clear_battery_guard_state
        return 1
    fi

    case "$batt" in
        ''|*[!0-9]*)
            clear_battery_guard_state
            return 1
            ;;
    esac

    stage="$(read_file_value "$BATTERY_GUARD_FILE" 2>/dev/null || true)"

    if [ "$batt" -le "$BATTERY_CRITICAL" ] 2>/dev/null; then
        if [ "$stage" != "critical" ]; then
            log "CRITICAL: Battery at ${batt}% — forcing sleep"
            notify "awake" "Battery critical (${batt}%). Sleeping now."
        fi
        printf 'critical\n' > "$BATTERY_GUARD_FILE"
        force_sleep
        return 0
    fi

    if [ "$batt" -le "$BATTERY_WARN" ] 2>/dev/null; then
        if [ "$stage" != "warn" ]; then
            log "WARNING: Battery at ${batt}%"
            notify "awake" "Battery low (${batt}%). Plug in soon."
            printf 'warn\n' > "$BATTERY_GUARD_FILE"
        fi
        return 1
    fi

    clear_battery_guard_state
    return 1
}

HOOK_STALE_SECONDS=120  # 2 min without heartbeat = idle/dead

fresh_hook_files() {
    # Return count of hook files with mtime < HOOK_STALE_SECONDS
    # Also clean up stale ones
    local now count=0
    now=$(date +%s)
    for f in /tmp/awake-claude-* /tmp/awake-codex-*; do
        [ -f "$f" ] || continue
        local mtime
        mtime=$(stat -f %m "$f" 2>/dev/null) || continue
        local age=$(( now - mtime ))
        if [ "$age" -lt "$HOOK_STALE_SECONDS" ]; then
            count=$(( count + 1 ))
        else
            log "Cleaning stale hook: $(basename "$f")" >&2
            rm -f "$f"
        fi
    done
    echo "$count"
}

agents_running() {
    # Check FRESH hook state files (heartbeat within last 2 min)
    local fresh
    fresh=$(fresh_hook_files)
    [ "$fresh" -gt 0 ] && return 0
    # Check process list for non-Claude agents (Claude uses hooks)
    local agent
    for agent in $AGENTS; do
        [ "$agent" = "claude" ] && continue  # Claude uses hooks only
        pgrep -x "$agent" >/dev/null 2>&1 && return 0
    done
    # Aider fallback: may run as python3 -m aider
    pgrep -f "python.*aider" >/dev/null 2>&1 && return 0
    return 1
}

agent_summary() {
    local parts=()
    local agent count
    for agent in $AGENTS; do
        if [ "$agent" = "claude" ]; then
            # Use hooks for claude (more accurate than pgrep)
            count=$(fresh_hook_files)
            [ "$count" -gt 0 ] && parts+=("claude ($count active)")
        else
            count=$(pgrep -x "$agent" 2>/dev/null | wc -l | tr -d ' ')
            [ "$count" -gt 0 ] && parts+=("$agent ($count)")
        fi
    done
    if [ ${#parts[@]} -eq 0 ]; then
        echo "none"
    else
        local IFS=", "; echo "${parts[*]}"
    fi
}

current_state() {
    cat "$STATE_FILE" 2>/dev/null || echo "unknown"
}

on_ac_power() {
    pmset -g batt 2>/dev/null | head -n 1 | grep -q "AC Power"
}

current_wifi_ssid() {
    if [ -n "${AWAKE_TEST_WIFI_SSID:-}" ]; then
        echo "$AWAKE_TEST_WIFI_SSID"
        return 0
    fi
    local airport="/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport"
    if [ -x "$airport" ]; then
        "$airport" -I 2>/dev/null | awk -F': ' '/ SSID/ {print $2; exit}'
        return 0
    fi
    networksetup -getairportnetwork en0 2>/dev/null | sed 's/^Current Wi-Fi Network: //'
}

external_display_connected() {
    if [ -n "${AWAKE_TEST_EXTERNAL_DISPLAY:-}" ]; then
        [ "$AWAKE_TEST_EXTERNAL_DISPLAY" = "1" ]
        return
    fi
    local count
    count="$(system_profiler SPDisplaysDataType 2>/dev/null | grep -c 'Resolution:')" || count=0
    [ "$count" -gt 1 ] 2>/dev/null
}

mode_label() {
    case "$1" in
        running) echo "Keep Running" ;;
        presenting) echo "Keep Presenting" ;;
        agent-safe) echo "Agent Safe" ;;
        *) echo "$1" ;;
    esac
}

why_summary() {
    local id mode resolved reason
    id="$(best_lease_id 2>/dev/null || true)"
    if [ -z "$id" ]; then
        echo "Normal sleep. No active leases."
        return 0
    fi
    mode="$(lease_field "$id" mode 2>/dev/null || echo agent-safe)"
    resolved="$(resolve_mode "$mode")"
    reason="$(lease_field "$id" reason 2>/dev/null || echo "Awake is active")"
    echo "$reason ($id, $(mode_label "$mode"), resolved as $(mode_label "$resolved"))"
}

restore_plan_summary() {
    local count
    count="$(lease_count)"
    if [ "$count" -eq 0 ]; then
        echo "Restore baseline power settings immediately."
    else
        echo "Keep current wake state until the remaining leases end."
    fi
}

rule_matches() {
    local id="$1"
    local type value
    type="$(rule_field "$id" type 2>/dev/null || true)"
    value="$(rule_field "$id" value 2>/dev/null || true)"
    case "$type" in
        process)
            pgrep -x "$value" >/dev/null 2>&1 || pgrep -f "$value" >/dev/null 2>&1
            ;;
        power)
            if [ "$value" = "ac" ]; then
                on_ac_power
            else
                ! on_ac_power
            fi
            ;;
        battery-above)
            local batt
            batt="$(get_battery_pct)"
            [ -n "$batt" ] && [ "$batt" -ge "$value" ] 2>/dev/null
            ;;
        battery-below)
            local batt
            batt="$(get_battery_pct)"
            [ -n "$batt" ] && [ "$batt" -le "$value" ] 2>/dev/null
            ;;
        wifi)
            [ "$(current_wifi_ssid)" = "$value" ]
            ;;
        display)
            [ "$value" = "external" ] && external_display_connected
            ;;
        *)
            return 1
            ;;
    esac
}

sync_rule_leases() {
    ensure_runtime_dirs
    local dir id mode reason priority rule_id
    for dir in "$LEASES_DIR"/rule-*; do
        [ -d "$dir" ] || continue
        rule_id="$(basename "$dir")"
        rule_id="${rule_id#rule-}"
        [ -d "$(rule_dir "$rule_id")" ] || rm -rf "$dir"
    done
    for dir in "$RULES_DIR"/*; do
        [ -d "$dir" ] || continue
        id="$(basename "$dir")"
        mode="$(rule_field "$id" mode 2>/dev/null || echo agent-safe)"
        reason="$(rule_field "$id" reason 2>/dev/null || echo "Rule $id matched")"
        priority="$(rule_field "$id" priority 2>/dev/null || echo 60)"
        if rule_matches "$id"; then
            lease_create_or_update "rule-$id" "rule" "$mode" "$reason" "$priority" "" "rule"
        else
            lease_remove "rule-$id"
        fi
    done
}

leases_json() {
    cleanup_expired_leases
    ensure_runtime_dirs
    local dir id first=true
    printf '['
    for dir in "$LEASES_DIR"/*; do
        [ -d "$dir" ] || continue
        id="$(basename "$dir")"
        $first || printf ','
        first=false
        local type mode reason priority started expires source
        type="$(lease_field "$id" type 2>/dev/null || true)"
        mode="$(lease_field "$id" mode 2>/dev/null || true)"
        reason="$(lease_field "$id" reason 2>/dev/null || true)"
        priority="$(lease_field "$id" priority 2>/dev/null || echo 0)"
        started="$(lease_field "$id" started_at 2>/dev/null || echo 0)"
        expires="$(lease_field "$id" expires_at 2>/dev/null || true)"
        source="$(lease_field "$id" source 2>/dev/null || true)"
        printf '{"id":"%s","type":"%s","mode":"%s","resolvedMode":"%s","reason":"%s","priority":%s,"startedAt":%s,"expiresAt":%s,"source":"%s"}' \
            "$(json_escape "$id")" \
            "$(json_escape "$type")" \
            "$(json_escape "$mode")" \
            "$(json_escape "$(resolve_mode "$mode")")" \
            "$(json_escape "$reason")" \
            "${priority:-0}" \
            "${started:-0}" \
            "${expires:-null}" \
            "$(json_escape "$source")"
    done
    printf ']'
}

rules_json() {
    ensure_runtime_dirs
    local dir id first=true
    printf '['
    for dir in "$RULES_DIR"/*; do
        [ -d "$dir" ] || continue
        id="$(basename "$dir")"
        $first || printf ','
        first=false
        local type value mode reason priority
        type="$(rule_field "$id" type 2>/dev/null || true)"
        value="$(rule_field "$id" value 2>/dev/null || true)"
        mode="$(rule_field "$id" mode 2>/dev/null || echo agent-safe)"
        reason="$(rule_field "$id" reason 2>/dev/null || true)"
        priority="$(rule_field "$id" priority 2>/dev/null || echo 60)"
        printf '{"id":"%s","type":"%s","value":"%s","mode":"%s","reason":"%s","priority":%s}' \
            "$(json_escape "$id")" \
            "$(json_escape "$type")" \
            "$(json_escape "$value")" \
            "$(json_escape "$mode")" \
            "$(json_escape "$reason")" \
            "${priority:-60}"
    done
    printf ']'
}

warnings_json() {
    local -a warnings=()
    has_passwordless_pmset || warnings+=("Sleep control not configured")
    has_passwordless_powermetrics || warnings+=("Temperature sampling not configured")
    if [ "$(lease_count)" -gt 0 ] && [ -n "$(get_battery_pct)" ] && ! is_charging; then
        local batt
        batt="$(get_battery_pct)"
        if [ -n "$batt" ] && [ "$batt" -le "$BATTERY_WARN" ] 2>/dev/null; then
            warnings+=("Battery is low while Awake is active")
        fi
    fi
    if [ "$(lease_count)" -eq 0 ] && ! timer_running && [ -z "$(active_daemon_pid 2>/dev/null || true)" ] && [[ "$(current_state)" == nosleep* ]]; then
        warnings+=("Nosleep state appears stale: no active leases are holding the machine awake")
    fi
    if [ ${#warnings[@]} -eq 0 ]; then
        printf '[]'
        return 0
    fi
    local first=true item
    printf '['
    for item in "${warnings[@]}"; do
        $first || printf ','
        first=false
        printf '"%s"' "$(json_escape "$item")"
    done
    printf ']'
}

python_settings_tool() {
    /usr/bin/python3 - "$BASELINE_FILE" "$@" <<'PY'
import json
import os
import re
import subprocess
import sys
import tempfile
import time

baseline_path = sys.argv[1]
args = sys.argv[2:]
if not args:
    raise SystemExit("missing action")

action = args[0]

SETTING_DEFS = {
    "sleep": {"kind": "minutes", "advanced": False},
    "displaysleep": {"kind": "minutes", "advanced": False},
    "disksleep": {"kind": "minutes", "advanced": False},
    "womp": {"kind": "bool", "advanced": False},
    "powernap": {"kind": "bool", "advanced": False},
    "lessbright": {"kind": "bool", "advanced": False},
    "lidwake": {"kind": "bool", "advanced": False},
    "acwake": {"kind": "bool", "advanced": False},
    "ttyskeepawake": {"kind": "bool", "advanced": True},
    "proximitywake": {"kind": "bool", "advanced": True},
    "standby": {"kind": "bool", "advanced": True},
    "autopoweroff": {"kind": "bool", "advanced": True},
    "hibernatemode": {"kind": "enum", "advanced": True, "allowed": {0, 3, 25}},
}
OVERRIDE_KEYS = ["sleep", "displaysleep", "standby", "hibernatemode"]
SOURCES = {"battery": "Battery Power:", "ac": "AC Power:"}
SOURCE_FLAGS = {"battery": "-b", "ac": "-c"}


def run(cmd):
    return subprocess.check_output(cmd, text=True)


def parse_custom():
    out = run(["pmset", "-g", "custom"])
    result = {source: {} for source in SOURCES}
    current = None
    for raw in out.splitlines():
        line = raw.rstrip()
        if not line.strip():
            continue
        matched = False
        for source, header in SOURCES.items():
            if line.strip() == header:
                current = source
                matched = True
                break
        if matched:
            continue
        if current is None:
            continue
        parts = line.split()
        if len(parts) < 2:
            continue
        key = parts[0]
        value = parts[-1]
        if key not in SETTING_DEFS:
            continue
        try:
            result[current][key] = int(value)
        except ValueError:
            continue
    return result


def parse_disablesleep():
    out = run(["pmset", "-g"])
    match = re.search(r"^\s*disablesleep\s+(\d+)\s*$", out, re.MULTILINE)
    return int(match.group(1)) if match else 0


def load_baseline():
    if not os.path.exists(baseline_path):
        return None
    with open(baseline_path) as f:
        return json.load(f)


def save_baseline(data):
    os.makedirs(os.path.dirname(baseline_path), exist_ok=True)
    fd, temp_path = tempfile.mkstemp(prefix="awake-baseline.", dir=os.path.dirname(baseline_path))
    try:
        with os.fdopen(fd, "w") as f:
            json.dump(data, f, indent=2, sort_keys=True)
            f.write("\n")
        os.replace(temp_path, baseline_path)
    finally:
        if os.path.exists(temp_path):
            os.unlink(temp_path)


def delete_baseline():
    try:
        os.unlink(baseline_path)
    except FileNotFoundError:
        pass


def build_payload():
    effective = parse_custom()
    baseline = load_baseline()
    active = bool(baseline and baseline.get("active"))
    baseline_sources = baseline["sources"] if active else effective
    return {
        "effective": effective,
        "baseline": baseline_sources,
        "overrideActive": active,
        "disablesleep": parse_disablesleep(),
        "baselineDisablesleep": baseline.get("disablesleep", 0) if active else parse_disablesleep(),
        "managedKeys": [k for k, meta in SETTING_DEFS.items() if not meta["advanced"]],
        "advancedKeys": [k for k, meta in SETTING_DEFS.items() if meta["advanced"]],
        "availableSources": [source for source, values in effective.items() if values],
    }


def coerce_value(key, value):
    meta = SETTING_DEFS[key]
    ivalue = int(value)
    kind = meta["kind"]
    if kind == "bool" and ivalue not in (0, 1):
        raise SystemExit(f"{key} expects 0 or 1")
    if kind == "enum" and ivalue not in meta["allowed"]:
        raise SystemExit(f"{key} expects one of {sorted(meta['allowed'])}")
    if kind == "minutes" and ivalue < 0:
        raise SystemExit(f"{key} expects a non-negative integer")
    return ivalue


def ensure_pairs(items):
    if len(items) == 0 or len(items) % 2 != 0:
        raise SystemExit("expected key/value pairs")
    pairs = []
    for i in range(0, len(items), 2):
        key = items[i]
        if key not in SETTING_DEFS:
            raise SystemExit(f"unsupported key: {key}")
        pairs.append((key, coerce_value(key, items[i + 1])))
    return pairs


if action in {"dump", "refresh"}:
    print(json.dumps(build_payload()))
elif action == "show":
    payload = build_payload()
    print("override active:", "yes" if payload["overrideActive"] else "no")
    for source in payload["availableSources"]:
        print(f"{source}:")
        print("  baseline:")
        for key in payload["baseline"][source]:
            print(f"    {key}: {payload['baseline'][source][key]}")
        print("  effective:")
        for key in payload["effective"][source]:
            print(f"    {key}: {payload['effective'][source][key]}")
elif action == "ensure-baseline":
    existing = load_baseline()
    if existing and existing.get("active"):
        print("existing")
    else:
        save_baseline({
            "version": 1,
            "active": True,
            "captured_at": int(time.time()),
            "disablesleep": parse_disablesleep(),
            "sources": parse_custom(),
        })
        print("captured")
elif action == "restore-baseline":
    baseline = load_baseline()
    if not baseline or not baseline.get("active"):
        raise SystemExit(1)
    else:
        for source, values in baseline["sources"].items():
            if not values:
                continue
            cmd = ["sudo", "-n", "pmset", SOURCE_FLAGS[source]]
            for key, value in values.items():
                if key in SETTING_DEFS:
                    cmd.extend([key, str(value)])
            if len(cmd) > 4:
                subprocess.check_call(cmd)
        subprocess.check_call(["sudo", "-n", "pmset", "-a", "disablesleep", str(baseline.get("disablesleep", 0))])
        delete_baseline()
        print("restored")
elif action == "baseline-get":
    source, key = args[1], args[2]
    baseline = load_baseline()
    if baseline and baseline.get("active"):
        value = baseline.get("sources", {}).get(source, {}).get(key)
    else:
        payload = build_payload()
        value = payload["baseline"].get(source, {}).get(key)
    if value is None:
        raise SystemExit(1)
    print(value)
elif action == "baseline-set":
    source = args[1]
    pairs = ensure_pairs(args[2:])
    baseline = load_baseline()
    if not baseline:
        baseline = {
            "version": 1,
            "active": False,
            "captured_at": int(time.time()),
            "disablesleep": parse_disablesleep(),
            "sources": parse_custom(),
        }
    baseline.setdefault("sources", {}).setdefault(source, {})
    for key, value in pairs:
        baseline["sources"][source][key] = value
    save_baseline(baseline)
    print("updated")
elif action == "apply-live":
    source = args[1]
    pairs = ensure_pairs(args[2:])
    if source not in SOURCE_FLAGS:
        raise SystemExit("source must be battery or ac")
    cmd = ["sudo", "-n", "pmset", SOURCE_FLAGS[source]]
    for key, value in pairs:
        cmd.extend([key, str(value)])
    subprocess.check_call(cmd)
    print("applied")
else:
    raise SystemExit(f"unknown action: {action}")
PY
}

settings_dump_json() {
    python_settings_tool dump
}

ensure_baseline_snapshot() {
    python_settings_tool ensure-baseline >/dev/null
}

restore_baseline_snapshot() {
    python_settings_tool restore-baseline >/dev/null 2>&1
}

restore_fallback_sleep_settings() {
    sudo -n pmset -a disablesleep 0 sleep 10 displaysleep 10 standby 1 hibernatemode 3 2>/dev/null
}

current_disablesleep_value() {
    pmset -g 2>/dev/null | awk '$1 == "disablesleep" { print $2; exit }'
}

mark_power_override_active() {
    local marker_tmp
    mkdir -p "$(dirname "$OVERRIDE_MARKER_FILE")"
    marker_tmp="${OVERRIDE_MARKER_FILE}.tmp.$$"
    printf '%s\n' "$(now_epoch)" > "$marker_tmp"
    mv "$marker_tmp" "$OVERRIDE_MARKER_FILE"
}

caffeinate_process_running() {
    local pid command
    pid="$(read_file_value "$CAFFEINE_PID_FILE" 2>/dev/null || true)"
    [ -n "$pid" ] || return 1
    pid_is_alive "$pid" || return 1
    command="$(ps -p "$pid" -o command= 2>/dev/null || true)"
    case "$command" in
        *caffeinate*) return 0 ;;
        *) return 1 ;;
    esac
}

stop_owned_caffeinate() {
    if caffeinate_process_running; then
        kill "$(cat "$CAFFEINE_PID_FILE")" 2>/dev/null || true
    fi
    rm -f "$CAFFEINE_PID_FILE"
}

baseline_setting_value() {
    local source="$1"
    local key="$2"
    python_settings_tool baseline-get "$source" "$key" 2>/dev/null
}

settings_apply_live() {
    local source="$1"
    shift
    python_settings_tool apply-live "$source" "$@" >/dev/null
}

settings_update_baseline() {
    local source="$1"
    shift
    python_settings_tool baseline-set "$source" "$@" >/dev/null
}

restore_normal_sleep_settings() {
    local state kernel_value
    state="$(current_state)"
    kernel_value="$(current_disablesleep_value 2>/dev/null || true)"
    if [[ "$state" != nosleep* ]] && [ ! -f "$CAFFEINE_PID_FILE" ] && [ ! -f "$BASELINE_FILE" ] && [ ! -f "$OVERRIDE_MARKER_FILE" ]; then
        if [ "$kernel_value" = "1" ]; then
            log "WARNING: disablesleep=1 is active but Awake has no ownership record; leaving the user setting unchanged"
        fi
        echo "normal" > "$STATE_FILE"
        rm -f "$WHY_FILE"
        return 0
    fi
    stop_owned_caffeinate
    if ! restore_baseline_snapshot; then
        if ! restore_fallback_sleep_settings; then
            log "ERROR: failed to restore normal sleep settings; recovery will retry on the next reconciliation"
            return 1
        fi
    fi
    echo "normal" > "$STATE_FILE"
    rm -f "$WHY_FILE" "$OVERRIDE_MARKER_FILE"
}

apply_presenting_mode() {
    local cs=$(current_state)
    if { [ "$cs" = "nosleep-full" ] || [ "$cs" = "nosleep" ]; } &&
        [ "$(current_disablesleep_value 2>/dev/null || true)" = "1" ] &&
        caffeinate_process_running; then
        # Migrate old format
        [ "$cs" = "nosleep" ] && echo "nosleep-full" > "$STATE_FILE"
        return 0
    fi
    log "Activating nosleep (full)"
    ensure_baseline_snapshot
    mark_power_override_active
    sudo -n pmset -a disablesleep 1 standby 0 hibernatemode 0 sleep 0 displaysleep 0 2>/dev/null || {
        log "ERROR: sudo pmset failed. Run: awake install"
        return 1
    }
    stop_owned_caffeinate
    caffeinate -disu &>/dev/null &
    echo $! > "$CAFFEINE_PID_FILE"
    echo "nosleep-full" > "$STATE_FILE"
}

apply_running_mode() {
    local cs=$(current_state)
    if [ "$cs" = "nosleep-display" ] &&
        [ "$(current_disablesleep_value 2>/dev/null || true)" = "1" ] &&
        caffeinate_process_running; then
        return 0
    fi
    log "Activating nosleep (display sleep allowed)"
    ensure_baseline_snapshot
    mark_power_override_active
    sudo -n pmset -a disablesleep 1 standby 0 hibernatemode 0 sleep 0 2>/dev/null || {
        log "ERROR: sudo pmset failed. Run: awake install"
        return 1
    }
    local battery_display
    local ac_display
    battery_display="$(baseline_setting_value battery displaysleep)"
    ac_display="$(baseline_setting_value ac displaysleep)"
    [ -n "$battery_display" ] && sudo -n pmset -b displaysleep "$battery_display" 2>/dev/null || true
    [ -n "$ac_display" ] && sudo -n pmset -c displaysleep "$ac_display" 2>/dev/null || true
    stop_owned_caffeinate
    caffeinate -isu &>/dev/null &
    echo $! > "$CAFFEINE_PID_FILE"
    echo "nosleep-display" > "$STATE_FILE"
}

reconcile_effective_state() {
    cleanup_expired_leases
    cleanup_stale_daemon_lease
    local id mode resolved reason
    id="$(best_lease_id 2>/dev/null || true)"
    if [ -z "$id" ]; then
        clear_battery_guard_state
        restore_normal_sleep_settings
        return $?
    fi
    ensure_active_lease_monitor || {
        log "ERROR: no healthy lease battery monitor; refusing to hold Awake leases"
        return 1
    }
    if enforce_battery_guard; then
        return 0
    fi
    mode="$(lease_field "$id" mode 2>/dev/null || echo agent-safe)"
    resolved="$(resolve_mode "$mode")"
    reason="$(lease_field "$id" reason 2>/dev/null || echo "Awake is active")"
    printf '%s\n' "$reason" > "$WHY_FILE"
    case "$resolved" in
        running) apply_running_mode ;;
        presenting) apply_presenting_mode ;;
        *) apply_presenting_mode ;;
    esac
}

restore_sleep_after_monitor_failure() {
    clear_manual_leases
    cancel_timer_session
    lease_remove "run-command"
    lease_remove "daemon-agent"
    local dir
    for dir in "$LEASES_DIR"/rule-*; do
        [ -d "$dir" ] || continue
        rm -rf "$dir"
    done
    restore_normal_sleep_settings
}

cleanup_invalid_leases() {
    cleanup_expired_leases
    cleanup_stale_daemon_lease

    local dir id type owner rule_id
    for dir in "$LEASES_DIR"/*; do
        [ -d "$dir" ] || continue
        id="$(basename "$dir")"
        if ! lease_is_well_formed "$id"; then
            log "Removing malformed lease: $id"
            lease_remove "$id"
            continue
        fi
        type="$(lease_field "$id" type 2>/dev/null || true)"
        case "$type:$id" in
            manual:manual-toggle)
                ;;
            timer:manual-timer)
                owner="$(lease_field "$id" owner_pid 2>/dev/null || true)"
                if ! timer_running || [ "$owner" != "$(read_file_value "$FOR_PID_FILE" 2>/dev/null || true)" ] || [ ! -f "$FOR_TOKEN_FILE" ] || [ ! -f "$FOR_END_FILE" ]; then
                    log "Removing stale timer lease"
                    lease_remove "$id"
                    rm -f "$FOR_PID_FILE" "$FOR_TOKEN_FILE" "$FOR_END_FILE"
                fi
                ;;
            command:run-command)
                owner="$(lease_field "$id" owner_pid 2>/dev/null || true)"
                if ! pid_is_alive "$owner"; then
                    log "Removing stale command lease"
                    lease_remove "$id"
                fi
                ;;
            rule:rule-*)
                rule_id="${id#rule-}"
                [ -d "$(rule_dir "$rule_id")" ] || lease_remove "$id"
                ;;
            *)
                log "Removing unknown lease: $id"
                lease_remove "$id"
                ;;
        esac
    done
}

recover_power_state_on_launch() {
    cleanup_invalid_leases
    lease_remove "daemon-agent"
    sync_rule_leases

    if agents_running || [ "$(lease_count)" -gt 0 ]; then
        return 0
    fi

    if [ -f "$BASELINE_FILE" ] || [ -f "$OVERRIDE_MARKER_FILE" ] || [[ "$(current_state)" == nosleep* ]]; then
        log "Recovering stale Awake power override"
    fi
    restore_normal_sleep_settings
}

activate_nosleep() {
    lease_create_or_update "manual-toggle" "manual" "presenting" "Manual awake session" 100 "" "cli"
    reconcile_effective_state || {
        log "ERROR: Battery monitor failed to start; refusing manual Awake session"
        restore_sleep_after_monitor_failure
        return 1
    }
}

activate_nosleep_display() {
    lease_create_or_update "manual-toggle" "manual" "running" "Manual awake session with display sleep allowed" 100 "" "cli"
    reconcile_effective_state || {
        log "ERROR: Battery monitor failed to start; refusing manual Awake session"
        restore_sleep_after_monitor_failure
        return 1
    }
}

activate_default_manual_session() {
    lease_create_or_update "manual-toggle" "manual" "$(current_default_mode)" "Manual awake session" 100 "" "cli"
    reconcile_effective_state || {
        log "ERROR: Battery monitor failed to start; refusing manual Awake session"
        restore_sleep_after_monitor_failure
        return 1
    }
}

activate_yessleep() {
    clear_manual_leases
    cancel_timer_session
    reconcile_effective_state
}

force_sleep() {
    log "Forcing sleep"
    lease_remove "daemon-agent"
    cancel_timer_session
    clear_manual_leases
    local dir
    for dir in "$LEASES_DIR"/rule-*; do
        [ -d "$dir" ] || continue
        rm -rf "$dir"
    done
    restore_normal_sleep_settings
    # Drop display power immediately, then keep asking macOS to sleep.
    # This is the emergency path for critical battery and should win even
    # if Awake was previously holding strong no-sleep assertions.
    sudo -n pmset displaysleepnow 2>/dev/null || true
    local attempt
    for attempt in 1 2 3; do
        sudo -n pmset sleepnow 2>/dev/null && return 0
        sleep 1
    done
    # Some setups ignore or reject pmset sleepnow even after Awake drops its
    # own assertions. Fall back to a direct Apple event so the machine still
    # attempts to sleep on critical battery.
    osascript -e 'tell application "System Events" to sleep' >/dev/null 2>&1 && return 0
    log "ERROR: Failed to force sleep via pmset sleepnow"
    return 1
}

force_sleep_if_unleased() {
    lease_remove "daemon-agent"
    if [ "$(lease_count)" -eq 0 ]; then
        force_sleep
    else
        reconcile_effective_state
    fi
}

cleanup() {
    trap - EXIT INT TERM
    if ! daemon_owns_runtime; then
        release_daemon_lock
        return 0
    fi
    log "Shutting down — restoring normal sleep"
    lease_remove "daemon-agent"
    local dir
    for dir in "$LEASES_DIR"/rule-*; do
        [ -d "$dir" ] || continue
        rm -rf "$dir"
    done
    local recovery_status=0
    reconcile_effective_state || recovery_status=$?
    rm -f "$PID_FILE" "$STATE_FILE" "$LAST_ACTIVE_FILE" "$CAFFEINE_PID_FILE" "$FOR_END_FILE" "$FOR_TOKEN_FILE" "$WHY_FILE"
    release_daemon_lock
    return "$recovery_status"
}

daemon_signal_exit() {
    trap - EXIT INT TERM
    if cleanup; then
        exit 0
    fi
    exit 1
}

parse_duration() {
    # Accepts: 30m, 2h, 45 (bare = minutes)
    local input="$1"
    local num unit
    if [[ "$input" =~ ^([0-9]+)(m|h)?$ ]]; then
        num="${BASH_REMATCH[1]}"
        unit="${BASH_REMATCH[2]:-m}"
        case "$unit" in
            m) echo $(( num * 60 )) ;;
            h) echo $(( num * 3600 )) ;;
        esac
    else
        echo ""
    fi
}

parse_until_time() {
    local input="$1"
    /usr/bin/python3 - "$input" <<'PY'
import datetime
import sys

raw = sys.argv[1].strip().lower()
patterns = ["%H:%M", "%I%p", "%I:%M%p"]
value = raw.replace(" ", "")
now = datetime.datetime.now()
target = None

for pattern in patterns:
    try:
        parsed = datetime.datetime.strptime(value, pattern)
        target = now.replace(hour=parsed.hour, minute=parsed.minute, second=0, microsecond=0)
        if target <= now:
            target += datetime.timedelta(days=1)
        break
    except ValueError:
        continue

if target is None:
    raise SystemExit(1)

print(int((target - now).total_seconds()))
PY
}

cmd_settings() {
    local sub="${1:-show}"
    shift || true

    case "$sub" in
        show)
            python_settings_tool show
            ;;
        dump|refresh)
            settings_dump_json
            ;;
        set|apply)
            local source="${1:-}"
            shift || true
            if [ "$source" != "battery" ] && [ "$source" != "ac" ]; then
                echo "Usage: awake settings $sub <battery|ac> <key> <value> [<key> <value> ...]"
                return 1
            fi
            if [[ "$(current_state)" == nosleep* ]]; then
                settings_update_baseline "$source" "$@"
                log "Saved baseline settings for $source (will apply when Awake is off)"
            else
                settings_apply_live "$source" "$@"
                log "Applied baseline settings for $source"
            fi
            ;;
        restore-baseline)
            restore_normal_sleep_settings
            ;;
        *)
            echo "Usage: awake settings [show|dump|refresh|set|apply|restore-baseline]"
            return 1
            ;;
    esac
}

cmd_temp() {
    local sub="${1:-json}"
    case "$sub" in
        json|value)
            python_temp_tool "$sub"
            ;;
        *)
            echo "Usage: awake temp [json|value]"
            return 1
            ;;
    esac
}

refresh_setup_flags() {
    local has_pmset=false
    local has_powermetrics=false
    local has_claude=false
    local has_claude_hook=false
    local has_codex=false
    local has_codex_notify=false

    has_passwordless_pmset && has_pmset=true
    has_passwordless_powermetrics && has_powermetrics=true
    claude_detected && has_claude=true
    claude_hook_installed && has_claude_hook=true
    codex_detected && has_codex=true
    codex_notify_installed && has_codex_notify=true

    HAS_PMSET=$has_pmset
    HAS_POWERMETRICS=$has_powermetrics
    HAS_CLAUDE=$has_claude
    HAS_CLAUDE_HOOK=$has_claude_hook
    HAS_CODEX=$has_codex
    HAS_CODEX_NOTIFY=$has_codex_notify
}

cmd_setup() {
    local sub="${1:-status-json}"
    case "$sub" in
        status|status-json)
            refresh_setup_flags
            setup_status_json
            ;;
        claude)
            install_claude_hooks
            log "Claude Code hooks: installed"
            ;;
        codex)
            install_codex_notify
            log "Codex notify: installed"
            ;;
        *)
            echo "Usage: awake setup [status-json|claude|codex]"
            return 1
            ;;
    esac
}

# --- Commands ---

cmd_daemon_start() {
    local background=false
    local existing_pid
    [ "${1:-}" = "--bg" ] && background=true
    ensure_runtime_dirs

    existing_pid="$(active_daemon_pid 2>/dev/null || true)"
    if [ -n "$existing_pid" ]; then
        log "Already running (PID $existing_pid). Use: awake stop"
        return 1
    fi
    rm -f "$PID_FILE"

    if ! sudo -n pmset -g >/dev/null 2>&1; then
        log "ERROR: passwordless sudo for pmset not configured"
        log "Run: echo '$USER ALL=(ALL) NOPASSWD: /usr/bin/pmset' | sudo tee /etc/sudoers.d/pmset"
        return 1
    fi

    if launch_agent_loaded; then
        log "Starting supervised daemon..."
        /bin/launchctl kickstart -k "$(launch_agent_target)" || {
            log "ERROR: launchd could not start $LAUNCH_AGENT_LABEL"
            return 1
        }
        log "Daemon handed to launchd — log: /tmp/awake.log"
        return 0
    fi

    if $background; then
        log "Starting in background..."
        nohup "$0" _daemon </dev/null >>/tmp/awake.log 2>&1 &
        local launched_pid=$!
        local attempt=0
        while [ "$attempt" -lt 20 ]; do
            if pid_matches_awake_daemon "$launched_pid"; then
                echo "$launched_pid" > "$PID_FILE"
                log "PID $launched_pid — log: /tmp/awake.log"
                return 0
            fi
            sleep 0.05
            attempt=$(( attempt + 1 ))
        done
        log "ERROR: daemon did not start"
        return 1
    fi

    "$0" _daemon
}

write_lease_monitor_agent() {
    local command_path
    command_path="$(monitor_command_path)"
    mkdir -p "$(dirname "$LEASE_MONITOR_PATH")"
    cat > "$LEASE_MONITOR_PATH" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>$LEASE_MONITOR_LABEL</string>
    <key>ProgramArguments</key>
    <array>
        <string>$command_path</string>
        <string>_lease-monitor</string>
    </array>
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
    <key>ThrottleInterval</key>
    <integer>2</integer>
    <key>ProcessType</key>
    <string>Background</string>
    <key>StandardOutPath</key>
    <string>/tmp/awake.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/awake.log</string>
</dict>
</plist>
EOF
}

cmd_lease_monitor_start() {
    local expected_generation="${1:-}" attempt=0
    has_passwordless_pmset || {
        log "ERROR: passwordless sudo for pmset not configured"
        return 1
    }
    rm -f "$LEASE_MONITOR_READY_FILE" "$LEASE_MONITOR_HEARTBEAT_FILE"
    write_lease_monitor_agent || return 1
    if lease_monitor_loaded; then
        launchctl bootout "$(lease_monitor_target)" >/dev/null 2>&1 || {
            log "ERROR: launchd could not reload $LEASE_MONITOR_LABEL"
            return 1
        }
    fi
    launchctl bootstrap "$(launch_agent_domain)" "$LEASE_MONITOR_PATH" >/dev/null 2>&1 || {
        log "ERROR: launchd could not load $LEASE_MONITOR_LABEL"
        return 1
    }
    launchctl kickstart -k "$(lease_monitor_target)" >/dev/null 2>&1 || {
        log "ERROR: launchd could not start $LEASE_MONITOR_LABEL"
        return 1
    }
    while [ "$attempt" -lt 50 ]; do
        if lease_monitor_is_healthy "$expected_generation"; then
            log "Lease battery monitor ready (PID $(lease_monitor_ready_pid "$expected_generation"))"
            return 0
        fi
        sleep 0.05
        attempt=$(( attempt + 1 ))
    done
    launchctl bootout "$(lease_monitor_target)" >/dev/null 2>&1 || true
    rm -f "$LEASE_MONITOR_READY_FILE" "$LEASE_MONITOR_HEARTBEAT_FILE"
    log "ERROR: lease battery monitor did not become healthy"
    return 1
}

ensure_active_lease_monitor() {
    # Every lease gets an independent launchd-supervised battery monitor.
    # It exits cleanly after the last lease and restarts after an unexpected exit.
    [ "$(lease_count)" -gt 0 ] || return 0
    local expected_generation
    expected_generation="$(lease_generation)"
    lease_monitor_is_healthy "$expected_generation" && return 0
    cmd_lease_monitor_start "$expected_generation"
}

lease_monitor_write_heartbeat() {
    printf '%s %s\n' "$(now_epoch)" "$(lease_generation)" > "$LEASE_MONITOR_HEARTBEAT_FILE"
}

lease_monitor_cleanup() {
    trap - EXIT INT TERM
    if [ "$(read_file_value "$LEASE_MONITOR_READY_FILE" 2>/dev/null || true)" = "$$" ]; then
        rm -f "$LEASE_MONITOR_READY_FILE" "$LEASE_MONITOR_HEARTBEAT_FILE"
    fi
}

lease_monitor_signal_exit() {
    lease_monitor_cleanup
    [ "$(lease_count)" -eq 0 ] && exit 0
    exit 1
}

cmd_lease_monitor() {
    trap lease_monitor_cleanup EXIT
    trap lease_monitor_signal_exit INT TERM
    ensure_runtime_dirs
    printf '%s\n' "$$" > "$LEASE_MONITOR_READY_FILE"
    lease_monitor_write_heartbeat
    log "Lease battery monitor started (PID $$, poll=${POLL_INTERVAL}s)"

    while true; do
        cleanup_invalid_leases
        if [ "$(lease_count)" -eq 0 ]; then
            local observed_generation
            observed_generation="$(lease_generation)"
            if [ "$(lease_count)" -eq 0 ] && [ "$(lease_generation)" = "$observed_generation" ]; then
                log "No active leases — stopping battery monitor"
                restore_normal_sleep_settings
                return 0
            fi
            continue
        fi
        lease_monitor_write_heartbeat
        enforce_battery_guard || true
        sleep "$POLL_INTERVAL"
    done
}

cmd_daemon() {
    trap cleanup EXIT
    trap daemon_signal_exit INT TERM
    ensure_runtime_dirs
    if ! acquire_daemon_lock; then
        log "Another awake daemon is already running"
        return 1
    fi
    echo $$ > "$PID_FILE"
    log "Daemon started (PID $$, poll=${POLL_INTERVAL}s, grace=${GRACE_SECONDS}s)"
    log "Watching: $AGENTS"

    recover_power_state_on_launch

    local grace_start=0

    while true; do

        sync_rule_leases

        # --- Agent detection / manual timer override ---
        if timer_running; then
            grace_start=0
        elif agents_running; then
            echo "$(now_epoch)" > "$LAST_ACTIVE_FILE"
            grace_start=0
            lease_create_or_update "daemon-agent" "daemon" "$(current_default_mode)" "Detected active coding agents" 70 "" "daemon"
        else
            lease_remove "daemon-agent"
            if [ "$(rule_lease_count)" -gt 0 ]; then
                grace_start=0
                if ! reconcile_effective_state; then
                    log "ERROR: lease battery monitor unavailable; restoring normal sleep"
                    restore_sleep_after_monitor_failure
                fi
                sleep "$POLL_INTERVAL"
                continue
            fi
            if [ "$grace_start" -eq 0 ]; then
                grace_start=$(now_epoch)
                log "No agents — grace period ($(( GRACE_SECONDS / 60 ))m)"
                notify "awake" "No agents detected. Sleeping in $(( GRACE_SECONDS / 60 )) min."
            fi

            local elapsed=$(( $(now_epoch) - grace_start ))
            local remaining=$(( GRACE_SECONDS - elapsed ))

            if [ "$remaining" -le 0 ]; then
                log "Grace expired — forcing sleep"
                notify "awake" "Grace period expired. Sleeping now."
                force_sleep_if_unleased
                grace_start=0
            else
                if [[ "$(current_state)" == nosleep* ]]; then
                    log "Grace: ${remaining}s remaining"
                fi
            fi
        fi

        if ! reconcile_effective_state; then
            log "ERROR: lease battery monitor unavailable; restoring normal sleep"
            restore_sleep_after_monitor_failure
            grace_start=0
        fi

        sleep "$POLL_INTERVAL"
    done
}

cmd_stop() {
    local pid=""
    # Kill for-timer if running
    if [ -f "$FOR_PID_FILE" ]; then
        cancel_timer_session
        log "Cancelled awake-for timer"
    fi

    pid="$(active_daemon_pid 2>/dev/null || true)"
    if [ -z "$pid" ]; then
        if [ -f "$PID_FILE" ]; then
            log "Stale pidfile (not an awake daemon)"
            rm -f "$PID_FILE"
        else
            log "Daemon not running"
        fi
    else
        log "Stopping daemon (PID $pid)..."
        kill "$pid"
        wait_for_pid_exit "$pid" || true
        pid_is_alive "$pid" && kill -9 "$pid" 2>/dev/null
        log "Stopped"
    fi

    local current_pid
    current_pid="$(active_daemon_pid 2>/dev/null || true)"
    if [ -n "$current_pid" ] && [ "$current_pid" != "$pid" ]; then
        log "Another daemon is already running (PID $current_pid); skipping restore"
        return 1
    fi

    clear_manual_leases
    lease_remove "daemon-agent"
    local dir
    for dir in "$LEASES_DIR"/rule-*; do
        [ -d "$dir" ] || continue
        rm -rf "$dir"
    done
    reconcile_effective_state
    rm -f "$LAST_ACTIVE_FILE" "$CAFFEINE_PID_FILE"
}

cmd_status() {
    if [ "${1:-}" = "--json" ]; then
        refresh_setup_flags
        setup_status_json
        return 0
    fi
    local running=false
    local daemon_pid=""
    daemon_pid="$(active_daemon_pid 2>/dev/null || true)"
    if [ -n "$daemon_pid" ]; then
        running=true
    fi

    local state=$(current_state)
    local agents=$(agent_summary)

    echo "awake daemon: $($running && echo "running (PID $daemon_pid)" || echo "not running")"
    echo "power state:  $state"
    echo "mode:         $(effective_mode_value 2>/dev/null || echo "normal")"
    echo "agents:       $agents"
    echo "watching:     $AGENTS"
    echo "why:          $(why_summary)"

    if [ -f "$LAST_ACTIVE_FILE" ]; then
        local last=$(cat "$LAST_ACTIVE_FILE")
        local ago=$(( $(now_epoch) - last ))
        if [ "$ago" -lt 60 ]; then
            echo "last active:  ${ago}s ago"
        else
            echo "last active:  $(( ago / 60 ))m ago"
        fi
    fi

    # Battery
    local batt=$(get_battery_pct)
    if [ -n "$batt" ]; then
        local charge_state="discharging"
        is_charging && charge_state="charging"
        echo "battery:      ${batt}% ($charge_state)"
    else
        echo "battery:      N/A (no battery or desktop Mac)"
    fi

    # For timer
    if timer_running; then
        echo "for timer:    active"
    fi
    echo "leases:       $(lease_count)"
    echo "rules:        $(rule_count)"

    # Sudoers
    if sudo -n pmset -g >/dev/null 2>&1; then
        echo "sudo pmset:   OK"
    else
        echo "sudo pmset:   NOT CONFIGURED"
    fi
    if sudo -n powermetrics -h >/dev/null 2>&1; then
        echo "sudo temp:    OK"
    else
        echo "sudo temp:    NOT CONFIGURED"
    fi
}

cmd_why() {
    if [ "${1:-}" = "--json" ]; then
        refresh_setup_flags
        setup_status_json
        return 0
    fi
    refresh_setup_flags
    local payload
    payload="$(setup_status_json)"
    /usr/bin/python3 - "$payload" <<'PY'
import json, sys
payload = json.loads(sys.argv[1])
print(payload.get("whyAwake", "Unknown"))
leases = payload.get("leases", [])
if leases:
    print("")
    print("Active leases:")
    for lease in sorted(leases, key=lambda l: (-int(l.get("priority", 0)), -int(l.get("startedAt", 0)))):
        exp = lease.get("expiresAt")
        suffix = f", expiresAt={exp}" if exp not in (None, "", 0) else ""
        print(f"- {lease.get('id')}: {lease.get('reason')} [{lease.get('mode')} -> {lease.get('resolvedMode')}{suffix}]")
PY
}

cmd_mode() {
    local sub="${1:-show}"
    case "$sub" in
        show|get)
            echo "$(current_default_mode)"
            ;;
        set)
            local mode="${2:-}"
            [ -n "$mode" ] || { echo "Usage: awake mode set <running|presenting|agent-safe>"; return 1; }
            set_default_mode "$mode" || return 1
            log "Default mode set to $(mode_label "$mode")"
            ;;
        *)
            echo "Usage: awake mode [show|get|set <running|presenting|agent-safe>]"
            return 1
            ;;
    esac
}

cmd_doctor() {
    if [ "${1:-}" = "--json" ]; then
        refresh_setup_flags
        setup_status_json
        return 0
    fi
    refresh_setup_flags
    echo "Awake doctor"
    echo "============"
    echo "mode:          $(current_default_mode)"
    echo "daemon:        $([ -n "$(active_daemon_pid 2>/dev/null || true)" ] && echo running || echo stopped)"
    echo "leases:        $(lease_count)"
    echo "rules:         $(rule_count)"
    echo "sleep control: $([ "$HAS_PMSET" = true ] && echo ok || echo missing)"
    echo "temperature:   $([ "$HAS_POWERMETRICS" = true ] && echo ok || echo missing)"
    echo "claude hook:   $([ "$HAS_CLAUDE_HOOK" = true ] && echo ok || echo missing)"
    echo "codex notify:  $([ "$HAS_CODEX_NOTIFY" = true ] && echo ok || echo missing)"
    echo "why:           $(why_summary)"
    local warnings
    warnings="$(warnings_json)"
    echo "warnings:      $warnings"
}

update_status_json() {
    local source_info source_type source_detail current_version app_version force_refresh
    source_info="$(detect_install_source)"
    source_type="${source_info%%|*}"
    source_detail="${source_info#*|}"
    current_version="$(current_package_version)"
    app_version="$(current_app_version)"
    force_refresh="${1:-false}"
    python_update_tool update-status \
        "$UPDATE_CACHE_FILE" \
        "$PACKAGE_NAME" \
        "$current_version" \
        "$app_version" \
        "$source_type" \
        "$source_detail" \
        "$GITHUB_RELEASES_URL" \
        "$force_refresh"
}

cmd_update() {
    local sub="${1:-status}"
    shift || true
    case "$sub" in
        status)
            local refresh=false
            [ "${1:-}" = "--refresh" ] && refresh=true
            if [ "${2:-}" = "--json" ] || [ "${1:-}" = "--json" ]; then
                update_status_json "$refresh"
                return 0
            fi
            local payload
            payload="$(update_status_json "$refresh")"
            /usr/bin/python3 - "$payload" <<'PY'
import json, sys
payload = json.loads(sys.argv[1])
print(f"current:       {payload.get('currentVersion')}")
print(f"app:           {payload.get('appVersion')}")
print(f"latest:        {payload.get('latestVersion')}")
print(f"source:        {payload.get('installSource')}")
print(f"self-update:   {'yes' if payload.get('canSelfUpdate') else 'no'}")
print(f"checked:       {payload.get('checkedAt')}")
if payload.get("error"):
    print("status:        check failed")
elif payload.get("updateAvailable"):
    print("status:        update available")
else:
    print("status:        up to date")
if payload.get("error"):
    print(f"warning:       {payload.get('error')}")
PY
            ;;
        apply)
            local force=false
            [ "${1:-}" = "--force" ] && force=true
            local payload source_type can_self update_available update_error update_command
            payload="$(update_status_json true)"
            source_type="$(/usr/bin/python3 - "$payload" <<'PY'
import json, sys
payload = json.loads(sys.argv[1])
print(payload.get("installSource", "unknown"))
print("true" if payload.get("canSelfUpdate") else "false")
print("true" if payload.get("updateAvailable") else "false")
print(payload.get("error") or "")
PY
)"
            local source_lines=()
            while IFS= read -r line; do
                source_lines+=("$line")
            done <<< "$source_type"
            source_type="${source_lines[0]:-unknown}"
            can_self="${source_lines[1]:-false}"
            update_available="${source_lines[2]:-false}"
            update_error="${source_lines[3]:-}"

            if [ "$update_available" != "true" ] && [ -n "$update_error" ] && ! $force; then
                log "Could not confirm whether an update is available: $update_error"
                return 1
            fi

            if [ "$update_available" != "true" ] && ! $force; then
                log "Awake is already up to date"
                return 0
            fi

            if [ "$can_self" != "true" ]; then
                log "Self-update is not supported for install source: $source_type"
                if [ "$source_type" = "repo" ]; then
                    log "Update from git, then rerun: awake install"
                fi
                return 1
            fi

            update_command="$(update_command_for_source "$source_type" 2>/dev/null || true)"
            [ -n "$update_command" ] || {
                log "Could not determine update command for source: $source_type"
                return 1
            }

            log "Updating Awake via $source_type..."
            case "$source_type" in
                npx)
                    run_with_timeout "$UPDATE_APPLY_TIMEOUT" npx --yes "$PACKAGE_NAME@latest" install
                    ;;
                npm-global)
                    run_with_timeout "$UPDATE_APPLY_TIMEOUT" npm install -g "$PACKAGE_NAME@latest" || return 1
                    local global_awake
                    global_awake="$(npm_global_awake_path 2>/dev/null || true)"
                    if [ -n "$global_awake" ]; then
                        run_with_timeout "$UPDATE_APPLY_TIMEOUT" "$global_awake" install
                    else
                        run_with_timeout "$UPDATE_APPLY_TIMEOUT" npx --yes "$PACKAGE_NAME@latest" install
                    fi
                    ;;
                *)
                    log "No supported updater for source: $source_type"
                    return 1
                    ;;
            esac
            ;;
        clear-cache)
            rm -f "$UPDATE_CACHE_FILE"
            log "Update cache cleared"
            ;;
        *)
            echo "Usage: awake update [status [--refresh] [--json]|apply [--force]|clear-cache]"
            return 1
            ;;
    esac
}

cmd_version() {
    current_package_version
}

cmd_rules() {
    ensure_runtime_dirs
    local sub="${1:-list}"
    shift || true
    case "$sub" in
        list)
            if [ "${1:-}" = "--json" ]; then
                printf '%s\n' "$(rules_json)"
                return 0
            fi
            local dir id
            for dir in "$RULES_DIR"/*; do
                [ -d "$dir" ] || continue
                id="$(basename "$dir")"
                echo "$id: $(rule_field "$id" type) $(rule_field "$id" value) -> $(rule_field "$id" mode)"
            done
            ;;
        export)
            printf '%s\n' "$(rules_json)"
            ;;
        add)
            local type="${1:-}"
            local value="${2:-}"
            shift 2 || true
            [ -n "$type" ] && [ -n "$value" ] || { echo "Usage: awake rules add <process|power|battery-above|battery-below|wifi|display> <value> [--mode MODE] [--reason TEXT]"; return 1; }
            local mode="agent-safe"
            local reason=""
            local priority="60"
            while [ "$#" -gt 0 ]; do
                case "$1" in
                    --mode) mode="${2:-}"; shift 2 ;;
                    --reason) reason="${2:-}"; shift 2 ;;
                    --priority) priority="${2:-60}"; shift 2 ;;
                    *) echo "Unknown option: $1"; return 1 ;;
                esac
            done
            case "$mode" in
                running|presenting|agent-safe) ;;
                *) echo "Invalid mode: $mode"; return 1 ;;
            esac
            [[ "$priority" =~ ^[0-9]+$ ]] || { echo "Priority must be an integer"; return 1; }
            local id
            id="$(printf '%s-%s-%s' "$type" "$value" "$(now_epoch)" | tr ' /' '--' | tr -cd '[:alnum:]._:-')"
            local dir
            dir="$(rule_dir "$id")"
            mkdir -p "$dir"
            printf '%s' "$type" > "$dir/type"
            printf '%s' "$value" > "$dir/value"
            printf '%s' "$mode" > "$dir/mode"
            printf '%s' "${reason:-Rule $type=$value matched}" > "$dir/reason"
            printf '%s' "$priority" > "$dir/priority"
            echo "$id"
            ;;
        rm|remove|delete)
            local id="${1:-}"
            [ -n "$id" ] || { echo "Usage: awake rules rm <id>"; return 1; }
            rm -rf "$(rule_dir "$id")"
            lease_remove "rule-$id"
            ;;
        *)
            echo "Usage: awake rules [list|list --json|export|add ...|rm <id>]"
            return 1
            ;;
    esac
}

cmd_for() {
    local duration_str="${1:-}"
    if [ -z "$duration_str" ]; then
        echo "Usage: awake for <duration>  (e.g., 30m, 2h, 45)"
        return 1
    fi

    local secs=$(parse_duration "$duration_str")
    if [ -z "$secs" ] || [ "$secs" -le 0 ] 2>/dev/null; then
        echo "Invalid duration: $duration_str (use: 30m, 2h, or 45)"
        return 1
    fi

    if ! sudo -n pmset -g >/dev/null 2>&1; then
        log "ERROR: passwordless sudo not configured"
        return 1
    fi

    # Kill previous for-timer
    if [ -f "$FOR_PID_FILE" ]; then
        kill "$(cat "$FOR_PID_FILE")" 2>/dev/null || true
        rm -f "$FOR_PID_FILE" "$FOR_END_FILE" "$FOR_TOKEN_FILE"
    fi
    lease_remove "manual-timer"

    local mins=$(( secs / 60 ))
    local end_epoch=$(( $(now_epoch) + secs ))
    local token
    token="$(now_epoch)-$$-$RANDOM"
    log "Nosleep for ${mins}m, then restoring Sleep OK"
    notify "awake" "Staying awake for ${mins} minutes, then restoring Sleep OK."

    # Write end time so UI can show countdown
    echo "$end_epoch" > "$FOR_END_FILE"
    echo "$token" > "$FOR_TOKEN_FILE"

    # Start the timer behind a publication handshake. The process waits until
    # its PID and complete lease are visible before it begins the countdown.
    (
        local timer_pid="$BASHPID"
        local waiting_for_agents=false
        local publish_attempt=0
        while [ "$publish_attempt" -lt 500 ]; do
            if timer_claim_is_current "$token" "$timer_pid" && lease_exists "manual-timer"; then
                break
            fi
            sleep 0.01
            publish_attempt=$(( publish_attempt + 1 ))
        done
        if ! timer_claim_is_current "$token" "$timer_pid" || ! lease_exists "manual-timer"; then
            exit 1
        fi
        sleep "$secs"
        while timer_claim_is_current "$token" "$timer_pid"; do
            if agents_running; then
                if [ "$waiting_for_agents" = false ]; then
                    log "Timer expired but agents still running — waiting to restore Sleep OK"
                    notify "awake" "Timer expired, waiting for agents to stop before restoring Sleep OK."
                    waiting_for_agents=true
                fi
                sleep "$POLL_INTERVAL"
                continue
            fi
            log "Timer expired — restoring Sleep OK"
            notify "awake" "Timer expired. Sleep OK restored."
            lease_remove "manual-timer"
            reconcile_effective_state
            if timer_claim_is_current "$token" "$timer_pid"; then
                rm -f "$FOR_PID_FILE" "$FOR_END_FILE" "$FOR_TOKEN_FILE"
            fi
            exit 0
        done
    ) &
    local timer_pid=$!
    echo "$timer_pid" > "$FOR_PID_FILE"
    if ! lease_create_or_update "manual-timer" "timer" "$(current_default_mode)" "Manual timer active" 90 "" "cli" "$timer_pid"; then
        kill "$timer_pid" 2>/dev/null || true
        rm -f "$FOR_PID_FILE" "$FOR_END_FILE" "$FOR_TOKEN_FILE"
        return 1
    fi
    if ! reconcile_effective_state; then
        cancel_timer_session
        restore_sleep_after_monitor_failure
        return 1
    fi
    if ! ensure_active_lease_monitor; then
        log "ERROR: Battery monitor failed to start; cancelling timer"
        restore_sleep_after_monitor_failure
        return 1
    fi
    disown "$timer_pid" 2>/dev/null || true

    echo "nosleep ON for ${mins}m, then Sleep OK. Cancel with: awake stop"
}

cmd_until() {
    local target="${1:-}"
    [ -n "$target" ] || { echo "Usage: awake until <HH:MM|5pm|5:30pm>"; return 1; }
    local secs
    secs="$(parse_until_time "$target" 2>/dev/null || true)"
    [ -n "$secs" ] || { echo "Invalid clock time: $target"; return 1; }
    local mins=$(( (secs + 59) / 60 ))
    cmd_for "${mins}m"
}

cmd_cancel_timer() {
    cancel_timer_session
    reconcile_effective_state
    log "Timer cancelled"
}

cmd_run_command() {
    if [ $# -eq 0 ]; then
        echo "Usage: awake run <command...>"
        return 1
    fi

    if ! sudo -n pmset -g >/dev/null 2>&1; then
        log "ERROR: passwordless sudo not configured"
        return 1
    fi

    lease_create_or_update "run-command" "command" "$(current_default_mode)" "Command running: $*" 85 "" "cli" "$$"
    if ! reconcile_effective_state; then
        log "ERROR: Battery monitor failed to start; refusing command session"
        lease_remove "run-command"
        restore_sleep_after_monitor_failure
        return 1
    fi
    if ! ensure_active_lease_monitor; then
        log "ERROR: Battery monitor failed to start; refusing command session"
        lease_remove "run-command"
        reconcile_effective_state
        return 1
    fi
    log "Running: $*"

    local exit_code=0
    local daemon_active=false
    "$@" || exit_code=$?

    if active_daemon_pid >/dev/null 2>&1; then
        daemon_active=true
    fi

    lease_remove "run-command"
    reconcile_effective_state
    if timer_running || $daemon_active || [ "$(lease_count)" -gt 0 ]; then
        log "Command finished (exit $exit_code) — leaving restore to active awake session"
    else
        log "Command finished (exit $exit_code)"
        rm -f "$CAFFEINE_PID_FILE" "$STATE_FILE"
    fi
    return $exit_code
}

cmd_ui() {
    local app="$LOCAL_BIN_DIR/Awake.app"
    if [ ! -d "$app" ]; then
        log "ERROR: $app not found. Run: awake install"
        return 1
    fi
    log "Launching menu bar UI..."
    open "$app"
}

cmd_screens() {
    local action="${1:-show}"
    local app="$LOCAL_BIN_DIR/Awake.app"
    if [ ! -d "$app" ]; then
        log "ERROR: $app not found. Run: awake install"
        return 1
    fi

    case "$action" in
        show|restore|off)
            # Ask the running app to restore its own snapshots before restarting.
            # The signal fallback exists for older releases that do not understand
            # the recovery URL, but we never launch a concurrent second instance.
            open "awake://blackout?action=off" >/dev/null 2>&1 || true
            sleep 0.2
            osascript -e 'tell application id "com.awake.menubar" to quit' >/dev/null 2>&1 || true
            local attempt=0
            while pgrep -x AwakeUI >/dev/null 2>&1 && [ "$attempt" -lt 50 ]; do
                sleep 0.1
                attempt=$(( attempt + 1 ))
            done
            if pgrep -x AwakeUI >/dev/null 2>&1; then
                pkill -TERM -x AwakeUI 2>/dev/null || true
                attempt=0
                while pgrep -x AwakeUI >/dev/null 2>&1 && [ "$attempt" -lt 50 ]; do
                    sleep 0.1
                    attempt=$(( attempt + 1 ))
                done
            fi
            if pgrep -x AwakeUI >/dev/null 2>&1; then
                log "ERROR: AwakeUI did not exit; refusing to launch another copy"
                return 1
            fi
            if ! open -na "$app" --args --restore-screens; then
                log "ERROR: failed to relaunch Awake for screen recovery"
                return 1
            fi
            log "Screen restore requested"
            ;;
        blackout|hide|on)
            open "awake://blackout?action=on"
            ;;
        toggle)
            open "awake://blackout?action=toggle"
            ;;
        *)
            echo "Usage: awake screens [show|blackout|toggle]"
            return 1
            ;;
    esac
}

cmd_install() {
    local auto_open=true
    [ "${AWAKE_INSTALL_NO_OPEN:-0}" = "1" ] && auto_open=false

    log "Installing awake..."

    mkdir -p "$LOCAL_BIN_DIR" "$LOCAL_APP_SRC_DIR" "$HOME/.config/awake"

    # Bootstrap local files from the current installation source when available.
    copy_if_present "$SCRIPT_DIR/awake" "$LOCAL_BIN_DIR/awake" || true
    copy_if_present "$SCRIPT_DIR/awake-build-ui" "$LOCAL_BIN_DIR/awake-build-ui" || true
    copy_if_present "$SCRIPT_DIR/awake-build-icon" "$LOCAL_BIN_DIR/awake-build-icon" || true
    copy_if_present "$SCRIPT_DIR/awake-hook" "$LOCAL_BIN_DIR/awake-hook" || true
    copy_if_present "$SCRIPT_DIR/awake-notify" "$LOCAL_BIN_DIR/awake-notify" || true
    copy_if_present "$SCRIPT_DIR/awake-package.json" "$LOCAL_PACKAGE_JSON" || true
    copy_if_present "$SCRIPT_DIR/package.json" "$LOCAL_PACKAGE_JSON" || true
    if [ -f "$REPO_APP_SRC_FILE" ]; then
        cp "$REPO_APP_SRC_FILE" "$LOCAL_APP_SRC_FILE"
        copy_if_present "$REPO_APP_PRIVATE_HEADER" "$LOCAL_APP_PRIVATE_HEADER" || true
    elif [ -f "$BUNDLED_APP_SRC_FILE" ]; then
        mkdir -p "$LOCAL_APP_SRC_DIR"
        cp "$BUNDLED_APP_SRC_FILE" "$LOCAL_APP_SRC_FILE"
        copy_if_present "$BUNDLED_APP_PRIVATE_HEADER" "$LOCAL_APP_PRIVATE_HEADER" || true
    fi

    if ! echo "$PATH" | tr ':' '\n' | grep -Fxq "$LOCAL_BIN_DIR"; then
        log "WARNING: $LOCAL_BIN_DIR is not on PATH"
        log "Add this to your shell profile: export PATH=\"$LOCAL_BIN_DIR:\$PATH\""
    fi

    # 1. Check sudoers
    if ! has_passwordless_pmset; then
        log "WARNING: passwordless sudo not configured"
        log "Run: echo '$USER ALL=(ALL) NOPASSWD: /usr/bin/pmset' | sudo tee /etc/sudoers.d/pmset"
    else
        log "sudoers: OK"
    fi
    if ! has_passwordless_powermetrics; then
        log "WARNING: CPU temperature graph needs passwordless sudo for powermetrics"
        log "Optional: echo '$USER ALL=(ALL) NOPASSWD: /usr/bin/powermetrics' | sudo tee /etc/sudoers.d/powermetrics"
    else
        log "powermetrics: OK"
    fi

    # 2. Check helper scripts
    if [ ! -x "$HOME/.local/bin/awake-hook" ]; then
        log "WARNING: ~/.local/bin/awake-hook not found or not executable"
    else
        log "awake-hook: OK"
    fi
    if [ ! -x "$HOME/.local/bin/awake-notify" ]; then
        log "WARNING: ~/.local/bin/awake-notify not found or not executable"
    else
        log "awake-notify: OK"
    fi

    # 3. Create config dir
    if [ ! -f "$CONFIG_FILE" ]; then
        cat > "$CONFIG_FILE" << 'CONF'
# awake configuration
# AGENTS="claude codex aider copilot amp opencode"
# GRACE_SECONDS=300
# BATTERY_CRITICAL=5
# BATTERY_WARN=15
# POLL_INTERVAL=15
CONF
        log "Config: created $CONFIG_FILE"
    else
        log "Config: $CONFIG_FILE exists"
    fi

    # 4. Build and install Swift menu bar UI
    if [ -f "$LOCAL_APP_SRC_FILE" ]; then
        if ! xcode-select -p >/dev/null 2>&1; then
            log "WARNING: Xcode Command Line Tools not found"
            log "Run: xcode-select --install"
        else
        log "Building Swift menu bar app..."
        if ! "$LOCAL_BIN_DIR/awake-build-ui"; then
            log "ERROR: failed to build the native menu bar app"
            return 1
        fi
        fi
    else
        log "WARNING: $LOCAL_APP_SRC_FILE not found, skipping UI build"
    fi

    # 5. Patch Claude Code settings.json
    if claude_detected; then
        install_claude_hooks
        log "Claude Code hooks: installed"
    else
        log "Claude Code: not installed (skipping)"
    fi

    # 5. Patch Codex config
    local codex_cfg
    codex_cfg="$(codex_config_path)"
    if [ -d "$HOME/.codex" ]; then
        if codex_notify_installed; then
            log "Codex notify: already configured"
        else
            install_codex_notify
            log "Codex notify: installed"
        fi
    else
        log "Codex: not installed (skipping)"
    fi

    write_install_metadata

    # An upgrade can inherit a manual lease created by an older Awake process.
    # Do not leave that durable lease active unless this build verified its monitor.
    if [ "$(lease_count)" -gt 0 ] && ! ensure_active_lease_monitor; then
        log "ERROR: existing Awake session has no healthy battery monitor; restoring normal sleep"
        restore_sleep_after_monitor_failure
        return 1
    fi

    if [ -d "$LOCAL_BIN_DIR/Awake.app" ]; then
        if $auto_open; then
            log "Opening Awake.app..."
            open "$LOCAL_BIN_DIR/Awake.app" >/dev/null 2>&1 || true
            log "Done. Awake.app should open now."
        else
            log "Done. Run: open ~/.local/bin/Awake.app"
        fi
    else
        log "Done. Start with: awake start"
    fi
}

cmd_uninstall() {
    if launch_agent_loaded; then
        /bin/launchctl bootout "$(launch_agent_target)" >/dev/null 2>&1 || true
    fi
    if lease_monitor_loaded; then
        launchctl bootout "$(lease_monitor_target)" >/dev/null 2>&1 || true
    fi
    rm -f "$LAUNCH_AGENT_PATH"
    rm -f "$LEASE_MONITOR_PATH" "$LEASE_MONITOR_READY_FILE" "$LEASE_MONITOR_HEARTBEAT_FILE"
    cmd_stop >/dev/null 2>&1 || true

    log "Removing hooks..."

    local settings="$HOME/.claude/settings.json"
    if [ -f "$settings" ]; then
        python3 -c "
import json
with open('$settings') as f:
    cfg = json.load(f)
hooks = cfg.get('hooks', {})
if 'PreToolUse' in hooks:
    hooks['PreToolUse'] = [h for h in hooks['PreToolUse'] if 'awake-hook' not in str(h)]
    if not hooks['PreToolUse']:
        del hooks['PreToolUse']
with open('$settings', 'w') as f:
    json.dump(cfg, f, indent=2)
    f.write('\n')
print('OK')
"
        log "Claude Code hooks: removed"
    fi

    local codex_cfg="$HOME/.codex/config.toml"
    if [ -f "$codex_cfg" ]; then
        sed -i '' '/awake-notify/d' "$codex_cfg"
        log "Codex notify: removed"
    fi

    log "Done"
}

# --- Main ---

case "${1:-}" in
    start)      cmd_daemon_start --bg ;;
    stop)       cmd_stop ;;
    sleep)      force_sleep ;;
    version|--version|-v) cmd_version ;;
    on)         activate_default_manual_session ;;
    nosleep)    activate_nosleep ;;
    nosleep-display) activate_nosleep_display ;;
    yessleep)   activate_yessleep ;;
    status)     shift; cmd_status "$@" ;;
    why)        shift; cmd_why "$@" ;;
    doctor)     shift; cmd_doctor "$@" ;;
    mode)       shift; cmd_mode "$@" ;;
    update)     shift; cmd_update "$@" ;;
    rules)      shift; cmd_rules "$@" ;;
    for)        cmd_for "${2:-}" ;;
    until)      cmd_until "${2:-}" ;;
    cancel-timer) cmd_cancel_timer ;;
    run)        shift; cmd_run_command "$@" ;;
    ui)         cmd_ui ;;
    screens)    shift; cmd_screens "$@" ;;
    install)    cmd_install ;;
    uninstall)  cmd_uninstall ;;
    _daemon)    cmd_daemon ;;
    _lease-monitor) cmd_lease_monitor ;;
    "")         cmd_daemon_start ;;
    settings)   shift; cmd_settings "$@" ;;
    setup)      shift; cmd_setup "$@" ;;
    temp)       shift; cmd_temp "$@" ;;
    *)          echo "Usage: awake [start|stop|sleep|version|on|nosleep|nosleep-display|yessleep|status [--json]|why|doctor|mode ...|update ...|rules ...|for <dur>|until <time>|cancel-timer|run <cmd>|ui|screens [show|blackout|toggle]|install|uninstall|settings ...|setup ...|temp [json|value]]"
                exit 1 ;;
esac
