# Copyright 2005-2025 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

import errno
import os
import stat

import portage
from portage.util import writemsg
from portage.versions import _pkg_str


class BinhostHandler:
    short_desc = "Generate a metadata index for binary packages"

    @staticmethod
    def name():
        return "binhost"

    def __init__(self):
        eroot = portage.settings["EROOT"]
        self._bintree = portage.db[eroot]["bintree"]
        self._bintree.populate()
        self._pkgindex_file = self._bintree._pkgindex_file
        self._pkgindex = self._bintree._load_pkgindex()

    def _need_update(self, cpv, data):
        if "MD5" not in data:
            return True

        size = data.get("SIZE")
        if size is None:
            return True

        mtime = data.get("_mtime_")
        if mtime is None:
            return True

        pkg_path = self._bintree.getname(cpv)
        try:
            s = os.lstat(pkg_path)
        except OSError as e:
            if e.errno not in (errno.ENOENT, errno.ESTALE):
                raise
            # We can't update the index for this one because
            # it disappeared.
            return False

        try:
            if int(mtime) != s[stat.ST_MTIME]:
                return True
            if int(size) != int(s.st_size):
                return True
        except ValueError:
            return True

        return False

    def check(self, **kwargs):
        onProgress = kwargs.get("onProgress", None)
        bintree = self._bintree
        # Force reindex in case pkgdir-index-trusted is enabled.
        bintree._populate_local(reindex=True)
        bintree.populated = True
        _instance_key = bintree.dbapi._instance_key
        cpv_all = self._bintree.dbapi.cpv_all()
        cpv_all.sort()
        maxval = len(cpv_all)
        if onProgress:
            onProgress(maxval, 0)
        pkgindex = self._pkgindex
        missing = []
        stale = []
        metadata = {}
        for d in pkgindex.packages:
            cpv = _pkg_str(d["CPV"], metadata=d, settings=bintree.settings)
            d["CPV"] = cpv
            metadata[_instance_key(cpv)] = d
            if not bintree.dbapi.cpv_exists(cpv):
                stale.append(cpv)
        for i, cpv in enumerate(cpv_all):
            d = metadata.get(_instance_key(cpv))
            if not d or self._need_update(cpv, d):
                missing.append(cpv)
            if onProgress:
                onProgress(maxval, i + 1)
        errors = [f"'{cpv}' is not in Packages" for cpv in missing]
        for cpv in stale:
            errors.append(f"'{cpv}' is not in the repository")

        errors.extend(self._check_compressed_index())

        if errors:
            return (False, errors)
        return (True, None)

    def _check_compressed_index(self):
        """Check the compressed index file for consistency. Return a list of error
        messages, or an empty list if no errors were found.
        """
        errors = []
        bintree = self._bintree
        timestamps = {}
        for suffix in ("", ".gz"):
            pkgindex_file = self._pkgindex_file + suffix
            try:
                st = os.stat(pkgindex_file)
            except OSError as e:
                timestamps[suffix] = None
                if e.errno == errno.ENOENT:
                    if suffix == "" or (
                        suffix == ".gz"
                        and "compress-index" in bintree.settings.features
                    ):
                        errors.append(f"Missing index file: {pkgindex_file}")
                else:
                    raise
            else:
                timestamps[suffix] = st[stat.ST_MTIME]

        if "compress-index" in bintree.settings.features and len(timestamps) == 2:
            if (
                timestamps[""] is not None
                and timestamps[".gz"] is not None
                and timestamps[""] != timestamps[".gz"]
            ):
                errors.append(
                    f"Uncompressed index timestamp '{timestamps['']}' is not equal to compressed index timestamp '{timestamps['.gz']}'"
                )

        if "compress-index" not in bintree.settings.features:
            if timestamps[".gz"] is not None:
                errors.append(
                    f"Compressed index exists but 'compress-index' feature is disabled: {self._pkgindex_file}.gz"
                )

        return errors

    def fix(self, **kwargs):
        onProgress = kwargs.get("onProgress", None)
        bintree = self._bintree
        # Force reindex in case pkgdir-index-trusted is enabled.
        bintree._populate_local(reindex=True)
        bintree.populated = True
        _instance_key = bintree.dbapi._instance_key
        cpv_all = self._bintree.dbapi.cpv_all()
        cpv_all.sort()
        maxval = 0
        if onProgress:
            onProgress(maxval, 0)
        pkgindex = self._pkgindex
        missing = []
        stale = []
        metadata = {}
        for d in pkgindex.packages:
            cpv = _pkg_str(d["CPV"], metadata=d, settings=bintree.settings)
            d["CPV"] = cpv
            metadata[_instance_key(cpv)] = d
            if not bintree.dbapi.cpv_exists(cpv):
                stale.append(cpv)

        for cpv in cpv_all:
            d = metadata.get(_instance_key(cpv))
            if not d or self._need_update(cpv, d):
                missing.append(cpv)

        if missing or stale or self._check_compressed_index():
            from portage import locks

            pkgindex_lock = locks.lockfile(self._pkgindex_file, wantnewlockfile=1)
            try:
                # Repopulate with lock held. If _populate_local returns
                # data then use that, since _load_pkgindex would return
                # stale data in this case.
                self._pkgindex = pkgindex = (
                    bintree._populate_local() or bintree._load_pkgindex()
                )
                cpv_all = self._bintree.dbapi.cpv_all()
                cpv_all.sort()

                # Recount stale/missing packages, with lock held.
                missing = []
                stale = []
                metadata = {}
                for d in pkgindex.packages:
                    cpv = _pkg_str(d["CPV"], metadata=d, settings=bintree.settings)
                    d["CPV"] = cpv
                    metadata[_instance_key(cpv)] = d
                    if not bintree.dbapi.cpv_exists(cpv):
                        stale.append(cpv)

                for cpv in cpv_all:
                    d = metadata.get(_instance_key(cpv))
                    if not d or self._need_update(cpv, d):
                        missing.append(cpv)

                maxval = len(missing)
                for i, cpv in enumerate(missing):
                    d = bintree._pkgindex_entry(cpv)
                    try:
                        bintree._eval_use_flags(cpv, d)
                    except portage.exception.InvalidDependString:
                        writemsg(
                            f"!!! Invalid binary package: '{bintree.getname(cpv)}'\n",
                            noiselevel=-1,
                        )
                    else:
                        metadata[_instance_key(cpv)] = d

                    if onProgress:
                        onProgress(maxval, i + 1)

                for cpv in stale:
                    del metadata[_instance_key(cpv)]

                # We've updated the pkgindex, so set it to
                # repopulate when necessary.
                bintree.populated = False

                del pkgindex.packages[:]
                pkgindex.packages.extend(metadata.values())
                bintree._update_pkgindex_header(self._pkgindex.header)
                bintree._pkgindex_write(self._pkgindex)

            finally:
                locks.unlockfile(pkgindex_lock)

        if onProgress:
            if maxval == 0:
                maxval = 1
            onProgress(maxval, maxval)
        return (True, None)
