# Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
import enum
import os
import shlex
from pathlib import Path

import pmb.build
import pmb.chroot
import pmb.config.pmaports
import pmb.helpers.pmaports
import pmb.helpers.run
import pmb.parse.apkindex
import pmb.parse.version
from pmb.core import Chroot
from pmb.core.arch import Arch
from pmb.core.context import get_context
from pmb.helpers import logging
from pmb.types import Apkbuild


def copy_to_buildpath(
    package: str, chroot: Chroot = Chroot.native(), no_override: bool = False
) -> None:
    # Sanity check
    aport = pmb.helpers.pmaports.find(package)
    if not os.path.exists(aport / "APKBUILD"):
        raise ValueError(f"Path does not contain an APKBUILD file: {aport}")

    # Clean up folder
    build = chroot / "home/pmos/build"
    if build.exists():
        pmb.helpers.run.root(["rm", "-rf", build])

    # Copy aport contents with resolved symlinks
    pmb.helpers.run.root(["mkdir", "-p", build])
    for entry in aport.iterdir():
        file = entry.name
        # Don't copy those dirs, as those have probably been generated by running `abuild`
        # on the host system directly and not cleaning up after itself.
        # Those dirs might contain broken symlinks and cp fails resolving them.
        if file in ["src", "pkg"]:
            logging.warning(f"WARNING: Not copying {file}, looks like a leftover from abuild")
            continue
        pmb.helpers.run.root(["cp", "-rL", aport / file, build / file])

    if not no_override:
        abuild_overrides(build / "APKBUILD")

    pmb.chroot.root(["chown", "-R", "pmos:pmos", "/home/pmos/build"], chroot)


def abuild_overrides(apkbuild: Path) -> None:
    """Override some abuild functions by patching the APKBUILD file."""
    if apkbuild.is_relative_to(get_context().config.work / "cache_git"):
        raise ValueError(f"Refusing to patch file in pmaports repo: {apkbuild}")

    # Patch the APKBUILD file
    override_path = pmb.config.pmb_src / "pmb/data/abuild_overrides.sh"
    pmb.helpers.run.root(["sh", "-c", f"cat {override_path} >> {apkbuild}"])


class BuildStatus(enum.Enum):
    # The binary package is outdated
    OUTDATED = "outdated"
    # There is no binary package
    NEW = "new"
    # Source package can't be built
    CANT_BUILD = "cant_build"
    # Building isn't needed
    UNNECESSARY = "unnecessary"

    def __str__(self) -> str:
        return self.value

    def necessary(self) -> bool:
        return self in [BuildStatus.OUTDATED, BuildStatus.NEW]


def get_status(arch: Arch | None, apkbuild: Apkbuild) -> BuildStatus:
    """
    Check if the package has already been built.

    Compared to abuild's check, this check also works for different architectures.

    :param arch: package target architecture
    :param apkbuild: from pmb.parse.apkbuild()
    :param indexes: list of APKINDEX.tar.gz paths
    :returns: boolean
    """
    package = apkbuild["pkgname"]
    version_pmaports = apkbuild["pkgver"] + "-r" + apkbuild["pkgrel"]
    msg = "Build is necessary for package '" + package + "': "

    # Can't build pmaport for arch: use Alpine's package (#1897)
    if arch and not pmb.helpers.pmaports.check_arches(apkbuild["arch"], arch):
        logging.verbose(
            f"{package}: build is not necessary, because pmaport"
            f" can't be built for {arch}. Using Alpine's binary"
            " package."
        )
        return BuildStatus.CANT_BUILD

    # Get version from APKINDEX
    index_data = pmb.parse.apkindex.package(package, arch, False)
    if not index_data:
        logging.debug(msg + "No binary package available")
        return BuildStatus.NEW

    # a) Binary repo has a newer version
    version_binary = index_data.version
    if pmb.parse.version.compare(version_binary, version_pmaports) == 1:
        logging.warning(
            f"WARNING: about to install {package} {version_binary}"
            f" (local pmaports: {version_pmaports}, consider"
            " 'pmbootstrap pull')"
        )
        return BuildStatus.UNNECESSARY

    # b) Local pmaports has a newer version
    if version_pmaports != version_binary:
        logging.debug(
            f"{msg}binary package out of date (binary: "
            f"{version_binary}, local pmaports: {version_pmaports})"
        )
        return BuildStatus.OUTDATED

    # Local pmaports and binary repo have the same version
    return BuildStatus.UNNECESSARY


