# [DEF:backend.src.api.routes.clean_release:Module]
# @TIER: STANDARD
# @SEMANTICS: api, clean-release, candidate-preparation, compliance
# @PURPOSE: Expose clean release endpoints for candidate preparation and subsequent compliance flow.
# @LAYER: API
# @RELATION: DEPENDS_ON -> backend.src.dependencies.get_clean_release_repository
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.preparation_service
# @INVARIANT: API never reports prepared status if preparation errors are present.

from __future__ import annotations

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

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field

from ...core.logger import belief_scope, logger
from ...dependencies import get_clean_release_repository, get_config_manager
from ...services.clean_release.preparation_service import prepare_candidate
from ...services.clean_release.repository import CleanReleaseRepository
from ...services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
from ...services.clean_release.report_builder import ComplianceReportBuilder
from ...services.clean_release.compliance_execution_service import ComplianceExecutionService, ComplianceRunError
from ...services.clean_release.dto import CandidateDTO, ManifestDTO, CandidateOverviewDTO, ComplianceRunDTO
from ...services.clean_release.enums import (
    ComplianceDecision,
    ComplianceStageName,
    ViolationCategory,
    ViolationSeverity,
    RunStatus,
    CandidateStatus,
)
from ...models.clean_release import (
    ComplianceRun,
    ComplianceStageRun,
    ComplianceViolation,
    CandidateArtifact,
    ReleaseCandidate,
)

router = APIRouter(prefix="/api/clean-release", tags=["Clean Release"])


# [DEF:PrepareCandidateRequest:Class]
# @PURPOSE: Request schema for candidate preparation endpoint.
class PrepareCandidateRequest(BaseModel):
    candidate_id: str = Field(min_length=1)
    artifacts: List[Dict[str, Any]] = Field(default_factory=list)
    sources: List[str] = Field(default_factory=list)
    operator_id: str = Field(min_length=1)
# [/DEF:PrepareCandidateRequest:Class]


# [DEF:StartCheckRequest:Class]
# @PURPOSE: Request schema for clean compliance check run startup.
class StartCheckRequest(BaseModel):
    candidate_id: str = Field(min_length=1)
    profile: str = Field(default="enterprise-clean")
    execution_mode: str = Field(default="tui")
    triggered_by: str = Field(default="system")
# [/DEF:StartCheckRequest:Class]


# [DEF:RegisterCandidateRequest:Class]
# @PURPOSE: Request schema for candidate registration endpoint.
class RegisterCandidateRequest(BaseModel):
    id: str = Field(min_length=1)
    version: str = Field(min_length=1)
    source_snapshot_ref: str = Field(min_length=1)
    created_by: str = Field(min_length=1)
# [/DEF:RegisterCandidateRequest:Class]


# [DEF:ImportArtifactsRequest:Class]
# @PURPOSE: Request schema for candidate artifact import endpoint.
class ImportArtifactsRequest(BaseModel):
    artifacts: List[Dict[str, Any]] = Field(default_factory=list)
# [/DEF:ImportArtifactsRequest:Class]


# [DEF:BuildManifestRequest:Class]
# @PURPOSE: Request schema for manifest build endpoint.
class BuildManifestRequest(BaseModel):
    created_by: str = Field(default="system")
# [/DEF:BuildManifestRequest:Class]


# [DEF:CreateComplianceRunRequest:Class]
# @PURPOSE: Request schema for compliance run creation with optional manifest pinning.
class CreateComplianceRunRequest(BaseModel):
    requested_by: str = Field(min_length=1)
    manifest_id: str | None = None
# [/DEF:CreateComplianceRunRequest:Class]


