from __future__ import annotations

import io
import json
import traceback
from dataclasses import dataclass, field
from enum import auto
from functools import cached_property
from pathlib import Path
from typing import Any

from py_app_dev.core.config import BaseConfigJSONMixin
from py_app_dev.core.exceptions import UserNotificationException

from .config import StringableEnum, stringable_enum_field_metadata
from .execution_context import UserRequest


@dataclass
class FeaturesReportRelevantFile:
    #: Provider name
    provider: str
    # Json configuration file generated with all selected features and their values
    json_config_file: Path


class ReportRelevantFileType(StringableEnum):
    """
    Describes the type of files relevant for reports generation.

    These are all Markdown or reStructuredText files to be included in the report.
    The exception are the HTML files, which are copied as-is.
    """

    #: Component documentation files (e.g., reStructuredText or Markdown files)
    DOCS = auto()
    #: Component source files documentation that shall be included in the report
    SOURCES = auto()
    TEST_RESULT = auto()
    LINT_RESULT = auto()
    COVERAGE_RESULT = auto()
    #: These are files that do not fit in any of the other categories but are still relevant for the report
    OTHER = auto()
    #: Generated html files that are relevant for the report and shall be copied as-is
    HTML = auto()


@dataclass
class ReportRelevantHtmlContent(BaseConfigJSONMixin):
    """
    Used to register generated html content relevant for the report.

    This content will be referenced in the `toctree` like `My Report <./path/index.html#http://>`.
    This is useful for documentation tools that generate html output directly.

    (!) I found this approach of referencing html content here:
    https://stackoverflow.com/questions/27979803/external-relative-link-in-sphinx-toctree-directive
    """

    #: Name to be displayed in the toctree
    name: str
    #: Relative path to the generated html index file (may have any name, not necessarily index.html)
    index_html: Path


@dataclass
class ReportRelevantFiles(BaseConfigJSONMixin):
    """Used to register files relevant for reports generation."""

    #: CMake target name that generates the files
    target: UserRequest
    #: Describe the files type (e.g., docs, component sources, etc.)
    file_type: ReportRelevantFileType = field(metadata=stringable_enum_field_metadata(ReportRelevantFileType))
    #: List of relevant files generated by the target
    files_to_be_included: list[Path]
    #: List of all relevant files generated by the target. Some files may be excluded from other files and need to be included manually.
    all_files: list[Path] = field(default_factory=list)
    #: Generated html content that is relevant for the report and shall be referenced as-is
    html_content: ReportRelevantHtmlContent | None = None


@dataclass
class VariantReportData(BaseConfigJSONMixin):
    """Variant configuration to be used by the report generation tools (e.g., Sphinx)."""

    files: list[ReportRelevantFiles]
    build_dir: Path

    @property
    def docs_files(self) -> list[Path]:
        return self.get_files_of_type(ReportRelevantFileType.DOCS)

    @property
    def sources(self) -> list[Path]:
        return self.get_files_of_type(ReportRelevantFileType.SOURCES)

    @property
    def test_results(self) -> list[Path]:
        return self.get_files_of_type(ReportRelevantFileType.TEST_RESULT)

    @property
    def lint_results(self) -> list[Path]:
        return self.get_files_of_type(ReportRelevantFileType.LINT_RESULT)

    @property
    def coverage_results(self) -> list[Path]:
        return self.get_files_of_type(ReportRelevantFileType.COVERAGE_RESULT)

    @property
    def other_files(self) -> list[Path]:
        return self.get_files_of_type(ReportRelevantFileType.OTHER)

    @property
    def html_content(self) -> list[ReportRelevantHtmlContent]:
        return [file.html_content for file in self.files if file.html_content]

    @property
    def all_files(self) -> list[Path]:
        """
        Collection of all files relevant for the component report generation.

        One must make sure all files can be found by the documentation tool.
        """
        return [file for entry in self.files for file in entry.files_to_be_included]

    def get_files_of_type(self, file_type: ReportRelevantFileType) -> list[Path]:
        relevant_files: list[ReportRelevantFiles] = [file for file in self.files if file.file_type == file_type]
        return [file for entry in relevant_files for file in entry.files_to_be_included]


@dataclass
class ComponentReportData(VariantReportData):
    """The component report data content is the same as a variant data content but with extra name."""

    name: str


@dataclass
class ReportData(BaseConfigJSONMixin):
    """Configuration used by the report generation tools (e.g., Sphinx)."""

    variant_name: str
    platform_name: str
    project_dir: Path
    # Updated only for single component reports
    component_name: str | None = None
    components: list[ComponentReportData] = field(default_factory=list)
    variant_data: VariantReportData | None = None
    #: JSON configuration file generated with all selected features and their values. The content of this file will be accessed with the `features` property
    features_json_config: Path | None = None

    @property
    def has_component_scope(self) -> bool:
        return self.component_name is not None

    def collect_all_files(self) -> list[Path]:
        result = []
        for comp in self.components:
            result.extend(comp.all_files)
        if self.variant_data:
            result.extend(self.variant_data.all_files)
        # Make result unique and keep order
        return list(dict.fromkeys(result))

    @cached_property
    def features(self) -> dict[str, Any]:
        """
        Return the features configuration, read once.

        The JSON file is parsed on first access and the result cached for the
        lifetime of this instance. Later file changes are intentionally ignored.
        """
        file_path = self.features_json_config
        if not file_path or not file_path.is_file():
            return {}
        try:
            parsed: dict[str, Any] = json.loads(file_path.read_text())
            # Return a shallow copy to shield internal cache from mutation
            return dict(parsed)
        except Exception as e:  # pragma: no cover - defensive path
            output = io.StringIO()
            traceback.print_exc(file=output)
            raise UserNotificationException(output.getvalue()) from e
