import ast
import os
from pathlib import Path
from typing import Mapping, Any

import structlog
import tomlkit
from commitizen.providers import UvProvider
from setuptools.config.expand import _find_module, _find_spec, _load_spec, StaticModule  # type: ignore[attr-defined]

logger = structlog.get_logger(__name__)
DEFAULT_BUILD_BACKEND = "setuptools.build_meta"
DYNAMIC_TABLE = {
    "setuptools.build_meta": "tool.setuptools.dynamic",
}


def set_attr(
    attr_desc: str,
    new_value: Any,
    package_dir: Mapping[str, str] | None = None,
    root_dir: str | os.PathLike[str] | None = None,
) -> None:
    """Set the value for an attribute from a module.

    This function will try and read the attribute from the specified module first.

    Examples:
        set_attr("package.attr", "value")
        set_attr("package.module.attr", "value")

    :param str attr_desc: Dot-separated string describing how to reach the
        attribute (see examples above)
    :param new_value: The value to set
    :param dict[str, str] package_dir: Mapping of package names to their
        location in disk (represented by paths relative to ``root_dir``).
    :param str root_dir: Path to directory containing all the packages in
        ``package_dir`` (current directory by default).
    :rtype: None
    """
    root_dir = root_dir or os.getcwd()
    attrs_path = attr_desc.strip().split(".")
    attr_name = attrs_path.pop()
    module_name = ".".join(attrs_path)
    module_name = module_name or "__init__"
    path = _find_module(module_name, package_dir, root_dir)
    spec = _find_spec(module_name, path)

    module = StaticModule(module_name, spec)
    try:
        value = getattr(module, attr_name)
    except Exception:
        # fallback to evaluate module
        module = _load_spec(spec, module_name)
        value = getattr(module, attr_name)

    module_path = Path(path)
    tree = ast.parse(module_path.read_text())
    for node in ast.walk(tree):
        if isinstance(node, ast.Assign):
            for target in node.targets:
                if (
                    isinstance(target, ast.Name)
                    and target.id == attr_name
                    and isinstance(node.value, ast.Constant)
                    and node.value.value == value
                ):
                    node.value = ast.Constant(value=new_value)
            pass
    module_path.write_text(ast.unparse(ast.fix_missing_locations(tree)))


def overwrite_file(
    file_path: Path | os.PathLike[str],
    new_content: str,
    root_dir: Path | os.PathLike[str] | None = None,
) -> None:
    """Modify the content of a file."""
    abs_root_dir = os.path.abspath(root_dir or os.getcwd())

    path = os.path.join(abs_root_dir, file_path)
    with open(path, "w", encoding="utf-8") as f:
        f.write(new_content)


class UVDynamicVersionProvider(UvProvider):
    """
    Extend UvProvider to read version dynamically from pyproject.toml
    """

    def get_build_backend(self) -> str:
        """Get build-backend from pyproject.toml file."""
        document = tomlkit.parse(self.file.read_text())
        build_system = document.get("build-system", {})
        if not build_system:
            logger.warn(
                f"build-system section missing in pyproject.toml, falling back to {DEFAULT_BUILD_BACKEND}"
            )
        return build_system.get("build-backend", DEFAULT_BUILD_BACKEND)

    def get_version(self) -> str:
        """Read version from pyproject.toml file."""
        match self.get_build_backend():
            case "setuptools.build_meta":
                from setuptools.config.pyprojecttoml import read_configuration

                loaded_toml = read_configuration(self.file)
                return loaded_toml["project"]["version"]
        raise ValueError("Unsupported build-backend for dynamic versioning")

    def set_version(self, version: str) -> None:
        """Set version in pyproject.toml file."""
        document = tomlkit.parse(self.file.read_text())
        project = document.get("project", {})

        if "version" in project:
            # Directly set version if present
            super().set_version(version)
            return

        match self.get_build_backend():
            case "setuptools.build_meta":
                from setuptools.config.pyprojecttoml import read_configuration

                loaded_toml = read_configuration(self.file)

                match loaded_toml["tool"]["setuptools"]["dynamic"]["version"]:
                    case {"attr": attr_name}:
                        try:
                            set_attr(
                                attr_name,
                                version,
                                root_dir=self.file.parent,
                                package_dir=None,
                            )
                        except ModuleNotFoundError:
                            # Fallback: try to find module in src/ directory
                            set_attr(
                                attr_name,
                                version,
                                root_dir=self.file.parent / "src",
                                package_dir=None,
                            )
                    case {"file": file_path}:
                        overwrite_file(file_path, version, root_dir=self.file.parent)

                self.set_lock_version(version)


if __name__ == "__main__":  # pragma: no cover
    from commitizen.config import BaseConfig

    config = BaseConfig()
    provider = UVDynamicVersionProvider(config)
    print(f"version: {provider.get_version()}")
    print("Setting version to 2.0.0")
    provider.set_version("2.0.0")
    print(f"new version: {provider.get_version()}")
