#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
PULSE Task Dispatcher Daemon (ASOP v1)
Manages task board lifecycle: stale claims, dependency unblocking, archival.

Optionally LLM-powered for smart decomposition (configurable via .env).
Default mode: zero LLM cost — first agent decomposes, dispatcher manages lifecycle only.

Dependencies: Python stdlib only (Law 4)
"""
import hashlib
import hmac
import json
import os
import secrets
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

# When run directly, ensure pulse package is importable
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))

from pulse.core.brain_utils import TASK_BOARD_FILE, atomic_update_json, now_iso as _now_iso

from pulse.tasks.decomposer import decompose_prompt
from pulse.tasks.dispatcher_lifecycle import (
    check_stale_claims,
    unblock_dependents,
    apply_capability_routing,
    archive_completed,
    score_agents_for_task,
)

POLL_INTERVAL = 2.0


def _ts():
    return int(datetime.now(timezone.utc).timestamp())


class TaskDispatcher:
    """Lifecycle manager for the ASOP task board."""

    def __init__(self, verbose=False):
        self.verbose = verbose
        self.provider = os.environ.get("DISPATCHER_PROVIDER", "none").lower()
        self.model = os.environ.get("DISPATCHER_MODEL", "")
        self.endpoint = os.environ.get("DISPATCHER_ENDPOINT", "")
        # H4: HMAC key for signing dispatches (loaded once, not vaulted here
        # since dispatcher is a long-running daemon that needs to verify continuously)
        self._hmac_key = os.environ.get("PULSE_BRAIN_KEY", "").encode() or None

    # --- H4: Dispatch Authentication ---

    def _sign_dispatch(self, dispatch_entry: dict) -> str:
        """HMAC-SHA256 signature of dispatch content. Returns hex digest."""
        if not self._hmac_key:
            return ""
        signable = json.dumps({
            "dispatch_id": dispatch_entry["dispatch_id"],
            "prompt": dispatch_entry["prompt"],
            "dispatched_at": dispatch_entry["dispatched_at"],
            "task_count": len(dispatch_entry.get("tasks", [])),
        }, sort_keys=True).encode()
        return hmac.new(self._hmac_key, signable, hashlib.sha256).hexdigest()

    def verify_dispatch(self, dispatch_entry: dict) -> bool:
        """Verify HMAC signature of a dispatch entry."""
        if not self._hmac_key:
            return True  # No key = no verification (graceful degradation)
        sig = dispatch_entry.get("_signature", "")
        if not sig:
            return False
        expected = self._sign_dispatch(dispatch_entry)
        return hmac.compare_digest(sig, expected)

    # --- Public API ---

    def dispatch(self, prompt: str, tasks: list[dict], founder_override: bool = False) -> str:
        """Write a new dispatch to the task board. Returns dispatch_id.
        Atomic read-modify-write to prevent race conditions (C4)."""
        # Layer 11: Inline compliance check at dispatch time
        try:
            from pulse.workers.runner.worker_compliance import check_task_compliance
            for t in tasks:
                passed, reason = check_task_compliance(t)
                if not passed:
                    t["status"] = "rejected"
                    t["rejection_reason"] = reason
        except ImportError:
            pass

        # G1: Unpredictable dispatch_id (timestamp + random hex)
        dispatch_id = f"d_{_ts()}_{secrets.token_hex(4)}"

        dispatch_entry = {
            "dispatch_id": dispatch_id,
            "prompt": prompt,
            "dispatched_at": _now_iso(),
            "tasks": tasks,
        }
        if founder_override:
            dispatch_entry["founder_override"] = True
        # H4: Sign dispatch for tamper detection
        sig = self._sign_dispatch(dispatch_entry)
        if sig:
            dispatch_entry["_signature"] = sig

        def _add_dispatch(board):
            if "dispatches" not in board:
                board["dispatches"] = []
            board["dispatches"].append(dispatch_entry)
            board["version"] = board.get("version", 0) + 1
            return board

        atomic_update_json(TASK_BOARD_FILE, _add_dispatch, default={})

        if self.verbose:
            print(f"[DISPATCHER] Dispatched {len(tasks)} tasks ({dispatch_id})")
        return dispatch_id

    def decompose_with_llm(self, prompt: str) -> list[dict]:
        """Use configured LLM to decompose a prompt into tasks.
        Returns list of task dicts. Falls back to empty list on failure."""
        return decompose_prompt(
            prompt,
            provider=self.provider,
            model=self.model,
            endpoint=self.endpoint,
            verbose=self.verbose
        )

    # --- Lifecycle Management (delegated to dispatcher_lifecycle module) ---

    def score_agents(self, task: dict) -> list[tuple[str, float]]:
        """Score all registered agents for a given task. Returns sorted list."""
        return score_agents_for_task(task)


    # --- Main Loop ---

    def run(self):
        """2-second poll loop: stale claims, unblock deps, archive completed."""
        print("[DISPATCHER] Task Dispatcher running...")

        while True:
            try:
                def _lifecycle(board):
                    if not board.get("dispatches"):
                        return board

                    # H4: Reject unsigned/tampered dispatches (Law 8)
                    if self._hmac_key:
                        for d in board.get("dispatches", []):
                            if not self.verify_dispatch(d):
                                if not d.get("_unsigned_flagged"):
                                    d["_unsigned_flagged"] = True
                                    if not d.get("founder_override"):
                                        for t in d.get("tasks", []):
                                            if t.get("status") == "open":
                                                t["status"] = "rejected"
                                                t["rejection_reason"] = "unsigned_dispatch"
                                        print(f"[DISPATCHER] REJECTED: Unsigned dispatch "
                                              f"'{d.get('dispatch_id', '?')}'")
                                    else:
                                        print(f"[DISPATCHER] WARNING: Unsigned dispatch "
                                              f"'{d.get('dispatch_id', '?')}' (Founder override)")
                            else:
                                d.pop("_unsigned_flagged", None)

                    modified = False
                    modified |= check_stale_claims(board, self.verbose)
                    modified |= unblock_dependents(board, self.verbose)
                    modified |= apply_capability_routing(board, self.verbose)
                    modified |= archive_completed(board, self.verbose)
                    if modified:
                        board["version"] = board.get("version", 0) + 1
                    return board

                atomic_update_json(TASK_BOARD_FILE, _lifecycle, default={})
                time.sleep(POLL_INTERVAL)

            except Exception as e:
                if self.verbose:
                    print(f"[DISPATCHER] Error: {e}")
                time.sleep(POLL_INTERVAL)


if __name__ == "__main__":
    v = "--verbose" in sys.argv or "-v" in sys.argv
    TaskDispatcher(verbose=v).run()
