#!/usr/bin/env python3
"""
Build context dataclass to hold all build state

REFACTOR NOTE: This module is being refactored to use sub-components (PathConfig,
BuildConfig, ArtifactRegistry, EnvConfig) to avoid god object anti-pattern.
The old interface is maintained for backward compatibility during the migration.
"""

import time
from pathlib import Path
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from .utils import (
    get_platform,
    get_platform_arch,
    get_executable_extension,
    join_paths,
    IS_WINDOWS,
    IS_MACOS,
)
from .env import EnvConfig
from .paths import get_package_root


# =============================================================================
# Sub-Components - New modular structure
# =============================================================================


class ArtifactRegistry:
    """
    Simple artifact tracking registry

    Tracks artifacts produced during the build process. Each artifact has a unique
    name (string) and a path (Path object). If you need to track multiple paths
    for the same logical artifact, use different names (e.g., "signed_app_arm64",
    "signed_app_x64").

    Example:
        artifacts = ArtifactRegistry()
        artifacts.add("built_app", Path("/path/to/BrowserOS.app"))
        app_path = artifacts.get("built_app")
        if artifacts.has("signed_app"):
            ...
    """

    def __init__(self):
        self._artifacts: Dict[str, Path] = {}

    def add(self, name: str, path: Path) -> None:
        """
        Register an artifact

        Args:
            name: Unique artifact name (e.g., "built_app", "signed_dmg")
            path: Path to the artifact

        Note:
            If an artifact with the same name already exists, it will be overwritten.
        """
        self._artifacts[name] = path

    def get(self, name: str) -> Path:
        """
        Get artifact path by name

        Args:
            name: Artifact name

        Returns:
            Path to the artifact

        Raises:
            KeyError: If artifact not found
        """
        return self._artifacts[name]

    def has(self, name: str) -> bool:
        """
        Check if artifact exists

        Args:
            name: Artifact name

        Returns:
            True if artifact exists, False otherwise
        """
        return name in self._artifacts

    def all(self) -> Dict[str, Path]:
        """Get all artifacts as a dictionary"""
        return self._artifacts.copy()


class PathConfig:
    """
    Path-related configuration

    Centralizes all path construction and validation logic. This prevents the
    BuildContext from becoming a god object with dozens of path-related methods.
    """

    def __init__(
        self,
        root_dir: Path,
        chromium_src: Optional[Path] = None,
        gn_flags_file: Optional[Path] = None,
    ):
        self.root_dir = root_dir
        self._chromium_src = chromium_src or Path()
        self._out_dir = "out/Default"
        self.gn_flags_file = gn_flags_file

    @property
    def chromium_src(self) -> Path:
        """Chromium source directory"""
        return self._chromium_src

    @chromium_src.setter
    def chromium_src(self, value: Path):
        """Set chromium source directory"""
        self._chromium_src = value

    @property
    def out_dir(self) -> str:
        """Output directory (relative to chromium_src)"""
        return self._out_dir

    @out_dir.setter
    def out_dir(self, value: str):
        """Set output directory"""
        self._out_dir = value


class BuildConfig:
    """
    Build-specific configuration

    Contains all build-related settings like architecture, build type, versions, etc.
    """

    def __init__(
        self,
        architecture: Optional[str] = None,
        build_type: str = "debug",
    ):
        self.architecture = architecture or get_platform_arch()
        self.build_type = build_type
        self.chromium_version = ""
        self.browseros_version = ""
        self.browseros_chromium_version = ""

        # App names - will be set based on platform
        self.CHROMIUM_APP_NAME = ""
        self.BROWSEROS_APP_NAME = ""
        self.BROWSEROS_APP_BASE_NAME = "BrowserOS"

        # Third party versions
        self.SPARKLE_VERSION = "2.7.0"

        # Set platform-specific app names
        self._set_app_names()

    def _set_app_names(self):
        """Set platform-specific application names"""
        if IS_WINDOWS():
            self.CHROMIUM_APP_NAME = f"chrome{get_executable_extension()}"
            self.BROWSEROS_APP_NAME = (
                f"{self.BROWSEROS_APP_BASE_NAME}{get_executable_extension()}"
            )
        elif IS_MACOS():
            self.CHROMIUM_APP_NAME = "Chromium.app"
            self.BROWSEROS_APP_NAME = f"{self.BROWSEROS_APP_BASE_NAME}.app"
        else:
            self.CHROMIUM_APP_NAME = "chrome"
            self.BROWSEROS_APP_NAME = self.BROWSEROS_APP_BASE_NAME.lower()


