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

from debputy import elf_util
from debputy.elf_util import ELF_LINKING_TYPE_DYNAMIC
from debputy.filesystem_scan import FSPath
from debputy.packages import BinaryPackage
from debputy.plugin.api import VirtualPath
from debputy.util import _warn

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)"),
]
SYMBOLS_TEMPLATE_FILE_DEFAULT_PATTERNS = [
    "debian/{{PACKAGE}}.symbols.{{_DEBPUTY_PKG_ARCH}}",
    "debian/{{PACKAGE}}.symbols.{{_DEBPUTY_PKG_ARCH_OS}}",
    "debian/{{PACKAGE}}.symbols",
]
SHLIBS_FILE_DEFAULT_PATTERNS = [
    "debian/{{PACKAGE}}.shlibs.{{_DEBPUTY_PKG_ARCH}}",
    "debian/{{PACKAGE}}.shlibs.{{_DEBPUTY_PKG_ARCH_OS}}",
    "debian/{{PACKAGE}}.shlibs",
]


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


def _resolve_dpkg_file(
    binary_package: BinaryPackage,
    manifest: "HighLevelManifest",
    path_patterns: List[str],
    single_package_fallback_path: Optional[str],
) -> Optional[str]:
    substitution = manifest.package_state_for(binary_package.name).substitution
    definition_source = "<built-in: resolving symbols file>"

    for path_pattern in path_patterns:
        path = substitution.substitute(path_pattern, definition_source)
        if os.path.isfile(path):
            return path
    if single_package_fallback_path is not None and os.path.isfile(
        single_package_fallback_path
    ):
        if len(list(manifest.all_packages)) == 1:
            return single_package_fallback_path
        _warn(
            f"Ignoring {single_package_fallback_path} for {binary_package.name} since this is a multi-binary package"
        )
    return None


def locate_symbols_template_file(
    binary_package: BinaryPackage,
    manifest: "HighLevelManifest",
) -> Optional[str]:
    # FIXME: Support manifest configuration
    path_patterns = SYMBOLS_TEMPLATE_FILE_DEFAULT_PATTERNS
    return _resolve_dpkg_file(
        binary_package,
        manifest,
        path_patterns,
        "debian/symbols"
        if path_patterns is SYMBOLS_TEMPLATE_FILE_DEFAULT_PATTERNS
        else None,
    )


def locate_shlibs_file(
    binary_package: BinaryPackage,
    manifest: "HighLevelManifest",
) -> Optional[str]:
    # FIXME: Support manifest configuration
    path_patterns = SHLIBS_FILE_DEFAULT_PATTERNS
    return _resolve_dpkg_file(
        binary_package,
        manifest,
        path_patterns,
        "debian/shlibs" if path_patterns is SHLIBS_FILE_DEFAULT_PATTERNS else None,
    )


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 compute_shlibs(
    binary_package: BinaryPackage,
    root_dir: str,
    fs_root: FSPath,
    manifest: "HighLevelManifest",
    soname_info_list: List[SONAMEInfo],
    udeb_package_name: Optional[str],
    generated_triggers: Set[str],
) -> None:
    assert not binary_package.is_udeb
    symbols_template_file = locate_symbols_template_file(binary_package, manifest)
    shlibs_file = os.path.join(root_dir, "DEBIAN", "shlibs")
    need_ldconfig = False
    so_files = elf_util.find_all_elf_files(
        fs_root,
        with_linking_type=ELF_LINKING_TYPE_DYNAMIC,
    )
    provided_shlibs_file = locate_shlibs_file(binary_package, manifest)

    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(root_dir, "DEBIAN", "symbols")
        symbols_cmd = [
            "dpkg-gensymbols",
            f"-p{binary_package.name}",
            f"-I{symbols_template_file}",
            f"-P{root_dir}",
            f"-O{symbols_file}",
        ]

        if so_files:
            symbols_cmd.extend(f"-e{x.fs_path}" for x in so_files)
            print(f"Executing for {binary_package.name}: {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")
