"""Parser for yarn.lock files (Yarn v1 and v2+)."""

import re
from pathlib import Path

from ..models import PackageHash


class YarnLockParser:
    """Parser for yarn.lock files.

    Yarn v1 format (custom text format):
    package-name@^1.0.0:
      version "1.2.3"
      resolved "https://..."
      integrity sha512-base64...

    Yarn v2+ (Berry) uses YAML format:
    "package-name@npm:^1.0.0":
      version: 1.2.3
      resolution: "package-name@npm:1.2.3"
      checksum: sha512-base64...
    """

    name = "yarn-lock"
    supported_files = ("yarn.lock",)
    ecosystem = "npm"

    def supports(self, lock_file_name: str) -> bool:
        return lock_file_name in self.supported_files

    def parse(self, lock_file_path: Path) -> list[PackageHash]:
        """Parse yarn.lock and extract hashes.

        Args:
            lock_file_path: Path to yarn.lock file

        Returns:
            List of PackageHash objects for all packages with integrity hashes.
        """
        content = lock_file_path.read_text()

        # Detect format by checking for YAML-style markers
        if content.startswith("# This file is generated by running"):
            # Yarn v2+ (Berry) YAML format
            return self._parse_berry_format(content)
        else:
            # Yarn v1 custom format
            return self._parse_v1_format(content)

    def _parse_v1_format(self, content: str) -> list[PackageHash]:
        """Parse Yarn v1 lockfile format.

        Deduplicates by (name, version) to return one hash per package@version.
        """
        hashes: list[PackageHash] = []
        seen: set[tuple[str, str]] = set()

        # Pattern for package entries
        # package-name@^1.0.0, package-name@~1.0.0:
        #   version "1.2.3"
        #   ...
        #   integrity sha512-...

        current_name: str | None = None
        current_version: str | None = None
        current_integrity: str | None = None

        def _add_package() -> None:
            nonlocal current_name, current_version, current_integrity
            if current_name and current_version and current_integrity:
                key = (current_name, current_version)
                if key not in seen:
                    seen.add(key)
                    pkg_hash = PackageHash.from_sri(
                        name=current_name,
                        version=current_version,
                        sri_hash=current_integrity,
                        artifact_type="tarball",
                    )
                    if pkg_hash:
                        hashes.append(pkg_hash)

        for line in content.split("\n"):
            # Package header line (e.g., "lodash@^4.17.21:")
            if line and not line.startswith(" ") and not line.startswith("#"):
                # Save previous package if complete
                _add_package()

                # Parse new package name from header
                current_name = self._extract_name_from_header(line)
                current_version = None
                current_integrity = None

            # Version line
            elif line.strip().startswith('version "'):
                match = re.search(r'version "([^"]+)"', line)
                if match:
                    current_version = match.group(1)

            # Integrity line
            elif line.strip().startswith("integrity "):
                match = re.search(r"integrity (sha\d+-[A-Za-z0-9+/=]+)", line)
                if match:
                    current_integrity = match.group(1)

        # Don't forget the last package
        _add_package()

        return hashes

    def _parse_berry_format(self, content: str) -> list[PackageHash]:
        """Parse Yarn v2+ (Berry) YAML lockfile format.

        Deduplicates by (name, version) to return one hash per package@version.
        """
        import yaml

        hashes: list[PackageHash] = []
        seen: set[tuple[str, str]] = set()

        try:
            data = yaml.safe_load(content)
        except yaml.YAMLError:
            return hashes

        if not isinstance(data, dict):
            return hashes

        for key, pkg_data in data.items():
            if not isinstance(pkg_data, dict):
                continue

            # Skip metadata keys
            if key.startswith("__"):
                continue

            # Extract package name from key (e.g., "lodash@npm:^4.17.21")
            name = self._extract_name_from_berry_key(key)
            if not name:
                continue

            version = pkg_data.get("version")
            checksum = pkg_data.get("checksum")

            if not version or not checksum:
                continue

            # Deduplicate by (name, version)
            pkg_key = (name, str(version))
            if pkg_key in seen:
                continue
            seen.add(pkg_key)

            # Berry checksums may have a prefix like "10/sha512-..."
            # or just "sha512-..."
            if "/" in checksum:
                checksum = checksum.split("/", 1)[-1]

            pkg_hash = PackageHash.from_sri(
                name=name,
                version=str(version),
                sri_hash=checksum,
                artifact_type="tarball",
            )
            if pkg_hash:
                hashes.append(pkg_hash)

        return hashes

    @staticmethod
    def _extract_name_from_header(header: str) -> str | None:
        """Extract package name from v1 header line."""
        # Handle formats like:
        # "lodash@^4.17.21:"
        # '"@scope/name@^1.0.0":'
        # "lodash@^4.17.21, lodash@~4.17.0:"

        header = header.strip().rstrip(":")

        # Remove quotes if present
        if header.startswith('"') and header.endswith('"'):
            header = header[1:-1]

        # Take first specifier if multiple
        if ", " in header:
            header = header.split(", ")[0]

        # Find the last @ that's not the start of a scoped package
        if header.startswith("@"):
            # Scoped package: @scope/name@version
            at_pos = header.find("@", 1)
        else:
            # Unscoped package: name@version
            at_pos = header.find("@")

        if at_pos == -1:
            return None

        return header[:at_pos]

    @staticmethod
    def _extract_name_from_berry_key(key: str) -> str | None:
        """Extract package name from Berry key."""
        # Handle formats like:
        # "lodash@npm:^4.17.21"
        # "@scope/name@npm:^1.0.0"

        # Remove quotes if present
        if key.startswith('"') and key.endswith('"'):
            key = key[1:-1]

        # Find @npm: or @workspace: etc.
        for protocol in ["@npm:", "@workspace:", "@patch:", "@portal:", "@link:"]:
            if protocol in key:
                return key.split(protocol)[0]

        # Fall back to finding last @ for version
        if key.startswith("@"):
            at_pos = key.find("@", 1)
        else:
            at_pos = key.find("@")

        if at_pos == -1:
            return None

        return key[:at_pos]