@dataclass
class Context:
    """
    Context Object pattern - ONE place for all build state
    """

    root_dir: Path = field(default_factory=get_package_root)
    chromium_src: Path = Path()
    out_dir: str = "out/Default"
    architecture: str = ""  # Will be set in __post_init__
    build_type: str = "debug"
    chromium_version: str = ""
    browseros_build_offset: str = ""
    browseros_chromium_version: str = ""
    semantic_version: str = ""  # e.g., "0.31.0" from resources/BROWSEROS_VERSION
    release_version: str = (
        ""  # Explicit version for release operations (overrides semantic_version)
    )
    github_repo: str = ""  # GitHub repo for release operations (owner/repo)
    start_time: float = 0.0

    # App names - will be set based on platform
    CHROMIUM_APP_NAME: str = ""
    BROWSEROS_APP_NAME: str = ""
    BROWSEROS_APP_BASE_NAME: str = "BrowserOS"  # Base name without extension

    # Third party
    SPARKLE_VERSION: str = "2.7.0"

    # Legacy artifacts dict - kept for backward compatibility
    # New code should use ctx.artifacts (ArtifactRegistry) instead
    artifacts: Dict[str, List[Path]] = field(default_factory=dict)

    # Fixed app path - used by UniversalBuildModule to prevent auto-detection
    # When set, get_app_path() returns this directly instead of auto-detecting
    _fixed_app_path: Optional[Path] = None

    # New sub-components (initialized in __post_init__)
    paths: PathConfig = field(init=False)
    build: BuildConfig = field(init=False)
    artifact_registry: ArtifactRegistry = field(init=False)  # New artifact system
    env: EnvConfig = field(init=False)

    def __post_init__(self):
        """Load version files and set platform/architecture-specific configurations"""
        # Initialize new sub-components
        self.paths = PathConfig(self.root_dir, self.chromium_src)
        self.build = BuildConfig(self.architecture, self.build_type)
        self.artifact_registry = ArtifactRegistry()  # New artifact system
        self.env = EnvConfig()

        # Set default gn_flags_file if not provided
        if not self.paths.gn_flags_file:
            self.paths.gn_flags_file = self.get_gn_flags_file()

        # Set platform-specific defaults
        if not self.architecture:
            self.architecture = get_platform_arch()
            self.build.architecture = self.architecture

        # Set platform-specific app names
        if IS_WINDOWS():
            self.CHROMIUM_APP_NAME = f"chrome{get_executable_extension()}"
            self.BROWSEROS_APP_NAME = (
                f"{self.BROWSEROS_APP_BASE_NAME}{get_executable_extension()}"
            )
        elif IS_MACOS():
            self.CHROMIUM_APP_NAME = "Chromium.app"
            self.BROWSEROS_APP_NAME = f"{self.BROWSEROS_APP_BASE_NAME}.app"
        else:
            self.CHROMIUM_APP_NAME = "chrome"
            self.BROWSEROS_APP_NAME = self.BROWSEROS_APP_BASE_NAME.lower()

        # Sync with BuildConfig
        self.build.CHROMIUM_APP_NAME = self.CHROMIUM_APP_NAME
        self.build.BROWSEROS_APP_NAME = self.BROWSEROS_APP_NAME

        # Set architecture-specific output directory with platform separator
        if IS_WINDOWS():
            self.out_dir = f"out\\Default_{self.architecture}"
        else:
            self.out_dir = f"out/Default_{self.architecture}"

        # Sync with PathConfig
        self.paths.out_dir = self.out_dir

        # Load version information using static methods
        if not self.chromium_version:
            self.chromium_version, version_dict = self._load_chromium_version(
                self.root_dir
            )
        else:
            # If chromium_version was provided, we still need to parse it for version_dict
            version_dict = {}

        if not self.browseros_build_offset:
            self.browseros_build_offset = self._load_browseros_build_offset(
                self.root_dir
            )

        # Load semantic version from resources/BROWSEROS_VERSION
        if not self.semantic_version:
            self.semantic_version = self._load_semantic_version(self.root_dir)

        # Set nxtscape_chromium_version as chromium version with BUILD + nxtscape_version
        if self.chromium_version and self.browseros_build_offset and version_dict:
            # Calculate new BUILD number by adding nxtscape_version to original BUILD
            new_build = int(version_dict["BUILD"]) + int(self.browseros_build_offset)
            self.browseros_chromium_version = f"{version_dict['MAJOR']}.{version_dict['MINOR']}.{new_build}.{version_dict['PATCH']}"

        # Sync versions with BuildConfig
        self.build.chromium_version = self.chromium_version
        self.build.browseros_version = self.browseros_build_offset
        self.build.browseros_chromium_version = self.browseros_chromium_version

        # Sync chromium_src with PathConfig (validation done by resolver)
        self.paths.chromium_src = self.chromium_src

        self.start_time = time.time()

    # === Initialization ===

    @classmethod
    def init_context(cls, config: Dict) -> "Context":
        """
        Initialize context from config
        Replaces __post_init__ logic for better testability

        Note: root_dir is always computed from package location, never from config.
        """
        chromium_src = (
            Path(config.get("chromium_src", ""))
            if config.get("chromium_src")
            else Path()
        )

        # Get architecture or use platform default
        arch = config.get("architecture") or get_platform_arch()

        # Create instance - root_dir uses default_factory (get_package_root)
        ctx = cls(
            chromium_src=chromium_src,
            architecture=arch,
            build_type=config.get("build_type", "debug"),
        )

        return ctx

    @staticmethod
    def _load_chromium_version(root_dir: Path):
        """
        Load chromium version from CHROMIUM_VERSION file
        Returns: (version_string, version_dict)
        """
        version_dict = {}
        version_file = join_paths(root_dir, "CHROMIUM_VERSION")

        if version_file.exists():
            # Parse VERSION file format: MAJOR=137\nMINOR=0\nBUILD=7151\nPATCH=69
            for line in version_file.read_text().strip().split("\n"):
                key, value = line.split("=")
                version_dict[key] = value

            # Construct chromium_version as MAJOR.MINOR.BUILD.PATCH
            chromium_version = f"{version_dict['MAJOR']}.{version_dict['MINOR']}.{version_dict['BUILD']}.{version_dict['PATCH']}"
            return chromium_version, version_dict

        return "", version_dict

    @staticmethod
    def _load_browseros_build_offset(root_dir: Path) -> str:
        """Load browseros build offset from config/BROWSEROS_BUILD_OFFSET"""
        version_file = join_paths(root_dir, "build", "config", "BROWSEROS_BUILD_OFFSET")
        if version_file.exists():
            return version_file.read_text().strip()
        return ""

    @staticmethod
    def _load_semantic_version(root_dir: Path) -> str:
        """Load semantic version from resources/BROWSEROS_VERSION

        File format:
            BROWSEROS_MAJOR=0
            BROWSEROS_MINOR=31
            BROWSEROS_BUILD=0
            BROWSEROS_PATCH=0

        Returns: "0.31.0" (PATCH only included if non-zero)
        """
        version_file = join_paths(root_dir, "resources", "BROWSEROS_VERSION")
        if not version_file.exists():
            return ""

        version_dict = {}
        for line in version_file.read_text().strip().split("\n"):
            line = line.strip()
            if not line or "=" not in line:
                continue
            key, value = line.split("=", 1)
            version_dict[key.strip()] = value.strip()

        major = version_dict.get("BROWSEROS_MAJOR", "0")
        minor = version_dict.get("BROWSEROS_MINOR", "0")
        build = version_dict.get("BROWSEROS_BUILD", "0")
        patch = version_dict.get("BROWSEROS_PATCH", "0")

        # Include patch only if non-zero
        if patch != "0":
            return f"{major}.{minor}.{build}.{patch}"
        elif build != "0":
            return f"{major}.{minor}.{build}"
        else:
            return f"{major}.{minor}.0"

    # Path getter methods
    def get_config_dir(self) -> Path:
        """Get build config directory"""
        return join_paths(self.root_dir, "build", "config")

    def get_gn_config_dir(self) -> Path:
        """Get GN config directory"""
        return join_paths(self.get_config_dir(), "gn")

    def get_gn_flags_file(self) -> Path:
        """Get GN flags file for current build type"""
        platform = get_platform()
        return join_paths(
            self.get_gn_config_dir(), f"flags.{platform}.{self.build_type}.gn"
        )

    def get_copy_resources_config(self) -> Path:
        """Get copy resources configuration file"""
        return join_paths(self.get_config_dir(), "copy_resources.yaml")

    def get_download_resources_config(self) -> Path:
        """Get download resources configuration file"""
        return join_paths(self.get_config_dir(), "download_resources.yaml")

    def get_sparkle_dir(self) -> Path:
        """Get Sparkle directory"""
        return join_paths(self.chromium_src, "third_party", "sparkle")

    def get_sparkle_url(self) -> str:
        """Get Sparkle download URL"""
        return f"https://github.com/sparkle-project/Sparkle/releases/download/{self.SPARKLE_VERSION}/Sparkle-{self.SPARKLE_VERSION}.tar.xz"

    def get_extensions_manifest_url(self) -> str:
        """Get CDN URL for bundled extensions update manifest"""
        # return "https://cdn.browseros.com/extensions/update-manifest.xml"
        return "https://cdn.browseros.com/extensions/update-manifest.alpha.xml"

    def get_entitlements_dir(self) -> Path:
        """Get entitlements directory"""
        return join_paths(self.root_dir, "resources", "entitlements")

    def get_pkg_dmg_path(self) -> Path:
        """Get pkg-dmg tool path (macOS only)"""
        return join_paths(self.chromium_src, "chrome", "installer", "mac", "pkg-dmg")

    def get_app_path(self) -> Path:
        """Get built app path

        For universal builds, checks if out/Default_universal/BrowserOS.app exists
        and returns that instead of the architecture-specific path.

        This allows downstream modules (sign, package) to work on the universal
        binary after UniversalBuildModule has run.

        Note: If _fixed_app_path is set, returns that directly (used by
        UniversalBuildModule to prevent auto-detection during arch-specific ops).
        """
        # If fixed path is set (for arch-specific operations), use it directly
        if self._fixed_app_path:
            return self._fixed_app_path

        # Check for universal binary first (macOS only)
        if IS_MACOS():
            universal_app = join_paths(
                self.chromium_src, "out/Default_universal", self.BROWSEROS_APP_NAME
            )
            if universal_app.exists():
                return universal_app

        # For debug builds, check if the app has a different name
        if self.build_type == "debug" and IS_MACOS():
            # Check for debug-branded app name
            debug_app_name = f"{self.BROWSEROS_APP_BASE_NAME} Dev.app"
            debug_app_path = join_paths(self.chromium_src, self.out_dir, debug_app_name)
            if debug_app_path.exists():
                return debug_app_path

        # Return architecture-specific path
        return join_paths(self.chromium_src, self.out_dir, self.BROWSEROS_APP_NAME)

    def get_chromium_app_path(self) -> Path:
        """Get original Chromium app path"""
        return join_paths(self.chromium_src, self.out_dir, self.CHROMIUM_APP_NAME)

    def get_gn_args_file(self) -> Path:
        """Get GN args file path"""
        return join_paths(self.chromium_src, self.out_dir, "args.gn")

    def get_notarization_zip(self) -> Path:
        """Get notarization zip path (macOS only)"""
        return join_paths(self.chromium_src, self.out_dir, "notarize.zip")

    def get_artifact_name(self, artifact_type: str) -> str:
        """Get standardized artifact filename

        Args:
            artifact_type: One of "dmg", "appimage", "deb", "installer", "installer_zip"

        Returns:
            Standardized filename, e.g., "BrowserOS_v0.31.0_arm64.dmg"
        """
        if not self.semantic_version:
            raise ValueError("semantic_version is not set to generate artifact name")

        version = self.semantic_version
        base = self.BROWSEROS_APP_BASE_NAME
        arch = self.architecture

        match artifact_type:
            case "dmg":
                return f"{base}_v{version}_{arch}.dmg"
            case "appimage":
                return f"{base}_v{version}_{arch}.AppImage"
            case "deb":
                deb_arch = {"x64": "amd64", "arm64": "arm64"}.get(arch, arch)
                return f"{base}_v{version}_{deb_arch}.deb"
            case "installer":
                return f"{base}_v{version}_{arch}_installer.exe"
            case "installer_zip":
                return f"{base}_v{version}_{arch}_installer.zip"
            case _:
                raise ValueError(f"Unknown artifact type: {artifact_type}")

    def get_browseros_chromium_version(self) -> str:
        """Get browseros chromium version string"""
        return self.browseros_chromium_version

    def get_browseros_version(self) -> str:
        """Get browseros version string (build offset)"""
        return self.browseros_build_offset

    def get_semantic_version(self) -> str:
        """Get semantic version from resources/BROWSEROS_VERSION

        Returns: e.g., "0.31.0"
        """
        return self.semantic_version

    def get_sparkle_version(self) -> str:
        """Get Sparkle-compatible version from browseros_chromium_version

        Sparkle uses BUILD.PATCH format for version comparison.
        Returns: e.g., "7231.69"
        """
        if not self.browseros_chromium_version:
            raise ValueError("browseros_chromium_version is not set")

        parts = self.browseros_chromium_version.split(".")
        if len(parts) < 4:
            raise ValueError(
                f"Invalid browseros_chromium_version format: {self.browseros_chromium_version}"
            )
        return f"{parts[2]}.{parts[3]}"

    def get_release_path(self, platform: str) -> str:
        """Get R2 path for release artifacts

        Args:
            platform: "macos", "win", or "linux"

        Returns: e.g., "releases/0.31.0/macos/"
        """
        return f"releases/{self.semantic_version}/{platform}/"

    def get_app_base_name(self) -> str:
        """Get app base name without extension"""
        return self.BROWSEROS_APP_BASE_NAME

    def get_dist_dir(self) -> Path:
        """Get distribution output directory with semantic version"""
        return join_paths(self.root_dir, "releases", self.semantic_version)

    # Dev CLI specific methods
    def get_patches_dir(self) -> Path:
        """Get individual patches directory"""
        return join_paths(self.root_dir, "chromium_patches")

    def get_chromium_replace_files_dir(self) -> Path:
        """Get chromium files replacement directory"""
        return join_paths(self.root_dir, "chromium_files")

    def get_features_yaml_path(self) -> Path:
        """Get features.yaml file path"""
        return join_paths(self.root_dir, "build", "features.yaml")

    def get_patch_path_for_file(self, file_path: str) -> Path:
        """Convert a chromium file path to patch file path"""
        return join_paths(self.get_patches_dir(), file_path)

    def get_series_patches_dir(self) -> Path:
        """Get series patches directory (GNU Quilt format)"""
        return join_paths(self.root_dir, "series_patches")
