defmodule Berrypod.Sync.MockupEnricher do
  @moduledoc """
  Oban worker that enriches Printful products with extra mockup angle images.

  After product sync, Printful products only have front-view preview images.
  This worker uses the legacy mockup generator API to produce extra angles
  (back, left, right, etc.) and appends them as additional product images.

  The hero colour gets full angle coverage (front, back, left, right).
  Other colours get a front-view mockup only (one API call each).

  Each product is processed as a separate job so failures don't block others.
  The temporary S3 URLs from the mockup generator are downloaded via the
  existing ImageDownloadWorker pipeline.
  """

  use Oban.Worker, queue: :images, max_attempts: 5

  alias Berrypod.Clients.Printful, as: Client
  alias Berrypod.Products
  alias Berrypod.Products.ProviderConnection
  alias Berrypod.Sync.ImageDownloadWorker

  require Logger

  @poll_interval_ms 3_000
  @max_poll_attempts 20
  @inter_color_delay_ms 5_000

  # Mockup generator config per catalog product type:
  # {placement, area_width, area_height}
  # Apparel/accessories use "front", flat products use "default"
  @product_configs %{
    1 => {"default", 4500, 6750},
    3 => {"default", 4800, 7200},
    19 => {"default", 3150, 1350},
    71 => {"front", 4500, 5100},
    146 => {"front", 4500, 4500},
    181 => {"default", 1092, 2286},
    274 => {"default", 3300, 3300},
    394 => {"default", 3900, 2925},
    395 => {"default", 9000, 10800}
  }

  # Stagger jobs to avoid hammering Printful's rate limits
  @job_stagger_seconds 45

  @doc """
  Enqueue mockup enrichment for a product.

  Accepts an optional `delay_index` to stagger jobs (each index adds
  #{@job_stagger_seconds}s of delay).
  """
  def enqueue(conn_id, product_id, delay_index \\ 0) do
    delay = delay_index * @job_stagger_seconds
    scheduled_at = DateTime.add(DateTime.utc_now(), delay, :second)

    %{provider_connection_id: conn_id, product_id: product_id}
    |> new(scheduled_at: scheduled_at)
    |> Oban.insert()
  end

  @impl Oban.Worker
  def perform(%Oban.Job{
        args: %{"provider_connection_id" => conn_id, "product_id" => product_id}
      }) do
    with %ProviderConnection{} = conn <- Products.get_provider_connection(conn_id),
         product when not is_nil(product) <- Products.get_product(product_id),
         :ok <- set_credentials(conn) do
      enrich_product(product)
    else
      nil -> {:cancel, :not_found}
      {:error, _} = error -> error
    end
  end

  defp enrich_product(product) do
    provider_data = product.provider_data || %{}

    catalog_product_id =
      provider_data["catalog_product_id"] || provider_data[:catalog_product_id]

    artwork_url = provider_data["artwork_url"] || provider_data[:artwork_url]

    color_variant_map =
      provider_data["color_variant_map"] || provider_data[:color_variant_map] || %{}

    cond do
      is_nil(catalog_product_id) ->
        Logger.info("[MockupEnricher] No catalog_product_id for #{product.title}, skipping")
        :ok

      is_nil(artwork_url) ->
        Logger.info("[MockupEnricher] No artwork_url for #{product.title}, skipping")
        :ok

      already_enriched?(product) ->
        Logger.info("[MockupEnricher] Already enriched #{product.title}, skipping")
        :ok

      true ->
        enrich_all_colours(product, catalog_product_id, artwork_url, color_variant_map)
    end
  end

  defp enrich_all_colours(product, catalog_product_id, artwork_url, color_variant_map) do
    colors = Map.to_list(color_variant_map)

    if colors == [] do
      # No colour info — fall back to single-variant enrichment
      Logger.info(
        "[MockupEnricher] No color_variant_map for #{product.title}, using first variant"
      )

      enrich_single_colour(product, catalog_product_id, nil, nil, artwork_url, :hero)
    else
      # First colour is the hero (gets full angles), rest get front-only
      [{hero_color, hero_variant_id} | other_colors] = colors

      case enrich_single_colour(
             product,
             catalog_product_id,
             hero_color,
             hero_variant_id,
             artwork_url,
             :hero
           ) do
        {:ok, hero_count} ->
          Logger.info("[MockupEnricher] Hero colour #{hero_color}: #{hero_count} image(s)")

          other_count =
            enrich_other_colours(product, catalog_product_id, artwork_url, other_colors)

          total = hero_count + other_count
          Logger.info("[MockupEnricher] Total #{total} extra image(s) for #{product.title}")
          :ok

        {:error, {429, _}} ->
          Logger.info("[MockupEnricher] Rate limited for #{product.title}, snoozing 60s")
          {:snooze, 60}

        {:error, {400, _} = reason} ->
          Logger.info("[MockupEnricher] #{product.title} not supported: #{inspect(reason)}")
          :ok

        {:error, reason} ->
          Logger.warning("[MockupEnricher] Failed for #{product.title}: #{inspect(reason)}")
          {:error, reason}
      end
    end
  end

  defp enrich_other_colours(product, catalog_product_id, artwork_url, colors) do
    Enum.reduce(colors, 0, fn {color_name, variant_id}, acc ->
      Process.sleep(@inter_color_delay_ms)

      case enrich_single_colour(
             product,
             catalog_product_id,
             color_name,
             variant_id,
             artwork_url,
             :front_only
           ) do
        {:ok, count} ->
          Logger.info("[MockupEnricher] Colour #{color_name}: #{count} image(s)")
          acc + count

        {:error, {429, _}} ->
          # Rate limited on a non-hero colour — log and continue with remaining
          Logger.info(
            "[MockupEnricher] Rate limited on #{color_name}, skipping remaining colours"
          )

          acc

        {:error, reason} ->
          Logger.warning("[MockupEnricher] Failed for colour #{color_name}: #{inspect(reason)}")
          acc
      end
    end)
  end

  defp enrich_single_colour(
         product,
         catalog_product_id,
         color_name,
         variant_id,
         artwork_url,
         mode
       ) do
    {placement, area_width, area_height} =
      Map.get(@product_configs, catalog_product_id, {"front", 4500, 5100})

    body = %{
      variant_ids: if(variant_id, do: [variant_id], else: []),
      format: "jpg",
      files: [
        %{
          placement: placement,
          image_url: artwork_url,
          position: %{
            area_width: area_width,
            area_height: area_height,
            width: area_width,
            height: area_height,
            top: 0,
            left: 0
          }
        }
      ]
    }

    with {:ok, task_data} <- Client.create_mockup_generator_task(catalog_product_id, body),
         task_key <- task_data["task_key"],
         {:ok, result} <- poll_generator_task(task_key) do
      images = extract_images(result, mode)

      if images == [] do
        {:ok, 0}
      else
        append_images_to_product(product, images, color_name)
      end
    end
  end

  defp poll_generator_task(task_key), do: poll_generator_task(task_key, 0)

  defp poll_generator_task(_task_key, attempt) when attempt >= @max_poll_attempts do
    {:error, :timeout}
  end

  defp poll_generator_task(task_key, attempt) do
    Process.sleep(@poll_interval_ms)

    case Client.get_mockup_generator_task(task_key) do
      {:ok, %{"status" => "completed"} = result} ->
        {:ok, result}

      {:ok, %{"status" => "error", "error" => error}} ->
        {:error, {:mockup_error, error}}

      {:ok, %{"status" => _pending}} ->
        poll_generator_task(task_key, attempt + 1)

      {:error, {429, _}} ->
        Process.sleep(60_000)
        poll_generator_task(task_key, attempt + 1)

      {:error, reason} ->
        {:error, reason}
    end
  end

  # Hero mode: collect all extra angle images (front, back, left, right, etc.)
  # Front-only mode: just the main mockup URL
  defp extract_images(result, :hero) do
    (result["mockups"] || [])
    |> Enum.flat_map(fn mockup ->
      (mockup["extra"] || [])
      |> Enum.map(fn extra ->
        %{src: extra["url"], alt: extra["title"]}
      end)
    end)
    |> Enum.reject(&is_nil(&1.src))
    |> Enum.uniq_by(& &1.src)
  end

  defp extract_images(result, :front_only) do
    (result["mockups"] || [])
    |> Enum.take(1)
    |> Enum.map(fn mockup ->
      %{src: mockup["mockup_url"], alt: "Front"}
    end)
    |> Enum.reject(&is_nil(&1.src))
  end

  defp append_images_to_product(product, extra_images, color_name) do
    existing_images = Products.list_product_images(product.id)
    next_position = max_position(existing_images) + 1

    results =
      extra_images
      |> Enum.with_index(next_position)
      |> Enum.map(fn {img, position} ->
        attrs = %{
          product_id: product.id,
          src: img.src,
          alt: img.alt,
          color: color_name,
          position: position
        }

        case Products.create_product_image(attrs) do
          {:ok, product_image} ->
            ImageDownloadWorker.enqueue(product_image.id)
            {:ok, product_image}

          {:error, reason} ->
            Logger.warning("[MockupEnricher] Failed to create image: #{inspect(reason)}")
            {:error, reason}
        end
      end)

    count = Enum.count(results, &match?({:ok, _}, &1))
    {:ok, count}
  end

  defp max_position([]), do: -1

  defp max_position(images) do
    images |> Enum.map(& &1.position) |> Enum.max()
  end

  # Check if this product already has mockup-enriched images (those with a color tag)
  defp already_enriched?(product) do
    images = Products.list_product_images(product.id)

    Enum.any?(images, fn img ->
      img.color != nil && img.alt in ["Front", "Back", "Left", "Right"]
    end)
  end

  defp set_credentials(conn) do
    case ProviderConnection.get_api_key(conn) do
      api_key when is_binary(api_key) ->
        Process.put(:printful_api_key, api_key)
        store_id = get_in(conn.config, ["store_id"])
        if store_id, do: Process.put(:printful_store_id, store_id)
        :ok

      nil ->
        {:error, :no_api_key}
    end
  end
end
