# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Tier matching, atomic task claiming, and task finalization.

Split from task_ops.py for Law 7 compliance (<300 lines per file).
Imports lazy helpers from task_ops to avoid circular dependencies.
"""
from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path

# Constants (imported lazily from task_ops at call time)

def _ops():
    """Lazy import to avoid circular dependency at module level."""
    from pulse.tasks import task_ops
    return task_ops

# Tier detection & matching

def tier_matches_strict(task: dict, worker_tier: str) -> bool:
    """Flexible tier match -- capable workers can grab simpler tasks."""
    rec = task.get("recommended_tier", "standard")
    complexity = task.get("complexity", "")
    task_level = rec if rec in ("premium", "standard", "fast") else "standard"
    if complexity in ("high", "complex"):
        task_level = "premium"
    elif complexity in ("medium", "moderate"):
        task_level = "standard"
    elif complexity in ("low", "simple"):
        task_level = "fast"

    if worker_tier == "premium":
        return True
    if worker_tier == "standard":
        return task_level in ("standard", "fast")
    if worker_tier == "fast":
        return task_level == "fast"
    return False

def task_age_seconds(task: dict) -> float:
    """How long a task has been sitting open (seconds)."""
    created = task.get("created_at") or task.get("dispatched_at", "")
    if not created:
        return 9999  # no timestamp = treat as old (eligible for fallback)
    try:
        created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
        return (datetime.now(timezone.utc) - created_dt).total_seconds()
    except (ValueError, TypeError):
        return 0.0

def tier_matches(task: dict, worker_tier: str) -> tuple[bool, bool]:
    """Returns (should_claim, is_fallback). Premium>=standard>=fast; no upward fallback."""
    if tier_matches_strict(task, worker_tier):
        return True, False
    return False, False

# Task claiming (atomic)

def claim_task(board_path: Path, agent_id: str, tier: str = "standard") -> dict | None:
    """Atomically claim a tier-matching task. Returns task dict or None.

    Pass 1: strict tier match.  Pass 2: fallback on stale tasks (>60s).
    """
    import re
    # Guard: reject bare agent names — must contain _digits (worker index or PID)
    if not re.search(r'_\d+', agent_id):
        return None

    ops = _ops()
    atomic = ops._get_atomic_update()
    if not atomic:
        return None

    claimed = [None]
    now_str = ops._now_iso()
    crypto = ops._get_crypto()

    def _do_claim(board):
        if not isinstance(board, dict):
            return board

        # Item 42: Working memory capacity — max 3 concurrent claims per agent
        try:
            active_claims = sum(
                1 for d in board.get("dispatches", [])
                for t in d.get("tasks", [])
                if t.get("claimed_by") == agent_id and t.get("status") in ("active", "claimed")
            )
            if active_claims >= 3:
                claimed[0] = {
                    "status": "rejected",
                    "reason": "working_memory_capacity",
                    "active": active_claims,
                }
                return board
        except Exception:
            pass  # fail-open: allow claim if check fails

        # Pass 1: tier match
        for d in board.get("dispatches", []):
            for t in d.get("tasks", []):
                status = t.get("status")
                if status not in ("open", "escalated"):
                    continue
                # Guard: skip if recently claimed by another agent
                if status == "open" and t.get("claimed_by") and t.get("claimed_by") != agent_id:
                    claimed_at = t.get("claimed_at", "")
                    if claimed_at:
                        try:
                            ct = datetime.fromisoformat(claimed_at.replace("Z", "+00:00"))
                            if (datetime.now(timezone.utc) - ct).total_seconds() < 10:
                                continue
                        except (ValueError, TypeError):
                            pass
                eligible = t.get("eligible_agents")
                if eligible and agent_id not in eligible:
                    continue
                match, fallback = tier_matches(t, tier)
                if (match and not fallback) or status == "escalated":
                    t["status"] = "active"
                    t["claimed_by"] = agent_id
                    t["claimed_at"] = now_str
                    if crypto:
                        t["_claim_signature"] = crypto.sign_string(
                            f"{agent_id}:{t.get('task_id', '')}:{now_str}", agent_id
                        )
                    claimed[0] = dict(t)
                    return board

        # Pass 2: fallback (stale tasks)
        for d in board.get("dispatches", []):
            for t in d.get("tasks", []):
                if t.get("status") != "open":
                    continue
                eligible = t.get("eligible_agents")
                if eligible and agent_id not in eligible:
                    continue
                match, fallback = tier_matches(t, tier)
                if match and fallback:
                    t["status"] = "active"
                    t["claimed_by"] = agent_id
                    t["claimed_at"] = now_str
                    t["_tier_fallback"] = True
                    if crypto:
                        t["_claim_signature"] = crypto.sign_string(
                            f"{agent_id}:{t.get('task_id', '')}:{now_str}", agent_id
                        )
                    claimed[0] = dict(t)
                    return board
            return board

        return board

    # D17: Apply choreography stagger to reduce lock contention
    try:
        from pulse.daemons.synthesis.swarm.swarm_choreographer import get_stagger_ms
        stagger = get_stagger_ms(board_path.name)
        if stagger > 0:
            import time as _t, random as _r
            _t.sleep(_r.uniform(0, stagger / 1000.0))
    except Exception:
        pass

    atomic(board_path, _do_claim, default={"dispatches": []})
    # Item 84: Predictive pre-fetch: warm hot cache for claimed task
    if claimed[0] and isinstance(claimed[0], dict) and not claimed[0].get("status") == "rejected":
        try:
            from pulse.brain.memory.hot_cache import write_hot as _wh84
            from pulse.core.brain_db import get_conn as _gc84
            _dom84 = claimed[0].get("domain", "general")
            _rows84 = _gc84().execute("SELECT id, content, confidence, domain, type, salience FROM lessons WHERE domain=? AND confidence>=0.7 ORDER BY salience DESC LIMIT 5", (_dom84,)).fetchall()
            for _r84 in _rows84: _wh84({**dict(_r84), "salience": max(float(_r84["salience"] or 4.0), 4.0)})
        except Exception: pass
    try:
        from pulse.core.autonomy_state import update_drives
        update_drives(is_idle=(claimed[0] is None))
    except Exception:
        pass
    return claimed[0]

def finalize_task(board_path: Path, task_id: str, agent_id: str,
                  outcome: str = "", status: str = "done",
                  tokens_in: int = 0, tokens_out: int = 0, cost: float = 0.0):
    """Mark a task as done/escalated/reopened on the board with stats."""
    ops = _ops()
    atomic = ops._get_atomic_update()
    if not atomic:
        return

    now = ops._now_iso()
    crypto = ops._get_crypto()

    def _update(board):
        if not isinstance(board, dict):
            return board
        for d in board.get("dispatches", []):
            for t in d.get("tasks", []):
                tid = t.get("task_id") or t.get("id", "")
                if tid != task_id:
                    continue
                # Guard: skip if already done (another agent beat us)
                if t.get("status") == "done" and status == "done":
                    return board
                # Absolute retry ceiling — force-close after 5 attempts
                board_retries = t.get("retry_count", 0)
                final_status = status
                final_outcome = outcome
                if final_status != "done" and board_retries >= 5:
                    final_status = "done"
                    final_outcome = outcome or f"POISON_PILL: force-closed after {board_retries} retries"

                if final_status == "done":
                    t["status"] = "done"
                    t["completed_at"] = now
                    # Item 22: Near-miss detection — success with low-confidence brain items
                    try:
                        _brain_items = t.get("brain_items_used", [])
                        _avg_conf = sum(float(bi.get("confidence", 0.5)) for bi in _brain_items) / max(len(_brain_items), 1)
                        if "POISON_PILL" not in (final_outcome or "") and _brain_items and len(_brain_items) >= 2 and _avg_conf < 0.5:
                            t["near_miss"] = True
                            t["near_miss_avg_confidence"] = round(_avg_conf, 3)
                    except Exception:
                        pass  # fail-open
                    # Item 75 (P18): Near-miss pattern extraction
                    try:
                        if t.get("near_miss"):
                            _dnm = t.get("domain", "general"); _anm = t.get("near_miss_avg_confidence", 0)
                            from pulse.brain.save_writers.jsonl_store import build_jsonl_entry, append_knowledge_jsonl
                            from pulse.core.brain_utils import PATTERNS_JSONL, now_iso as _niso
                            append_knowledge_jsonl(PATTERNS_JSONL, build_jsonl_entry(
                                description=f"Near-miss in domain '{_dnm}': avg confidence {_anm:.2f}. More evidence needed.",
                                source="task_claiming", timestamp=_niso(), domain=_dnm, confidence=0.6, salience=2.0, knowledge_type="pattern"))
                    except Exception: pass  # fail-open
                    # Wire prediction feedback — Oracle learns from task outcomes
                    try:
                        from pulse.daemons.neural.prediction_feedback import record_prediction_outcome
                        _success = "POISON_PILL" not in (final_outcome or "")
                        record_prediction_outcome(task_id, success=_success, agent_name=agent_id, task_title=t.get("title", ""))
                    except Exception:
                        pass  # Never block task completion on feedback failure
                    # Wire 3: Grounding — promote brain items used in this task
                    try:
                        from pulse.brain.integrity.grounding import maybe_promote
                        for item_id in t.get("brain_item_ids", []):
                            maybe_promote(item_id, "lessons", trigger="task_success", evidence={"task_id": task_id})
                    except Exception:
                        pass
                    # Item 149: Agent collaboration history
                    try:
                        from pulse.brain.metacognition.metamemory_p34 import record_agent_collaboration
                        _task_domain = t.get("domain", "general")
                        _co_agent = None
                        for _d2 in board.get("dispatches", []):
                            for _t2 in _d2.get("tasks", []):
                                _other = _t2.get("completed_by") or _t2.get("claimed_by")
                                if _other and _other != agent_id and _t2.get("domain", "general") == _task_domain:
                                    _co_agent = _other
                                    break
                            if _co_agent:
                                break
                        if _co_agent:
                            record_agent_collaboration(agent_id, _co_agent, _task_domain, "POISON_PILL" not in (final_outcome or ""))
                    except Exception: pass
                    # Item 180: session archival reinforcement on completion
                    try:
                        from pulse.brain import working_memory as _wm180
                        _wm180.rotate_session(agent_id)
                    except Exception:
                        pass
                    if crypto:
                        t["_done_signature"] = crypto.sign_string(f"{agent_id}:{task_id}:{now}", agent_id)
                elif final_status == "escalated":
                    t["status"] = "escalated"
                    t["retry_count"] = board_retries + 1
                    current = t.get("recommended_tier", "standard")
                    t["escalate_to"] = "standard" if current == "fast" else "premium"
                else:
                    t["status"] = "open"
                    t["retry_count"] = board_retries + 1
                    t["claimed_by"] = None
                    t["claimed_at"] = None

                    # Self-escalation: bump tier if task is stuck
                    if board_retries >= 2:
                        _ot = t.get("recommended_tier", "standard")
                        if _ot == "fast": t["recommended_tier"] = "standard"; print(f"[PULSE] Escalating {tid}: fast->standard")
                        elif _ot == "standard": t["recommended_tier"] = "premium"; print(f"[PULSE] Escalating {tid}: standard->premium")

                t["completed_by"] = agent_id
                t["outcome"] = (outcome.strip() or "Task completed without detailed output.")[:500]
                t["tokens_in"] = t.get("tokens_in", 0) + tokens_in
                t["tokens_out"] = t.get("tokens_out", 0) + tokens_out
                t["estimated_cost_usd"] = t.get("estimated_cost_usd", 0.0) + cost
                return board
        return board

    atomic(board_path, _update, default={"dispatches": []})
