"""PEP 517 build hooks for Polytropos.

This module generates `__manifest__.py` from `pyproject.toml` configuration
and delegates wheel/editable builds to whool.

Only functions required by PEP 517 are public.
"""

import os
import shutil
import tarfile
import tempfile
from pathlib import Path
from typing import Any

from whool.buildapi import _build_wheel

from . import manifest
from .primitives import (
    check_release_compatibility,
    get_exclusion_patterns,
    get_odoo_release,
    get_pyproject_data,
)


def _get_module_info(_root: Path, pyproject_data: dict[str, Any]) -> tuple[str, str]:
    """Extract module name and version from pyproject.toml.

    The module name is determined in this order:
    1. [tool.polytropos.manifest.name] (explicit override)
    2. Extract from [project] if it starts with "odoo-addon-"
    3. [project] name as-is (backward compatibility)

    Args:
        _root: Path to the project root
        pyproject_data: Parsed pyproject.toml

    Returns:
        Tuple of (module_name, module_version)

    Raises:
        ValueError: If the package name doesn't start with "odoo-addon-"
    """
    project = pyproject_data.get("project", {})

    name = project.get("name", "")
    module_version = project.get("version", "1.0.0")

    if not name:
        raise ValueError("Module name not found in pyproject.toml")

    # Check for explicit module name in [[tool.polytropos.manifest]] (base config only)
    manifest_data = (
        pyproject_data.get("tool", {}).get("polytropos", {}).get("manifest", [])
    )
    if not isinstance(manifest_data, list):
        raise ValueError(
            "[tool.polytropos.manifest] must be a list, if defined. "
            "Use [[tool.polytropos.manifest]] (double brackets) in pyproject.toml."
        )
    explicit_name = next(
        (item.get("name") for item in manifest_data if "releases" not in item),
        None,
    )
    if explicit_name:
        return explicit_name, module_version

    module_name = name[len("odoo-addon-") :]

    # Convert hyphens to underscores for Odoo module name
    module_name = module_name.replace("-", "_")

    return module_name, module_version


def _copy_project_to_addon_dir(
    src: Path,
    dst: Path,
    generated_manifest: dict[str, Any],
) -> None:
    """Copy project files to addon directory for building.

    Args:
        src: Source project directory
        dst: Destination addon directory (named after the addon)
        generated_manifest: Generated manifest content
    """
    dst.mkdir(parents=True, exist_ok=True)

    for item in src.iterdir():
        if item.name in ("build", "dist", "__pycache__", ".git", ".venv"):
            continue
        if item.is_file():
            if item.name.endswith(".pyc"):
                continue
            shutil.copy2(item, dst / item.name)
        elif item.is_dir():
            shutil.copytree(item, dst / item.name, dirs_exist_ok=True)

    manifest.write_manifest(generated_manifest, dst / "__manifest__.py")


def _generate_manifest(
    root: Path,
    pyproject_data: dict[str, Any],
    odoo_release: str,
) -> tuple[str, dict[str, Any]]:
    """Generate manifest from pyproject.toml.

    Args:
        root: Path to project root
        pyproject_data: Parsed pyproject.toml
        odoo_release: Odoo release version

    Returns:
        Tuple of (module_name, generated_manifest)
    """
    module_name, module_version = _get_module_info(root, pyproject_data)
    generated_manifest = manifest.generate_manifest(pyproject_data, odoo_release)
    return module_name, generated_manifest


def build_wheel(
    wheel_directory: str,
    config_settings: dict[str, Any] | None = None,
    _metadata_directory: str | None = None,
) -> str:
    """Build a wheel for the Odoo module.

    PEP 517 mandatory hook.

    Args:
        wheel_directory: Directory to place the wheel in
        config_settings: PEP 517 config_settings
        _metadata_directory: Unused, PEP 517 requirement

    Returns:
        Path to the built wheel file
    """
    root = Path(os.getcwd())
    wheel_dir = Path(wheel_directory)

    pyproject_data = get_pyproject_data(root)
    odoo_release = get_odoo_release(config_settings, pyproject_data)
    check_release_compatibility(odoo_release, pyproject_data)
    module_name, generated_manifest = _generate_manifest(
        root, pyproject_data, odoo_release
    )

    with tempfile.TemporaryDirectory() as tmpdir:
        addon_dir = Path(tmpdir) / module_name
        _copy_project_to_addon_dir(root, addon_dir, generated_manifest)
        return _build_wheel(addon_dir, wheel_dir, editable=False)


