import logging
import re
from copy import (
    deepcopy,
)
from typing import (
    cast,
)

from pydantic import (
    ValidationError,
)

from labels.artifact.relationship import (
    Relationship,
    RelationshipType,
)
from labels.file.dependency_type import (
    DependencyType,
)
from labels.file.location import (
    Location,
)
from labels.file.location_read_closer import (
    LocationReadCloser,
)
from labels.file.resolver import (
    Resolver,
)
from labels.file.scope import (
    Scope,
)
from labels.internal.collection.types import (
    IndexedDict,
    IndexedList,
    ParsedValue,
)
from labels.internal.collection.yaml import (
    parse_yaml_with_tree_sitter,
)
from labels.model.core import (
    Digest,
    Language,
    Package,
    PackageType,
)
from labels.pkg.cataloger.generic.parser import (
    Environment,
)
from labels.pkg.cataloger.javascript.model import (
    PnpmEntry,
)
from labels.pkg.cataloger.javascript.package import (
    package_url,
)
from labels.utils.strings import format_exception

LOGGER = logging.getLogger(__name__)

VERSION_PATTERN = re.compile(r"(\d+\.\d+\.\d+(-[0-9A-Za-z\.]+)?)")


def extract_package_name_from_key_dependency(item: str) -> str | None:
    # Regex pattern to extract the package name
    pattern = r"^@?[\w-]+/[\w-]+$"
    match = re.match(pattern, item)
    if match:
        return match.group(0)
    return None


def extract_version_from_value_dependency(item: str) -> str | None:
    # Regex pattern to extract the version number before any parentheses
    pattern = r"^(\d+\.\d+\.\d+)"
    match = re.match(pattern, item)
    if match:
        return match.group(1)
    return None


def _get_package(
    packages: list[Package],
    dep_name: str | None,
    dep_version: str | None,
) -> Package | None:
    return next(
        (x for x in packages if x.name == dep_name and x.version == dep_version),
        None,
    )


def _process_relationships(
    dependencies: IndexedDict[str, ParsedValue],
    packages: list[Package],
    current_package: Package | None,
) -> list[Relationship]:
    relationships: list[Relationship] = []
    for raw_dep_name, raw_dep_version in dependencies.items():
        if not isinstance(raw_dep_version, str):
            continue
        dep_name = extract_package_name_from_key_dependency(
            raw_dep_name,
        )
        dep_version = extract_version_from_value_dependency(
            raw_dep_version,
        )
        if (
            dep := _get_package(
                packages,
                dep_name,
                dep_version,
            )
            and current_package
        ):
            relationships.append(
                Relationship(
                    from_=dep,
                    to_=current_package,
                    type=RelationshipType.DEPENDENCY_OF_RELATIONSHIP,
                ),
            )
    return relationships


def _generate_relations_relationship(
    package_yaml: IndexedDict[str, ParsedValue],
    packages: list[Package],
) -> list[Relationship]:
    relationships: list[Relationship] = []
    packages_items = package_yaml["packages"]
    if not isinstance(packages_items, IndexedDict):
        return relationships
    for package_key, package_value in packages_items.items():
        if not isinstance(package_value, IndexedDict):
            continue
        if match_ := re.search(r"/(@?[^@]+)@(\d+\.\d+\.\d+)", package_key):
            package_name = match_.groups()[0]
            package_version = match_.groups()[1]
            current_package = _get_package(
                packages,
                dep_name=package_name,
                dep_version=package_version,
            )
            dependencies = package_value.get("dependencies")
            if dependencies and isinstance(dependencies, IndexedDict):
                relationships.extend(
                    _process_relationships(dependencies, packages, current_package),
                )
    return relationships


def _get_package_metadata(
    package_value: IndexedDict[str, ParsedValue],
    *,
    is_dev: bool,
) -> PnpmEntry:
    resolution_value = package_value.get("resolution")
    integrity_value = (
        resolution_value.get("integrity") if isinstance(resolution_value, IndexedDict) else None
    )

    integrity_value = integrity_value if isinstance(integrity_value, str) else None

    return PnpmEntry(
        is_dev=is_dev if isinstance(is_dev, bool) else False,
        integrity=Digest(
            algorithm="sha-512",
            value=integrity_value,
        ),
    )


