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

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

DEFAULT_BUILD_BACKEND = "setuptools.build_meta"
DYNAMIC_TABLE = {
    "setuptools.build_meta": "tool.setuptools.dynamic",
}
VERSION_REGEX = (
    r"^\s*\t*[A-Za-z_][A-Za-z_0-9]+\s=\s(?P<q>['\"])(?P<version>{value})(?P=q)"
)


def replace_version(matchobj, new_version: str):
    start, end = matchobj.start("version"), matchobj.end("version")
    full = matchobj.group(0)
    return f"{full[: start - matchobj.start(0)]}{new_version}{full[end - matchobj.start(0) :]}"


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)
    module_text = module_path.read_text(encoding="utf-8")
    pattern = re.compile(VERSION_REGEX.format(value=re.escape(value)))
    module_text = "\n".join(
        pattern.sub(lambda m: replace_version(m, new_value), line)
        for line in module_text.split("\n")
    )
    module_path.write_text(module_text, encoding="utf-8")


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", {})
        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)


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()}")
