# [DEF:backend.src.models.clean_release:Module]
# @TIER: CRITICAL
# @SEMANTICS: clean-release, models, lifecycle, policy, manifest, compliance
# @PURPOSE: Define clean release domain entities and validation contracts for enterprise compliance flow.
# @LAYER: Domain
# @RELATION: BINDS_TO -> specs/023-clean-repo-enterprise/data-model.md
# @INVARIANT: Enterprise-clean policy always forbids external sources.

from __future__ import annotations

from datetime import datetime
from enum import Enum
from typing import List, Optional

from pydantic import BaseModel, Field, model_validator


# [DEF:ReleaseCandidateStatus:Class]
# @PURPOSE: Lifecycle states for release candidate.
class ReleaseCandidateStatus(str, Enum):
    DRAFT = "draft"
    PREPARED = "prepared"
    COMPLIANT = "compliant"
    BLOCKED = "blocked"
    RELEASED = "released"
# [/DEF:ReleaseCandidateStatus:Class]


# [DEF:ProfileType:Class]
# @PURPOSE: Supported profile identifiers.
class ProfileType(str, Enum):
    ENTERPRISE_CLEAN = "enterprise-clean"
    DEVELOPMENT = "development"
# [/DEF:ProfileType:Class]


# [DEF:ClassificationType:Class]
# @PURPOSE: Manifest classification outcomes for artifacts.
class ClassificationType(str, Enum):
    REQUIRED_SYSTEM = "required-system"
    ALLOWED = "allowed"
    EXCLUDED_PROHIBITED = "excluded-prohibited"
# [/DEF:ClassificationType:Class]


# [DEF:RegistryStatus:Class]
# @PURPOSE: Registry lifecycle status.
class RegistryStatus(str, Enum):
    ACTIVE = "active"
    INACTIVE = "inactive"
# [/DEF:RegistryStatus:Class]


# [DEF:CheckFinalStatus:Class]
# @PURPOSE: Final status for compliance check run.
class CheckFinalStatus(str, Enum):
    RUNNING = "running"
    COMPLIANT = "compliant"
    BLOCKED = "blocked"
    FAILED = "failed"
# [/DEF:CheckFinalStatus:Class]


# [DEF:ExecutionMode:Class]
# @PURPOSE: Execution channel for compliance checks.
class ExecutionMode(str, Enum):
    TUI = "tui"
    CI = "ci"
# [/DEF:ExecutionMode:Class]


# [DEF:CheckStageName:Class]
# @PURPOSE: Mandatory check stages.
class CheckStageName(str, Enum):
    DATA_PURITY = "data_purity"
    INTERNAL_SOURCES_ONLY = "internal_sources_only"
    NO_EXTERNAL_ENDPOINTS = "no_external_endpoints"
    MANIFEST_CONSISTENCY = "manifest_consistency"
# [/DEF:CheckStageName:Class]


# [DEF:CheckStageStatus:Class]
# @PURPOSE: Stage-level execution status.
class CheckStageStatus(str, Enum):
    PASS = "pass"
    FAIL = "fail"
    SKIPPED = "skipped"
# [/DEF:CheckStageStatus:Class]


# [DEF:ViolationCategory:Class]
# @PURPOSE: Normalized compliance violation categories.
class ViolationCategory(str, Enum):
    DATA_PURITY = "data-purity"
    EXTERNAL_SOURCE = "external-source"
    MANIFEST_INTEGRITY = "manifest-integrity"
    POLICY_CONFLICT = "policy-conflict"
    OPERATIONAL_RISK = "operational-risk"
# [/DEF:ViolationCategory:Class]


# [DEF:ViolationSeverity:Class]
# @PURPOSE: Severity levels for violation triage.
class ViolationSeverity(str, Enum):
    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"
# [/DEF:ViolationSeverity:Class]


# [DEF:ReleaseCandidate:Class]
# @PURPOSE: Candidate metadata for clean-release workflow.
# @PRE: candidate_id, source_snapshot_ref are non-empty.
# @POST: Model instance is valid for lifecycle transitions.
class ReleaseCandidate(BaseModel):
    candidate_id: str
    version: str
    profile: ProfileType
    created_at: datetime
    created_by: str
    source_snapshot_ref: str
    status: ReleaseCandidateStatus = ReleaseCandidateStatus.DRAFT

    @model_validator(mode="after")
    def _validate_non_empty(self):
        if not self.candidate_id.strip():
            raise ValueError("candidate_id must be non-empty")
        if not self.source_snapshot_ref.strip():
            raise ValueError("source_snapshot_ref must be non-empty")
        return self
# [/DEF:ReleaseCandidate:Class]


# [DEF:CleanProfilePolicy:Class]
# @PURPOSE: Policy contract for artifact/source decisions.
class CleanProfilePolicy(BaseModel):
    policy_id: str
    policy_version: str
    active: bool
    prohibited_artifact_categories: List[str] = Field(default_factory=list)
    required_system_categories: List[str] = Field(default_factory=list)
    external_source_forbidden: bool = True
    internal_source_registry_ref: str
    effective_from: datetime
    effective_to: Optional[datetime] = None
    profile: ProfileType = ProfileType.ENTERPRISE_CLEAN

    @model_validator(mode="after")
    def _validate_policy(self):
        if self.profile == ProfileType.ENTERPRISE_CLEAN:
            if not self.external_source_forbidden:
                raise ValueError("enterprise-clean policy requires external_source_forbidden=true")
            if not self.prohibited_artifact_categories:
                raise ValueError("enterprise-clean policy requires prohibited_artifact_categories")
        if not self.internal_source_registry_ref.strip():
            raise ValueError("internal_source_registry_ref must be non-empty")
        return self
