"""PipelineQueue -- git-aware, approval-gated task pipeline.

Wraps WorkQueue with the full pipeline lifecycle:

    enqueue  -> (staleness check at claim) -> claim -> execute
             -> self_review -> approve/reject -> complete/requeue

State is stored entirely in WorkItem.metadata -- WorkQueue itself is
unchanged.  PipelineQueue is the single entry point for all pipeline
operations.

Usage::

    from pathlib import Path
    from cohort.pipeline import PipelineQueue

    q = PipelineQueue(data_dir=Path("data"))

    # Enqueue a task
    task = q.enqueue(
        description="Add GET /users endpoint",
        requester="backend_dev",
        file_manifest=["src/routes/users.py", "tests/test_users.py"],
        priority="high",
    )

    # Set deliverables (acceptance criteria) before claim
    q.set_deliverables(task, [
        {"name": "Endpoint returns 200"},
        {"name": "Tests pass"},
    ])

    # Claim next task (staleness check runs here)
    claimed = q.claim_next()
    if claimed is None:
        # queue empty or already active
        ...
    elif claimed.staleness_level in ("uncertain", "conflicted"):
        # task was bounced -- inspect claimed.escalated_to
        ...

    # After execution -- attach self-review
    q.submit_review(
        task=claimed,
        files_modified=["src/routes/users.py", "tests/test_users.py"],
        changes_summary="Added GET /users, 3 tests written.",
        deliverable_evaluations=[
            {"name": "Endpoint returns 200", "status": "met"},
            {"name": "Tests pass", "status": "met"},
        ],
    )

    # Approve
    q.approve(claimed)

    # Complete (after services restarted, changes applied)
    q.complete(claimed)
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Dict, List, Optional

from cohort.pipeline.audit_log import AuditLog
from cohort.pipeline.deliverables import deliverables_ready, set_deliverables
from cohort.pipeline.pipeline_task import PipelineTask, ReviewStatus, StalenessLevel
from cohort.pipeline.requeue import requeue
from cohort.pipeline.self_review import SelfReviewReport, self_review
from cohort.pipeline.staleness import assess_staleness, capture_sha
from cohort.work_queue import WorkItem, WorkQueue

logger = logging.getLogger(__name__)


class PipelineQueue:
    """Git-aware, approval-gated task pipeline built on WorkQueue.

    Parameters
    ----------
    data_dir:
        Directory for all persistence files.
        - WorkQueue: ``{data_dir}/work_queue.json``
        - AuditLog:  ``{data_dir}/pipeline_audit.jsonl``
    repo_dir:
        Root of the git repository for staleness checks.
        Defaults to ``data_dir`` parent or cwd.
    require_deliverables:
        If True (default), claim_next will skip tasks that have no
        deliverables set yet.  Set False to allow bare queue usage.
    """

    def __init__(
        self,
        data_dir: Path,
        repo_dir: Optional[Path] = None,
        require_deliverables: bool = True,
    ) -> None:
        self._queue = WorkQueue(data_dir)
        self._audit = AuditLog(data_dir)
        self._repo_dir = repo_dir or data_dir.parent
        self._require_deliverables = require_deliverables

    # ------------------------------------------------------------------
    # Enqueue
    # ------------------------------------------------------------------

    def enqueue(
        self,
        description: str,
        requester: str,
        file_manifest: Optional[List[str]] = None,
        priority: str = "medium",
        agent_id: Optional[str] = None,
        depends_on: Optional[List[str]] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> PipelineTask:
        """Enqueue a new task, capturing git SHA at queue time.

        Parameters
        ----------
        description:
            What needs to be done.
        requester:
            Agent ID or "human".
        file_manifest:
            Files this task intends to touch (used for staleness detection).
        priority:
            ``critical | high | medium | low``
        agent_id:
            Target agent for this task.
        depends_on:
            List of WorkItem IDs this task waits for.
        metadata:
            Extra metadata to attach.
        """
        sha = capture_sha(self._repo_dir)
        branch = self._capture_branch()

        meta: Dict[str, Any] = dict(metadata or {})
        meta["file_manifest"] = file_manifest or []
        meta["requeue_count"] = meta.get("requeue_count", 0)
        if sha:
            meta["git_sha_at_queue"] = sha
        if branch:
            meta["git_branch"] = branch

        item = self._queue.enqueue(
            description=description,
            requester=requester,
            priority=priority,
            agent_id=agent_id,
            depends_on=depends_on,
            metadata=meta,
        )
        task = PipelineTask(item)
        self._audit.record("enqueued", task_id=item.id, actor=requester, payload={
            "priority": priority,
            "file_manifest": file_manifest or [],
            "git_sha": sha,
        })
        logger.info("[pipeline] enqueued %s (%s)", item.id, priority)
        return task

    # ------------------------------------------------------------------
    # Deliverables
    # ------------------------------------------------------------------

    def set_deliverables(self, task: PipelineTask, items: List[dict]) -> None:
        """Set acceptance criteria on a task."""
        set_deliverables(task, items)
        self._queue._save_to_disk()  # flush metadata change
        self._audit.record("deliverables_set", task_id=task.item.id, payload={
            "count": len(items),
            "names": [d.get("name") for d in items],
        })

    # ------------------------------------------------------------------
    # Claim
    # ------------------------------------------------------------------

    def claim_next(self) -> Optional[PipelineTask]:
        """Claim the next eligible task, running staleness detection.

        Returns
        -------
        PipelineTask or None
            - None: queue empty or already active
            - task with staleness_level ``clean`` or ``trivial``: proceed
            - task with staleness_level ``uncertain``: escalated to orchestrator
            - task with staleness_level ``conflicted``: escalated to human
            - task with review_status ``reviewing``: waiting for approval
        """
        # Check if a task is sitting in review (not yet approved/rejected)
        active = self._queue.get_active()
        if active is not None:
            task = PipelineTask(active)
            if task.review_status in (ReviewStatus.PENDING, ReviewStatus.APPROVED):
                logger.info("[pipeline] active task %s in review state", active.id)
                return task
            return task  # Already active for some other reason

        # Skip tasks without deliverables if required
        if self._require_deliverables:
            eligible_ids = {
                item.id for item in self._queue.list_items(status="queued")
                if deliverables_ready(PipelineTask(item))
            }
            if not eligible_ids:
                return None

        result = self._queue.claim_next()
        if result.get("error") or result.get("item") is None:
            return None

        item = WorkItem.from_dict(result["item"])
        # Re-fetch from queue to get live object
        live_item = self._queue.get_item(item.id)
        if live_item is None:
            return None
        task = PipelineTask(live_item)

        # Staleness detection
        staleness = assess_staleness(
            sha_at_queue=task.git_sha_at_queue,
            file_manifest=task.file_manifest,
            repo_dir=self._repo_dir,
        )
        task.staleness_level = staleness.level
        task.staleness_report = staleness.report
        self._queue._save_to_disk()

        self._audit.record("claimed", task_id=item.id, payload={
            "staleness": staleness.level,
            "report": staleness.report,
        })

        if staleness.level == StalenessLevel.CONFLICTED:
            task.escalated_to = "human"
            task.escalation_reason = staleness.report
            self._queue._save_to_disk()
            self._audit.record("escalated", task_id=item.id, actor="pipeline", payload={
                "to": "human",
                "reason": staleness.report,
            })
            logger.warning("[pipeline] %s conflicted -- escalated to human", item.id)

        elif staleness.level == StalenessLevel.UNCERTAIN:
            task.escalated_to = "orchestrator"
            task.escalation_reason = staleness.report
            self._queue._save_to_disk()
            self._audit.record("escalated", task_id=item.id, actor="pipeline", payload={
                "to": "orchestrator",
                "reason": staleness.report,
            })
            logger.warning("[pipeline] %s uncertain -- escalated to orchestrator", item.id)

        return task

    # ------------------------------------------------------------------
    # Review
    # ------------------------------------------------------------------

    def submit_review(
        self,
        task: PipelineTask,
        files_modified: List[str],
        changes_summary: str,
        reviewer: str = "agent",
        deliverable_evaluations: Optional[List[Dict[str, Any]]] = None,
        notes: Optional[str] = None,
    ) -> SelfReviewReport:
        """Attach a self-review report after execution."""
        report = self_review(
            task=task,
            files_modified=files_modified,
            changes_summary=changes_summary,
            reviewer=reviewer,
            deliverable_evaluations=deliverable_evaluations,
            notes=notes,
        )
        task.review_status = ReviewStatus.PENDING
        self._queue._save_to_disk()
        self._audit.record("review_submitted", task_id=task.item.id, actor=reviewer, payload={
            "passed": report.passed,
            "files_modified": files_modified,
        })
        return report

    def approve(self, task: PipelineTask, actor: str = "human") -> None:
        """Approve a task that is pending review."""
        task.review_status = ReviewStatus.APPROVED
        self._queue._save_to_disk()
        self._audit.record("approved", task_id=task.item.id, actor=actor)
        logger.info("[pipeline] approved %s", task.item.id)

    def reject(
        self,
        task: PipelineTask,
        reason: Optional[str] = None,
        actor: str = "human",
    ) -> None:
        """Reject a task, marking it for potential requeue."""
        task.review_status = ReviewStatus.REJECTED
        self._queue._save_to_disk()
        self._audit.record("rejected", task_id=task.item.id, actor=actor, payload={
            "reason": reason,
        })
        logger.info("[pipeline] rejected %s reason=%r", task.item.id, reason)

    # ------------------------------------------------------------------
    # Complete / fail
    # ------------------------------------------------------------------

    def complete(self, task: PipelineTask, actor: str = "human") -> None:
        """Mark a task as fully complete (changes applied)."""
        task.review_status = ReviewStatus.APPLIED
        self._queue._save_to_disk()
        self._queue.complete(task.item.id, result="applied")
        self._audit.record("completed", task_id=task.item.id, actor=actor)
        logger.info("[pipeline] completed %s", task.item.id)

    def fail(self, task: PipelineTask, reason: Optional[str] = None) -> None:
        """Fail a task (unrecoverable error)."""
        self._queue.fail(task.item.id, reason=reason)
        self._audit.record("failed", task_id=task.item.id, payload={"reason": reason})
        logger.warning("[pipeline] failed %s reason=%r", task.item.id, reason)

    def requeue(
        self,
        task: PipelineTask,
        reason: Optional[str] = None,
        priority_override: Optional[str] = None,
    ) -> Optional[PipelineTask]:
        """Requeue a rejected task (respects MAX_REQUEUES cap).

        Returns the new PipelineTask, or None if cap reached.
        """
        # Fail the current item first
        self._queue.fail(task.item.id, reason=f"requeued: {reason or 'no reason'}")
        self._audit.record("requeued_from", task_id=task.item.id, payload={"reason": reason})

        new_item = requeue(self._queue, task, reason=reason, priority_override=priority_override)
        if new_item is None:
            self._audit.record("requeue_cap_reached", task_id=task.item.id)
            return None

        new_task = PipelineTask(new_item)
        # Carry deliverables forward
        if task.deliverables:
            # Reset deliverable statuses for fresh attempt
            fresh = [{"name": d["name"], "description": d.get("description", "")}
                     for d in task.deliverables]
            set_deliverables(new_task, fresh)
            self._queue._save_to_disk()

        self._audit.record("enqueued", task_id=new_item.id, actor="pipeline", payload={
            "requeued_from": task.item.id,
            "requeue_count": new_task.requeue_count,
        })
        return new_task

    # ------------------------------------------------------------------
    # Inspection
    # ------------------------------------------------------------------

    def get_task(self, item_id: str) -> Optional[PipelineTask]:
        item = self._queue.get_item(item_id)
        return PipelineTask(item) if item else None

    def list_tasks(self, status: Optional[str] = None) -> List[PipelineTask]:
        return [PipelineTask(item) for item in self._queue.list_items(status=status)]

    def get_active(self) -> Optional[PipelineTask]:
        item = self._queue.get_active()
        return PipelineTask(item) if item else None

    def audit_events(
        self,
        task_id: Optional[str] = None,
        event: Optional[str] = None,
        limit: Optional[int] = None,
    ) -> List[Dict[str, Any]]:
        return self._audit.read(task_id=task_id, event=event, limit=limit)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _capture_branch(self) -> Optional[str]:
        import subprocess
        try:
            result = subprocess.run(
                ["git", "rev-parse", "--abbrev-ref", "HEAD"],
                cwd=str(self._repo_dir),
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode == 0:
                return result.stdout.strip()
        except Exception:
            pass
        return None
