from concurrent.futures import (
    ThreadPoolExecutor,
)
from fluid_sbom.artifact.relationship import (
    Relationship,
    RelationshipType,
)
from fluid_sbom.file.location_read_closer import (
    LocationReadCloser,
)
from fluid_sbom.file.resolver import (
    Resolver,
)
from fluid_sbom.pkg.cataloger.debian.package import (
    new_dpkg_package,
)
from fluid_sbom.pkg.cataloger.debian.parse_dpkg_info_files import (
    parse_dpkg_conffile_info,
)
from fluid_sbom.pkg.cataloger.generic.parser import (
    Environment,
)
from fluid_sbom.pkg.dpkg import (
    DpkgDBEntry,
)
from fluid_sbom.pkg.package import (
    Package,
)
from fluid_sbom.utils.file import (
    parse_bytes,
)
from multiprocessing import (
    cpu_count,
)
from pydantic import (
    BaseModel,
    ConfigDict,
)
import re
from typing import (
    cast,
    TextIO,
)


class DpkgField(BaseModel):
    name: str
    value: str
    model_config = ConfigDict(frozen=True)


def _split_deps(value: str) -> list[str]:
    fields = value.split(",")
    return [field.strip() for field in fields if field.strip()]


def extract_source_version(source: str) -> tuple[str, str | None]:
    if match_result := re.compile(
        r"(?P<name>\S+)( \((?P<version>.*)\))?"
    ).match(source):
        result = match_result.groupdict()
        return result["name"], result["version"] or ""
    return source, ""


def handle_new_key_value(line: str) -> tuple[str | None, str]:
    if ":" in line and not line.startswith(" "):
        key, value = line.split(":", 1)
        value = value.strip()
        match key:
            case "Installed-Size":
                value = str(parse_bytes(value))
        return key, value

    return None, line


def parse_dpkg_status(reader: TextIO) -> list[DpkgDBEntry]:
    content = reader.read()
    entries = []
    for package in content.strip().split("\n\n"):
        lines = package.split("\n")
        data = {}
        prev_key = ""
        for line in lines:
            key, value = handle_new_key_value(line)
            if key is not None:
                data[key] = value
                prev_key = key
            elif prev_key in data:
                data[prev_key] = f"{data[prev_key]}\n{value}"
        if all(not value for value in data.values()):
            continue
        source_name, source_version = extract_source_version(
            data.get("Source", "")
        )
        entry = DpkgDBEntry(
            package=data.get("Package", ""),
            source=source_name,
            version=data.get("Version", ""),
            source_version=source_version or "",
            architecture=data.get("Architecture", ""),
            maintainer=data.get("Maintainer", ""),
            installed_size=int(data.get("Installed-Size", "0")),
            description=data.get("Description", "").strip(),
            provides=_split_deps(data.get("Provides", "")),
            dependencies=_split_deps(data.get("Depends", "")),
            pre_dependencies=_split_deps(data.get("Pre-Depends", "")),
            files=parse_dpkg_conffile_info(data.get("Conffiles", "")),
        )
        entries.append(entry)

    return entries


def parse_dpkg_db(
    resolver: Resolver | None,
    _env: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    entries: list[DpkgDBEntry] = parse_dpkg_status(reader.read_closer)
    with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
        result = [
            pkg if isinstance(pkg, Package) else (pkg[0], pkg[1])
            for pkg in filter(
                None,
                executor.map(
                    lambda entry: new_dpkg_package(
                        entry,
                        reader.location,
                        resolver,
                        _env.linux_release if _env else None,
                    ),
                    entries,
                ),
            )
            if isinstance(pkg, (Package, tuple))
        ]
        packages = [
            item
            for pkg in result
            for item in (pkg if isinstance(pkg, tuple) else [pkg])
        ]
    return packages, associate_relationships(packages)


def associate_relationships(  # NOSONAR
    pkgs: list[Package],
) -> list[Relationship]:
    lookup: dict[str, list[Package]] = {}
    relationships: list[Relationship] = []
    for pkg in pkgs:
        lookup.setdefault(pkg.name, []).append(pkg)
    for pkg in pkgs:
        metadata = cast(DpkgDBEntry, pkg.metadata)
        for provides in metadata.provides or []:
            key = strip_version_specifier(provides)
            lookup.setdefault(key, []).append(pkg)

    for pkg in pkgs:
        metadata = cast(DpkgDBEntry, pkg.metadata)
        all_deps = [
            *(metadata.dependencies or []),
            *(metadata.pre_dependencies or []),
        ]
        for dep_specifier in all_deps:
            deps = split_package_choices(dep_specifier)
            for dep in deps:
                for dep_pkg in lookup.get(dep, []):
                    relationships.append(
                        Relationship(
                            from_=dep_pkg,
                            to_=pkg,
                            type=RelationshipType.DEPENDENCY_OF_RELATIONSHIP,
                            data=None,
                        )
                    )

    return relationships


def strip_version_specifier(item: str) -> str:
    # Define the characters that indicate the start of a version specifier
    specifiers = "[(<>="

    # Find the index of the first occurrence of any specifier character
    index = next(
        (i for i, char in enumerate(item) if char in specifiers), None
    )

    # If no specifier character is found, return the original string
    if index is None:
        return item.strip()

    # Return the substring up to the first specifier character, stripped of
    # leading/trailing whitespace
    return item[:index].strip()


def split_package_choices(value: str) -> list[str]:
    fields = value.split("|")
    return [
        strip_version_specifier(field) for field in fields if field.strip()
    ]
