# [DEF:backend.src.models.report:Module]
# @TIER: CRITICAL
# @SEMANTICS: reports, models, pydantic, normalization, pagination
# @PURPOSE: Canonical report schemas for unified task reporting across heterogeneous task types.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.core.task_manager.models
# @INVARIANT: Canonical report fields are always present for every report item.

# [SECTION: IMPORTS]
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional

from pydantic import BaseModel, Field, field_validator, model_validator
# [/SECTION]


# [DEF:TaskType:Class]
# @TIER: CRITICAL
# @INVARIANT: Must contain valid generic task type mappings.
# @SEMANTICS: enum, type, task
# @PURPOSE: Supported normalized task report types.
class TaskType(str, Enum):
    LLM_VERIFICATION = "llm_verification"
    BACKUP = "backup"
    MIGRATION = "migration"
    DOCUMENTATION = "documentation"
    CLEAN_RELEASE = "clean_release"
    UNKNOWN = "unknown"
# [/DEF:TaskType:Class]


# [DEF:ReportStatus:Class]
# @TIER: CRITICAL
# @INVARIANT: TaskStatus enum mapping logic holds.
# @SEMANTICS: enum, status, task
# @PURPOSE: Supported normalized report status values.
class ReportStatus(str, Enum):
    SUCCESS = "success"
    FAILED = "failed"
    IN_PROGRESS = "in_progress"
    PARTIAL = "partial"
# [/DEF:ReportStatus:Class]


# [DEF:ErrorContext:Class]
# @TIER: CRITICAL
# @INVARIANT: The properties accurately describe error state.
# @SEMANTICS: error, context, payload
# @PURPOSE: Error and recovery context for failed/partial reports.
#
# @TEST_CONTRACT: ErrorContextModel ->
# {
#   required_fields: {
#       message: str
#   },
#   optional_fields: {
#       code: str,
#       next_actions: list[str]
#   }
# }
# @TEST_FIXTURE: basic_error -> {"message": "Connection timeout", "code": "ERR_504", "next_actions": ["retry"]}
# @TEST_EDGE: missing_message -> {"code": "ERR_504"}
class ErrorContext(BaseModel):
    code: Optional[str] = None
    message: str
    next_actions: List[str] = Field(default_factory=list)
# [/DEF:ErrorContext:Class]


# [DEF:TaskReport:Class]
# @TIER: CRITICAL
# @INVARIANT: Must represent canonical task record attributes.
# @SEMANTICS: report, model, summary
# @PURPOSE: Canonical normalized report envelope for one task execution.
#
# @TEST_CONTRACT: TaskReportModel ->
# {
#   required_fields: {
#       report_id: str,
#       task_id: str,
#       task_type: TaskType,
#       status: ReportStatus,
#       updated_at: datetime,
#       summary: str
#   },
#   invariants: [
#       "report_id is a non-empty string",
#       "task_id is a non-empty string",
#       "summary is a non-empty string"
#   ]
# }
# @TEST_FIXTURE: valid_task_report ->
# {
#   report_id: "rep-123",
#   task_id: "task-456",
#   task_type: "migration",
#   status: "success",
#   updated_at: "2026-02-26T12:00:00Z",
#   summary: "Migration completed successfully"
# }
# @TEST_EDGE: empty_report_id -> {"report_id": "  ", "task_id": "task-456", "task_type": "migration", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}
# @TEST_EDGE: empty_summary -> {"report_id": "rep-123", "task_id": "task-456", "task_type": "migration", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": ""}
# @TEST_EDGE: invalid_task_type -> {"report_id": "rep-123", "task_id": "task-456", "task_type": "invalid_type", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}
# @TEST_INVARIANT: non_empty_validators -> verifies: [empty_report_id, empty_summary]
class TaskReport(BaseModel):
    report_id: str
    task_id: str
    task_type: TaskType
    status: ReportStatus
    started_at: Optional[datetime] = None
    updated_at: datetime
    summary: str
    details: Optional[Dict[str, Any]] = None
    validation_record: Optional[Dict[str, Any]] = None # Extended for US2
    error_context: Optional[ErrorContext] = None
    source_ref: Optional[Dict[str, Any]] = None

    @field_validator("report_id", "task_id", "summary")
    @classmethod
    def _non_empty_str(cls, value: str) -> str:
        if not isinstance(value, str) or not value.strip():
            raise ValueError("Value must be a non-empty string")
        return value.strip()
# [/DEF:TaskReport:Class]


