import dataclasses
import os
import re
import shutil
import stat
import subprocess
from contextlib import suppress
from typing import Optional, Set, List, Tuple, TYPE_CHECKING, Dict

from debputy import elf_util
from debputy.elf_util import ELF_LINKING_TYPE_DYNAMIC
from debputy.filesystem_scan import FSPath
from debputy.packager_provided_files import PackagerProvidedFile
from debputy.packages import BinaryPackage
from debputy.plugin.api import VirtualPath
from debputy.util import print_command

if TYPE_CHECKING:
    from debputy.highlevel_manifest import HighLevelManifest


HAS_SONAME = re.compile(r"\s+SONAME\s+(\S+)")
SONAME_FORMATS = [
    re.compile(r"\s+SONAME\s+((.*)[.]so[.](.*))"),
    re.compile(r"\s+SONAME\s+((.*)-(\d.*)[.]so)"),
]


@dataclasses.dataclass
class SONAMEInfo:
    path: VirtualPath
    full_soname: str
    library: str
    major_version: Optional[str]


def extract_so_name(
    binary_package: BinaryPackage,
    path: VirtualPath,
) -> Optional[SONAMEInfo]:
    objdump = binary_package.cross_command("objdump")
    output = subprocess.check_output([objdump, "-p", path.fs_path], encoding="utf-8")
    for r in SONAME_FORMATS:
        m = r.search(output)
        if m:
            full_soname, library, major_version = m.groups()
            return SONAMEInfo(path, full_soname, library, major_version)
    m = HAS_SONAME.search(output)
    if not m:
        return None
    full_soname = m.group(1)
    return SONAMEInfo(path, full_soname, full_soname, None)


def extract_soname_info(
    binary_package: BinaryPackage,
    fs_root: FSPath,
) -> List[SONAMEInfo]:
    so_files = elf_util.find_all_elf_files(
        fs_root,
        with_linking_type=ELF_LINKING_TYPE_DYNAMIC,
    )
    result = []
    for so_file in so_files:
        soname_info = extract_so_name(binary_package, so_file)
        if not soname_info:
            continue
        result.append(soname_info)
    return result


def _compute_shlibs_content(
    binary_package: BinaryPackage,
    manifest: "HighLevelManifest",
    soname_info_list: List[SONAMEInfo],
    udeb_package_name: Optional[str],
) -> Tuple[List[str], bool]:
    shlibs_file_contents = []
    udeb_lines = []
    unversioned_so_seen = False
    seen = set()
    version = manifest.package_state_for(binary_package.name).binary_version
    if version is not None:
        upstream_version = re.sub(r"-[^-]+$", "", version)
    else:
        upstream_version = manifest.substitution.substitute(
            "{{DEB_VERSION_EPOCH_UPSTREAM}}", "<internal-usage>"
        )

    dependency = f"{binary_package.name} (>= {upstream_version})"
    udeb_dependency = (
        f"{udeb_package_name} (>= {upstream_version})" if udeb_package_name else None
    )

    for soname_info in soname_info_list:
        if soname_info.major_version is None:
            unversioned_so_seen = True
            continue
        content_line = (
            f"{soname_info.library} {soname_info.major_version} {dependency}\n"
        )
        if content_line in seen:
            continue
        seen.add(content_line)
        shlibs_file_contents.append(content_line)
        if udeb_dependency:
            udeb_lines.append(
                f"udeb: {soname_info.library} {soname_info.major_version} {udeb_dependency}\n"
            )

    shlibs_file_contents.extend(udeb_lines)

    return shlibs_file_contents, unversioned_so_seen


def _resolve_provided_file(
    basename: str,
    reserved_packager_provided_files: Dict[str, List[PackagerProvidedFile]],
) -> Optional[str]:
    matches = reserved_packager_provided_files.get(basename)
    if matches is None:
        return None
    assert len(matches) < 2
    if matches:
        return os.path.join("debian", matches[0].source_basename)
    return None


def compute_shlibs(
    binary_package: BinaryPackage,
    control_output_dir: str,
    fs_root: FSPath,
    manifest: "HighLevelManifest",
    soname_info_list: List[SONAMEInfo],
    udeb_package_name: Optional[str],
    generated_triggers: Set[str],
    reserved_packager_provided_files: Dict[str, List[PackagerProvidedFile]],
) -> None:
    assert not binary_package.is_udeb
    shlibs_file = os.path.join(control_output_dir, "shlibs")
    need_ldconfig = False
    so_files = elf_util.find_all_elf_files(
        fs_root,
        with_linking_type=ELF_LINKING_TYPE_DYNAMIC,
    )
    provided_shlibs_file = _resolve_provided_file(
        "shlibs",
        reserved_packager_provided_files,
    )
    symbols_template_file = _resolve_provided_file(
        "symbols",
        reserved_packager_provided_files,
    )

    if provided_shlibs_file:
        need_ldconfig = True
        unversioned_so_seen = False
        shutil.copyfile(provided_shlibs_file, shlibs_file)
    else:
        shlibs_file_contents, unversioned_so_seen = _compute_shlibs_content(
            binary_package,
            manifest,
            soname_info_list,
            udeb_package_name,
        )

        if shlibs_file_contents:
            need_ldconfig = True
            with open(shlibs_file, "wt", encoding="utf-8") as fd:
                fd.writelines(shlibs_file_contents)

    if symbols_template_file:
        symbols_file = os.path.join(control_output_dir, "symbols")
        symbols_cmd = [
            "dpkg-gensymbols",
            f"-p{binary_package.name}",
            f"-I{symbols_template_file}",
            f"-P{control_output_dir}",
            f"-O{symbols_file}",
        ]

        if so_files:
            symbols_cmd.extend(f"-e{x.fs_path}" for x in so_files)
            print_command(*symbols_cmd)
            # TODO: ensure we do the dpkg-gensymbols call for all packages before erroring out.
            # The kde symbols helper depend on that behaviour.
            subprocess.check_call(symbols_cmd)

        with suppress(FileNotFoundError):
            st = os.stat(symbols_file)
            if stat.S_ISREG(st.st_mode) and st.st_size == 0:
                os.unlink(symbols_file)
            elif unversioned_so_seen:
                need_ldconfig = True

    if need_ldconfig:
        generated_triggers.add("activate-noawait ldconfig")
