# Copyright 2005-2025 Gentoo Authors
# Author(s): Brian Harring (ferringb@gentoo.org)
# License: GPL2

import errno
import re
import stat
import tempfile
from operator import attrgetter

import portage
from portage import os
from portage import _encodings
from portage import _unicode_encode
from portage.cache import cache_errors, flat_hash
import portage.eclass_cache
from portage.cache.template import reconstruct_eclasses
from portage.cache.mappings import ProtectedDict


# this is the old cache format, flat_list.  count maintained here.
magic_line_count = 22

# store the current key order *here*.


class database(flat_hash.database):
    complete_eclass_entries = False
    auxdbkey_order = (
        "DEPEND",
        "RDEPEND",
        "SLOT",
        "SRC_URI",
        "RESTRICT",
        "HOMEPAGE",
        "LICENSE",
        "DESCRIPTION",
        "KEYWORDS",
        "IDEPEND",
        "INHERITED",
        "IUSE",
        "REQUIRED_USE",
        "PDEPEND",
        "BDEPEND",
        "EAPI",
        "PROPERTIES",
        "DEFINED_PHASES",
    )

    autocommits = True
    serialize_eclasses = False

    _hashed_re = re.compile("^(\\w+)=([^\n]*)")

    def __init__(self, location, *args, **config):
        loc = location
        super().__init__(location, *args, **config)
        self.location = os.path.join(loc, "metadata", "cache")
        self.ec = None
        self.raise_stat_collision = False

    def _parse_data(self, data, cpv):
        _hashed_re_match = self._hashed_re.match
        d = {}

        for line in data:
            hashed = False
            hashed_match = _hashed_re_match(line)
            if hashed_match is None:
                d.clear()
                try:
                    for i, key in enumerate(self.auxdbkey_order):
                        d[key] = data[i]
                except IndexError:
                    pass
                break
            else:
                d[hashed_match.group(1)] = hashed_match.group(2)

        if "_eclasses_" not in d:
            if "INHERITED" in d:
                if self.ec is None:
                    self.ec = portage.eclass_cache.cache(self.location[:-15])
                getter = attrgetter(self.validation_chf)
                try:
                    ec_data = self.ec.get_eclass_data(d["INHERITED"].split())
                    d["_eclasses_"] = {
                        k: (v.eclass_dir, getter(v)) for k, v in ec_data.items()
                    }
                except KeyError as e:
                    # INHERITED contains a non-existent eclass.
                    raise cache_errors.CacheCorruption(cpv, e)
            else:
                d["_eclasses_"] = {}
        elif isinstance(d["_eclasses_"], str):
            # We skip this if flat_hash.database._parse_data() was called above
            # because it calls reconstruct_eclasses() internally.
            d["_eclasses_"] = reconstruct_eclasses(None, d["_eclasses_"])

        return d

    def _setitem(self, cpv, values):
        if "_eclasses_" in values:
            values = ProtectedDict(values)
            values["INHERITED"] = " ".join(sorted(values["_eclasses_"]))

        new_content = []
        for k in self.auxdbkey_order:
            new_content.append(values.get(k, ""))
            new_content.append("\n")
        for i in range(magic_line_count - len(self.auxdbkey_order)):
            new_content.append("\n")
        new_content = "".join(new_content)
        new_content = _unicode_encode(
            new_content, _encodings["repo.content"], errors="backslashreplace"
        )

        new_fp = os.path.join(self.location, cpv)
        try:
            f = open(
                _unicode_encode(new_fp, encoding=_encodings["fs"], errors="strict"),
                "rb",
            )
        except OSError:
            pass
        else:
            try:
                try:
                    existing_st = os.fstat(f.fileno())
                    existing_content = f.read()
                finally:
                    f.close()
            except OSError:
                pass
            else:
                existing_mtime = existing_st[stat.ST_MTIME]
                if (
                    values["_mtime_"] == existing_mtime
                    and existing_content == new_content
                ):
                    return

                if (
                    self.raise_stat_collision
                    and values["_mtime_"] == existing_mtime
                    and len(new_content) == existing_st.st_size
                ):
                    raise cache_errors.StatCollision(
                        cpv, new_fp, existing_mtime, existing_st.st_size
                    )

        s = cpv.rfind("/")
        tempfile_kwargs = dict(
            prefix=_unicode_encode(
                f".update.{portage.getpid()}.{cpv[s + 1:]}",
                encoding=_encodings["fs"],
                errors="strict",
            ),
            dir=_unicode_encode(
                os.path.join(self.location, cpv[:s]),
                encoding=_encodings["fs"],
                errors="strict",
            ),
            delete=False,
            mode="wb",
        )
        try:
            myf = tempfile.NamedTemporaryFile(**tempfile_kwargs)
            fp = myf.name
        except OSError as e:
            if errno.ENOENT == e.errno:
                try:
                    self._ensure_dirs(cpv)
                    myf = tempfile.NamedTemporaryFile(**tempfile_kwargs)
                    fp = myf.name
                except OSError as e:
                    raise cache_errors.CacheCorruption(cpv, e)
            else:
                raise cache_errors.CacheCorruption(cpv, e)

        try:
            myf.write(new_content)
        finally:
            myf.close()
        self._ensure_access(fp, mtime=values["_mtime_"])

        try:
            os.rename(fp, new_fp)
        except OSError as e:
            try:
                os.unlink(fp)
            except OSError:
                pass
            raise cache_errors.CacheCorruption(cpv, e)