# [DEF:ReportQuery:Class]
# @TIER: CRITICAL
# @INVARIANT: Time and pagination queries are mutually consistent.
# @SEMANTICS: query, filter, search
# @PURPOSE: Query object for server-side report filtering, sorting, and pagination.
#
# @TEST_CONTRACT: ReportQueryModel ->
# {
#   optional_fields: {
#       page: int, page_size: int, task_types: list[TaskType], statuses: list[ReportStatus],
#       time_from: datetime, time_to: datetime, search: str, sort_by: str, sort_order: str
#   },
#   invariants: [
#       "page >= 1", "1 <= page_size <= 100",
#       "sort_by in {'updated_at', 'status', 'task_type'}",
#       "sort_order in {'asc', 'desc'}",
#       "time_from <= time_to if both exist"
#   ]
# }
# @TEST_FIXTURE: valid_query -> {"page": 1, "page_size":20, "sort_by": "updated_at", "sort_order": "desc"}
# @TEST_EDGE: invalid_page_size_large -> {"page_size": 150}
# @TEST_EDGE: invalid_sort_by -> {"sort_by": "unknown_field"}
# @TEST_EDGE: invalid_time_range -> {"time_from": "2026-02-26T12:00:00Z", "time_to": "2026-02-25T12:00:00Z"}
# @TEST_INVARIANT: attribute_constraints_enforced -> verifies: [invalid_page_size_large, invalid_sort_by, invalid_time_range]
class ReportQuery(BaseModel):
    page: int = Field(default=1, ge=1)
    page_size: int = Field(default=20, ge=1, le=100)
    task_types: List[TaskType] = Field(default_factory=list)
    statuses: List[ReportStatus] = Field(default_factory=list)
    time_from: Optional[datetime] = None
    time_to: Optional[datetime] = None
    search: Optional[str] = Field(default=None, max_length=200)
    sort_by: str = Field(default="updated_at")
    sort_order: str = Field(default="desc")

    @field_validator("sort_by")
    @classmethod
    def _validate_sort_by(cls, value: str) -> str:
        allowed = {"updated_at", "status", "task_type"}
        if value not in allowed:
            raise ValueError(f"sort_by must be one of: {', '.join(sorted(allowed))}")
        return value

    @field_validator("sort_order")
    @classmethod
    def _validate_sort_order(cls, value: str) -> str:
        if value not in {"asc", "desc"}:
            raise ValueError("sort_order must be 'asc' or 'desc'")
        return value

    @model_validator(mode="after")
    def _validate_time_range(self):
        if self.time_from and self.time_to and self.time_from > self.time_to:
            raise ValueError("time_from must be less than or equal to time_to")
        return self
# [/DEF:ReportQuery:Class]


# [DEF:ReportCollection:Class]
# @TIER: CRITICAL
# @INVARIANT: Represents paginated data correctly.
# @SEMANTICS: collection, pagination
# @PURPOSE: Paginated collection of normalized task reports.
#
# @TEST_CONTRACT: ReportCollectionModel ->
# {
#   required_fields: {
#       items: list[TaskReport], total: int, page: int, page_size: int, has_next: bool, applied_filters: ReportQuery
#   },
#   invariants: ["total >= 0", "page >= 1", "page_size >= 1"]
# }
# @TEST_FIXTURE: empty_collection -> {"items": [], "total": 0, "page": 1, "page_size": 20, "has_next": False, "applied_filters": {}}
# @TEST_EDGE: negative_total -> {"items": [], "total": -5, "page": 1, "page_size": 20, "has_next": False, "applied_filters": {}}
class ReportCollection(BaseModel):
    items: List[TaskReport]
    total: int = Field(ge=0)
    page: int = Field(ge=1)
    page_size: int = Field(ge=1)
    has_next: bool
    applied_filters: ReportQuery
# [/DEF:ReportCollection:Class]


# [DEF:ReportDetailView:Class]
# @TIER: CRITICAL
# @INVARIANT: Incorporates a report and logs correctly.
# @SEMANTICS: view, detail, logs
# @PURPOSE: Detailed report representation including diagnostics and recovery actions.
#
# @TEST_CONTRACT: ReportDetailViewModel ->
# {
#   required_fields: {report: TaskReport},
#   optional_fields: {timeline: list[dict], diagnostics: dict, next_actions: list[str]}
# }
# @TEST_FIXTURE: valid_detail -> {"report": {"report_id": "rep-1", "task_id": "task-1", "task_type": "backup", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}}
# @TEST_EDGE: missing_report -> {}
class ReportDetailView(BaseModel):
    report: TaskReport
    timeline: List[Dict[str, Any]] = Field(default_factory=list)
    diagnostics: Optional[Dict[str, Any]] = None
    next_actions: List[str] = Field(default_factory=list)
# [/DEF:ReportDetailView:Class]

# [/DEF:backend.src.models.report:Module]