"""Dependency module for models."""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from functools import cached_property
from re import Pattern
from typing import Final

_EXTRA_PATTERN: Final[Pattern[str]] = re.compile(r"\[([^]]+)]")
_VERSION_PATTERN: Final[Pattern[str]] = re.compile(r"[<>=!~].*$")


@dataclass(frozen=True)
class Dependency:
    """Represents a Python package dependency."""

    name: str = ""
    version: str | None = None
    extras: set[str] = field(default_factory=set)
    requires: set[str] = field(default_factory=set)

    @staticmethod
    def from_str(dep_str: str) -> Dependency:
        """Create a Dependency instance from a dependency string.

        Args:
            dep_str (str): The dependency string in the format "name[extras]version".

        Returns
        -------
            Dependency: The created Dependency instance.
        """
        # Parse the dependency string to extract name, extras, and version
        name = ""
        version = None  # Use None initially, will be set if version is found
        extras = set()

        # First, extract extras if present (text within square brackets)
        extras_match = _EXTRA_PATTERN.search(dep_str)
        if extras_match:
            extras_str = extras_match.group(1)
            extras = {extra.strip() for extra in extras_str.split(",")}
            # Remove the extras part from the string for further processing
            dep_str = dep_str[: extras_match.start()] + dep_str[extras_match.end() :]

        # Extract version specifier (starts with comparison operators like >=, <=, ==, etc.)
        version_match = _VERSION_PATTERN.search(dep_str)
        if version_match:
            version = version_match.group()
            dep_str = dep_str[: version_match.start()].strip()

        # Remaining part is the name
        name = dep_str.strip()

        return Dependency(name=name, version=version, extras=extras, requires=set())

    def __post_init__(self):
        """Perform post-initialization tasks for the Dependency instance."""
        # Normalize the name after initialization
        # This ensures that names like "Requests-OAuthLib" become "requests_oauthlib"
        object.__setattr__(self, "name", self.name.lower().replace("-", "_"))

    @cached_property
    def normalized_name(self) -> str:
        """Return the normalized name of the dependency."""
        return self.name

    def __str__(self) -> str:
        """Return the string representation of the dependency.

        Returns
        -------
            str: The string representation of the dependency.
        """
        version_str = self.version or ""
        if self.extras:
            # Pre-cache sorted extras to avoid repeated sorting
            sorted_extras = ",".join(sorted(self.extras))
            return f"{self.name}[{sorted_extras}]{version_str}"
        return f"{self.name}{version_str}"

    def __hash__(self) -> int:
        """Hash the dependency based on its name, version, and extras.

        Returns
        -------
            int: The hash value of the dependency.
        """
        return hash((self.name, self.version, frozenset(self.extras)))
