import re
from typing import cast

from bs4 import BeautifulSoup, Tag

from labels.model.file import LocationReadCloser
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.dotnet.package_builder import new_dotnet_package
from labels.parsers.cataloger.utils import get_enriched_location

PACKAGE = re.compile(r".+\\packages\\(?P<package_info>[^\s\\]*)\\.+")
DEP_INFO = re.compile(r"(?P<package_name>.*?)\.(?P<version>\d+[^\s]*)$")


PACKAGE_REFERENCE_ELEMENT = "packagereference"
PRIVATE_ASSETS_ELEMENT = "privateassets"
REFERENCE_ELEMENT = "reference"
HINT_PATH_ELEMENT = "hintpath"


def parse_csproj(
    _resolver: Resolver | None,
    _environment: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    file_content = BeautifulSoup(reader.read_closer.read(), features="html.parser")

    packages = _collect_packages(file_content, reader)

    return packages, []


def _collect_packages(root: BeautifulSoup, reader: LocationReadCloser) -> list[Package]:
    packages: list[Package] = []

    package_references: list[Tag] = root.find_all(PACKAGE_REFERENCE_ELEMENT, recursive=True)
    for raw_package in package_references:
        package_name = cast("str | None", raw_package.get("include"))
        version = cast("str | None", raw_package.get("version"))

        is_dev = _is_dev_dependency(raw_package)
        new_location = get_enriched_location(
            reader.location,
            line=raw_package.sourceline,
            is_transitive=False,
            is_dev=is_dev,
            package_manager=PackageManager.NUGET,
        )

        package = new_dotnet_package(package_name, version, new_location)
        if package:
            packages.append(package)

    packages.extend(_collect_package_from_reference_dependencies(root, reader))

    return packages


def _is_dev_dependency(pkg: Tag) -> bool:
    checking_attr = "all"

    element = pkg.find(PRIVATE_ASSETS_ELEMENT)
    if isinstance(element, Tag):
        return element.get_text(strip=True).lower() == checking_attr

    attr = pkg.get(PRIVATE_ASSETS_ELEMENT)
    if isinstance(attr, str):
        return attr.strip().lower() == checking_attr

    return False


def _collect_package_from_reference_dependencies(
    raw_package: Tag, reader: LocationReadCloser
) -> list[Package]:
    packages: list[Package] = []
    references: list[Tag] = raw_package.find_all(REFERENCE_ELEMENT, recursive=True)

    for reference in references:
        package = _extract_package_from_reference(reference, reader)
        if package:
            packages.append(package)

    return packages


def _extract_package_from_reference(reference: Tag, reader: LocationReadCloser) -> Package | None:
    package = _extract_from_hint_path(reference, reader)
    if package:
        return package

    return _extract_from_include_attribute(reference, reader)


def _extract_from_hint_path(reference: Tag, reader: LocationReadCloser) -> Package | None:
    dll_path = reference.find(HINT_PATH_ELEMENT)
    if not isinstance(dll_path, Tag):
        return None

    dll_package = PACKAGE.match(dll_path.text)
    if not dll_package:
        return None

    package_info = DEP_INFO.match(dll_package.group("package_info"))
    if not package_info:
        return None

    new_location = get_enriched_location(
        reader.location,
        line=dll_path.sourceline,
        is_transitive=False,
        package_manager=PackageManager.NUGET,
    )

    name: str = package_info.group("package_name").lower()
    version: str = package_info.group("version")

    return new_dotnet_package(name, version, new_location)


def _extract_from_include_attribute(raw_package: Tag, reader: LocationReadCloser) -> Package | None:
    include = raw_package.get("include")
    if not isinstance(include, str):
        return None

    include_info = include.replace(" ", "").split(",")

    package_name = include_info[0].strip()
    version = _get_version(include_info)

    if not package_name or not version:
        return None

    new_location = get_enriched_location(
        reader.location,
        line=raw_package.sourceline,
        is_transitive=False,
        package_manager=PackageManager.NUGET,
    )

    return new_dotnet_package(package_name, version, new_location)


def _get_version(include_info: list[str]) -> str | None:
    return next(
        (
            package_info.lstrip("Version=")
            for package_info in include_info
            if package_info.startswith("Version=")
        ),
        None,
    )
