"""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 logging
import os
import shutil
import tarfile
import tempfile
from email.generator import Generator
from email.message import Message
from pathlib import Path
from typing import Any

from whool.buildapi import _build_wheel

from . import manifest
from .manifest import addon_name, generate_manifest
from .primitives import (
    check_dynamic_version,
    check_release_compatibility,
    get_exclusion_patterns,
    get_odoo_release,
    get_pyproject_data,
    get_readme_info,
    get_src_dir,
)

_logger = logging.getLogger(__name__)


def _copy_project_to_addon_dir(
    src: Path,
    dst: Path,
    generated_manifest: dict[str, Any],
    src_subdir: str | None = None,
) -> 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
        src_subdir: Optional subdirectory containing module files (e.g., "src")
    """
    dst.mkdir(parents=True, exist_ok=True)

    if src_subdir:
        copy_root = src / src_subdir
        if not copy_root.is_dir():
            raise FileNotFoundError(f"Source directory not found: {copy_root}")
    else:
        copy_root = src

    for item in copy_root.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 _validate_readme_in_metadata(
    wheel_path: Path,
    readme_info: tuple[str, str] | None,
    _root: Path,
) -> None:
    """Validate that readme is properly included in wheel METADATA.

    Args:
        wheel_path: Path to the built wheel
        readme_info: Tuple of (readme_filename, content_type) or None
        _root: Project root directory (unused, for signature compatibility)

    Raises:
        ValueError: If readme is specified but not found in METADATA
    """
    if not readme_info:
        return

    readme_filename, expected_content_type = readme_info

    from zipfile import ZipFile

    with ZipFile(wheel_path, "r") as whl:
        metadata_files = [
            n for n in whl.namelist() if n.endswith(".dist-info/METADATA")
        ]
        if not metadata_files:
            raise FileNotFoundError("No METADATA found in wheel")

        metadata_content = whl.read(metadata_files[0]).decode("utf-8")

        has_content_type = False
        has_description_content = False
        found_content_type_line = False

        for line in metadata_content.splitlines():
            if line.startswith("Description-Content-Type:"):
                content_type = line.split(":", 1)[1].strip()
                has_content_type = True
                found_content_type_line = True
                if content_type != expected_content_type:
                    _logger.warning(
                        "Expected Description-Content-Type '%s', got '%s'",
                        expected_content_type,
                        content_type,
                    )
            elif found_content_type_line and line.strip():
                has_description_content = True
                break
            elif found_content_type_line and not line.strip():
                continue

        if not has_content_type:
            raise ValueError(
                f"readme={readme_filename} specified in pyproject.toml but "
                "Description-Content-Type not found in wheel METADATA"
            )

        if not has_description_content:
            raise ValueError(
                f"readme={readme_filename} specified in pyproject.toml but "
                "Description content not found in wheel METADATA"
            )


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)
    check_dynamic_version(pyproject_data)
    odoo_release = get_odoo_release(config_settings, pyproject_data)
    check_release_compatibility(odoo_release, pyproject_data)
    generated_manifest = generate_manifest(pyproject_data, odoo_release)
    module_name = addon_name(pyproject_data["project"]["name"])
    src_subdir = get_src_dir(root)
    readme_info = get_readme_info(pyproject_data)

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

        wheel_path = wheel_dir / wheel_filename
        _validate_readme_in_metadata(wheel_path, readme_info, root)

        return wheel_filename


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)
    check_dynamic_version(pyproject_data)
    module_name = addon_name(pyproject_data["project"]["name"])
    module_version = pyproject_data["project"]["version"]

    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)
    check_dynamic_version(pyproject_data)
    odoo_release = get_odoo_release(config_settings, pyproject_data)

    check_release_compatibility(odoo_release, pyproject_data)

    generated_manifest = generate_manifest(pyproject_data, odoo_release)
    src_dir = get_src_dir(root)

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

    addon_dir = root / src_dir if src_dir else root
    manifest.write_manifest(generated_manifest, addon_dir / "__manifest__.py")
    _write_pkg_info(addon_dir, pyproject_data, generated_manifest)
    return _build_wheel(addon_dir, editable_dir, editable=True)


def _write_pkg_info(
    addon_dir: Path,
    pyproject_data: dict[str, Any],
    generated_manifest: dict[str, Any],
) -> None:
    """Write PKG-INFO to addon directory for whool metadata precomputation.

    Args:
        addon_dir: Path to the addon directory
        pyproject_data: Parsed pyproject.toml data
        generated_manifest: Generated __manifest__.py content
    """
    msg = Message()
    msg["Metadata-Version"] = "2.1"
    msg["Name"] = pyproject_data["project"]["name"]
    msg["Version"] = generated_manifest["version"]

    pkg_info_path = addon_dir / "PKG-INFO"
    with open(pkg_info_path, "w", encoding="utf-8") as f:
        Generator(f, mangle_from_=False, maxheaderlen=0).flatten(msg)


build_wheel_for_editable = build_editable
