"""BLCE Review Workflow — consultant review/approval state machine.

Manages the review lifecycle for proposed dimensions and facts:
  proposed → reviewed → approved → built
  proposed → reviewed → rejected

Provides audit trail via ReviewEntry records on ProposedModel.review_log.
"""
from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from .contracts import (
    ProposedDimension,
    ProposedFact,
    ProposedModel,
    ReviewEntry,
    ReviewStatus,
)

logger = logging.getLogger(__name__)

# Valid transitions
_TRANSITIONS = {
    "proposed": {"reviewed"},
    "reviewed": {"approved", "rejected"},
    "approved": {"built"},
    "rejected": set(),
    "built": set(),
}


class ReviewWorkflow:
    """Manage consultant review/approval for proposed model objects."""

    def review_model(self, model: ProposedModel) -> Dict[str, Any]:
        """Return review summary with counts per status.

        Args:
            model: The proposed model to summarise.

        Returns:
            Dict with status counts and pending items.
        """
        return self.get_review_summary(model)

    def approve_dimension(
        self,
        model: ProposedModel,
        dim_id: str,
        reviewer: str = "",
        comments: str = "",
    ) -> ReviewEntry:
        """Transition a dimension to approved (via reviewed if still proposed).

        Args:
            model: The proposed model containing the dimension.
            dim_id: The dimension_id to approve.
            reviewer: Name of the reviewer.
            comments: Optional review comments.

        Returns:
            ReviewEntry recording the action.

        Raises:
            ValueError: If dim_id not found or invalid transition.
        """
        dim = self._find_dimension(model, dim_id)
        # Auto-advance proposed → reviewed → approved
        if dim.review_status == "proposed":
            dim.review_status = "reviewed"
        self._validate_transition(dim.review_status, "approved")
        dim.review_status = "approved"

        entry = ReviewEntry(
            object_type="dimension",
            object_id=dim.dimension_id,
            object_name=dim.name,
            status=ReviewStatus.APPROVED,
            reviewer=reviewer,
            comments=comments,
            approved_at=datetime.now(timezone.utc).isoformat(),
        )
        model.review_log.append(entry)
        logger.info("Approved dimension %s by %s", dim.name, reviewer)
        return entry

    def approve_fact(
        self,
        model: ProposedModel,
        fact_id: str,
        reviewer: str = "",
        comments: str = "",
    ) -> ReviewEntry:
        """Transition a fact to approved (via reviewed if still proposed).

        Args:
            model: The proposed model containing the fact.
            fact_id: The fact_id to approve.
            reviewer: Name of the reviewer.
            comments: Optional review comments.

        Returns:
            ReviewEntry recording the action.

        Raises:
            ValueError: If fact_id not found or invalid transition.
        """
        fact = self._find_fact(model, fact_id)
        if fact.review_status == "proposed":
            fact.review_status = "reviewed"
        self._validate_transition(fact.review_status, "approved")
        fact.review_status = "approved"

        entry = ReviewEntry(
            object_type="fact",
            object_id=fact.fact_id,
            object_name=fact.name,
            status=ReviewStatus.APPROVED,
            reviewer=reviewer,
            comments=comments,
            approved_at=datetime.now(timezone.utc).isoformat(),
        )
        model.review_log.append(entry)
        logger.info("Approved fact %s by %s", fact.name, reviewer)
        return entry

    def reject_item(
        self,
        model: ProposedModel,
        object_type: str,
        object_id: str,
        reviewer: str = "",
        comments: str = "",
    ) -> ReviewEntry:
        """Reject a dimension or fact.

        Args:
            model: The proposed model.
            object_type: "dimension" or "fact".
            object_id: The dimension_id or fact_id.
            reviewer: Name of the reviewer.
            comments: Rejection reason.

        Returns:
            ReviewEntry recording the rejection.
        """
        if object_type == "dimension":
            obj = self._find_dimension(model, object_id)
            if obj.review_status == "proposed":
                obj.review_status = "reviewed"
            self._validate_transition(obj.review_status, "rejected")
            obj.review_status = "rejected"
            name = obj.name
        elif object_type == "fact":
            obj = self._find_fact(model, object_id)
            if obj.review_status == "proposed":
                obj.review_status = "reviewed"
            self._validate_transition(obj.review_status, "rejected")
            obj.review_status = "rejected"
            name = obj.name
        else:
            raise ValueError(f"object_type must be 'dimension' or 'fact', got '{object_type}'")

        entry = ReviewEntry(
            object_type=object_type,
            object_id=object_id,
            object_name=name,
            status=ReviewStatus.REJECTED,
            reviewer=reviewer,
            comments=comments,
            reviewed_at=datetime.now(timezone.utc).isoformat(),
        )
        model.review_log.append(entry)
        logger.info("Rejected %s %s by %s: %s", object_type, name, reviewer, comments)
        return entry

    def get_approved_model(self, model: ProposedModel) -> ProposedModel:
        """Return a new model containing only approved dimensions and facts.

        Use this before DDL generation so rejected items are excluded.

        Args:
            model: The full proposed model.

        Returns:
            A filtered ProposedModel with only approved items.
        """
        approved_dims = [d for d in model.dimensions if d.review_status == "approved"]
        approved_facts = [f for f in model.facts if f.review_status == "approved"]

        return ProposedModel(
            model_id=model.model_id,
            client_id=model.client_id,
            dimensions=approved_dims,
            facts=approved_facts,
            bus_matrix=model.bus_matrix,
            measure_dictionary=model.measure_dictionary,
            conflicts=model.conflicts,
            business_rules=model.business_rules,
            mapping_report=model.mapping_report,
            measure_validations=model.measure_validations,
            review_log=model.review_log,
        )

    def get_review_summary(self, model: ProposedModel) -> Dict[str, Any]:
        """Get counts by status and list of pending items.

        Args:
            model: The proposed model.

        Returns:
            Dict with status_counts, pending_dimensions, pending_facts.
        """
        status_counts: Dict[str, int] = {}
        pending_dims: List[Dict[str, str]] = []
        pending_facts: List[Dict[str, str]] = []

        for d in model.dimensions:
            status_counts[d.review_status] = status_counts.get(d.review_status, 0) + 1
            if d.review_status in ("proposed", "reviewed"):
                pending_dims.append({"id": d.dimension_id, "name": d.name, "status": d.review_status})

        for f in model.facts:
            status_counts[f.review_status] = status_counts.get(f.review_status, 0) + 1
            if f.review_status in ("proposed", "reviewed"):
                pending_facts.append({"id": f.fact_id, "name": f.name, "status": f.review_status})

        return {
            "status_counts": status_counts,
            "pending_dimensions": pending_dims,
            "pending_facts": pending_facts,
            "total_reviews": len(model.review_log),
        }

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

    @staticmethod
    def _find_dimension(model: ProposedModel, dim_id: str) -> ProposedDimension:
        for d in model.dimensions:
            if d.dimension_id == dim_id:
                return d
        raise ValueError(f"Dimension {dim_id} not found in model.")

    @staticmethod
    def _find_fact(model: ProposedModel, fact_id: str) -> ProposedFact:
        for f in model.facts:
            if f.fact_id == fact_id:
                return f
        raise ValueError(f"Fact {fact_id} not found in model.")

    @staticmethod
    def _validate_transition(current: str, target: str) -> None:
        allowed = _TRANSITIONS.get(current, set())
        if target not in allowed:
            raise ValueError(
                f"Cannot transition from '{current}' to '{target}'. "
                f"Allowed: {allowed or 'none'}"
            )

