from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import Any, Optional

from mashumaro import DataClassDictMixin

from yanga.domain.component_analyzer import ComponentAnalyzer
from yanga.domain.execution_context import (
    ExecutionContext,
    UserRequest,
    UserRequestScope,
    UserRequestTarget,
    UserVariantRequest,
)

from .cmake_backend import (
    CMakeAddExecutable,
    CMakeAddLibrary,
    CMakeComment,
    CMakeCustomTarget,
    CMakeElement,
    CMakeIncludeDirectories,
    CMakePath,
    CMakeTargetIncludeDirectories,
    IncludeScope,
)
from .generator import CMakeGenerator


@dataclass
class CreateExecutableConfig(DataClassDictMixin):
    #: If this is enabled, all includes are defined globally and not component specific
    use_global_includes: bool = True


class CreateExecutableCMakeGenerator(CMakeGenerator):
    """Generates CMake elements to build an executable for a variant."""

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

    @cached_property
    def config_obj(self) -> CreateExecutableConfig:
        """Lazily creates and caches a GTestCMakeGeneratorConfig instance."""
        return CreateExecutableConfig.from_dict(self.config) if self.config else CreateExecutableConfig()

    @property
    def variant_name(self) -> Optional[str]:
        return self.execution_context.variant_name

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

    def create_variant_cmake_elements(self) -> list[CMakeElement]:
        elements: list[CMakeElement] = []
        if self.config_obj.use_global_includes:
            elements.append(self.get_include_directories())
        else:
            elements.append(CMakeComment("Use global includes for all components disabled."))
        # TODO: I do not like that I have to know here that the components are object libraries
        component_library_targets = [CMakeAddLibrary(component.name).target_name for component in self.execution_context.components]
        variant_executable = CMakeAddExecutable(
            "${PROJECT_NAME}",
            sources=[],
            libraries=component_library_targets,
        )

        elements.append(variant_executable)
        elements.append(
            CMakeCustomTarget(
                name=UserVariantRequest(self.variant_name, UserRequestTarget.BUILD).target_name,
                description=f"Build variant {self.variant_name}",
                commands=[],
                depends=[variant_executable.name],
            )
        )
        elements.append(
            CMakeCustomTarget(
                name=UserVariantRequest(self.variant_name, UserRequestTarget.COMPILE).target_name,
                description=f"Compile variant {self.variant_name}",
                commands=[],
                depends=component_library_targets,
            )
        )

        return elements

    def get_include_directories(self) -> CMakeIncludeDirectories:
        collector = ComponentAnalyzer(
            self.execution_context.components,
            self.execution_context.create_artifacts_locator(),
        )
        include_dirs = collector.collect_include_directories() + self.execution_context.include_directories
        return CMakeIncludeDirectories([CMakePath(path) for path in include_dirs])

    def get_component_include_directories(self, component_analyzer: ComponentAnalyzer) -> list[CMakePath]:
        """Get include directories specific to this component."""
        include_dirs = component_analyzer.collect_include_directories() + self.execution_context.include_directories
        return [CMakePath(path) for path in include_dirs]

    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())
            sources = component_analyzer.collect_sources()
            component_library = CMakeAddLibrary(component.name, sources)
            elements.append(component_library)

            # Add component-specific include directories when global includes are disabled
            if not self.config_obj.use_global_includes:
                include_dirs: list[CMakePath] = self.get_component_include_directories(component_analyzer)
                if include_dirs:
                    # Determine include scope: use PRIVATE for libraries with sources, INTERFACE for header-only
                    scope = IncludeScope.INTERFACE if not sources else IncludeScope.PRIVATE
                    target_includes = CMakeTargetIncludeDirectories(component_library.target_name, include_dirs, scope)
                    elements.append(target_includes)

            elements.append(
                CMakeCustomTarget(
                    UserRequest(
                        UserRequestScope.COMPONENT,
                        self.variant_name,
                        component.name,
                        UserRequestTarget.COMPILE,
                    ).target_name,
                    f"Compile component {component.name}",
                    [],
                    [component_library.target_name],
                )
            )
            elements.append(
                CMakeCustomTarget(
                    UserRequest(
                        UserRequestScope.COMPONENT,
                        self.variant_name,
                        component.name,
                        UserRequestTarget.BUILD,
                    ).target_name,
                    f"Compile component {component.name}",
                    [],
                    [component_library.target_name],
                )
            )
        return elements
