from __future__ import annotations

import abc
import enum
from functools import cached_property
from typing import TYPE_CHECKING, AbstractSet, Any, Iterable, Mapping

import tomlkit

from pdm import termui
from pdm.exceptions import PdmUsageError
from pdm.project.toml_file import TOMLFile

if TYPE_CHECKING:
    from pdm.models.repositories import LockedRepository

GENERATED_COMMENTS = [
    "This file is @generated by PDM.",
    "It is not intended for manual editing.",
]
FLAG_STATIC_URLS = "static_urls"
FLAG_CROSS_PLATFORM = "cross_platform"
FLAG_DIRECT_MINIMAL_VERSIONS = "direct_minimal_versions"
FLAG_INHERIT_METADATA = "inherit_metadata"


class Compatibility(enum.IntEnum):
    NONE = 0  # The lockfile can't be read by the current version of PDM.
    SAME = 1  # The lockfile version is the same as the current version of PDM.
    BACKWARD = 2  # The current version of PDM is newer than the lockfile version.
    FORWARD = 3  # The current version of PDM is older than the lockfile version.


class Lockfile(TOMLFile, metaclass=abc.ABCMeta):
    SUPPORTED_FLAGS: AbstractSet[str]

    @property
    @abc.abstractmethod
    def hash(self) -> tuple[str, str]:
        """The content hash algo and hash value of the pyproject.toml to generate this lockfile."""
        pass

    @abc.abstractmethod
    def update_hash(self, hash_value: str, algo: str = "sha256") -> None:
        """Update the content hash of the lockfile."""
        pass

    @property
    @abc.abstractmethod
    def groups(self) -> list[str] | None:
        """The groups defined in the lockfile, or None if no groups are defined."""
        return []

    @property
    @abc.abstractmethod
    def strategy(self) -> set[str]:
        """The strategies used in the lockfile."""
        return set()

    @cached_property
    @abc.abstractmethod
    def default_strategies(self) -> set[str]:
        """The default strategies to be used if no strategies are defined in the lockfile."""
        return set()

    def apply_strategy_change(self, changes: Iterable[str]) -> set[str]:
        """Apply the given strategy changes to the current strategy."""
        original = self.strategy
        supported = self.SUPPORTED_FLAGS
        for change in changes:
            change = change.replace("-", "_").lower()
            if change.startswith("no_"):
                if change[3:] not in supported:
                    raise PdmUsageError(f"Invalid strategy flag: {change[3:]}, supported: {', '.join(supported)}")
                original.discard(change[3:])
            else:
                if change not in supported:
                    raise PdmUsageError(f"Invalid strategy flag: {change}, supported: {', '.join(supported)}")
                original.add(change)
        return original

    def compare_groups(self, groups: Iterable[str]) -> list[str]:
        """Compare the given groups with the lockfile groups and return the groups that are not in the lockfile."""
        if not self.groups:
            return []
        return list(set(groups).difference(self.groups))

    def set_data(self, data: Mapping[str, Any]) -> None:
        doc = tomlkit.document()
        for line in GENERATED_COMMENTS:
            doc.append(None, tomlkit.comment(line))
        doc.update(data)
        super().set_data(doc)

    def write(self, show_message: bool = True) -> None:
        super().write()
        if show_message:
            self.ui.echo(f"Changes are written to [success]{self._path.name}[/].", verbosity=termui.Verbosity.NORMAL)

    def __getitem__(self, key: str) -> dict:
        return self._data[key]  # type: ignore[return-value]

    @abc.abstractmethod
    def compatibility(self) -> Compatibility:
        """We use a three-part versioning scheme for lockfiles:
        The first digit represents backward compatibility and the second digit represents forward compatibility.
        """

    @abc.abstractmethod
    def format_lockfile(self, repository: LockedRepository, groups: Iterable[str] | None, strategy: set[str]) -> None:
        """Format lock file from a dict of resolved candidates, a mapping of dependencies
        and a collection of package summaries.
        """
