defmodule Berrypod.Images.VariantCache do
  @moduledoc """
  Ensures all image variants exist on startup.

  This GenServer runs at startup and checks for:
  1. Database images with incomplete variants_status or missing disk files
  2. Mockup source files missing their generated variants

  For any images missing variants, it enqueues Oban jobs to regenerate them.
  """

  use GenServer
  require Logger

  alias Berrypod.Repo
  alias Berrypod.Media.Image, as: ImageSchema
  alias Berrypod.Images.{Optimizer, OptimizeWorker}
  alias Berrypod.Products
  alias Berrypod.Sync.ImageDownloadWorker
  import Ecto.Query

  # Source mockups bundled in the release
  defp release_mockup_dir, do: Application.app_dir(:berrypod, "priv/static/mockups")

  # Where mockup variants live — persistent volume in prod, release dir in dev
  defp mockup_dir do
    Application.get_env(:berrypod, :mockup_dir) || release_mockup_dir()
  end

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @impl true
  def init(_opts) do
    Task.start(fn -> ensure_all_variants() end)
    {:ok, %{}}
  end

  defp ensure_all_variants do
    Logger.info("[VariantCache] Checking image variant cache...")
    File.mkdir_p!(Optimizer.cache_dir())

    reset_stale_sync_status()
    ensure_database_image_variants()
    ensure_mockup_variants()
    ensure_product_image_downloads()
  end

  # Reset any provider connections stuck in "syncing" status from interrupted syncs
  defp reset_stale_sync_status do
    {count, _} = Products.reset_stale_sync_status()

    if count > 0 do
      Logger.info("[VariantCache] Reset #{count} stale sync status(es) to idle")
    end
  end

  defp ensure_database_image_variants do
    # Only load IDs and source_width for the disk check — avoids loading BLOBs
    # Skip tiny images (< 100 bytes) — they're placeholders that can't be processed
    incomplete_ids =
      ImageSchema
      |> where([i], i.variants_status != "complete" or is_nil(i.variants_status))
      |> where([i], i.is_svg == false)
      |> where([i], i.file_size >= 100)
      |> select([i], {i.id, i.source_width})
      |> Repo.all()

    complete_missing_ids =
      ImageSchema
      |> where([i], i.variants_status == "complete")
      |> where([i], i.is_svg == false)
      |> where([i], not is_nil(i.source_width))
      |> select([i], {i.id, i.source_width})
      |> Repo.all()
      |> Enum.reject(fn {id, source_width} ->
        Optimizer.disk_variants_exist?(id, source_width)
      end)

    to_process = incomplete_ids ++ complete_missing_ids

    if to_process == [] do
      Logger.info("[VariantCache] All database image variants up to date")
    else
      Logger.info("[VariantCache] Enqueueing #{length(to_process)} images with missing variants")

      Enum.each(to_process, fn {id, _source_width} -> OptimizeWorker.enqueue(id) end)
    end
  end

  defp ensure_mockup_variants do
    output = mockup_dir()
    release = release_mockup_dir()

    # When using a persistent volume, copy source WebPs from the release
    # so variants can be generated there (and survive deploys).
    if output != release and File.dir?(release) do
      File.mkdir_p!(output)

      Path.wildcard(Path.join(release, "*.webp"))
      |> Enum.reject(&is_variant?/1)
      |> Enum.each(fn src ->
        dest = Path.join(output, Path.basename(src))
        unless File.exists?(dest), do: File.cp!(src, dest)
      end)
    end

    if File.dir?(output) do
      sources =
        Path.wildcard(Path.join(output, "*.webp"))
        |> Enum.reject(&is_variant?/1)

      missing = Enum.reject(sources, &mockup_variants_exist?/1)

      if missing == [] do
        Logger.info("[VariantCache] All mockup variants up to date")
      else
        Logger.info("[VariantCache] Enqueueing #{length(missing)} mockups for processing")
        Enum.each(missing, &OptimizeWorker.enqueue_mockup/1)
      end
    end
  end

  defp is_variant?(path) do
    basename = Path.basename(path) |> Path.rootname()
    String.match?(basename, ~r/-(400|800|1200|thumb)$/)
  end

  defp mockup_variants_exist?(source_path) do
    basename = Path.basename(source_path) |> Path.rootname()
    dir = Path.dirname(source_path)

    expected =
      ["#{basename}-thumb.jpg"] ++
        for w <- [400, 800, 1200], ext <- ["avif", "webp", "jpg"], do: "#{basename}-#{w}.#{ext}"

    Enum.all?(expected, &File.exists?(Path.join(dir, &1)))
  end

  defp ensure_product_image_downloads do
    pending = Products.list_pending_downloads(limit: 500)

    if pending == [] do
      Logger.info("[VariantCache] All product images downloaded")
    else
      Logger.info("[VariantCache] Enqueueing #{length(pending)} product images for download")
      Enum.each(pending, fn image -> ImageDownloadWorker.enqueue(image.id) end)
    end
  end
end
