#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Shared task operations — process checks, orchestrator management, worker PIDs,
agent registry, and tier detection.  Tier matching / claiming / finalization
live in task_claiming.py (split for Law 7 compliance).

Used by: pulse_cli.py, cli/cmd_swarm.py, pulse_worker.py
"""
from __future__ import annotations

import hashlib
import json
import os
import subprocess
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent

from pulse.core.brain_utils import (
    ROOT_DIR, STATE_DIR, DAEMONS_DIR, BRAIN_TOOLS_DIR as TOOLS_DIR,
    TASK_BOARD_FILE as TASK_BOARD, PID_FILE, DAEMON_PIDS_FILE as DAEMON_PIDS,
    WORKER_PIDS_FILE,
    now_iso as _now_iso, is_process_running,
)

# Lazy-loaded singletons
_crypto = None
_crypto_loaded = False
_atomic_update_json = None
_atomic_loaded = False

TIER_FALLBACK_SECONDS = 60


def _get_crypto():
    global _crypto, _crypto_loaded
    if not _crypto_loaded:
        _crypto_loaded = True
        try:
            from pulse.core.pulse_crypto import PulseCrypto
            _crypto = PulseCrypto()
        except Exception:
            _crypto = None
    return _crypto


def _get_atomic_update():
    global _atomic_update_json, _atomic_loaded
    if not _atomic_loaded:
        _atomic_loaded = True
        try:
            from pulse.core.brain_utils import atomic_update_json
            _atomic_update_json = atomic_update_json
        except ImportError:
            _atomic_update_json = None
    return _atomic_update_json


# ---------------------------------------------------------------------------
# Re-exports from task_claiming  (preserves public API)
# ---------------------------------------------------------------------------
from .task_claiming import (  # noqa: E402
    tier_matches_strict,
    task_age_seconds,
    tier_matches,
    claim_task,
    finalize_task,
)

# ---------------------------------------------------------------------------
# Re-exports from task_ops_workers  (preserves public API)
# ---------------------------------------------------------------------------
from .task_ops_workers import (  # noqa: E402
    AGENT_REGISTRY_FILE,
    load_worker_pids,
    save_worker_pids,
    update_registry,
    patch_task_description,
    load_json,
)


# ---------------------------------------------------------------------------
# Process management
# ---------------------------------------------------------------------------

def kill_process(pid: int):
    """Kill a process AND its entire child tree (cross-platform)."""
    if os.name == "nt":
        # /T = kill process tree (parent + all children)
        subprocess.run(
            ["taskkill", "/F", "/T", "/PID", str(pid)],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
            creationflags=0x08000000,
        )
    else:
        import signal
        # Workers spawn with start_new_session=True, so killpg hits the group
        try:
            os.killpg(os.getpgid(pid), signal.SIGKILL)
        except (ProcessLookupError, PermissionError):
            try:
                os.kill(pid, signal.SIGKILL)
            except ProcessLookupError:
                pass


# ---------------------------------------------------------------------------
# Tier detection
# ---------------------------------------------------------------------------

def detect_tier(model: str) -> str:
    """Deterministic tier detection from model name."""
    m = model.lower()
    if any(k in m for k in ("opus", "o3", "gpt-4", "gemini-3-pro", "grok-4",
                             "5.1-codex-max", "5.3-codex")):
        return "premium"
    if any(k in m for k in ("haiku", "flash", "gpt-4o-mini", "flash-lite",
                             "mistral-small", "o4-mini")):
        return "fast"
    # standard: sonnet, gemini pro, deepseek, 5.2-codex, etc.
    return "standard"


# ---------------------------------------------------------------------------
# Orchestrator
# ---------------------------------------------------------------------------

def is_orchestrator_running() -> bool:
    if not PID_FILE.exists():
        return False
    try:
        pid = int(PID_FILE.read_text().strip())
        return is_process_running(pid)
    except Exception:
        return False


def ensure_orchestrator():
    """Auto-start orchestrator as headless daemon if not running."""
    if is_orchestrator_running():
        return
    # Kill orphan daemons from crashed session BEFORE starting new orchestrator.
    # Prevents old zombies from racing with new daemons for DB locks.
    try:
        from pulse.daemons.orchestrator.lifecycle import _kill_orphan_daemons
        _kill_orphan_daemons()
    except Exception:
        pass
    print("[PULSE] Orchestrator not running — starting capture daemons...")
    orch = DAEMONS_DIR / "orchestrator" / "core.py"
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    # S32: use context managers so file handles are closed after Popen inherits them
    with open(STATE_DIR / "pulse_orchestrator.log", "a", encoding="utf-8") as log_out, \
         open(STATE_DIR / "pulse_orchestrator.err", "a", encoding="utf-8") as log_err:
        if os.name == "nt":
            subprocess.Popen(
                [sys.executable, str(orch), "--verbose"],
                stdout=log_out, stderr=log_err,
                creationflags=subprocess.CREATE_NO_WINDOW,
            )
        else:
            subprocess.Popen(
                [sys.executable, str(orch), "--verbose"],
                stdout=log_out, stderr=log_err,
                start_new_session=True,
            )
    time.sleep(2)
    print("[PULSE] Orchestrator launched (headless). Logs: state/pulse_orchestrator.log")


def verify_constitution() -> bool:
    """Verify constitution hash at boot. Returns True if valid or unverifiable."""
    const_path = ROOT_DIR / "Corpus_Callosum" / "CONSTITUTION.md"
    env_path = ROOT_DIR / ".env"
    if not const_path.exists() or not env_path.exists():
        return True
    target_hash = None
    for line in env_path.read_text(encoding="utf-8").splitlines():
        if line.strip().startswith("CONSTITUTION_HASH="):
            target_hash = line.split("=", 1)[1].strip().strip("'\"")
            break
    if not target_hash:
        return True
    actual = hashlib.sha256(const_path.read_bytes()).hexdigest()
    return actual == target_hash