# [/DEF:CleanProfilePolicy:Class]


# [DEF:ResourceSourceEntry:Class]
# @PURPOSE: One internal source definition.
class ResourceSourceEntry(BaseModel):
    source_id: str
    host: str
    protocol: str
    purpose: str
    allowed_paths: List[str] = Field(default_factory=list)
    enabled: bool = True
# [/DEF:ResourceSourceEntry:Class]


# [DEF:ResourceSourceRegistry:Class]
# @PURPOSE: Allowlist of internal sources.
class ResourceSourceRegistry(BaseModel):
    registry_id: str
    name: str
    entries: List[ResourceSourceEntry]
    updated_at: datetime
    updated_by: str
    status: RegistryStatus = RegistryStatus.ACTIVE

    @model_validator(mode="after")
    def _validate_registry(self):
        if not self.entries:
            raise ValueError("registry entries cannot be empty")
        if self.status == RegistryStatus.ACTIVE and not any(e.enabled for e in self.entries):
            raise ValueError("active registry must include at least one enabled entry")
        return self
# [/DEF:ResourceSourceRegistry:Class]


# [DEF:ManifestItem:Class]
# @PURPOSE: One artifact entry in manifest.
class ManifestItem(BaseModel):
    path: str
    category: str
    classification: ClassificationType
    reason: str
    checksum: Optional[str] = None
# [/DEF:ManifestItem:Class]


# [DEF:ManifestSummary:Class]
# @PURPOSE: Aggregate counters for manifest decisions.
class ManifestSummary(BaseModel):
    included_count: int = Field(ge=0)
    excluded_count: int = Field(ge=0)
    prohibited_detected_count: int = Field(ge=0)
# [/DEF:ManifestSummary:Class]


# [DEF:DistributionManifest:Class]
# @PURPOSE: Deterministic release composition for audit.
class DistributionManifest(BaseModel):
    manifest_id: str
    candidate_id: str
    policy_id: str
    generated_at: datetime
    generated_by: str
    items: List[ManifestItem]
    summary: ManifestSummary
    deterministic_hash: str

    @model_validator(mode="after")
    def _validate_counts(self):
        if self.summary.included_count + self.summary.excluded_count != len(self.items):
            raise ValueError("manifest summary counts must match items size")
        return self
# [/DEF:DistributionManifest:Class]


# [DEF:CheckStageResult:Class]
# @PURPOSE: Per-stage compliance result.
class CheckStageResult(BaseModel):
    stage: CheckStageName
    status: CheckStageStatus
    details: Optional[str] = None
    duration_ms: Optional[int] = Field(default=None, ge=0)
# [/DEF:CheckStageResult:Class]


# [DEF:ComplianceCheckRun:Class]
# @PURPOSE: One execution run of compliance pipeline.
class ComplianceCheckRun(BaseModel):
    check_run_id: str
    candidate_id: str
    policy_id: str
    started_at: datetime
    finished_at: Optional[datetime] = None
    final_status: CheckFinalStatus = CheckFinalStatus.RUNNING
    triggered_by: str
    execution_mode: ExecutionMode
    checks: List[CheckStageResult] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_terminal_integrity(self):
        if self.final_status == CheckFinalStatus.COMPLIANT:
            mandatory = {c.stage: c.status for c in self.checks}
            required = {
                CheckStageName.DATA_PURITY,
                CheckStageName.INTERNAL_SOURCES_ONLY,
                CheckStageName.NO_EXTERNAL_ENDPOINTS,
                CheckStageName.MANIFEST_CONSISTENCY,
            }
            if not required.issubset(mandatory.keys()):
                raise ValueError("compliant run requires all mandatory stages")
            if any(mandatory[s] != CheckStageStatus.PASS for s in required):
                raise ValueError("compliant run requires PASS on all mandatory stages")
        return self
# [/DEF:ComplianceCheckRun:Class]


# [DEF:ComplianceViolation:Class]
# @PURPOSE: Normalized violation row for triage and blocking decisions.
class ComplianceViolation(BaseModel):
    violation_id: str
    check_run_id: str
    category: ViolationCategory
    severity: ViolationSeverity
    location: str
    evidence: Optional[str] = None
    remediation: str
    blocked_release: bool
    detected_at: datetime

    @model_validator(mode="after")
    def _validate_violation(self):
        if self.category == ViolationCategory.EXTERNAL_SOURCE and not self.blocked_release:
            raise ValueError("external-source violation must block release")
        if self.severity == ViolationSeverity.CRITICAL and not self.remediation.strip():
            raise ValueError("critical violation requires remediation")
        return self
# [/DEF:ComplianceViolation:Class]


# [DEF:ComplianceReport:Class]
# @PURPOSE: Final report payload for operator and audit systems.
class ComplianceReport(BaseModel):
    report_id: str
    check_run_id: str
    candidate_id: str
    generated_at: datetime
    final_status: CheckFinalStatus
    operator_summary: str
    structured_payload_ref: str
    violations_count: int = Field(ge=0)
    blocking_violations_count: int = Field(ge=0)

    @model_validator(mode="after")
    def _validate_report_counts(self):
        if self.blocking_violations_count > self.violations_count:
            raise ValueError("blocking_violations_count cannot exceed violations_count")
        if self.final_status == CheckFinalStatus.BLOCKED and self.blocking_violations_count <= 0:
            raise ValueError("blocked report requires blocking violations")
        return self
# [/DEF:ComplianceReport:Class]
# [/DEF:backend.src.models.clean_release:Module]