import re
from typing import cast

import packaging.version
import pefile

from labels.model.file import Location, LocationReadCloser
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.dotnet.package_builder import new_dotnet_package

SPACE_REGEX = re.compile(r"[\s]+")
NUMBER_REGEX = re.compile(r"\d")
VERSION_PUNCTUATION_REGEX = re.compile(r"[.,]+")


def parse_dotnet_portable_executable(
    _resolver: Resolver | None,
    _environment: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    pe_representation = _safe_parse_version_pe_file(reader)
    if not pe_representation:
        return [], []

    packages = _collect_packages(pe_representation, reader.location)

    return packages, []


def _safe_parse_version_pe_file(reader: LocationReadCloser) -> pefile.PE | None:
    if not reader.location.coordinates:
        return None

    try:
        pe_representation = pefile.PE(reader.location.coordinates.real_path, fast_load=False)
    except pefile.PEFormatError:  # type: ignore[misc]
        return None

    return pe_representation


def _collect_packages(pe_representation: pefile.PE, location: Location) -> list[Package]:
    packages: list[Package] = []

    version_resource = _parse_version_resource(pe_representation)
    if not version_resource:
        return packages

    name = _find_name(version_resource)
    version = _find_version(version_resource)

    dotnet_package = new_dotnet_package(name, version, location)
    if dotnet_package:
        packages.append(dotnet_package)

    return packages


def _parse_version_resource(portable: pefile.PE) -> dict[str, str] | None:
    if not hasattr(portable, "VS_VERSIONINFO"):
        return None

    for idx, _ in enumerate(portable.VS_VERSIONINFO):
        if not hasattr(portable, "FileInfo") or len(portable.FileInfo) <= idx:
            continue

        parsed_file_info = _process_file_info(portable.FileInfo[idx])
        if parsed_file_info:
            return parsed_file_info

    return None


def _process_file_info(
    file_info: list["pefile._StringFileInfo"],
) -> dict[str, str] | None:
    for entry in file_info:
        if not hasattr(entry, "StringTable"):
            continue

        parsed_file_info = _process_string_table(entry.StringTable)
        if parsed_file_info:
            return parsed_file_info

    return None


def _process_string_table(
    string_table: list["pefile._StringTable"],
) -> dict[str, str]:
    string_table_dict: dict[str, str] = {}

    for string_table_entry in string_table:
        string_table_dict["LangID"] = string_table_entry.LangID.decode("utf-8")
        for key, value in string_table_entry.entries.items():
            string_table_dict[key.decode("utf-8")] = value.decode("utf-8")

    return string_table_dict


def _find_name(version_resources: dict[str, str]) -> str:
    name_fields = [
        "ProductName",
        "FileDescription",
        "InternalName",
        "OriginalFilename",
    ]

    if _is_microsoft(version_resources):
        name_fields = [
            "FileDescription",
            "InternalName",
            "OriginalFilename",
            "ProductName",
        ]

    for field in name_fields:
        value = _space_normalize(version_resources.get(field, ""))
        if value:
            return value

    return ""


def _is_microsoft(version_resources: dict[str, str]) -> bool:
    company_name = version_resources.get("CompanyName", "").lower()
    product_name = version_resources.get("ProductName", "").lower()

    return "microsoft" in company_name or "microsoft" in product_name


def _space_normalize(value: str) -> str:
    value = value.strip()

    if value == "":
        return ""

    value = value.encode("utf-8", "replace").decode("utf-8")

    value = SPACE_REGEX.sub(" ", value)

    value = re.sub(r"[\x00-\x1f]", "", value)

    value = SPACE_REGEX.sub(" ", value)

    return value.strip()


def _find_version(version_resources: dict[str, str]) -> str:
    raw_product_version = version_resources.get("ProductVersion", "")
    raw_file_version = version_resources.get("FileVersion", "")

    product_version = _extract_version(raw_product_version)
    file_version = _extract_version(raw_file_version)

    if not product_version and not file_version:
        return ""

    if not product_version:
        return file_version

    if not file_version:
        return product_version

    semantic_choice = _keep_greater_semantic_version(product_version, file_version)
    if semantic_choice:
        return semantic_choice

    return _choose_by_detail_and_numbers(product_version, file_version)


def _choose_by_detail_and_numbers(product_version: str, file_version: str) -> str:
    product_detail = _punctuation_count(product_version)
    file_detail = _punctuation_count(file_version)

    has_num_product = _contains_number(product_version)
    has_num_file = _contains_number(file_version)

    if has_num_file and file_detail > 0 and (not has_num_product or file_detail > product_detail):
        return file_version

    if has_num_product:
        return product_version
    if has_num_file:
        return file_version

    return product_version or file_version


def _keep_greater_semantic_version(product_version: str, file_version: str) -> str:
    try:
        semantic_product_version = packaging.version.parse(product_version)
    except ValueError:
        return ""

    try:
        semantic_file_version = packaging.version.parse(file_version)
    except ValueError:
        return product_version

    if semantic_product_version == semantic_file_version:
        return ""

    if semantic_file_version > semantic_product_version:
        return file_version

    return product_version


def _punctuation_count(string: str) -> int:
    return len(cast("list[str]", VERSION_PUNCTUATION_REGEX.findall(string)))


def _extract_version(version: str) -> str:
    version = version.strip()

    result = ""

    for index, character in enumerate(version.split()):
        if _contains_number(result) and not _contains_number(character):
            return result

        if index == 0:
            result = character
        else:
            result += " " + character

    return result


def _contains_number(string: str) -> bool:
    return any(character.isdigit() for character in string)