def index_repo(arch: Arch | None = None) -> None:
    """
    Recreate the APKINDEX.tar.gz for a specific repo, and clear the parsing
    cache for that file for the current pmbootstrap session (to prevent
    rebuilding packages twice, in case the rebuild takes less than a second).

    :param arch: when not defined, re-index all repos
    """
    pmb.build.init()

    paths: list[Path] = []

    for channel in pmb.config.pmaports.all_channels():
        pkgdir: Path = get_context().config.work / "packages" / channel
        if arch:
            paths.append(pkgdir / arch)
        else:
            paths += list(pkgdir.glob("*"))

    for path in paths:
        if path.is_dir():
            path_channel, path_arch = path.parts[-2:]
            path_repo_chroot = Path("/mnt/pmbootstrap/packages") / path_channel / path_arch
            logging.debug(f"(native) index {path_channel}/{path_arch} repository")
            description = str(datetime.datetime.now())
            commands = [
                # Wrap the index command with sh so we can use '*.apk'
                [
                    "sh",
                    "-c",
                    "apk -q index --output APKINDEX.tar.gz_"
                    " --description " + shlex.quote(description) + ""
                    " --rewrite-arch " + shlex.quote(path_arch) + " *.apk",
                ],
                ["abuild-sign", "APKINDEX.tar.gz_"],
                ["mv", "APKINDEX.tar.gz_", "APKINDEX.tar.gz"],
            ]
            pmb.chroot.userm(commands, working_dir=path_repo_chroot)
        else:
            logging.debug(f"NOTE: Can't build index for: {path}")
        pmb.parse.apkindex.clear_cache(path / "APKINDEX.tar.gz")


def configure_abuild(chroot: Chroot, verify: bool = False) -> None:
    """
    Set the correct JOBS count in ``abuild.conf``.

    :param verify: internally used to test if changing the config has worked.
    """
    jobs = get_context().config.jobs
    path = chroot / "etc/abuild.conf"
    prefix = "export JOBS="
    with path.open(encoding="utf-8") as handle:
        for line in handle:
            if not line.startswith(prefix):
                continue
            if line != (prefix + str(jobs) + "\n"):
                if verify:
                    raise RuntimeError(
                        f"Failed to configure abuild: {path}"
                        "\nTry to delete the file"
                        "(or zap the chroot)."
                    )
                pmb.chroot.root(
                    ["sed", "-i", "-e", f"s/^{prefix}.*/{prefix}{jobs}/", "/etc/abuild.conf"],
                    chroot,
                )
                configure_abuild(chroot, True)
            return
    pmb.chroot.root(["sed", "-i", f"$ a\\{prefix}{jobs}", "/etc/abuild.conf"], chroot)


def configure_ccache(chroot: Chroot = Chroot.native(), verify: bool = False) -> None:
    """
    Set the maximum ccache size.

    :param verify: internally used to test if changing the config has worked.
    """
    # Check if the settings have been set already
    arch = chroot.arch
    config = get_context().config
    path = config.work / f"cache_ccache_{arch}" / "ccache.conf"
    if os.path.exists(path):
        with open(path, encoding="utf-8") as handle:
            for line in handle:
                if line == ("max_size = " + config.ccache_size + "\n"):
                    return
    if verify:
        raise RuntimeError(
            f"Failed to configure ccache: {path}\nTry to delete the file (or zap the chroot)."
        )

    # Set the size and verify
    pmb.chroot.user(["ccache", "--max-size", config.ccache_size], chroot)
    configure_ccache(chroot, True)