def _add_directory(
    tar: tarfile.TarFile, dir_path: Path, arcname_prefix: str, exclude: set[str]
) -> None:
    """Recursively add directory to tar archive.

    Args:
        tar: TarFile object to add files to
        dir_path: Directory to add
        arcname_prefix: Prefix for the archive name
        exclude: Set of patterns to exclude
    """
    if not dir_path.exists():
        return
    for item in dir_path.iterdir():
        if item.name in exclude or item.name.endswith(".pyc"):
            continue
        if item.is_file():
            tar.add(item, arcname=f"{arcname_prefix}/{item.name}")
        elif item.is_dir():
            _add_directory(tar, item, f"{arcname_prefix}/{item.name}", exclude)


def _create_sdist_archive(
    root: Path,
    sdist_path: Path,
    sdist_name: str,
    exclude: set[str],
) -> None:
    """Create sdist tarball from project files.

    Args:
        root: Project root directory
        sdist_path: Path for the output sdist archive
        sdist_name: Name for the sdist archive
        exclude: Set of patterns to exclude
    """
    with tarfile.open(sdist_path, "w:gz") as tar:
        tar.add(root / "pyproject.toml", arcname=f"{sdist_name}/pyproject.toml")

        for item in root.iterdir():
            if item.name in exclude or item.name in (
                "pyproject.toml",
                "__manifest__.py",
            ):
                continue
            if item.is_file() and not item.name.endswith(".pyc"):
                tar.add(item, arcname=f"{sdist_name}/{item.name}")
            elif item.is_dir() and item.suffix != ".pyc":
                _add_directory(tar, item, f"{sdist_name}/{item.name}", exclude)


def build_sdist(
    sdist_directory: str,
    config_settings: dict[str, Any] | None = None,  # noqa: ARG001
) -> str:
    """Build a source distribution for the Odoo module.

    PEP 517 mandatory hook.

    The sdist is version-independent - it contains `pyproject.toml` and source files,
    but NOT a version-specific `__manifest__.py`. The manifest is generated at build time.

    Args:
        sdist_directory: Directory to place the sdist in
        config_settings: PEP 517 config_settings (unused)

    Returns:
        Path to the built sdist archive
    """
    root = Path(os.getcwd())
    sdist_dir = Path(sdist_directory)

    pyproject_data = get_pyproject_data(root)
    module_name, module_version = _get_module_info(root, pyproject_data)

    sdist_name = f"{module_name}-{module_version}"
    sdist_path = sdist_dir / f"{sdist_name}.tar.gz"

    exclude = get_exclusion_patterns(root)
    _create_sdist_archive(root, sdist_path, sdist_name, exclude)
    return f"{sdist_name}.tar.gz"


def build_editable(
    editable_directory: str,
    config_settings: dict[str, Any] | None = None,
    _metadata_directory: str | None = None,
) -> str:
    """Build an editable install for the Odoo module.

    PEP 660 hook.

    Args:
        editable_directory: Directory to place editable install files
        config_settings: Build configuration
        _metadata_directory: Metadata directory

    Returns:
        Basename of the built wheel (for compatibility)
    """
    root = Path(os.getcwd())
    editable_dir = Path(editable_directory)

    pyproject_data = get_pyproject_data(root)
    odoo_release = get_odoo_release(config_settings, pyproject_data)

    check_release_compatibility(odoo_release, pyproject_data)

    module_name, generated_manifest = _generate_manifest(
        root, pyproject_data, odoo_release
    )

    data_files = generated_manifest.get("data", [])
    for data_file in data_files:
        if not (root / data_file).exists():
            raise FileNotFoundError(f"Data file not found: {data_file}")

    manifest.write_manifest(generated_manifest, root / "__manifest__.py")

    with tempfile.TemporaryDirectory() as tmpdir:
        addon_dir = Path(tmpdir) / module_name
        _copy_project_to_addon_dir(root, addon_dir, generated_manifest)
        return _build_wheel(addon_dir, editable_dir, editable=True)


build_wheel_for_editable = build_editable
