"""Module for handling reports generated by AutoPkg recipes in cloud-autopkg-runner."""

import plistlib
from pathlib import Path
from typing import Any, TypedDict

from cloud_autopkg_runner import logger
from cloud_autopkg_runner.exceptions import InvalidPlistContents


class RecipeReportFailedItem(TypedDict):
    """Represents a failed item in a recipe report.

    Attributes:
        message: A string containing the error message.
        recipe: A string containing the name of the recipe that failed.
        traceback: A string containing the traceback information.
    """

    message: str
    recipe: str
    traceback: str


class RecipeReportSummaryResults(TypedDict):
    """Represents summary results in a recipe report.

    Attributes:
        header: A list of strings representing the header row of the summary table.
        summary_text: A string containing a summary of the results.
        data_rows: A list of dictionaries, where each dictionary represents a row
            in the summary table.
    """

    header: list[str]
    summary_text: str
    data_rows: list[dict[str, Any]]


class RecipeReportContents(TypedDict):
    """Represents the contents of a recipe report.

    Attributes:
        failures: A list of RecipeReportFailedItem dictionaries, representing
            the items that failed during the recipe run.
        summary_results: A dictionary mapping summary result keys to
            RecipeReportSummaryResults dictionaries.
    """

    failures: list[RecipeReportFailedItem]
    summary_results: dict[str, RecipeReportSummaryResults]


class ConsolidatedReport(TypedDict):
    """Represents a consolidated report of a recipe run.

    This TypedDict combines information from various parts of the
    RecipeReport to provide a summary of the recipe's execution.

    Attributes:
        failed_items: A list of RecipeReportFailedItem dictionaries, representing
            the items that failed during the recipe run.
        downloaded_items: A list of dictionaries representing the files that
            were downloaded during the recipe run.
        pkg_built_items: A list of dictionaries representing the packages
            that were built during the recipe run.
        munki_imported_items: A list of dictionaries representing the items
            that were imported into Munki during the recipe run.
    """

    failed_items: list[RecipeReportFailedItem]
    downloaded_items: list[dict[str, Any]]
    pkg_built_items: list[dict[str, Any]]
    munki_imported_items: list[dict[str, Any]]


class RecipeReport:
    """Represents the report generated after running a recipe.

    This class provides a way to store and access information about the
    results of running an AutoPkg recipe. It encapsulates the path to the
    report file, the parsed contents of the report, and a flag indicating
    whether the report has been successfully parsed.

    Attributes:
        _report_file_path: Path to the report file.
        _contents: RecipeReportContents dictionary containing the parsed contents.
        _parsed: Boolean indicating whether the report has been parsed.
    """

    def __init__(self, report_path: Path) -> None:
        """Initialize a RecipeReport object.

        Sets the path to the report file. A report file is not created.

        Args:
            report_path: The path where the report file is (or will be) located.
                This path will be used to store the recipe's results.
        """
        self._report_file_path: Path = report_path
        self._contents: RecipeReportContents
        self._parsed: bool = False

    @property
    def contents(self) -> RecipeReportContents:
        """Returns the recipe's contents as a dictionary.

        Returns:
            The recipe's contents as a RecipeReportContents TypedDict.
        """
        return self._contents

    @property
    def failures(self) -> list[RecipeReportFailedItem]:
        """Returns a list of any failures encountered during the recipe run.

        Returns:
            A list of RecipeReportFailedItem dictionaries, each representing a
            failure that occurred.
        """
        return self._contents["failures"]

    @property
    def summary_results(self) -> dict[str, RecipeReportSummaryResults]:
        """Returns a dictionary of summary reports related to the run.

        Returns:
            A dictionary mapping summary result keys (strings) to
            RecipeReportSummaryResults dictionaries. Each
            RecipeReportSummaryResults dictionary contains information about
            a particular aspect of the recipe run, such as downloads or imports.
        """
        return self._contents["summary_results"]

    def file_path(self) -> Path:
        """Returns the path to the report file.

        Returns:
            The path to the report file as a Path object.
        """
        return self._report_file_path

    def refresh_contents(self) -> None:
        """Parses the recipe report from the plist file and stores the results.

        Raises:
            InvalidPlistContents: If the plist file is invalid or cannot be parsed.
            FileNotFoundError: If the report file does not exist.
            OSError: If an I/O error occurs while reading the file.
        """
        try:
            self._contents: RecipeReportContents = plistlib.loads(
                self.file_path().read_bytes()
            )
            self._parsed = True
        except plistlib.InvalidFileException as exc:
            self._parsed = False
            raise InvalidPlistContents(self.file_path()) from exc

    def consolidate_report(self) -> ConsolidatedReport:
        """Consolidates a report of the recipe run for final processing.

        Combines information from various sections of the recipe report into a
        single, structured data object. This includes information about failed
        items, downloaded files, built packages, and Munki imports. This function
        requires the report to already be parsed with `refresh_contents`.

        Returns:
            A ConsolidatedReport dictionary containing the files downloaded, packages
            built, any failures, and Munki imports.
        """
        logger.debug(f"Parsing the report at {self.file_path()}...")

        if not self._parsed:
            logger.info("Reading the contents of the report...")
            self.refresh_contents()

        # Pkgs could be built by many different processors, scan for them
        pkg_builds: list[dict[str, Any]] = []
        for key in self.summary_results:
            if "pkg" in key:
                pkg_builds.extend(self._get_items_list(key))

        return ConsolidatedReport(
            failed_items=self.failures,
            downloaded_items=self._get_items_list("url_downloader_summary_result"),
            pkg_built_items=pkg_builds,
            munki_imported_items=self._get_items_list("munki_importer_summary_result"),
        )

    def _get_items_list(self, key: str) -> list[dict[str, Any]]:
        """Get items from a summary.

        Extracts the data_rows list from a summary results dictionary, if the
        specified key exists.

        Args:
            key: The key to the desired summary results dictionary (e.g.,
                "url_downloader_summary_result").

        Returns:
            A list of dictionaries representing the items in the summary, or an
            empty list if the key is not found or the data_rows list is missing.
        """
        if key in self.summary_results:
            return self.summary_results[key]["data_rows"]
        return []
