"""Deliverables gate -- acceptance criteria that must be set before execution.

A deliverable is a named criterion with a status.  The gate blocks task
execution until all deliverables are defined (by an orchestrator or human).

Usage::

    task = PipelineTask(item)
    set_deliverables(task, [
        {"name": "API endpoint returns 200", "description": "GET /users works"},
        {"name": "Tests pass", "description": "pytest exits 0"},
    ])

    # Later, after execution:
    update_deliverable(task, "Tests pass", DeliverableStatus.MET)
    all_met(task)  # -> True/False
"""

from __future__ import annotations

from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from enum import Enum
from typing import List, Optional

from cohort.pipeline.pipeline_task import PipelineTask


class DeliverableStatus(str, Enum):
    PENDING = "pending"   # Not yet evaluated
    MET = "met"           # Criterion satisfied
    UNMET = "unmet"       # Criterion not satisfied
    SKIPPED = "skipped"   # Waived by approver


@dataclass
class Deliverable:
    name: str
    description: str = ""
    status: str = DeliverableStatus.PENDING
    notes: Optional[str] = None

    def to_dict(self) -> dict:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: dict) -> "Deliverable":
        return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})


def set_deliverables(task: PipelineTask, items: List[dict]) -> None:
    """Set acceptance criteria on a task.  Overwrites any existing deliverables.

    Parameters
    ----------
    task:
        The PipelineTask to update.
    items:
        List of dicts with at minimum ``{"name": str}``.
        Optional keys: ``description``, ``status``, ``notes``.
    """
    deliverables = [
        Deliverable(
            name=d["name"],
            description=d.get("description", ""),
            status=d.get("status", DeliverableStatus.PENDING),
            notes=d.get("notes"),
        ).to_dict()
        for d in items
    ]
    task.deliverables = deliverables
    task.deliverables_set_at = datetime.now(timezone.utc).isoformat()


def update_deliverable(
    task: PipelineTask,
    name: str,
    status: DeliverableStatus,
    notes: Optional[str] = None,
) -> bool:
    """Update the status of a single deliverable by name.

    Returns True if found and updated, False if not found.
    """
    for d in task.deliverables:
        if d.get("name") == name:
            d["status"] = status
            if notes is not None:
                d["notes"] = notes
            # write list back so the property setter fires
            task.deliverables = task.deliverables
            return True
    return False


def all_met(task: PipelineTask) -> bool:
    """Return True if all deliverables are MET or SKIPPED."""
    deliverables = task.deliverables
    if not deliverables:
        return False
    return all(
        d.get("status") in (DeliverableStatus.MET, DeliverableStatus.SKIPPED)
        for d in deliverables
    )


def deliverables_ready(task: PipelineTask) -> bool:
    """Return True if deliverables have been set (gate is open for execution)."""
    return bool(task.deliverables and task.deliverables_set_at)


def unmet_deliverables(task: PipelineTask) -> List[str]:
    """Return names of deliverables that are UNMET."""
    return [
        d["name"]
        for d in task.deliverables
        if d.get("status") == DeliverableStatus.UNMET
    ]
