import re

from labels.model.file import Location, LocationReadCloser
from labels.model.indexables import IndexedDict, IndexedList, ParsedValue
from labels.model.package import Package
from labels.model.package_manager import PackageManager
from labels.model.relationship import Relationship
from labels.model.release import Environment
from labels.model.resolver import Resolver
from labels.parsers.cataloger.javascript.common import (
    DependencyMap,
    add_dependency_to_root_relationship,
    build_relationships,
    create_root_package_from_package_json,
    find_root_location,
    get_dev_dependencies_from_package_json,
    get_direct_dependencies_from_package_json,
    group_packages_by_name,
)
from labels.parsers.cataloger.javascript.package_builder import new_simple_npm_package
from labels.parsers.cataloger.utils import get_enriched_location, parse_integrity_to_health_metadata
from labels.parsers.collection.json import parse_json_with_tree_sitter

VERSION_PATTERN = re.compile(r"^(.+)@(\d+\.\d+\.\d+(?:-[\w.]+)?)$")


def parse_bun_lock(
    _resolver: Resolver | None,
    _environment: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    content = reader.read_closer.read()
    file_content = parse_json_with_tree_sitter(_strip_jsonc_comments(content))

    if not isinstance(file_content, IndexedDict):
        return [], []

    direct_dependencies = get_direct_dependencies_from_package_json(reader.location)
    dev_dependencies = get_dev_dependencies_from_package_json(reader.location)
    root_package = create_root_package_from_package_json(
        reader.location, package_manager=PackageManager.BUN
    )

    packages = _collect_packages(
        reader.location,
        file_content,
        direct_dependencies,
        dev_dependencies,
    )

    if root_package:
        packages.append(root_package)

    dependency_map = _build_dependency_map(file_content)
    key_map = _build_key_map(file_content)

    relationships = build_relationships(packages, dependency_map, reader.location, key_map)

    if root_package:
        root_relationships = _create_root_relationships(
            root_package, packages, direct_dependencies, reader.location
        )
        relationships.extend(root_relationships)

    return packages, relationships


def _strip_jsonc_comments(content: str) -> str:
    lines = content.split("\n")
    result = []
    for line in lines:
        stripped = line.lstrip()
        if stripped.startswith("//"):
            continue
        result.append(line)
    return "\n".join(result)


def _collect_packages(
    location: Location,
    file_content: IndexedDict[str, ParsedValue],
    direct_dependencies: set[str],
    dev_dependencies: set[str],
) -> list[Package]:
    packages: list[Package] = []

    packages_dict = file_content.get("packages")
    if not isinstance(packages_dict, IndexedDict):
        return packages

    for package_key, package_entry in packages_dict.items():
        if not isinstance(package_entry, IndexedList):
            continue

        name_version = _parse_package_entry(package_entry)
        if not name_version:
            continue

        name, version = name_version

        is_direct = name in direct_dependencies
        is_transitive = not is_direct
        is_dev = name in dev_dependencies if is_direct else None

        position = packages_dict.get_key_position(package_key)
        new_location = get_enriched_location(
            location,
            line=position.start.line,
            is_transitive=is_transitive,
            is_dev=is_dev,
            package_manager=PackageManager.BUN,
        )

        integrity_str = package_entry[3] if len(package_entry) > 3 else None
        health_metadata = parse_integrity_to_health_metadata(
            str(integrity_str) if isinstance(integrity_str, str) else None
        )

        package = new_simple_npm_package(new_location, name, version, health_metadata)
        if package:
            packages.append(package)

    return packages


def _parse_package_entry(entry: IndexedList[ParsedValue]) -> tuple[str, str] | None:
    if len(entry) < 1:
        return None

    first_element = entry[0]
    if not isinstance(first_element, str):
        return None

    match = VERSION_PATTERN.match(first_element)
    if not match:
        return None

    return match.group(1), match.group(2)


def _build_dependency_map(file_content: IndexedDict[str, ParsedValue]) -> DependencyMap:
    dependency_map: DependencyMap = {}

    packages_dict = file_content.get("packages")
    if not isinstance(packages_dict, IndexedDict):
        return dependency_map

    for package_entry in packages_dict.values():
        result = _extract_package_dependencies(package_entry)
        if result:
            map_key, deps = result
            dependency_map[map_key] = deps

    return dependency_map


def _extract_package_dependencies(
    package_entry: ParsedValue,
) -> tuple[str, dict[str, str]] | None:
    if not isinstance(package_entry, IndexedList):
        return None

    name_version = _parse_package_entry(package_entry)
    if not name_version or len(package_entry) < 3:
        return None

    name, version = name_version
    deps_obj = package_entry[2]
    if not isinstance(deps_obj, IndexedDict):
        return None

    dependencies_dict = deps_obj.get("dependencies")
    if not isinstance(dependencies_dict, IndexedDict):
        return None

    deps = {
        dep_name: _extract_version(dep_version)
        for dep_name, dep_version in dependencies_dict.items()
        if isinstance(dep_version, str)
    }

    if not deps:
        return None

    return f"{name}@{version}", deps


def _build_key_map(
    file_content: IndexedDict[str, ParsedValue],
) -> dict[tuple[str, int], str]:
    key_map: dict[tuple[str, int], str] = {}

    packages_dict = file_content.get("packages")
    if not isinstance(packages_dict, IndexedDict):
        return key_map

    for package_key, package_entry in packages_dict.items():
        if not isinstance(package_entry, IndexedList):
            continue

        name_version = _parse_package_entry(package_entry)
        if not name_version:
            continue

        name, version = name_version
        position = packages_dict.get_key_position(package_key)
        key_map[(name, position.start.line)] = f"{name}@{version}"

    return key_map


def _extract_version(version_spec: str) -> str:
    match = re.match(r"^(\d+\.\d+\.\d+(?:-[\w.]+)?)", version_spec)
    if match:
        return match.group(1)
    return version_spec


def _create_root_relationships(
    root_package: Package,
    packages: list[Package],
    direct_dependencies: set[str],
    location: Location,
) -> list[Relationship]:
    relationships: list[Relationship] = []

    if not (location.coordinates and location.coordinates.real_path):
        return relationships

    lockfile_path = location.coordinates.real_path
    root_location = find_root_location(root_package, lockfile_path)

    if not root_location:
        return relationships

    packages_by_name = group_packages_by_name(packages, root_package.id_)

    for dep_name in direct_dependencies:
        for dep_pkg in packages_by_name.get(dep_name, []):
            add_dependency_to_root_relationship(
                dep_pkg, root_package, root_location, lockfile_path, relationships
            )

    return relationships
