defmodule Refuge.RemoteCache.DiskStore do
  @moduledoc """
  A disk-based store for the Bazel remote cache.
  Handles storage and retrieval of content-addressed blobs.
  """
  
  use GenServer
  require Logger

  @default_cache_dir "priv/bazel_cache"
  @ac_dir "ac"  # Action Cache directory
  @cas_dir "cas" # Content Addressable Storage directory
  
  # Client API

  @doc """
  Starts the disk store.
  """
  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @doc """
  Stores a blob in the Content Addressable Storage.
  """
  def store_cas_blob(hash, data) when is_binary(hash) and is_binary(data) do
    GenServer.call(__MODULE__, {:store_cas_blob, hash, data})
  end

  @doc """
  Retrieves a blob from the Content Addressable Storage.
  Returns nil if the blob doesn't exist.
  """
  def get_cas_blob(hash) when is_binary(hash) do
    GenServer.call(__MODULE__, {:get_cas_blob, hash})
  end

  @doc """
  Checks if the given hashes exist in the Content Addressable Storage.
  Returns a list of hashes that are missing.
  """
  def find_missing_cas_blobs(hashes) when is_list(hashes) do
    GenServer.call(__MODULE__, {:find_missing_cas_blobs, hashes})
  end

  @doc """
  Stores an action result in the Action Cache.
  """
  def store_action_result(hash, data) when is_binary(hash) and is_binary(data) do
    GenServer.call(__MODULE__, {:store_action_result, hash, data})
  end

  @doc """
  Retrieves an action result from the Action Cache.
  Returns nil if the action result doesn't exist.
  """
  def get_action_result(hash) when is_binary(hash) do
    GenServer.call(__MODULE__, {:get_action_result, hash})
  end

  # Server callbacks

  @impl true
  def init(opts) do
    cache_dir = Keyword.get(opts, :cache_dir, @default_cache_dir)
    
    # Create cache directories if they don't exist
    cas_path = Path.join(cache_dir, @cas_dir)
    ac_path = Path.join(cache_dir, @ac_dir)
    
    File.mkdir_p!(cas_path)
    File.mkdir_p!(ac_path)
    
    Logger.info("Initialized Bazel remote cache store at #{cache_dir}")
    
    {:ok, %{
      cache_dir: cache_dir,
      cas_path: cas_path,
      ac_path: ac_path
    }}
  end

  @impl true
  def handle_call({:store_cas_blob, hash, data}, _from, state) do
    path = get_cas_path(hash, state.cas_path)
    ensure_parent_dir(path)
    
    case File.write(path, data) do
      :ok ->
        Logger.debug("Stored CAS blob: #{hash}")
        {:reply, :ok, state}
      {:error, reason} ->
        Logger.error("Failed to store CAS blob #{hash}: #{inspect(reason)}")
        {:reply, {:error, reason}, state}
    end
  end

  @impl true
  def handle_call({:get_cas_blob, hash}, _from, state) do
    path = get_cas_path(hash, state.cas_path)
    
    result = case File.read(path) do
      {:ok, data} ->
        Logger.debug("Retrieved CAS blob: #{hash}")
        {:ok, data}
      {:error, :enoent} ->
        Logger.debug("CAS blob not found: #{hash}")
        {:error, :not_found}
      {:error, reason} ->
        Logger.error("Failed to read CAS blob #{hash}: #{inspect(reason)}")
        {:error, reason}
    end
    
    {:reply, result, state}
  end

  @impl true
  def handle_call({:find_missing_cas_blobs, hashes}, _from, state) do
    missing = Enum.filter(hashes, fn hash ->
      path = get_cas_path(hash, state.cas_path)
      not File.exists?(path)
    end)
    
    Logger.debug("Found #{length(missing)} missing CAS blobs out of #{length(hashes)}")
    {:reply, missing, state}
  end

  @impl true
  def handle_call({:store_action_result, hash, data}, _from, state) do
    path = get_ac_path(hash, state.ac_path)
    ensure_parent_dir(path)
    
    case File.write(path, data) do
      :ok ->
        Logger.debug("Stored action result: #{hash}")
        {:reply, :ok, state}
      {:error, reason} ->
        Logger.error("Failed to store action result #{hash}: #{inspect(reason)}")
        {:reply, {:error, reason}, state}
    end
  end

  @impl true
  def handle_call({:get_action_result, hash}, _from, state) do
    path = get_ac_path(hash, state.ac_path)
    
    result = case File.read(path) do
      {:ok, data} ->
        Logger.debug("Retrieved action result: #{hash}")
        {:ok, data}
      {:error, :enoent} ->
        Logger.debug("Action result not found: #{hash}")
        {:error, :not_found}
      {:error, reason} ->
        Logger.error("Failed to read action result #{hash}: #{inspect(reason)}")
        {:error, reason}
    end
    
    {:reply, result, state}
  end

  # Helper functions

  defp get_cas_path(hash, base_path) do
    # Use the first two characters as a directory to avoid too many files in one directory
    prefix = String.slice(hash, 0, 2)
    Path.join([base_path, prefix, hash])
  end

  defp get_ac_path(hash, base_path) do
    # Use the first two characters as a directory to avoid too many files in one directory
    prefix = String.slice(hash, 0, 2)
    Path.join([base_path, prefix, hash])
  end

  defp ensure_parent_dir(path) do
    path
    |> Path.dirname()
    |> File.mkdir_p!()
  end
end
