from cbuild.core import logger, paths, template, chroot
from cbuild.apk import sign as asign

import glob
import shlex
import pathlib
import subprocess

_scriptlets = {
    ".pre-install": True,
    ".pre-upgrade": True,
    ".pre-deinstall": True,
    ".post-install": True,
    ".post-upgrade": True,
    ".post-deinstall": True,
    ".trigger": True,
}

def genpkg(pkg, repo, arch, binpkg):
    if not pkg.destdir.is_dir():
        pkg.log_warn(f"cannot find pkg destdir, skipping...")
        return

    binpath = repo / binpkg

    repo.mkdir(parents = True, exist_ok = True)

    pargs = [
        "--info", f"name:{pkg.pkgname}",
        "--info", f"version:{pkg.pkgver}-r{pkg.pkgrel}",
        "--info", f"description:{pkg.pkgdesc}",
        "--info", f"arch:{arch}",
        "--info", f"license:{pkg.license}",
        "--info", f"origin:{pkg.rparent.pkgname}",
        "--info", f"maintainer:{pkg.rparent.maintainer}",
        "--info", f"url:{pkg.rparent.url}",
        "--info", f"build-time:{int(pkg.rparent.source_date_epoch)}"
    ]

    # only record commits in non-dirty repos
    if pkg.rparent.git_revision and not pkg.rparent.git_dirty:
        pargs += ["--info", f"repo-commit:{pkg.rparent.git_revision}"]

    # dependencies of any sort
    deps = []

    # explicit package depends
    for c in pkg.depends:
        ploc = c.find("!")
        if ploc > 0:
            deps.append(c[0:ploc].removeprefix("virtual:"))
        else:
            deps.append(c.removeprefix("virtual:"))

    # sort before adding more
    deps.sort()

    # shlib requires
    if hasattr(pkg, "so_requires"):
        pkg.so_requires.sort()
        deps += map(lambda v: f"so:{v}", sorted(pkg.so_requires))

    # .pc file requires
    if hasattr(pkg, "pc_requires"):
        deps += map(lambda v: f"pc:{v}", sorted(pkg.pc_requires))

    if len(deps) > 0:
        pargs += ["--info", f"depends:{' '.join(deps)}"]

    # install-if
    if len(pkg.install_if) > 0:
        pargs += ["--info", f"install-if:{' '.join(pkg.install_if)}"]

    # providers
    provides = []

    # explicit provides
    provides += sorted(pkg.provides)

    # shlib provides
    if hasattr(pkg, "aso_provides"):
        provides += map(
            lambda x: f"so:{x[0]}={x[1]}",
            sorted(pkg.aso_provides, key = lambda x: x[0])
        )

    # .pc file provides
    if hasattr(pkg, "pc_provides"):
        provides += map(
            lambda x: f"pc:{x}", sorted(pkg.pc_provides)
        )

    # command provides
    if hasattr(pkg, "cmd_provides"):
        provides += map(
            lambda x: f"cmd:{x}", sorted(pkg.cmd_provides)
        )

    if len(provides) > 0:
        pargs += ["--info", f"provides:{' '.join(provides)}"]

    # replaces
    replaces = sorted(pkg.replaces)

    if len(replaces) > 0:
        pargs += ["--info", f"replaces:{' '.join(replaces)}"]

    # priority
    if pkg.priority > 0:
        pargs += ["--info", f"priority:{pkg.priority}"]

    # scripts including trigger scripts
    sclist = []

    for f in (pkg.statedir / "scriptlets").glob(f"{pkg.pkgname}.*"):
        if f.is_file() and f.suffix in _scriptlets:
            sclist.append(f.suffix[1:])

    sclist.sort()

    for f in sclist:
        # get in-chroot path to that
        scp = pkg.rparent.chroot_builddir / (
            pkg.statedir.relative_to(pkg.rparent.builddir)
        ) / f"scriptlets/{pkg.pkgname}.{f}"
        # pass it
        pargs += ["--script", f"{f}:{scp}"]

    # trigger paths
    for t in pkg.triggers:
        p = pathlib.Path(t)
        if not p or not p.is_absolute():
            pkg.error(f"invalid trigger path: {t}")
        pargs += ["--trigger", t]

    # signing key
    signkey = asign.get_keypath(pkg.rparent.signing_key)
    if signkey:
        if pkg.rparent.stage > 0:
            pargs += ["--sign-key", f"/tmp/{signkey.name}"]
        else:
            pargs += ["--sign-key", signkey]

    # generate a wrapper script for fakeroot ownership
    wscript = """
#!/bin/sh
set -e
"""

    needscript = False

    # at this point permissions are already applied, we just need owners
    for f in pkg.file_modes:
        fpath = pkg.chroot_destdir / f
        recursive = False
        if len(pkg.file_modes[f]) == 4:
            uname, gname, fmode, recursive = pkg.file_modes[f]
        else:
            uname, gname, fmode = pkg.file_modes[f]
        # avoid noops
        if (uname == "root" or uname == 0) and (gname == "root" or gname == 0):
            continue
        # now we know it's needed
        needscript = True
        # handle recursive owner
        if recursive:
            chcmd = "chown -R"
        else:
            chcmd = "chown"
        wscript += f"""{chcmd} {uname}:{gname} {shlex.quote(str(fpath))}\n"""

    # execute what we were wrapping
    wscript += """exec "$@"\n"""

    # TODO: recommends (once implemented in apk)

    if pkg.rparent.stage == 0:
        # disable wrapper script unless we have a real chroot
        needscript = False
        cbpath = binpath
    else:
        srepo = paths.stage_repository()
        cbpath = pathlib.Path("/stagepkgs") / binpath.relative_to(srepo)

    try:
        logger.get().out(f"Creating {binpkg} in repository {repo}...")

        # in stage 0 we need to use the host apk, avoid fakeroot while at it
        # we just use bwrap to pretend we're root and that's all we need
        if pkg.rparent.stage == 0:
            ret = subprocess.run([
                "bwrap", "--bind", "/", "/", "--uid", "0", "--gid", "0", "--",
                paths.apk(), "mkpkg", "--files", pkg.chroot_destdir,
                "--output", cbpath, *pargs
            ], capture_output = True)
        else:
            ret = chroot.enter(
                "apk", "mkpkg", "--files", pkg.chroot_destdir,
                "--output", cbpath, *pargs,
                capture_output = True, bootstrapping = False,
                ro_root = True, ro_build = True, ro_dest = False,
                unshare_all = True, mount_binpkgs = True,
                fakeroot = True, binpkgs_rw = True,
                signkey = signkey, wrapper = wscript if needscript else None
            )

        if ret.returncode != 0:
            logger.get().out_plain(">> stderr:")
            logger.get().out_plain(ret.stderr.decode())
            pkg.error(f"failed to generate package")

    finally:
        pkg.rparent._stage[repo] = True

