import re

import pefile
import semver
from packageurl import (
    PackageURL,
)
from pydantic import (
    BaseModel,
)

from labels.artifact.relationship import (
    Relationship,
)
from labels.context import (
    LOGGER,
)
from labels.file.location_read_closer import (
    LocationReadCloser,
)
from labels.file.resolver import (
    Resolver,
)
from labels.model.core import (
    Language,
    Package,
    PackageType,
)
from labels.pkg.cataloger.generic.parser import (
    Environment,
)


class DotnetPortableExecutableEntry(BaseModel):
    assembly_version: str | None = None
    legal_copyright: str | None = None
    company_name: str | None = None
    product_name: str | None = None
    product_version: str | None = None
    comments: str | None = None
    internal_name: str | None = None


# Define the regular expressions
space_regex = re.compile(r"[\s]+")
number_regex = re.compile(r"\d")
version_punctuation_regex = re.compile(r"[.,]+")


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

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

        stringtable_dict = process_file_info(
            potable.FileInfo[idx],  # type: ignore
        )
        if stringtable_dict:
            return stringtable_dict

    return None


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

        stringtable_dict = process_string_table(entry.StringTable)
        if stringtable_dict:
            return stringtable_dict

    return None


def process_string_table(
    string_table: list[pefile.Structure],
) -> dict[str, str]:
    stringtable_dict = {}
    for st_entry in string_table:
        stringtable_dict["LangID"] = st_entry.LangID  # type: ignore
        for key, value in st_entry.entries.items():  # type: ignore
            stringtable_dict[key.decode("utf-8")] = value.decode("utf-8")
    return stringtable_dict


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:
    # Trim leading and trailing whitespace
    value = value.strip()

    if value == "":
        return ""

    # Ensure valid UTF-8 text
    value = value.encode("utf-8", "replace").decode("utf-8")

    value = space_regex.sub(" ", value)

    # Remove other non-space, non-printable characters
    value = re.sub(r"[\x00-\x1f]", "", value)

    # Consolidate all space characters again
    value = space_regex.sub(" ", value)

    # Finally, remove any remaining surrounding whitespace
    return value.strip()


def find_name(version_resources: dict[str, str]) -> str:
    # Define the order of fields to check for the name
    name_fields = [
        "ProductName",
        "FileDescription",
        "InternalName",
        "OriginalFilename",
    ]

    # Check if the version resources are from Microsoft
    if is_microsoft(version_resources):
        # Adjust the order of fields for Microsoft-authored files
        name_fields = [
            "FileDescription",
            "InternalName",
            "OriginalFilename",
            "ProductName",
        ]

    # Iterate over the fields to find a non-empty, normalized name
    for field in name_fields:
        value = space_normalize(version_resources.get(field, ""))
        if value:
            return value

    # Return an empty string if no valid name is found
    return ""


def contains_number(string: str) -> bool:
    return any(char.isdigit() for char in string)


def extract_version(version: str) -> str:
    # Trim leading and trailing whitespace
    version = version.strip()

    out = ""

    # Split the version string into fields and iterate over them
    for index, char in enumerate(version.split()):
        # If the output already has a number but the current segment does not,
        # return the output
        if contains_number(out) and not contains_number(char):
            return out

        # Append the current field to the output
        if index == 0:
            out = char
        else:
            out += " " + char

    return out


def keep_greater_semantic_version(product_version: str, file_version: str) -> str:
    try:
        semantic_product_version = semver.VersionInfo.parse(product_version)
    except ValueError:
        LOGGER.debug(
            ("Unable to create semantic version from portable executable product version %s"),
            product_version,
        )
        return ""

    try:
        semantic_file_version = semver.VersionInfo.parse(file_version)
    except ValueError:
        LOGGER.debug(
            ("Unable to create semantic version from portable executable file version %s"),
            file_version,
        )
        return product_version

    # Make no choice when they are semantically equal
    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:
    # Find all matches of the punctuation regex and return their count
    return len(version_punctuation_regex.findall(string))


def find_version(version_resources: dict[str, str]) -> str:
    product_version = extract_version(version_resources.get("ProductVersion", ""))
    file_version = extract_version(version_resources.get("FileVersion", ""))

    semantic_version_compare_result = keep_greater_semantic_version(product_version, file_version)

    if semantic_version_compare_result:
        return semantic_version_compare_result

    product_version_detail = punctuation_count(product_version)
    file_version_detail = punctuation_count(file_version)

    if contains_number(product_version) and product_version_detail >= file_version_detail:
        return product_version

    if contains_number(file_version) and file_version_detail > 0:
        return file_version

    if contains_number(product_version):
        return product_version

    if contains_number(file_version):
        return file_version

    return product_version


def build_dot_net_package(
    version_resource: dict[str, str],
    reader: LocationReadCloser,
) -> Package | None:
    name = find_name(version_resource)
    if not name:
        LOGGER.debug(
            "Unable to find name for portable executable in file %s",
            reader.location.path(),
        )
        return None
    version = find_version(version_resource)
    if not version:
        LOGGER.debug(
            "Unable to find version for portable executable in file %s",
            reader.location.path(),
        )
        return None
    metadata = DotnetPortableExecutableEntry(
        assembly_version=version_resource.get("Assembly Version"),
        legal_copyright=version_resource.get("LegalCopyright"),
        comments=version_resource.get("Comments"),
        internal_name=version_resource.get("InternalName"),
        company_name=version_resource.get("CompanyName"),
        product_name=version_resource.get("ProductName"),
        product_version=version_resource.get("ProductVersion"),
    )

    return Package(
        name=name,
        version=version,
        locations=[reader.location],
        type=PackageType.DotnetPkg,
        language=Language.DOTNET,
        p_url=PackageURL(
            type="nuget",
            namespace="",
            name=name,
            version=version,
            qualifiers={},
            subpath="",
        ).to_string(),
        metadata=metadata,
        licenses=[],
    )


def parse_dotnet_portable_executable(
    _resolver: Resolver | None,
    _env: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    if not reader.location.coordinates:
        return [], []
    try:
        pe_representation = pefile.PE(reader.location.coordinates.real_path, fast_load=False)
    except pefile.PEFormatError:
        return [], []

    version_resource = parse_version_resource(pe_representation)
    if not version_resource:
        LOGGER.debug(
            ("Unable to find version resource for portable executable in file %s"),
            reader.location.path(),
        )
        return [], []
    dotnet_package = build_dot_net_package(version_resource, reader)
    if not dotnet_package:
        LOGGER.debug(
            "Unable to build package for portable executable in file %s",
            reader.location.path(),
        )
        return [], []
    return [dotnet_package], []
