import logging
from collections.abc import (
    Iterator,
)
from copy import (
    deepcopy,
)
from typing import (
    cast,
)

from more_itertools import (
    flatten,
)
from packageurl import (
    PackageURL,
)
from pydantic import (
    ValidationError,
)

from labels.artifact.relationship import (
    Relationship,
    RelationshipType,
)
from labels.file.dependency_type import (
    DependencyType,
)
from labels.file.location_read_closer import (
    LocationReadCloser,
)
from labels.file.resolver import (
    Resolver,
)
from labels.internal.collection.json import (
    parse_json_with_tree_sitter,
)
from labels.internal.collection.types import (
    IndexedDict,
    ParsedValue,
)
from labels.model.core import (
    Language,
    Package,
    PackageType,
)
from labels.pkg.cataloger.generic.parser import (
    Environment,
)
from labels.utils.strings import format_exception

LOGGER = logging.getLogger(__name__)
EMPTY_DICT: IndexedDict[str, ParsedValue] = IndexedDict()


def _get_relationships(
    packages: list[Package],
    dependencies: dict[str, ParsedValue],
) -> list[Relationship]:
    results: list[Relationship] = []
    for package_name, depens in dependencies.items():
        if not isinstance(depens, IndexedDict):
            continue
        deps = (p for p in packages for dep_name in depens if dep_name == p.name)
        if current_package := next(
            (x for x in packages if x.name == package_name),
            None,
        ):
            results.extend(
                Relationship(
                    from_=x,
                    to_=current_package,
                    type=RelationshipType.DEPENDENCY_OF_RELATIONSHIP,
                )
                for x in deps
            )
    return results


def parse_dotnet_package_lock(
    _resolver: Resolver | None,
    _env: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    package_json = cast(
        IndexedDict[str, ParsedValue],
        parse_json_with_tree_sitter(reader.read_closer.read()),
    )
    packages: list[Package] = []
    target_deps: ParsedValue = package_json.get("dependencies", IndexedDict())
    if not isinstance(target_deps, IndexedDict):
        LOGGER.warning("No deps found in package JSON")
        return ([], [])
    dependencies: dict[str, ParsedValue] = {}

    for package_name, package_value in cast(
        Iterator[IndexedDict[str, ParsedValue]],
        flatten(x.items() for x in target_deps.values() if isinstance(x, IndexedDict)),
    ):
        if not isinstance(package_value, IndexedDict):
            continue

        is_transitive: str = package_value.get("type", "") == "Transitive"

        version: str | None = package_value.get("resolved")

        if not package_name or not version:
            continue

        location = deepcopy(reader.location)
        if location.coordinates:
            location.coordinates.line = package_value.position.start.line
            location.dependency_type = (
                DependencyType.TRANSITIVE if is_transitive else DependencyType.DIRECT
            )

        dependencies[package_name] = package_value.get("dependencies", EMPTY_DICT)
        try:
            packages.append(
                Package(
                    name=package_name,
                    version=version,
                    locations=[location],
                    licenses=[],
                    type=PackageType.DotnetPkg,
                    language=Language.DOTNET,
                    metadata=None,
                    p_url=PackageURL(
                        type="nuget",
                        namespace="",
                        name=package_name,
                        version=version,
                        qualifiers={},
                        subpath="",
                    ).to_string(),
                ),
            )
        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": location.path(),
                    },
                },
            )
            continue

    relationships: list[Relationship] = _get_relationships(packages, dependencies)

    return packages, relationships
