# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import contextlib
import json
import os
import re
import tempfile

import gclient_utils
import lockfile
import scm


@contextlib.contextmanager
def _AtomicFileWriter(path, mode="w"):
    """Atomic file writer context manager."""
    # Create temp file in same directory to ensure atomic rename works
    fd, temp_path = tempfile.mkstemp(
        dir=os.path.dirname(path), text="b" not in mode
    )
    try:
        with os.fdopen(fd, mode) as f:
            yield f
        # Atomic rename
        gclient_utils.safe_replace(temp_path, path)
    except Exception:
        # Cleanup on failure
        if os.path.exists(temp_path):
            os.remove(temp_path)
        raise


class GerritCache(object):
    """Simple JSON file-based cache for Gerrit API results."""

    def __init__(self, root_dir):
        self.root_dir = root_dir
        self.cache_path = self._get_cache_path()

    @staticmethod
    def get_repo_code_owners_enabled_key(host, project):
        return "code-owners.%s.%s.enabled" % (host, project)

    @staticmethod
    def get_host_code_owners_enabled_key(host):
        return "code-owners.%s.enabled" % host

    def _get_cache_path(self):
        path = os.environ.get("DEPOT_TOOLS_GERRIT_CACHE_PATH")
        if path:
            return path

        try:
            path = scm.GIT.GetConfig(
                self.root_dir, "depot-tools.gerrit-cache-path"
            )
            if path:
                return path
        except Exception:
            pass

        try:
            if self.root_dir:
                # Use a deterministic name based on the repo path
                sanitized_path = re.sub(
                    r"[^a-zA-Z0-9]", "_", os.path.abspath(self.root_dir)
                )
                filename = "depot_tools_gerrit_cache_%s.json" % sanitized_path
                path = os.path.join(tempfile.gettempdir(), filename)
            else:
                fd, path = tempfile.mkstemp(
                    prefix="depot_tools_gerrit_cache_", suffix=".json"
                )
                os.close(fd)

            if not os.path.exists(path):
                # Initialize with empty JSON object
                with open(path, "w") as f:
                    json.dump({}, f)

            try:
                scm.GIT.SetConfig(
                    self.root_dir, "depot-tools.gerrit-cache-path", path
                )
            except Exception:
                # If we can't set config (e.g. not a git repo and no env var
                # set), just return the temp path. It will be a per-process
                # cache in that case.
                pass
            return path
        except Exception:
            # Fallback to random temp file if everything else fails
            fd, path = tempfile.mkstemp(prefix="gerrit_cache_", suffix=".json")
            os.close(fd)
            return path

    def get(self, key):
        if not self.cache_path:
            return None
        try:
            with lockfile.lock(self.cache_path, timeout=1):
                if os.path.exists(self.cache_path):
                    with open(self.cache_path, "r") as f:
                        try:
                            data = json.load(f)
                            return data.get(key)
                        except ValueError:
                            # Corrupt cache file, treat as miss
                            return None
        except Exception:
            # Ignore cache errors
            return None

    def set(self, key, value):
        if not self.cache_path:
            return
        try:
            with lockfile.lock(self.cache_path, timeout=1):
                data = {}
                if os.path.exists(self.cache_path):
                    with open(self.cache_path, "r") as f:
                        try:
                            data = json.load(f)
                        except ValueError:
                            # Corrupt cache, start fresh
                            data = {}
                data[key] = value
                with _AtomicFileWriter(self.cache_path, "w") as f:
                    json.dump(data, f)
        except Exception:
            # Ignore cache errors
            pass

    def getBoolean(self, key):
        """Returns the value for key as a boolean, or None if missing."""
        val = self.get(key)
        if val is None:
            return None
        return bool(val)

    def setBoolean(self, key, value):
        """Sets the value for key as a boolean."""
        self.set(key, bool(value))
