# #region report_service [C:5] [TYPE Module] [SEMANTICS report, task, filter, paginate, aggregate]
# @BRIEF Aggregate, normalize, filter, and paginate task reports for unified list/detail API use cases.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [TaskManager]
# @RELATION DEPENDS_ON -> [TaskReport]
# @RELATION DEPENDS_ON -> [ReportQuery]
# @RELATION DEPENDS_ON -> [ReportCollection]
# @RELATION DEPENDS_ON -> [ReportDetailView]
# @RELATION DEPENDS_ON -> [normalize_task_report]
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
# @INVARIANT  ReportService maintains consistent report structure
# @DATA_CONTRACT  ReportQuery -> ReportRow; session_id -> valid UUID
# @PRE  session is active and valid
# @POST  Returns Report with generated summary
# @SIDE_EFFECT  Read-only database operations; logs report generation

from datetime import UTC, datetime

from ...core.logger import belief_scope
from ...core.task_manager import TaskManager
from ...models.report import (
    ReportCollection,
    ReportDetailView,
    ReportQuery,
    ReportStatus,
    TaskReport,
    TaskType,
)
from ..clean_release.repository import CleanReleaseRepository
from .normalizer import normalize_task_report


# #region ReportsService [C:5] [TYPE Class]
# @BRIEF Service layer for list/detail report retrieval and normalization.
# @PRE  TaskManager dependency is initialized.
# @POST  Provides deterministic list/detail report responses.
# @RELATION DEPENDS_ON -> [TaskManager]
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
# @RELATION CALLS -> [normalize_task_report]
# @SIDE_EFFECT  Reads task history and optional clean-release repository state without mutating source records.
# @DATA_CONTRACT  Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None]
# @INVARIANT  Service methods are read-only over task history source.
#
# @TEST_CONTRACT  ReportsServiceModel ->
# {
#   required_fields: {task_manager: TaskManager},
#   invariants: [
#       "list_reports returns a matching ReportCollection",
#       "get_report_detail returns a valid ReportDetailView or None"
#   ]
# }
# @TEST_FIXTURE  valid_service -> {"task_manager": "MockTaskManager()"}
# @TEST_EDGE  empty_task_list -> returns empty ReportCollection
# @TEST_EDGE  report_not_found -> get_report_detail returns None
# @TEST_INVARIANT  consistent_pagination -> verifies: [valid_service]
class ReportsService:
    # region init [TYPE Function]
    # @PURPOSE: Initialize service with TaskManager dependency.
    # @PRE  task_manager is a live TaskManager instance.
    # @POST  self.task_manager is assigned and ready for read operations.
    # @INVARIANT  Constructor performs no task mutations.
    # @RELATION BINDS_TO ->[ReportsService]
    # @RELATION DEPENDS_ON ->[TaskManager]
    # @RELATION DEPENDS_ON ->[CleanReleaseRepository]
    # @SIDE_EFFECT  Stores collaborator references for later read-only report projections.
    # @DATA_CONTRACT  Input[TaskManager, Optional[CleanReleaseRepository]] -> Output[ReportsService]
    # @PARAM  task_manager (TaskManager) - Task manager providing source task history.
    def __init__(
        self,
        task_manager: TaskManager,
        clean_release_repository: CleanReleaseRepository | None = None,
    ):
        with belief_scope("__init__"):
            self.task_manager = task_manager
            self.clean_release_repository = clean_release_repository

    # endregion init

    # region _load_normalized_reports [TYPE Function]
    # @PURPOSE: Build normalized reports from all available tasks.
    # @PRE  Task manager returns iterable task history records.
    # @POST  Returns normalized report list preserving source cardinality.
    # @INVARIANT  Every returned item is a TaskReport.
    # @RETURN  List[TaskReport] - Reports sorted later by list logic.
    def _load_normalized_reports(self) -> list[TaskReport]:
        with belief_scope("_load_normalized_reports"):
            tasks = self.task_manager.get_all_tasks()
            reports = [normalize_task_report(task) for task in tasks]
            return reports

    # endregion _load_normalized_reports

    # region _to_utc_datetime [TYPE Function]
    # @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.
    # @PRE  value is either datetime or None.
    # @POST  Returns UTC-aware datetime or None.
    # @INVARIANT  Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.
    # @PARAM  value (Optional[EXT:Python:datetime]) - Source datetime value.
    # @RETURN  Optional[EXT:Python:datetime] - UTC-aware datetime or None.
    def _to_utc_datetime(self, value: datetime | None) -> datetime | None:
        with belief_scope("_to_utc_datetime"):
            if value is None:
                return None
            if value.tzinfo is None:
                return value.replace(tzinfo=UTC)
            return value.astimezone(UTC)

    # endregion _to_utc_datetime

    # region _datetime_sort_key [TYPE Function]
    # @PURPOSE: Produce stable numeric sort key for report timestamps.
    # @PRE  report contains updated_at datetime.
    # @POST  Returns float timestamp suitable for deterministic sorting.
    # @INVARIANT  Mixed naive/aware datetimes never raise TypeError.
    # @PARAM  report (TaskReport) - Report item.
    # @RETURN  float - UTC timestamp key.
    def _datetime_sort_key(self, report: TaskReport) -> float:
        with belief_scope("_datetime_sort_key"):
            updated = self._to_utc_datetime(report.updated_at)
            if updated is None:
                return 0.0
            return updated.timestamp()

    # endregion _datetime_sort_key

    # region _matches_query [TYPE Function]
    # @PURPOSE: Apply query filtering to a report.
    # @PRE  report and query are normalized schema instances.
    # @POST  Returns True iff report satisfies all active query filters.
    # @INVARIANT  Filter evaluation is side-effect free.
    # @PARAM  report (TaskReport) - Candidate report.
    # @PARAM  query (ReportQuery) - Applied query.
    # @RETURN  bool - True if report matches all filters.
    def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:
        with belief_scope("_matches_query"):
            if query.task_types and report.task_type not in query.task_types:
                return False
            if query.statuses and report.status not in query.statuses:
                return False
            report_updated_at = self._to_utc_datetime(report.updated_at)
            query_time_from = self._to_utc_datetime(query.time_from)
            query_time_to = self._to_utc_datetime(query.time_to)

            if (
                query_time_from
                and report_updated_at
                and report_updated_at < query_time_from
            ):
                return False
            if (
                query_time_to
                and report_updated_at
                and report_updated_at > query_time_to
            ):
                return False
            if query.search:
                needle = query.search.lower()
                haystack = f"{report.summary} {report.task_type.value} {report.status.value}".lower()
                if needle not in haystack:
                    return False
            return True

    # endregion _matches_query

    # region _sort_reports [TYPE Function]
    # @PURPOSE: Sort reports deterministically according to query settings.
    # @PRE  reports contains only TaskReport items.
    # @POST  Returns reports ordered by selected sort field and order.
    # @INVARIANT  Sorting criteria are deterministic for equal input.
    # @PARAM  reports (List[TaskReport]) - Filtered reports.
    # @PARAM  query (ReportQuery) - Sort config.
    # @RETURN  List[TaskReport] - Sorted reports.
    def _sort_reports(
        self, reports: list[TaskReport], query: ReportQuery
    ) -> list[TaskReport]:
        with belief_scope("_sort_reports"):
            reverse = query.sort_order == "desc"

            if query.sort_by == "status":
                reports.sort(key=lambda item: item.status.value, reverse=reverse)
            elif query.sort_by == "task_type":
                reports.sort(key=lambda item: item.task_type.value, reverse=reverse)
            else:
                reports.sort(key=self._datetime_sort_key, reverse=reverse)

            return reports

    # endregion _sort_reports

    # region list_reports [TYPE Function]
    # @PURPOSE: Return filtered, sorted, paginated report collection.
    # @PRE  query has passed schema validation.
    # @POST  Returns {items,total,page,page_size,has_next,applied_filters}.
    # @PARAM  query (ReportQuery) - List filters and pagination.
    # @RETURN  ReportCollection - Paginated unified reports payload.
    def list_reports(self, query: ReportQuery) -> ReportCollection:
        with belief_scope("list_reports"):
            reports = self._load_normalized_reports()
            filtered = [
                report for report in reports if self._matches_query(report, query)
            ]
            sorted_reports = self._sort_reports(filtered, query)

            total = len(sorted_reports)
            start = (query.page - 1) * query.page_size
            end = start + query.page_size
            items = sorted_reports[start:end]
            has_next = end < total

            return ReportCollection(
                items=items,
                total=total,
                page=query.page,
                page_size=query.page_size,
                has_next=has_next,
                applied_filters=query,
            )

    # endregion list_reports

    # region get_report_detail [TYPE Function]
    # @PURPOSE: Return one normalized report with timeline/diagnostics/next actions.
    # @PRE  report_id exists in normalized report set.
    # @POST  Returns normalized detail envelope with diagnostics and next actions where applicable.
    # @PARAM  report_id (str) - Stable report identifier.
    # @RETURN  Optional[ReportDetailView] - Detailed report or None if not found.
    def get_report_detail(self, report_id: str) -> ReportDetailView | None:
        with belief_scope("get_report_detail"):
            reports = self._load_normalized_reports()
            target = next(
                (report for report in reports if report.report_id == report_id), None
            )
            if not target:
                return None

            timeline = []
            if target.started_at:
                timeline.append(
                    {"event": "started", "at": target.started_at.isoformat()}
                )
            timeline.append({"event": "updated", "at": target.updated_at.isoformat()})

            diagnostics = target.details or {}
            if not diagnostics:
                diagnostics = {"note": "Not provided"}
            if target.error_context:
                diagnostics["error_context"] = target.error_context.model_dump()

            if (
                target.task_type == TaskType.CLEAN_RELEASE
                and self.clean_release_repository is not None
            ):
                run_id = None
                if isinstance(diagnostics, dict):
                    result_payload = diagnostics.get("result")
                    if isinstance(result_payload, dict):
                        run_id = result_payload.get("run_id") or result_payload.get(
                            "check_run_id"
                        )
                if run_id:
                    run = self.clean_release_repository.get_check_run(str(run_id))
                    if run is not None:
                        diagnostics["clean_release_run"] = {
                            "run_id": run.id,
                            "candidate_id": run.candidate_id,
                            "status": run.status,
                            "final_status": run.final_status,
                            "requested_by": run.requested_by,
                        }
                        linked_report = next(
                            (
                                item
                                for item in self.clean_release_repository.reports.values()
                                if item.run_id == run.id
                            ),
                            None,
                        )
                        if linked_report is not None:
                            diagnostics["clean_release_report"] = {
                                "report_id": linked_report.id,
                                "final_status": linked_report.final_status,
                            }

            next_actions = []
            if target.error_context and target.error_context.next_actions:
                next_actions = target.error_context.next_actions
            elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
                next_actions = ["Review diagnostics", "Retry task if applicable"]

            return ReportDetailView(
                report=target,
                timeline=timeline,
                diagnostics=diagnostics,
                next_actions=next_actions,
            )

    # endregion get_report_detail


# #endregion ReportsService

# #endregion report_service
