from pathlib import Path
from typing import Any, Optional

from yanga.cmake.artifacts_locator import BuildArtifact, CMakeArtifactsLocator
from yanga.cmake.cmake_backend import CMakeCommand, CMakeComment, CMakeCustomCommand, CMakeCustomTarget, CMakeElement, CMakePath
from yanga.cmake.coverage import CoverageArtifactsLocator
from yanga.cmake.generator import CMakeGenerator
from yanga.docs.sphinx import SphinxConfig
from yanga.domain.component_analyzer import ComponentAnalyzer
from yanga.domain.execution_context import ExecutionContext, UserRequest, UserRequestScope, UserRequestTarget
from yanga.domain.reports import ReportRelevantFiles, ReportRelevantFileType


class ReportCMakeGenerator(CMakeGenerator):
    def __init__(
        self,
        execution_context: ExecutionContext,
        output_dir: Path,
        config: Optional[dict[str, Any]] = None,
    ) -> None:
        super().__init__(execution_context, output_dir, config)
        self.artifacts_locator = CMakeArtifactsLocator(output_dir, execution_context.create_artifacts_locator())

    def generate(self) -> list[CMakeElement]:
        elements: list[CMakeElement] = []
        elements.append(CMakeComment(f"Generated by {self.__class__.__name__}"))
        elements.extend(self.create_components_cmake_elements())
        elements.extend(self.create_variant_cmake_elements())
        return elements

    def create_variant_cmake_elements(self) -> list[CMakeElement]:
        elements: list[CMakeElement] = []
        variant_report_dir = self.artifacts_locator.cmake_variant_reports_dir

        # Create custom command to generate the targets data documentation
        targets_data_doc_file = self.artifacts_locator.cmake_build_dir.joinpath("targets_data.md")
        targets_data_file = self.artifacts_locator.get_build_artifact(BuildArtifact.TARGETS_DATA)
        targets_data_cmd = CMakeCustomCommand(
            description="Generate variant targets data documentation",
            depends=[targets_data_file],
            outputs=[targets_data_doc_file],
            commands=[
                CMakeCommand(
                    "yanga_cmd",
                    [
                        "targets_doc",
                        "--variant-targets-data-file",
                        targets_data_file,
                        "--output-file",
                        targets_data_doc_file,
                    ],
                ),
            ],
        )
        elements.append(targets_data_cmd)

        # Register the variant targets data documentation file as relevant for the variant report
        variant_user_request = UserRequest(UserRequestScope.VARIANT, target=UserRequestTarget.DOCS)
        self.execution_context.data_registry.insert(
            ReportRelevantFiles(
                target=variant_user_request,
                files_to_be_included=[targets_data_doc_file.to_path()],
                file_type=ReportRelevantFileType.OTHER,
            ),
            variant_user_request.target_name,
        )
        # Check if there are any coverage reports registered for the variant
        coverage_reports = any(
            entry
            for entry in self.execution_context.data_registry.find_data(ReportRelevantFiles)
            if entry.file_type == ReportRelevantFileType.COVERAGE_RESULT and entry.target.scope == UserRequestScope.VARIANT
        )

        # Create variant results target to collect all component and variant results relevant for the report
        results_target_depends: list[str | CMakePath] = [
            self.artifacts_locator.get_build_artifact(BuildArtifact.REPORT_CONFIG),
            *[
                UserRequest(UserRequestScope.COMPONENT, target=UserRequestTarget.RESULTS, component_name=component.name).target_name
                for component in self.execution_context.components
            ],
        ]
        if targets_data_cmd.outputs:
            results_target_depends.extend(targets_data_cmd.outputs)
        if coverage_reports:
            results_target_depends.append(UserRequest(UserRequestScope.VARIANT, target=UserRequestTarget.COVERAGE).target_name)
            # The html coverage reports are generated in the component specific reports directories.
            # We need to create custom commands to copy them to the variant report directory.
            components_with_coverage_results = [
                entry.target.component_name
                for entry in self.execution_context.data_registry.find_data(ReportRelevantFiles)
                if entry.file_type == ReportRelevantFileType.COVERAGE_RESULT and entry.target.scope == UserRequestScope.COMPONENT and entry.target.component_name is not None
            ]
            for component_name in components_with_coverage_results:
                artifacts_locator = CoverageArtifactsLocator.from_cmake_artifacts_locator(self.artifacts_locator)
                # Get the directory where the component coverage html report was generated. This directory will be copied to the variant report directory
                component_coverage_html_dir = artifacts_locator.get_component_coverage_html_dir(component_name)
                coverage_doc_file = artifacts_locator.get_component_build_artifact(component_name, BuildArtifact.COVERAGE_DOC)
                # We need to find where will the coverage doc file html result will be generates in the variant report directory
                # The component coverage doc file is in the build directory and will be included by the sphinx build with its relative path to the project root
                # This means that the coverage.html generated file will have the relative path to the project root too inside the report directory
                # Now we need to determine the coverage.html path based on the coverage.md path and the sphinx output directory
                coverage_doc_file_relative_path = coverage_doc_file.to_path().relative_to(self.artifacts_locator.project_root_dir).parent.as_posix()
                component_variant_coverage_html_dir = variant_report_dir.joinpath(coverage_doc_file_relative_path).joinpath("coverage")
                # Create custom command to copy the component coverage html report to the variant report directory
                copy_coverage_html_cmd = CMakeCustomCommand(
                    description=f"Copy coverage html report for component {component_name} to variant report directory",
                    depends=[],
                    outputs=[component_variant_coverage_html_dir],
                    commands=[
                        CMakeCommand(
                            "${CMAKE_COMMAND}",
                            [
                                "-E",
                                "copy_directory",
                                component_coverage_html_dir,
                                component_variant_coverage_html_dir,
                            ],
                        ),
                    ],
                )
                elements.append(copy_coverage_html_cmd)
                if copy_coverage_html_cmd.outputs:
                    results_target_depends.extend(copy_coverage_html_cmd.outputs)

        results_target = CMakeCustomTarget(
            name=UserRequest(
                UserRequestScope.VARIANT,
                target=UserRequestTarget.RESULTS,
            ).target_name,
            description=f"Run all targets for all component results for {self.execution_context.variant_name}",
            commands=[],
            depends=results_target_depends,
        )
        elements.append(results_target)
        elements.append(
            CMakeCustomTarget(
                name=UserRequest(
                    UserRequestScope.VARIANT,
                    target=UserRequestTarget.REPORT,
                ).target_name,
                description=f"Run sphinx build for variant {self.execution_context.variant_name}",
                commands=[
                    CMakeCommand(
                        "${CMAKE_COMMAND}",
                        [
                            "-E",
                            "make_directory",
                            variant_report_dir,
                        ],
                    ),
                    CMakeCommand(
                        "${CMAKE_COMMAND}",
                        [
                            "-E",
                            "env",
                            f"{SphinxConfig.REPORT_CONFIGURATION_FILE_ENV_NAME}={self.artifacts_locator.get_build_artifact(BuildArtifact.REPORT_CONFIG)}",
                            "--",
                            "sphinx-build",
                            "-E",
                            "-b",
                            "html",
                            self.artifacts_locator.cmake_project_dir,
                            variant_report_dir,
                        ],
                    ),
                ],
                depends=[
                    self.artifacts_locator.get_build_artifact(BuildArtifact.REPORT_CONFIG),
                    results_target.name,
                ],
            )
        )
        return elements

    def create_components_cmake_elements(self) -> list[CMakeElement]:
        elements: list[CMakeElement] = []
        for component in self.execution_context.components:
            component_analyzer = ComponentAnalyzer([component], self.execution_context.create_artifacts_locator())
            component_build_dir = self.artifacts_locator.get_component_build_dir(component.name)
            report_config_output_file = self.artifacts_locator.get_component_build_artifact(component.name, BuildArtifact.REPORT_CONFIG)
            source_files: list[CMakePath] = []
            if not (component.docs and component.docs.exclude_productive_code):
                source_files.extend([CMakePath(source) for source in component_analyzer.collect_sources()])

            # Check if there are any test results registered for the component
            test_results = any(
                entry
                for entry in self.execution_context.data_registry.find_data(ReportRelevantFiles)
                # TODO: when test results are registered, use the TEST_RESULT type.
                #       Currently, we assume that tests were executed if there are coverage results
                if entry.file_type == ReportRelevantFileType.COVERAGE_RESULT and entry.target.component_name == component.name
            )
            if test_results:
                source_files.extend([CMakePath(source) for source in component_analyzer.collect_test_sources()])
            source_files_output_md = [component_build_dir.joinpath(f"{source_file.to_path().name}.md") for source_file in source_files]
            component_docs_target = UserRequest(
                UserRequestScope.COMPONENT,
                target=UserRequestTarget.DOCS,
                component_name=component.name,
            )

            if source_files_output_md:
                elements.append(
                    CMakeCustomTarget(
                        name=component_docs_target.target_name,
                        description=f"Generate sources docs for component {component.name}",
                        commands=[
                            CMakeCommand(
                                "clanguru",
                                [
                                    "docs",
                                    "--source-file",
                                    source_file,
                                    "--output-file",
                                    output_md,
                                    "--compilation-database",
                                    self.artifacts_locator.get_build_artifact(BuildArtifact.COMPILE_COMMANDS),
                                    "--format",
                                    "myst",
                                ],
                            )
                            for source_file, output_md in zip(source_files, source_files_output_md)
                        ],
                        depends=[self.artifacts_locator.get_build_artifact(BuildArtifact.COMPILE_COMMANDS)],
                        byproducts=source_files_output_md,
                    )
                )
                # Register the component sources md files as relevant for the component report
                self.execution_context.data_registry.insert(
                    ReportRelevantFiles(
                        target=component_docs_target,
                        files_to_be_included=[md_output.to_path() for md_output in source_files_output_md],
                        file_type=ReportRelevantFileType.SOURCES,
                    ),
                    component_docs_target.target_name,
                )

            docs_source_files = [CMakePath(source) for source in component_analyzer.collect_docs_sources()]
            if docs_source_files:
                # Register the component documentation files as relevant for the component report
                self.execution_context.data_registry.insert(
                    ReportRelevantFiles(
                        target=component_docs_target,
                        files_to_be_included=[source.to_path() for source in docs_source_files],
                        file_type=ReportRelevantFileType.DOCS,
                    ),
                    component_docs_target.target_name,
                )

            component_results_target = UserRequest(
                UserRequestScope.COMPONENT,
                target=UserRequestTarget.RESULTS,
                component_name=component.name,
            )

            # Collect all component targets that registered files relevant for the report
            result_targets = [entry.target for entry in self.execution_context.data_registry.find_data(ReportRelevantFiles) if entry.target.component_name == component.name]
            # Make list unique and keep order
            result_targets = list(dict.fromkeys(result_targets))

            elements.append(
                CMakeCustomTarget(
                    name=component_results_target.target_name,
                    description=f"Execute targets to get all results for component {component.name}",
                    commands=[],
                    depends=[
                        self.artifacts_locator.get_build_artifact(BuildArtifact.REPORT_CONFIG),
                        *[target.target_name for target in result_targets],
                    ],
                )
            )
            component_report_target = UserRequest(
                UserRequestScope.COMPONENT,
                target=UserRequestTarget.REPORT,
                component_name=component.name,
            )

            component_report_dir = self.artifacts_locator.get_component_reports_dir(component.name)
            elements.append(
                CMakeCustomTarget(
                    name=component_report_target.target_name,
                    description=f"Generate report for component {component.name}",
                    commands=[
                        CMakeCommand(
                            "${CMAKE_COMMAND}",
                            [
                                "-E",
                                "make_directory",
                                component_report_dir,
                            ],
                        ),
                        CMakeCommand(
                            "yanga_cmd",
                            [
                                "report_config",
                                "--component-name",
                                component.name,
                                "--output-file",
                                report_config_output_file,
                                "--variant-report-config",
                                self.artifacts_locator.get_build_artifact(BuildArtifact.REPORT_CONFIG),
                            ],
                        ),
                        CMakeCommand(
                            "${CMAKE_COMMAND}",
                            [
                                "-E",
                                "env",
                                f"REPORT_CONFIGURATION_FILE={report_config_output_file}",
                                "--",
                                "sphinx-build",
                                "-E",
                                "-b",
                                "html",
                                self.artifacts_locator.cmake_project_dir,
                                component_report_dir,
                            ],
                        ),
                    ],
                    depends=[
                        component_results_target.target_name,
                    ],
                )
            )

        return elements
