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.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
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)

    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 = new_simple_npm_package(new_location, name, version)
        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
