"""
Phase 51: Priority Task Queue with Ordered Execution.

A priority-based queue for scheduling and dispatching tasks to agents.
Tasks are ordered by priority (critical > high > normal > low), then by
enqueue time. Supports deadline tracking, filtered dequeue by agent skills,
and queue statistics.

Storage:
  state/broker/queue/
    tasks.json       — current queue state
    history.jsonl    — completed / dequeued task log
"""

import json
import logging
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from brynq.server import BROKER_DIR, _write_json_sync, _read_jsonl_sync, _append_jsonl_sync

logger = logging.getLogger(__name__)

# ── Paths ──────────────────────────────────────────────────────────────────

QUEUE_DIR = BROKER_DIR / "queue"
TASKS_FILE = QUEUE_DIR / "tasks.json"
HISTORY_FILE = QUEUE_DIR / "history.jsonl"

for _d in (QUEUE_DIR,):
    _d.mkdir(parents=True, exist_ok=True)

# ── Priority Ordering ─────────────────────────────────────────────────────

PRIORITY_ORDER = {"critical": 0, "high": 1, "normal": 2, "low": 3}
VALID_PRIORITIES = set(PRIORITY_ORDER.keys())


# ── Internal helpers ──────────────────────────────────────────────────────

def _read_queue() -> List[dict]:
    """Load the current queue from disk."""
    if not TASKS_FILE.exists():
        return []
    try:
        return json.loads(TASKS_FILE.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return []


def _save_queue(queue: List[dict]) -> None:
    """Persist queue to disk."""
    _write_json_sync(TASKS_FILE, queue)


def _sort_queue(queue: List[dict]) -> List[dict]:
    """Sort by priority rank then by enqueued_at timestamp."""
    return sorted(queue, key=lambda t: (
        PRIORITY_ORDER.get(t.get("priority", "normal"), 2),
        t.get("enqueued_at", ""),
    ))


# ── Public API ────────────────────────────────────────────────────────────

def enqueue(
    task_id: str,
    priority: str = "normal",
    deadline: Optional[str] = None,
    tags: Optional[List[str]] = None,
) -> dict:
    """Add a task to the queue.

    Args:
        task_id: Unique task identifier.
        priority: One of critical, high, normal, low.
        deadline: Optional ISO-8601 deadline string.
        tags: Optional list of skill/tag strings for filtered dequeue.

    Returns:
        The enqueued task record.
    """
    if priority not in VALID_PRIORITIES:
        raise ValueError(f"Invalid priority '{priority}'. Must be one of {VALID_PRIORITIES}")

    queue = _read_queue()

    # Prevent duplicate task_ids
    for t in queue:
        if t["task_id"] == task_id:
            raise ValueError(f"Task '{task_id}' is already in the queue")

    now = datetime.now(timezone.utc).isoformat()
    task = {
        "task_id": task_id,
        "priority": priority,
        "enqueued_at": now,
        "deadline": deadline,
        "tags": tags or [],
        "status": "queued",
    }

    queue.append(task)
    queue = _sort_queue(queue)
    _save_queue(queue)

    logger.info("Enqueued task %s with priority %s", task_id, priority)
    return task


def dequeue(agent_id: Optional[str] = None, skills: Optional[List[str]] = None) -> Optional[dict]:
    """Pop the highest-priority task from the queue.

    Args:
        agent_id: Optional agent identifier (recorded on the dequeued task).
        skills: Optional skill list; if provided, only tasks whose tags
                overlap with the skills will be considered.

    Returns:
        The dequeued task record, or None if queue is empty / no match.
    """
    queue = _read_queue()
    if not queue:
        return None

    queue = _sort_queue(queue)

    selected_idx = None
    for i, task in enumerate(queue):
        if task.get("status") != "queued":
            continue
        if skills is not None:
            task_tags = set(task.get("tags", []))
            if not task_tags.intersection(set(skills)):
                continue
        selected_idx = i
        break

    if selected_idx is None:
        return None

    task = queue.pop(selected_idx)
    task["status"] = "dequeued"
    task["dequeued_at"] = datetime.now(timezone.utc).isoformat()
    if agent_id:
        task["agent_id"] = agent_id

    _save_queue(queue)
    _append_jsonl_sync(HISTORY_FILE, task)

    logger.info("Dequeued task %s for agent %s", task["task_id"], agent_id)
    return task


def peek(n: int = 5) -> List[dict]:
    """View the top N tasks without removing them."""
    queue = _read_queue()
    queue = _sort_queue(queue)
    queued = [t for t in queue if t.get("status") == "queued"]
    return queued[:n]


def requeue(task_id: str, new_priority: Optional[str] = None) -> dict:
    """Put a task back into the queue, optionally changing its priority.

    Works for tasks that were previously dequeued (found in history) or
    tasks still in the queue that need re-prioritizing.
    """
    if new_priority and new_priority not in VALID_PRIORITIES:
        raise ValueError(f"Invalid priority '{new_priority}'. Must be one of {VALID_PRIORITIES}")

    queue = _read_queue()

    # Check if task is already in the queue
    for t in queue:
        if t["task_id"] == task_id:
            if new_priority:
                t["priority"] = new_priority
            t["status"] = "queued"
            t.pop("dequeued_at", None)
            t.pop("agent_id", None)
            queue = _sort_queue(queue)
            _save_queue(queue)
            return t

    # Otherwise re-create from history or as fresh entry
    now = datetime.now(timezone.utc).isoformat()
    task = {
        "task_id": task_id,
        "priority": new_priority or "normal",
        "enqueued_at": now,
        "deadline": None,
        "tags": [],
        "status": "queued",
    }

    # Try to find in history for metadata
    history = _read_jsonl_sync(HISTORY_FILE)
    for h in reversed(history):
        if h.get("task_id") == task_id:
            task["tags"] = h.get("tags", [])
            task["deadline"] = h.get("deadline")
            if not new_priority:
                task["priority"] = h.get("priority", "normal")
            break

    queue.append(task)
    queue = _sort_queue(queue)
    _save_queue(queue)

    logger.info("Requeued task %s with priority %s", task_id, task["priority"])
    return task


def get_queue_stats() -> dict:
    """Return queue statistics.

    Returns:
        Dict with counts_by_priority, total, avg_wait_seconds, overdue_count.
    """
    queue = _read_queue()
    queued = [t for t in queue if t.get("status") == "queued"]
    now = datetime.now(timezone.utc)

    counts: Dict[str, int] = {p: 0 for p in VALID_PRIORITIES}
    wait_times: List[float] = []
    overdue = 0

    for t in queued:
        p = t.get("priority", "normal")
        counts[p] = counts.get(p, 0) + 1

        enqueued_at = t.get("enqueued_at")
        if enqueued_at:
            try:
                et = datetime.fromisoformat(enqueued_at)
                wait_times.append((now - et).total_seconds())
            except (ValueError, TypeError):
                pass

        deadline = t.get("deadline")
        if deadline:
            try:
                dl = datetime.fromisoformat(deadline)
                if now > dl:
                    overdue += 1
            except (ValueError, TypeError):
                pass

    avg_wait = sum(wait_times) / len(wait_times) if wait_times else 0.0

    return {
        "total": len(queued),
        "counts_by_priority": counts,
        "avg_wait_seconds": round(avg_wait, 2),
        "overdue_count": overdue,
    }


def get_overdue(threshold_minutes: int = 30) -> List[dict]:
    """Return tasks whose deadline has passed or that have waited longer
    than threshold_minutes."""
    queue = _read_queue()
    now = datetime.now(timezone.utc)
    overdue_tasks = []

    for t in queue:
        if t.get("status") != "queued":
            continue

        deadline = t.get("deadline")
        if deadline:
            try:
                dl = datetime.fromisoformat(deadline)
                if now > dl:
                    overdue_tasks.append(t)
                    continue
            except (ValueError, TypeError):
                pass

        enqueued_at = t.get("enqueued_at")
        if enqueued_at:
            try:
                et = datetime.fromisoformat(enqueued_at)
                if (now - et).total_seconds() > threshold_minutes * 60:
                    overdue_tasks.append(t)
            except (ValueError, TypeError):
                pass

    return overdue_tasks


def clear_completed() -> int:
    """Remove completed/dequeued tasks from the queue. Returns count removed."""
    queue = _read_queue()
    before = len(queue)
    queue = [t for t in queue if t.get("status") == "queued"]
    after = len(queue)
    _save_queue(queue)
    removed = before - after
    if removed:
        logger.info("Cleared %d completed tasks from queue", removed)
    return removed