# [DEF:register_candidate_v2_endpoint:Function]
# @PURPOSE: Register a clean-release candidate for headless lifecycle.
# @PRE: Candidate identifier is unique.
# @POST: Candidate is persisted in DRAFT status.
@router.post("/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED)
async def register_candidate_v2_endpoint(
    payload: RegisterCandidateRequest,
    repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
    existing = repository.get_candidate(payload.id)
    if existing is not None:
        raise HTTPException(status_code=409, detail={"message": "Candidate already exists", "code": "CANDIDATE_EXISTS"})

    candidate = ReleaseCandidate(
        id=payload.id,
        version=payload.version,
        source_snapshot_ref=payload.source_snapshot_ref,
        created_by=payload.created_by,
        created_at=datetime.now(timezone.utc),
        status=CandidateStatus.DRAFT.value,
    )
    repository.save_candidate(candidate)

    return CandidateDTO(
        id=candidate.id,
        version=candidate.version,
        source_snapshot_ref=candidate.source_snapshot_ref,
        created_at=candidate.created_at,
        created_by=candidate.created_by,
        status=CandidateStatus(candidate.status),
    )
# [/DEF:register_candidate_v2_endpoint:Function]


# [DEF:import_candidate_artifacts_v2_endpoint:Function]
# @PURPOSE: Import candidate artifacts in headless flow.
# @PRE: Candidate exists and artifacts array is non-empty.
# @POST: Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.
@router.post("/candidates/{candidate_id}/artifacts")
async def import_candidate_artifacts_v2_endpoint(
    candidate_id: str,
    payload: ImportArtifactsRequest,
    repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
    candidate = repository.get_candidate(candidate_id)
    if candidate is None:
        raise HTTPException(status_code=404, detail={"message": "Candidate not found", "code": "CANDIDATE_NOT_FOUND"})
    if not payload.artifacts:
        raise HTTPException(status_code=400, detail={"message": "Artifacts list is required", "code": "ARTIFACTS_EMPTY"})

    for artifact in payload.artifacts:
        required = ("id", "path", "sha256", "size")
        for field_name in required:
            if field_name not in artifact:
                raise HTTPException(
                    status_code=400,
                    detail={"message": f"Artifact missing field '{field_name}'", "code": "ARTIFACT_INVALID"},
                )

        artifact_model = CandidateArtifact(
            id=str(artifact["id"]),
            candidate_id=candidate_id,
            path=str(artifact["path"]),
            sha256=str(artifact["sha256"]),
            size=int(artifact["size"]),
            detected_category=artifact.get("detected_category"),
            declared_category=artifact.get("declared_category"),
            source_uri=artifact.get("source_uri"),
            source_host=artifact.get("source_host"),
            metadata_json=artifact.get("metadata_json", {}),
        )
        repository.save_artifact(artifact_model)

    if candidate.status == CandidateStatus.DRAFT.value:
        candidate.transition_to(CandidateStatus.PREPARED)
        repository.save_candidate(candidate)

    return {"status": "success"}
# [/DEF:import_candidate_artifacts_v2_endpoint:Function]


# [DEF:build_candidate_manifest_v2_endpoint:Function]
# @PURPOSE: Build immutable manifest snapshot for prepared candidate.
# @PRE: Candidate exists and has imported artifacts.
# @POST: Returns created ManifestDTO with incremented version.
@router.post("/candidates/{candidate_id}/manifests", response_model=ManifestDTO, status_code=status.HTTP_201_CREATED)
async def build_candidate_manifest_v2_endpoint(
    candidate_id: str,
    payload: BuildManifestRequest,
    repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
    from ...services.clean_release.manifest_service import build_manifest_snapshot

    try:
        manifest = build_manifest_snapshot(
            repository=repository,
            candidate_id=candidate_id,
            created_by=payload.created_by,
        )
    except ValueError as exc:
        raise HTTPException(status_code=400, detail={"message": str(exc), "code": "MANIFEST_BUILD_ERROR"})

    return ManifestDTO(
        id=manifest.id,
        candidate_id=manifest.candidate_id,
        manifest_version=manifest.manifest_version,
        manifest_digest=manifest.manifest_digest,
        artifacts_digest=manifest.artifacts_digest,
        created_at=manifest.created_at,
        created_by=manifest.created_by,
        source_snapshot_ref=manifest.source_snapshot_ref,
        content_json=manifest.content_json,
    )
# [/DEF:build_candidate_manifest_v2_endpoint:Function]


# [DEF:get_candidate_overview_v2_endpoint:Function]
# @PURPOSE: Return expanded candidate overview DTO for headless lifecycle visibility.
# @PRE: Candidate exists.
# @POST: Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.
@router.get("/candidates/{candidate_id}/overview", response_model=CandidateOverviewDTO)
async def get_candidate_overview_v2_endpoint(
    candidate_id: str,
    repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
    candidate = repository.get_candidate(candidate_id)
    if candidate is None:
        raise HTTPException(status_code=404, detail={"message": "Candidate not found", "code": "CANDIDATE_NOT_FOUND"})

    manifests = repository.get_manifests_by_candidate(candidate_id)
    latest_manifest = sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0] if manifests else None

    runs = [run for run in repository.check_runs.values() if run.candidate_id == candidate_id]
    latest_run = sorted(runs, key=lambda run: run.requested_at or datetime.min.replace(tzinfo=timezone.utc), reverse=True)[0] if runs else None

    latest_report = None
    if latest_run is not None:
        latest_report = next((r for r in repository.reports.values() if r.run_id == latest_run.id), None)

    latest_policy_snapshot = repository.get_policy(latest_run.policy_snapshot_id) if latest_run else None
    latest_registry_snapshot = repository.get_registry(latest_run.registry_snapshot_id) if latest_run else None

    approval_decisions = getattr(repository, "approval_decisions", [])
    latest_approval = (
        sorted(
            [item for item in approval_decisions if item.candidate_id == candidate_id],
            key=lambda item: item.decided_at or datetime.min.replace(tzinfo=timezone.utc),
            reverse=True,
        )[0]
        if approval_decisions
        and any(item.candidate_id == candidate_id for item in approval_decisions)
        else None
    )

    publication_records = getattr(repository, "publication_records", [])
    latest_publication = (
        sorted(
            [item for item in publication_records if item.candidate_id == candidate_id],
            key=lambda item: item.published_at or datetime.min.replace(tzinfo=timezone.utc),
            reverse=True,
        )[0]
        if publication_records
        and any(item.candidate_id == candidate_id for item in publication_records)
        else None
    )

    return CandidateOverviewDTO(
        candidate_id=candidate.id,
        version=candidate.version,
        source_snapshot_ref=candidate.source_snapshot_ref,
        status=CandidateStatus(candidate.status),
        latest_manifest_id=latest_manifest.id if latest_manifest else None,
        latest_manifest_digest=latest_manifest.manifest_digest if latest_manifest else None,
        latest_run_id=latest_run.id if latest_run else None,
        latest_run_status=RunStatus(latest_run.status) if latest_run else None,
        latest_report_id=latest_report.id if latest_report else None,
        latest_report_final_status=ComplianceDecision(latest_report.final_status) if latest_report else None,
        latest_policy_snapshot_id=latest_policy_snapshot.id if latest_policy_snapshot else None,
        latest_policy_version=latest_policy_snapshot.policy_version if latest_policy_snapshot else None,
        latest_registry_snapshot_id=latest_registry_snapshot.id if latest_registry_snapshot else None,
        latest_registry_version=latest_registry_snapshot.registry_version if latest_registry_snapshot else None,
        latest_approval_decision=latest_approval.decision if latest_approval else None,
        latest_publication_id=latest_publication.id if latest_publication else None,
        latest_publication_status=latest_publication.status if latest_publication else None,
    )
# [/DEF:get_candidate_overview_v2_endpoint:Function]


# [DEF:prepare_candidate_endpoint:Function]
# @PURPOSE: Prepare candidate with policy evaluation and deterministic manifest generation.
# @PRE: Candidate and active policy exist in repository.
# @POST: Returns preparation result including manifest reference and violations.
@router.post("/candidates/prepare")
async def prepare_candidate_endpoint(
    payload: PrepareCandidateRequest,
    repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
    try:
        result = prepare_candidate(
            repository=repository,
            candidate_id=payload.candidate_id,
            artifacts=payload.artifacts,
            sources=payload.sources,
            operator_id=payload.operator_id,
        )
        return result
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail={"message": str(exc), "code": "CLEAN_PREPARATION_ERROR"},
        )
# [/DEF:prepare_candidate_endpoint:Function]


# [DEF:start_check:Function]
# @PURPOSE: Start and finalize a clean compliance check run and persist report artifacts.
# @PRE: Active policy and candidate exist.
# @POST: Returns accepted payload with check_run_id and started_at.
@router.post("/checks", status_code=status.HTTP_202_ACCEPTED)
async def start_check(
    payload: StartCheckRequest,
    repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
    with belief_scope("clean_release.start_check"):
        logger.reason("Starting clean-release compliance check run")
        policy = repository.get_active_policy()
        if policy is None:
            raise HTTPException(status_code=409, detail={"message": "Active policy not found", "code": "POLICY_NOT_FOUND"})

        candidate = repository.get_candidate(payload.candidate_id)
        if candidate is None:
            raise HTTPException(status_code=409, detail={"message": "Candidate not found", "code": "CANDIDATE_NOT_FOUND"})

        manifests = repository.get_manifests_by_candidate(payload.candidate_id)
        if not manifests:
            raise HTTPException(status_code=409, detail={"message": "No manifest found for candidate", "code": "MANIFEST_NOT_FOUND"})
        latest_manifest = sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0]

        orchestrator = CleanComplianceOrchestrator(repository)
        run = orchestrator.start_check_run(
            candidate_id=payload.candidate_id,
            policy_id=policy.id,
            requested_by=payload.triggered_by,
            manifest_id=latest_manifest.id,
        )

        forced = [
            ComplianceStageRun(
                id=f"stage-{run.id}-1",
                run_id=run.id,
                stage_name=ComplianceStageName.DATA_PURITY.value,
                status=RunStatus.SUCCEEDED.value,
                decision=ComplianceDecision.PASSED.value,
                details_json={"message": "ok"}
            ),
            ComplianceStageRun(
                id=f"stage-{run.id}-2",
                run_id=run.id,
                stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY.value,
                status=RunStatus.SUCCEEDED.value,
                decision=ComplianceDecision.PASSED.value,
                details_json={"message": "ok"}
            ),
            ComplianceStageRun(
                id=f"stage-{run.id}-3",
                run_id=run.id,
                stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,
                status=RunStatus.SUCCEEDED.value,
                decision=ComplianceDecision.PASSED.value,
                details_json={"message": "ok"}
            ),
            ComplianceStageRun(
                id=f"stage-{run.id}-4",
                run_id=run.id,
                stage_name=ComplianceStageName.MANIFEST_CONSISTENCY.value,
                status=RunStatus.SUCCEEDED.value,
                decision=ComplianceDecision.PASSED.value,
                details_json={"message": "ok"}
            ),
        ]
        run = orchestrator.execute_stages(run, forced_results=forced)
        run = orchestrator.finalize_run(run)

        if run.final_status == ComplianceDecision.BLOCKED.value:
            logger.explore("Run ended as BLOCKED, persisting synthetic external-source violation")
            violation = ComplianceViolation(
                id=f"viol-{run.id}",
                run_id=run.id,
                stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,
                code="EXTERNAL_SOURCE_DETECTED",
                severity=ViolationSeverity.CRITICAL.value,
                message="Replace with approved internal server",
                evidence_json={"location": "external.example.com"}
            )
            repository.save_violation(violation)

        builder = ComplianceReportBuilder(repository)
        report = builder.build_report_payload(run, repository.get_violations_by_run(run.id))
        builder.persist_report(report)
        logger.reflect(f"Compliance report persisted for run_id={run.id}")

        return {
            "check_run_id": run.id,
            "candidate_id": run.candidate_id,
            "status": "running",
            "started_at": run.started_at.isoformat() if run.started_at else None,
        }
# [/DEF:start_check:Function]


# [DEF:get_check_status:Function]
# @PURPOSE: Return terminal/intermediate status payload for a check run.
# @PRE: check_run_id references an existing run.
# @POST: Deterministic payload shape includes checks and violations arrays.
@router.get("/checks/{check_run_id}")
async def get_check_status(check_run_id: str, repository: CleanReleaseRepository = Depends(get_clean_release_repository)):
    with belief_scope("clean_release.get_check_status"):
        run = repository.get_check_run(check_run_id)
        if run is None:
            raise HTTPException(status_code=404, detail={"message": "Check run not found", "code": "CHECK_NOT_FOUND"})

        logger.reflect(f"Returning check status for check_run_id={check_run_id}")
        return {
            "check_run_id": run.id,
            "candidate_id": run.candidate_id,
            "final_status": run.final_status,
            "started_at": run.started_at.isoformat() if run.started_at else None,
            "finished_at": run.finished_at.isoformat() if run.finished_at else None,
            "checks": [], # TODO: Map stages if needed
            "violations": [], # TODO: Map violations if needed
        }
# [/DEF:get_check_status:Function]


# [DEF:get_report:Function]
# @PURPOSE: Return persisted compliance report by report_id.
# @PRE: report_id references an existing report.
# @POST: Returns serialized report object.
@router.get("/reports/{report_id}")
async def get_report(report_id: str, repository: CleanReleaseRepository = Depends(get_clean_release_repository)):
    with belief_scope("clean_release.get_report"):
        report = repository.get_report(report_id)
        if report is None:
            raise HTTPException(status_code=404, detail={"message": "Report not found", "code": "REPORT_NOT_FOUND"})

        logger.reflect(f"Returning compliance report report_id={report_id}")
        return report.model_dump()
# [/DEF:get_report:Function]
# [/DEF:backend.src.api.routes.clean_release:Module]