import io
import json
import traceback
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional

import yaml
from mashumaro.config import BaseConfig
from mashumaro.mixins.json import DataClassJSONMixin
from py_app_dev.core.exceptions import UserNotificationException
from py_app_dev.core.logging import logger
from pypeline.domain.pipeline import PipelineStep

from yanga.domain.config import WestManifest, WestManifestFile
from yanga.domain.execution_context import ExecutionContext


@dataclass
class WestInstallExecutionInfo(DataClassJSONMixin):
    dependency_dirs: list[Path] = field(default_factory=list)

    class Config(BaseConfig):
        # Make sure to omit None values during serialization
        omit_none = True

    @classmethod
    def from_json_file(cls, file_path: Path) -> "WestInstallExecutionInfo":
        try:
            result = cls.from_dict(json.loads(file_path.read_text()))
        except Exception as e:
            output = io.StringIO()
            traceback.print_exc(file=output)
            raise UserNotificationException(output.getvalue()) from e
        return result

    def to_json_string(self) -> str:
        return json.dumps(self.to_dict(), indent=2)

    def to_json_file(self, file_path: Path) -> None:
        file_path.parent.mkdir(parents=True, exist_ok=True)
        file_path.write_text(self.to_json_string())


class WestInstall(PipelineStep[ExecutionContext]):
    def __init__(self, execution_context: ExecutionContext, group_name: str, config: Optional[dict[str, Any]] = None) -> None:
        super().__init__(execution_context, group_name, config)
        self.logger = logger.bind()
        self.artifacts_locator = execution_context.create_artifacts_locator()
        self._collected_dependencies: Optional[WestManifest] = None
        self.execution_info = WestInstallExecutionInfo()
        # One needs to keep track of the installed external dependencies to get the required paths
        # even if the step does not need to run.
        self.execution_info_file = self.output_dir.joinpath("west_install_exec_info.json")

    @property
    def output_dir(self) -> Path:
        if self.execution_context.variant:
            return self.execution_context.create_artifacts_locator().variant_build_dir
        else:
            return self.artifacts_locator.build_dir

    def get_name(self) -> str:
        return self.__class__.__name__

    @property
    def dependency_dirs(self) -> list[Path]:
        return self.execution_info.dependency_dirs

    @property
    def west_manifest_file(self) -> Path:
        """Generated west.yaml file path in variant-specific directory."""
        return self.artifacts_locator.variant_build_dir.joinpath("west.yaml")

    @property
    def global_west_manifest_file(self) -> Path:
        """Global west.yaml file path (if exists)."""
        return self.project_root_dir.joinpath("west.yaml")

    def _collect_dependencies(self) -> WestManifest:
        """Collect West dependencies from global, platform, and variant configurations."""
        collected_manifest = WestManifest()

        # Start with global west.yaml if it exists
        if self.global_west_manifest_file.exists():
            try:
                global_manifest_file = WestManifestFile.from_file(self.global_west_manifest_file)
                global_manifest = global_manifest_file.manifest

                # Merge remotes
                for remote in global_manifest.remotes:
                    if remote not in collected_manifest.remotes:
                        collected_manifest.remotes.append(remote)

                # Merge projects (dependencies)
                for project in global_manifest.projects:
                    if project not in collected_manifest.projects:
                        collected_manifest.projects.append(project)

            except Exception as e:
                self.logger.warning(f"Failed to parse global west.yaml: {e}")

        # Add platform dependencies
        if self.execution_context.platform and self.execution_context.platform.west_manifest:
            platform_manifest = self.execution_context.platform.west_manifest

            # Merge remotes
            for remote in platform_manifest.remotes:
                if remote not in collected_manifest.remotes:
                    collected_manifest.remotes.append(remote)

            # Merge projects (dependencies)
            for project in platform_manifest.projects:
                if project not in collected_manifest.projects:
                    collected_manifest.projects.append(project)

        # Add variant dependencies
        if self.execution_context.variant and self.execution_context.variant.west_manifest:
            variant_manifest = self.execution_context.variant.west_manifest

            # Merge remotes
            for remote in variant_manifest.remotes:
                if remote not in collected_manifest.remotes:
                    collected_manifest.remotes.append(remote)

            # Merge projects (dependencies)
            for project in variant_manifest.projects:
                if project not in collected_manifest.projects:
                    collected_manifest.projects.append(project)

        return collected_manifest

    def _generate_west_manifest(self, manifest: WestManifest) -> None:
        """Generate west.yaml file from collected dependencies."""
        if not manifest.remotes and not manifest.projects:
            self.logger.info("No West dependencies found. Skipping west.yaml generation.")
            return

        west_config = {"manifest": {"remotes": [remote.to_dict() for remote in manifest.remotes], "projects": [project.to_dict() for project in manifest.projects]}}

        # Convert url_base back to url-base for west compatibility
        for remote in west_config["manifest"]["remotes"]:
            if "url_base" in remote:
                remote["url-base"] = remote.pop("url_base")

        # Ensure build directory exists
        self.west_manifest_file.parent.mkdir(parents=True, exist_ok=True)

        with open(self.west_manifest_file, "w") as f:
            yaml.dump(west_config, f, default_flow_style=False)

        self.logger.info(f"Generated west.yaml with {len(manifest.projects)} dependencies")

    def run(self) -> int:
        self.logger.debug(f"Run {self.get_name()} step. Output dir: {self.output_dir}")

        try:
            # Collect dependencies from all sources
            self._collected_dependencies = self._collect_dependencies()

            # Generate the combined west.yaml file
            self._generate_west_manifest(self._collected_dependencies)

            # Only proceed with west commands if we have dependencies
            if not self._collected_dependencies.projects:
                self.logger.info("No West dependencies to install.")
                return 0

            # Initialize west workspace with shared external directory
            # The west.yaml is variant-specific, but dependencies go to external dependencies dir
            self.execution_context.create_process_executor(
                [
                    "west",
                    "init",
                    "-l",
                    "--mf",
                    self.west_manifest_file.as_posix(),
                    # For some reason west needs a dummy project to be specified when using -l. No "do not care" directory will ever be created.
                    self.artifacts_locator.external_dependencies_dir.joinpath("do_not_care").as_posix(),
                ],
                cwd=self.project_root_dir,
            ).execute()

            # Update dependencies in the shared external directory
            self.execution_context.create_process_executor(
                ["west", "update"],
                cwd=self.artifacts_locator.external_dependencies_dir,
            ).execute()

            # Track the created dependency directories
            self._track_dependency_directories()

            # Save execution info for dependency tracking
            self.execution_info.to_json_file(self.execution_info_file)

        except Exception as e:
            raise UserNotificationException(f"Failed to initialize and update with west: {e}") from e

        return 0

    def _track_dependency_directories(self) -> None:
        """Track all directories created by west for dependencies."""
        if not self._collected_dependencies:
            return

        external_dir = self.artifacts_locator.external_dependencies_dir

        # Add the main external dependencies directory
        if external_dir.exists():
            self.execution_info.dependency_dirs.append(external_dir)

        # Add individual dependency directories based on their configured paths
        for project in self._collected_dependencies.projects:
            # Extract the actual directory name from the path
            # If path is "external/clanguru", we want just "clanguru"
            # because west creates directories relative to the external_dir
            path_parts = Path(project.path).parts
            if path_parts and path_parts[0] == "external" and len(path_parts) > 1:
                # Use the directory name after "external/"
                dependency_dir = external_dir / path_parts[1]
            else:
                # Use the full path as-is
                dependency_dir = external_dir / project.path

            if dependency_dir.exists():
                self.execution_info.dependency_dirs.append(dependency_dir)
                self.logger.debug(f"Tracked dependency directory: {dependency_dir}")

        # Make the list unique while preserving order
        self.execution_info.dependency_dirs = list(dict.fromkeys(self.execution_info.dependency_dirs))

    def get_inputs(self) -> list[Path]:
        inputs = []

        # Add global west.yaml if it exists
        if self.global_west_manifest_file.exists():
            inputs.append(self.global_west_manifest_file)

        # Add user config files (platform and variant configs are in there)
        inputs.extend(self.execution_context.user_config_files)

        return inputs

    def get_outputs(self) -> list[Path]:
        outputs = [self.west_manifest_file, self.execution_info_file]

        # Add tracked dependency directories if available (after run() has been called)
        if self.execution_info.dependency_dirs:
            outputs.extend(self.execution_info.dependency_dirs)
        # Fallback: add main external directory if dependencies exist but haven't been tracked yet
        elif self._collected_dependencies and self._collected_dependencies.projects:
            outputs.append(self.artifacts_locator.external_dependencies_dir)

        return outputs

    def update_execution_context(self) -> None:
        if self.execution_info_file.exists():
            execution_info = WestInstallExecutionInfo.from_json_file(self.execution_info_file)
            # Make the dependency directories available to subsequent steps
            if execution_info.dependency_dirs:
                # Make the list unique and keep the order
                unique_paths = list(dict.fromkeys(execution_info.dependency_dirs))
                self.execution_context.add_install_dirs(unique_paths)
