import re
from abc import ABC, abstractmethod
from typing import Any, TypedDict

from casefy import casefy
from hatchling.version.core import DEFAULT_PATTERN
from hatchling.version.source.plugin.interface import VersionSourceInterface
from pydantic import AliasChoices, Field

from hatch_gradle_version.common.codegen import write_code
from hatch_gradle_version.common.gradle import GradleVersion
from hatch_gradle_version.common.model import HookModel, KebabModel, ProjectPath

PY_VERSION_REGEX = re.compile(
    r'(?i)^(PY_VERSION) *= *([\'"])v?(?P<version>.+?)\2',
    flags=re.MULTILINE,
)

DEFAULT_REGEX = re.compile(DEFAULT_PATTERN, flags=re.MULTILINE)


class VersionData(TypedDict):
    version: str
    gradle_version: str
    py_version: str
    extra_versions: dict[str, str]
    full_gradle_version: GradleVersion


class GradleVersionRegex(KebabModel):
    pattern: re.Pattern[str] = Field(
        validation_alias=AliasChoices("pattern", "pat", "regex"),
    )
    replacement: str = Field(
        r"\1",
        validation_alias=AliasChoices("replacement", "repl"),
    )


class BaseVersionSource(HookModel, VersionSourceInterface, ABC):
    source: str
    py_path: ProjectPath
    scheme: str | None = None
    gradle_version_regex: re.Pattern[str] | GradleVersionRegex | None = None
    """If this is a standalone regex, it should contain exactly one match group,
    representing the part of the raw Gradle version to keep."""

    @abstractmethod
    def get_gradle_version(self) -> GradleVersion: ...

    def fmt_raw_gradle_version(self, raw: str, extra: dict[str, str]) -> str:
        match self.gradle_version_regex:
            case re.Pattern() as pattern:
                parsed = pattern.sub(r"\1", raw)
            case GradleVersionRegex(pattern=pattern, replacement=replacement):
                parsed = pattern.sub(replacement, raw)
            case None:
                parsed = raw
        return parsed.format_map(extra)

    def get_version_data(self):
        gradle_version = self.get_gradle_version()
        py_version = self.get_py_version()

        version = gradle_version.full_version(py_version)

        version_data = VersionData(
            version=version,
            gradle_version=str(gradle_version),
            py_version=py_version,
            extra_versions=gradle_version.extra_versions,
            full_gradle_version=gradle_version,
        )

        # write here because otherwise the other version constants get outdated
        self.set_version(version, version_data)
        return dict(version_data)

    def get_py_version(self) -> str:
        # load py_version from a file
        contents = self.py_path.read_text("utf-8")

        match = PY_VERSION_REGEX.search(contents) or DEFAULT_REGEX.search(contents)
        if not match:
            raise ValueError(
                f"Unable to parse the version from the file: {self.py_path}"
            )

        return match["version"]

    def set_version(self, version: str, version_data: VersionData | dict[str, Any]):
        # __version__.py
        write_code(
            self.py_path,
            f"""\
            # This file is auto-generated by hatch-gradle-version.
            # Only the value of PY_VERSION is editable. Do not edit other values.

            PY_VERSION = "{version_data["py_version"]}"
            """,
        )

        # __gradle_version__.py
        write_code(
            self.py_path.with_name("__gradle_version__.py"),
            f"""\
            # This file is auto-generated by hatch-gradle-version. Do not edit.

            GRADLE_VERSION = "{version_data["gradle_version"]}"
            FULL_VERSION = "{version}"
            """,
            *(
                f"{format_key(key, 'GRADLE_VERSION', 'FULL_VERSION')}={value!r}"
                for key, value in sorted(version_data["extra_versions"].items())
            ),
        )


def format_key(key: str, *reserved: str):
    key = casefy.constcase(key)
    if key in reserved:
        return f"{key}_"
    return key