def invoke(pkg):
    arch = pkg.rparent.profile().arch
    binpkg = f"{pkg.pkgname}-{pkg.pkgver}-r{pkg.pkgrel}.apk"

    stagebase = paths.stage_repository()
    repobase = stagebase / pkg.rparent.repository

    if pkg.pkgname.endswith("-dbg"):
        repo = repobase / "debug" / arch
    else:
        repo = repobase / arch

    genpkg(pkg, repo, arch, binpkg)

    for apkg, adesc, iif, takef in template.autopkgs:
        binpkg = f"{pkg.pkgname}-{apkg}-{pkg.pkgver}-r{pkg.pkgrel}.apk"

        # is an explicit package, do not autosplit that
        if pkg.pkgname.endswith(f"-{apkg}"):
            continue

        # explicitly defined, so do not try autosplit
        foundpkg = False
        for sp in pkg.rparent.subpkg_list:
            if sp.pkgname == f"{pkg.pkgname}-{apkg}":
                foundpkg = True
                break
        if foundpkg:
            continue

        ddest = pkg.rparent.destdir_base / f"{pkg.pkgname}-{apkg}-{pkg.pkgver}"

        # destdir does not exist, so skip
        if not ddest.is_dir():
            continue

        # subpkg repository
        srepo = repo
        if apkg == "dbg":
            srepo = repobase / "debug" / arch

        # create a temporary subpkg instance
        # it's only complete enough to satisfy the generator
        spkg = template.Subpackage(
            f"{pkg.pkgname}-{apkg}", pkg.rparent, pkg.pkgdesc
        )

        genpkg(spkg, srepo, arch, binpkg)
