# #region ApprovalService [C:5] [TYPE Module] [SEMANTICS clean-release, approval, gate, compliance]
# @BRIEF Enforce approval/rejection gates over immutable compliance reports.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @RELATION DEPENDS_ON -> [AuditService]
# @INVARIANT  Approval is allowed only for PASSED report bound to candidate; decisions are append-only.
# @PRE  Report with PASSED final_status exists for candidate
# @POST  Approval decision appended; candidate lifecycle advanced
# @SIDE_EFFECT  Persists approval decisions, transitions candidate status
# @DATA_CONTRACT  ApprovalRequest -> ApprovalDecision

from __future__ import annotations

from datetime import UTC, datetime
from uuid import uuid4

from ...core.logger import belief_scope, logger
from ...models.clean_release import ApprovalDecision
from .audit_service import audit_preparation
from .enums import ApprovalDecisionType, CandidateStatus, ComplianceDecision
from .exceptions import ApprovalGateError
from .repository import CleanReleaseRepository


# #region _get_or_init_decisions_store [TYPE Function]
# @BRIEF Provide append-only in-memory storage for approval decisions.
# @PRE  repository is initialized.
# @POST  Returns mutable decision list attached to repository.
def _get_or_init_decisions_store(
    repository: CleanReleaseRepository,
) -> list[ApprovalDecision]:
    decisions = getattr(repository, "approval_decisions", None)
    if decisions is None:
        decisions = []
        repository.approval_decisions = decisions
    return decisions


# #endregion _get_or_init_decisions_store


# #region _latest_decision_for_candidate [TYPE Function]
# @BRIEF Resolve latest approval decision for candidate from append-only store.
# @PRE  candidate_id is non-empty.
# @POST  Returns latest ApprovalDecision or None.
def _latest_decision_for_candidate(
    repository: CleanReleaseRepository, candidate_id: str
) -> ApprovalDecision | None:
    decisions = _get_or_init_decisions_store(repository)
    scoped = [item for item in decisions if item.candidate_id == candidate_id]
    if not scoped:
        return None
    return sorted(
        scoped,
        key=lambda item: item.decided_at or datetime.min.replace(tzinfo=UTC),
        reverse=True,
    )[0]


# #endregion _latest_decision_for_candidate


# #region _resolve_candidate_and_report [TYPE Function]
# @BRIEF Validate candidate/report existence and ownership prior to decision persistence.
# @PRE  candidate_id and report_id are non-empty.
# @POST  Returns tuple(candidate, report); raises ApprovalGateError on contract violation.
def _resolve_candidate_and_report(
    repository: CleanReleaseRepository,
    *,
    candidate_id: str,
    report_id: str,
):
    candidate = repository.get_candidate(candidate_id)
    if candidate is None:
        raise ApprovalGateError(f"candidate '{candidate_id}' not found")

    report = repository.get_report(report_id)
    if report is None:
        raise ApprovalGateError(f"report '{report_id}' not found")

    if report.candidate_id != candidate_id:
        raise ApprovalGateError("report belongs to another candidate")

    return candidate, report


# #endregion _resolve_candidate_and_report


# #region approve_candidate [TYPE Function]
# @BRIEF Persist immutable APPROVED decision and advance candidate lifecycle to APPROVED.
# @PRE  Candidate exists, report belongs to candidate, report final_status is PASSED, candidate not already APPROVED.
# @POST  Approval decision is appended and candidate transitions to APPROVED.
def approve_candidate(
    *,
    repository: CleanReleaseRepository,
    candidate_id: str,
    report_id: str,
    decided_by: str,
    comment: str | None = None,
) -> ApprovalDecision:
    with belief_scope("approval_service.approve_candidate"):
        logger.reason(
            f"Evaluating approve gate candidate_id={candidate_id} report_id={report_id}"
        )

        if not decided_by or not decided_by.strip():
            raise ApprovalGateError("decided_by must be non-empty")

        candidate, report = _resolve_candidate_and_report(
            repository,
            candidate_id=candidate_id,
            report_id=report_id,
        )

        if report.final_status != ComplianceDecision.PASSED.value:
            raise ApprovalGateError("approve requires PASSED compliance report")

        latest = _latest_decision_for_candidate(repository, candidate_id)
        if (
            latest is not None
            and latest.decision == ApprovalDecisionType.APPROVED.value
        ):
            raise ApprovalGateError("candidate is already approved")

        if candidate.status == CandidateStatus.APPROVED.value:
            raise ApprovalGateError("candidate is already approved")

        try:
            if candidate.status != CandidateStatus.CHECK_PASSED.value:
                raise ApprovalGateError(
                    f"candidate status '{candidate.status}' cannot transition to APPROVED"
                )
            candidate.transition_to(CandidateStatus.APPROVED)
            repository.save_candidate(candidate)
        except ApprovalGateError:
            raise
        except Exception as exc:
            logger.explore(
                f"Candidate transition to APPROVED failed candidate_id={candidate_id}: {exc}"
            )
            raise ApprovalGateError(str(exc)) from exc

        decision = ApprovalDecision(
            id=f"approve-{uuid4()}",
            candidate_id=candidate_id,
            report_id=report_id,
            decision=ApprovalDecisionType.APPROVED.value,
            decided_by=decided_by,
            decided_at=datetime.now(UTC),
            comment=comment,
        )
        _get_or_init_decisions_store(repository).append(decision)
        audit_preparation(
            candidate_id, "APPROVED", repository=repository, actor=decided_by
        )
        logger.reflect(
            f"Approval persisted candidate_id={candidate_id} decision_id={decision.id}"
        )
        return decision


# #endregion approve_candidate


# #region reject_candidate [TYPE Function]
# @BRIEF Persist immutable REJECTED decision without promoting candidate lifecycle.
# @PRE  Candidate exists and report belongs to candidate.
# @POST  Rejected decision is appended; candidate lifecycle is unchanged.
def reject_candidate(
    *,
    repository: CleanReleaseRepository,
    candidate_id: str,
    report_id: str,
    decided_by: str,
    comment: str | None = None,
) -> ApprovalDecision:
    with belief_scope("approval_service.reject_candidate"):
        logger.reason(
            f"Evaluating reject decision candidate_id={candidate_id} report_id={report_id}"
        )

        if not decided_by or not decided_by.strip():
            raise ApprovalGateError("decided_by must be non-empty")

        _resolve_candidate_and_report(
            repository,
            candidate_id=candidate_id,
            report_id=report_id,
        )

        decision = ApprovalDecision(
            id=f"reject-{uuid4()}",
            candidate_id=candidate_id,
            report_id=report_id,
            decision=ApprovalDecisionType.REJECTED.value,
            decided_by=decided_by,
            decided_at=datetime.now(UTC),
            comment=comment,
        )
        _get_or_init_decisions_store(repository).append(decision)
        audit_preparation(
            candidate_id, "REJECTED", repository=repository, actor=decided_by
        )
        logger.reflect(
            f"Rejection persisted candidate_id={candidate_id} decision_id={decision.id}"
        )
        return decision


# #endregion reject_candidate

# #endregion ApprovalService