def manage_coordinates(
    package_yaml: IndexedDict[str, ParsedValue],
    package_key: str,
    package_name: str,
    direct_dependencies: list[ParsedValue],
    base_location: Location,
) -> Location:
    current_location = deepcopy(base_location)
    if current_location.coordinates:
        packages = package_yaml["packages"]
        if not isinstance(packages, IndexedDict):
            return current_location
        position = packages.get_key_position(package_key)
        current_location.coordinates.line = position.start.line
        is_transitive = package_name not in direct_dependencies
        current_location.dependency_type = (
            DependencyType.TRANSITIVE if is_transitive else DependencyType.DIRECT
        )
    return current_location


def process_package_string(
    package: str,
    spec: IndexedDict[str, ParsedValue],
) -> tuple[str, str] | None:
    if package.startswith("github"):
        pkg_name = spec.get("name", "")
        pkg_version = spec.get("version", "")
    else:
        pkg_info: list[str] = VERSION_PATTERN.split(package.strip("\"'"))
        if len(pkg_info) < 2:
            return None
        pkg_name = pkg_info[0].lstrip("/")[0:-1]
        pkg_version = pkg_info[1]

    if not isinstance(pkg_name, str) or not isinstance(pkg_version, str):
        return None
    return pkg_name, pkg_version


def _process_package(
    package_key: str,
    pkg_spec: IndexedDict[str, ParsedValue],
    package_yaml: IndexedDict[str, ParsedValue],
    direct_dependencies: list[ParsedValue],
    base_location: Location,
) -> Package | None:
    if match_ := process_package_string(package_key, pkg_spec):
        package_name = match_[0]
        package_version = match_[1]

        if not package_name or not package_version:
            return None
        is_dev = pkg_spec.get("dev")
        current_location: Location = manage_coordinates(
            package_yaml,
            package_key,
            package_name,
            direct_dependencies,
            base_location,
        )
        is_dev = is_dev if isinstance(is_dev, bool) else False
        current_location.scope = Scope.DEV if is_dev else Scope.PROD
        try:
            return Package(
                name=package_name,
                version=package_version,
                locations=[current_location],
                language=Language.JAVASCRIPT,
                licenses=[],
                type=PackageType.NpmPkg,
                p_url=package_url(package_name, package_version),
                metadata=_get_package_metadata(pkg_spec, is_dev=is_dev),
            )
        except ValidationError as ex:
            LOGGER.warning(
                "Malformed package. Required fields are missing or data types are incorrect.",
                extra={
                    "extra": {
                        "exception": format_exception(str(ex)),
                        "location": current_location.path(),
                    },
                },
            )
    return None


def parse_pnpm_lock(
    _: Resolver | None,
    __: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    package_yaml: IndexedDict[str, ParsedValue] = cast(
        IndexedDict[str, ParsedValue],
        parse_yaml_with_tree_sitter(reader.read_closer.read()),
    )

    if not package_yaml:
        return [], []
    dependencies = package_yaml.get("dependencies")
    dev_dependencies = package_yaml.get("devDependencies")
    direct_dependencies: list[ParsedValue] = []
    if isinstance(dependencies, IndexedList) and isinstance(dev_dependencies, IndexedList):
        direct_dependencies = [*dev_dependencies, *dependencies]
    packages: list[Package] = []
    relationships: list[Relationship] = []
    packages_items = package_yaml["packages"]
    if not isinstance(packages_items, IndexedDict):
        return [], []
    for package_key, pkg_spec in packages_items.items():
        if not isinstance(pkg_spec, IndexedDict):
            continue
        if package := _process_package(
            package_key,
            pkg_spec,
            package_yaml,
            direct_dependencies,
            reader.location,
        ):
            packages.append(package)

    relationships = _generate_relations_relationship(package_yaml, packages)

    return packages, relationships
