"""High-level fluent API for building Xcode projects from scratch."""

import os
from pathlib import Path

from .filetypes import (
    PRODUCT_EXTENSIONS,
    PRODUCT_FILE_TYPES,
    file_type_for_extension,
    is_resource_file,
)
from .keygen import KeyGenerator
from .objects import (
    PBXBuildFile,
    PBXFileReference,
    PBXFileSystemSynchronizedRootGroup,
    PBXFrameworksBuildPhase,
    PBXGroup,
    PBXNativeTarget,
    PBXObject,
    PBXProject,
    PBXResourcesBuildPhase,
    PBXShellScriptBuildPhase,
    PBXSourcesBuildPhase,
    XCBuildConfiguration,
    XCConfigurationList,
)
from .scheme import generate_scheme
from .serializer import serialize_project
from .settings import (
    project_debug_settings,
    project_release_settings,
    target_debug_settings,
    target_release_settings,
)


class ProjectBuilder:
    """Builds a complete .xcodeproj bundle.

    Usage:
        project = ProjectBuilder("MyApp", bundle_id="com.example.myapp")
        project.set_deployment_target("18.0")
        project.add_swift_sources("MyApp/", glob="**/*.swift")
        project.add_resources("MyApp/Resources/")
        project.add_asset_catalog("MyApp/Assets.xcassets")
        project.save("MyApp.xcodeproj")
    """

    def __init__(
        self,
        name: str,
        *,
        bundle_id: str,
        product_type: str = "com.apple.product-type.application",
        deployment_target: str = "18.0",
        swift_version: str = "5.0",
        object_version: str = "56",
    ) -> None:
        """Initialize a new Xcode project builder.

        Args:
            name: The project and target name.
            bundle_id: The application bundle identifier.
            product_type: The Xcode product type identifier.
            deployment_target: The minimum iOS deployment target version.
            swift_version: The Swift language version.
            object_version: The pbxproj objectVersion ("56" classic, "77" modern file-sync).

        """
        self.name = name
        self.bundle_id = bundle_id
        self.product_type = product_type
        self.deployment_target = deployment_target
        self.swift_version = swift_version
        self.object_version = object_version

        self._keygen = KeyGenerator()
        self._objects: dict[str, PBXObject] = {}

        # Maps directory path -> PBXGroup object_key (for building group hierarchy)
        self._group_map: dict[str, str] = {}

        self._build_skeleton()

    def _key(self) -> str:
        return self._keygen.generate()

    def _register(self, obj: PBXObject) -> str:
        self._objects[obj.object_key] = obj
        return obj.object_key

    def _build_skeleton(self) -> None:
        """Create the minimal project structure."""
        # Product file reference
        product_ext = PRODUCT_EXTENSIONS.get(self.product_type, ".app")
        product_file_type = PRODUCT_FILE_TYPES.get(self.product_type, "wrapper.application")
        product_name = f"{self.name}{product_ext}"

        self._product_ref = PBXFileReference(
            self._key(),
            path=product_name,
            source_tree="BUILT_PRODUCTS_DIR",
            explicit_file_type=product_file_type,
            include_in_index="0",
        )
        self._register(self._product_ref)

        # Build phases
        self._sources_phase = PBXSourcesBuildPhase(self._key())
        self._register(self._sources_phase)

        self._resources_phase = PBXResourcesBuildPhase(self._key())
        self._register(self._resources_phase)

        self._frameworks_phase = PBXFrameworksBuildPhase(self._key())
        self._register(self._frameworks_phase)

        # Products group
        self._products_group = PBXGroup(
            self._key(),
            children=[self._product_ref.object_key],
            name="Products",
        )
        self._register(self._products_group)

        # Source group (named after the target)
        # In v77, this is a PBXFileSystemSynchronizedRootGroup (files auto-sync from disk)
        self._file_sync_group: PBXFileSystemSynchronizedRootGroup | None = None
        if self.object_version == "77":
            self._file_sync_group = PBXFileSystemSynchronizedRootGroup(
                self._key(),
                path=self.name,
            )
            self._register(self._file_sync_group)
            source_child = self._file_sync_group.object_key
        else:
            self._source_group = PBXGroup(
                self._key(),
                name=self.name,
                path=self.name,
            )
            self._register(self._source_group)
            self._group_map[self.name] = self._source_group.object_key
            source_child = self._source_group.object_key

        # Main group (root, no name/path)
        self._main_group = PBXGroup(
            self._key(),
            children=[
                source_child,
                self._products_group.object_key,
            ],
        )
        self._register(self._main_group)

        # Build configurations - project level
        proj_debug = XCBuildConfiguration(
            self._key(),
            name="Debug",
            build_settings=project_debug_settings(deployment_target=self.deployment_target),
        )
        self._register(proj_debug)

        proj_release = XCBuildConfiguration(
            self._key(),
            name="Release",
            build_settings=project_release_settings(deployment_target=self.deployment_target),
        )
        self._register(proj_release)

        self._project_config_list = XCConfigurationList(
            self._key(),
            build_configurations=[proj_debug.object_key, proj_release.object_key],
            comment_name="",
        )
        self._register(self._project_config_list)

        # Build configurations - target level
        tgt_debug = XCBuildConfiguration(
            self._key(),
            name="Debug",
            build_settings=target_debug_settings(
                bundle_id=self.bundle_id,
                deployment_target=self.deployment_target,
                swift_version=self.swift_version,
            ),
        )
        self._register(tgt_debug)
        self._target_debug_config = tgt_debug

        tgt_release = XCBuildConfiguration(
            self._key(),
            name="Release",
            build_settings=target_release_settings(
                bundle_id=self.bundle_id,
                deployment_target=self.deployment_target,
                swift_version=self.swift_version,
            ),
        )
        self._register(tgt_release)
        self._target_release_config = tgt_release

        self._target_config_list = XCConfigurationList(
            self._key(),
            build_configurations=[tgt_debug.object_key, tgt_release.object_key],
            comment_name=self.name,
        )
        self._register(self._target_config_list)

        # Native target
        file_sync_groups = [self._file_sync_group.object_key] if self._file_sync_group else None
        self._target = PBXNativeTarget(
            self._key(),
            name=self.name,
            product_name=self.name,
            product_type=self.product_type,
            product_reference=self._product_ref.object_key,
            build_configuration_list=self._target_config_list.object_key,
            build_phases=[
                self._sources_phase.object_key,
                self._frameworks_phase.object_key,
                self._resources_phase.object_key,
            ],
            file_system_synchronized_groups=file_sync_groups,
        )
        self._register(self._target)

        # Project root
        is_v77 = self.object_version == "77"
        self._project = PBXProject(
            self._key(),
            build_configuration_list=self._project_config_list.object_key,
            main_group=self._main_group.object_key,
            targets=[self._target.object_key],
            product_ref_group=self._products_group.object_key,
            compatibility_version=None if is_v77 else "Xcode 14.0",
            minimized_project_reference_proxies="1" if is_v77 else None,
            preferred_project_object_version="77" if is_v77 else None,
            attributes={
                "BuildIndependentTargetsInParallel": "1",
                "LastSwiftUpdateCheck": "1600",
                "LastUpgradeCheck": "1600",
                "TargetAttributes": {
                    self._target.object_key: {
                        "CreatedOnToolsVersion": "16.0",
                    },
                },
            },
        )
        self._register(self._project)

    # -- Fluent configuration methods --

    def set_deployment_target(self, version: str) -> "ProjectBuilder":
        """Set the iOS deployment target version."""
        self.deployment_target = version
        # Update all configs that have this setting
        for obj in self._objects.values():
            if isinstance(obj, XCBuildConfiguration):
                if "IPHONEOS_DEPLOYMENT_TARGET" in obj.build_settings:
                    obj.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = version
        return self

    def set_swift_version(self, version: str) -> "ProjectBuilder":
        """Set the Swift language version."""
        self.swift_version = version
        self._target_debug_config.build_settings["SWIFT_VERSION"] = version
        self._target_release_config.build_settings["SWIFT_VERSION"] = version
        return self

    def set_development_team(self, team_id: str) -> "ProjectBuilder":
        """Set the development team ID for code signing."""
        self._target_debug_config.build_settings["DEVELOPMENT_TEAM"] = team_id
        self._target_release_config.build_settings["DEVELOPMENT_TEAM"] = team_id
        return self

    def set_build_setting(
        self,
        key: str,
        value: str | list[str] | tuple[str, ...],
        *,
        config: str | None = None,
    ) -> "ProjectBuilder":
        """Set a custom build setting.

        Args:
            key: The build setting name.
            value: The value (string or list of strings).
            config: If set, only apply to "Debug" or "Release". None = both.

        """
        for obj in self._objects.values():
            if not isinstance(obj, XCBuildConfiguration):
                continue
            if config and obj.name != config:
                continue
            obj.build_settings[key] = value
        return self

    def add_file_sync_group(self, path: str) -> "ProjectBuilder":
        """Add an additional file-system-synchronized root group (v77 only).

        In objectVersion 77, Xcode auto-discovers files from disk via these groups
        instead of listing each file individually in the project.

        Args:
            path: On-disk directory path relative to the project root.

        """
        group = PBXFileSystemSynchronizedRootGroup(self._key(), path=path)
        self._register(group)
        # Add to main group children
        self._main_group.children.insert(-1, group.object_key)  # before Products
        # Add to target's sync groups
        if self._target.file_system_synchronized_groups is None:
            self._target.file_system_synchronized_groups = []
        self._target.file_system_synchronized_groups.append(group.object_key)
        return self

    # -- File/source management --

    def _ensure_group(self, dir_path: str) -> str:
        """Ensure a PBXGroup exists for the given directory path, creating parents as needed.

        Returns the object_key of the group.
        """
        if dir_path in self._group_map:
            return self._group_map[dir_path]

        parts = Path(dir_path).parts
        current_path = ""
        parent_key = self._main_group.object_key

        for part in parts:
            current_path = str(Path(current_path) / part) if current_path else part
            if current_path in self._group_map:
                parent_key = self._group_map[current_path]
                continue

            group = PBXGroup(
                self._key(),
                name=part,
                path=part,
            )
            self._register(group)
            self._group_map[current_path] = group.object_key

            # Add to parent's children
            parent_obj = self._objects[parent_key]
            if isinstance(parent_obj, PBXGroup):
                parent_obj.children.append(group.object_key)

            parent_key = group.object_key

        return self._group_map[dir_path]

    def add_swift_source(self, path: str) -> "ProjectBuilder":
        """Add a single Swift source file to the project."""
        return self._add_file(path, phase="sources")

    def add_resource(self, path: str) -> "ProjectBuilder":
        """Add a single resource file to the project."""
        return self._add_file(path, phase="resources")

    def add_asset_catalog(self, path: str) -> "ProjectBuilder":
        """Add an asset catalog (.xcassets) to the project."""
        return self._add_file(path, phase="resources")

    def _add_file(self, path: str, *, phase: str) -> "ProjectBuilder":
        """Add a file to the project with proper group hierarchy.

        Args:
            path: Path relative to the project root (e.g., "MyApp/Views/ContentView.swift").
            phase: Which build phase: "sources", "resources", or "none".

        """
        p = Path(path)
        filename = p.name
        ext = p.suffix

        # Ensure parent group exists
        dir_path = str(p.parent) if str(p.parent) != "." else self.name
        group_key = self._ensure_group(dir_path)

        # Create file reference
        file_ref = PBXFileReference(
            self._key(),
            path=filename,
            last_known_file_type=file_type_for_extension(ext),
        )
        self._register(file_ref)

        # Add to group
        group = self._objects[group_key]
        if isinstance(group, PBXGroup):
            group.children.append(file_ref.object_key)

        # Create build file and add to phase
        if phase != "none":
            build_file = PBXBuildFile(
                self._key(),
                file_ref=file_ref.object_key,
                file_ref_comment=filename,
            )
            self._register(build_file)

            if phase == "sources":
                self._sources_phase.files.append(build_file.object_key)
            elif phase == "resources":
                self._resources_phase.files.append(build_file.object_key)

        return self

    def add_swift_sources(self, directory: str, *, glob: str = "**/*.swift") -> "ProjectBuilder":
        """Add all Swift files matching a glob pattern from a directory.

        Args:
            directory: Base directory to search (relative to CWD).
            glob: Glob pattern for file matching.

        """
        base = Path(directory)
        for filepath in sorted(base.glob(glob)):
            if filepath.is_file():
                self.add_swift_source(str(filepath))
        return self

    def add_resources(self, directory: str, *, glob: str = "**/*") -> "ProjectBuilder":
        """Add all resource files from a directory.

        Args:
            directory: Base directory to search (relative to CWD).
            glob: Glob pattern for file matching.

        """
        base = Path(directory)
        for filepath in sorted(base.glob(glob)):
            if filepath.is_file() and is_resource_file(str(filepath)):
                self.add_resource(str(filepath))
        return self

    def add_shell_script_phase(
        self,
        name: str,
        script: str,
        *,
        shell: str = "/bin/sh",
    ) -> "ProjectBuilder":
        """Add a shell script build phase to the target."""
        phase = PBXShellScriptBuildPhase(
            self._key(),
            name=name,
            shell_path=shell,
            shell_script=script,
        )
        self._register(phase)
        self._target.build_phases.append(phase.object_key)
        return self

    # -- Output --

    def save(self, output_path: str) -> None:
        """Write the .xcodeproj bundle to disk.

        Creates:
            {output_path}/project.pbxproj
            {output_path}/xcshareddata/xcschemes/{name}.xcscheme
        """
        os.makedirs(output_path, exist_ok=True)

        # Write pbxproj
        pbxproj_content = serialize_project(
            self._project.object_key,
            self._objects,
            object_version=self.object_version,
        )
        pbxproj_path = os.path.join(output_path, "project.pbxproj")
        with open(pbxproj_path, "w", encoding="utf-8") as f:
            f.write(pbxproj_content)

        # Write scheme
        product_ext = PRODUCT_EXTENSIONS.get(self.product_type, ".app")
        scheme_content = generate_scheme(
            project_name=self.name,
            target_name=self.name,
            target_id=self._target.object_key,
            product_name=f"{self.name}{product_ext}",
        )
        scheme_dir = os.path.join(output_path, "xcshareddata", "xcschemes")
        os.makedirs(scheme_dir, exist_ok=True)
        scheme_path = os.path.join(scheme_dir, f"{self.name}.xcscheme")
        with open(scheme_path, "w", encoding="utf-8") as f:
            f.write(scheme_content)
