defmodule Berrypod.Mockups.PrintfulGenerator do
  @moduledoc """
  Generates product mockups and/or creates demo products using the Printful API.

  Two independent capabilities, composable via options:

  1. **Mockup images** — uses the v2 mockup tasks API to generate on-product
     mockup images for the theme preview. Images are saved to `priv/static/mockups/`.

  2. **Sync products** — creates real products in the Printful store via the
     v1 store products API. These can later be synced into the shop.
  """

  alias Berrypod.Clients.Printful, as: Client
  alias Berrypod.Images.Optimizer
  alias Berrypod.Mockups.Generator

  @output_dir "priv/static/mockups"
  @poll_interval_ms 2_000
  @max_poll_attempts 30
  @api_delay_ms 3_000
  @max_retries 3

  # ============================================================================
  # Catalog config
  # ============================================================================

  @doc """
  Maps product types to Printful catalog product IDs, techniques, and variant IDs.

  Variant IDs are needed for sync product creation. Catalog product IDs and
  techniques are needed for mockup generation.
  """
  def catalog_config do
    %{
      # Multiple variant IDs = multiple colour/size mockups per product
      canvas: %{
        catalog_product_id: 3,
        technique: "digital",
        variant_ids: [
          {19303, "16″×24″"},
          {19309, "20″×24″"},
          {19315, "24″×30″"},
          {825, "24″×36″"}
        ]
      },
      tshirt: %{
        catalog_product_id: 71,
        technique: "dtg",
        variant_ids: [
          {4017, "Black / M"},
          {4027, "Ash / M"},
          {4022, "Aqua / M"},
          {4082, "Gold / M"},
          {8452, "Forest / M"}
        ]
      },
      hoodie: %{
        catalog_product_id: 146,
        technique: "dtg",
        variant_ids: [
          {5531, "Black / M"},
          {20553, "Ash / M"},
          {21636, "Carolina Blue / M"},
          {5555, "Dark Chocolate / M"}
        ]
      },
      tote: %{
        catalog_product_id: 274,
        technique: "cut-sew",
        variant_ids: [
          {9039, "Black"},
          {9040, "Red"},
          {9041, "Yellow"}
        ]
      },
      mug: %{
        catalog_product_id: 19,
        technique: "sublimation",
        variant_ids: [{1320, "White / 11oz"}]
      },
      blanket: %{
        catalog_product_id: 395,
        technique: "sublimation",
        variant_ids: [{13222, "60″×80″"}]
      },
      laptop_sleeve: %{
        catalog_product_id: 394,
        technique: "sublimation",
        variant_ids: [{10984, "13″"}]
      },
      phone_case: %{
        catalog_product_id: 181,
        technique: "uv",
        variant_ids: [{17616, "iPhone 15 Pro Max"}]
      },
      poster: %{
        catalog_product_id: 1,
        technique: "digital",
        variant_ids: [
          {3876, "12″×18″"},
          {3877, "16″×20″"},
          {1, "18″×24″"}
        ]
      }
    }
  end

  @doc """
  Product definitions filtered to types available in Printful's catalog.

  Reuses artwork URLs and slugs from the shared Printify generator definitions,
  but skips product types not available in Printful (cushion, notebook).
  """
  def product_definitions do
    supported = Map.keys(catalog_config())

    Generator.product_definitions()
    |> Enum.filter(fn def -> def.product_type in supported end)
  end

  # ============================================================================
  # Main entry point
  # ============================================================================

  @doc """
  Generate mockups and/or create products.

  ## Options

    * `:mockups` — generate mockup images (default: true)
    * `:products` — create sync products in Printful (default: true)
    * `:cleanup` — delete created sync products after (default: false)
  """
  def generate_all(opts \\ []) do
    do_mockups = Keyword.get(opts, :mockups, true)
    do_products = Keyword.get(opts, :products, true)
    cleanup = Keyword.get(opts, :cleanup, false)

    definitions = product_definitions()

    IO.puts("Starting Printful generation...")
    IO.puts("  Mockups: #{do_mockups}, Products: #{do_products}, Cleanup: #{cleanup}")
    IO.puts("  #{length(definitions)} product definitions")
    IO.puts("")

    # Generate mockup images
    mockup_results =
      if do_mockups do
        generate_mockups(definitions)
      else
        []
      end

    # Create sync products
    product_results =
      if do_products do
        create_products(definitions)
      else
        []
      end

    # Cleanup if requested
    if cleanup and do_products do
      IO.puts("")
      IO.puts("Cleaning up created products...")

      product_results
      |> Enum.filter(&match?({:ok, _, _}, &1))
      |> Enum.each(fn {:ok, slug, product_id} ->
        IO.puts("  Deleting #{slug} (#{product_id})...")
        Client.delete_sync_product(product_id)
        Process.sleep(200)
      end)

      IO.puts("Cleanup complete.")
    end

    IO.puts("")
    IO.puts("Printful generation complete!")

    %{mockups: mockup_results, products: product_results}
  end

  # ============================================================================
  # Mockup generation (v2 mockup tasks)
  # ============================================================================

  defp generate_mockups(definitions) do
    IO.puts("Generating mockup images...")
    IO.puts("")

    definitions
    |> Enum.with_index()
    |> Enum.map(fn {product_def, index} ->
      if index > 0, do: Process.sleep(@api_delay_ms)
      IO.puts("  Mockup: #{product_def.name}")

      case generate_single_mockup(product_def) do
        {:ok, paths} ->
          IO.puts("    ✓ #{length(paths)} mockup(s)")
          {:ok, product_def.slug, paths}

        {:error, reason} ->
          IO.puts("    ✗ #{inspect(reason)}")
          {:error, product_def.slug, reason}
      end
    end)
  end

  defp generate_single_mockup(product_def) do
    config = catalog_config()[product_def.product_type]

    variant_ids = Enum.map(config.variant_ids, fn {id, _label} -> id end)

    task_body = %{
      format: "jpg",
      products: [
        %{
          source: "catalog",
          catalog_product_id: config.catalog_product_id,
          catalog_variant_ids: variant_ids,
          placements: [
            %{
              placement: "front",
              technique: config.technique,
              layers: [
                %{type: "file", url: product_def.artwork_url}
              ]
            }
          ]
        }
      ]
    }

    with {:ok, task_data} <- Client.create_mockup_task(task_body),
         task_id <- extract_task_id(task_data),
         {:ok, completed} <- poll_mockup_task(task_id),
         mockup_urls <- extract_mockup_urls(completed) do
      download_mockups(product_def.slug, mockup_urls)
    end
  end

  defp extract_task_id(data) when is_list(data), do: hd(data)["id"]
  defp extract_task_id(%{"id" => id}), do: id
  defp extract_task_id(data), do: data["id"]

  defp poll_mockup_task(task_id) do
    poll_mockup_task(task_id, 0)
  end

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

  defp poll_mockup_task(task_id, attempt) do
    Process.sleep(@poll_interval_ms)

    case Client.get_mockup_tasks(%{"id" => task_id}) do
      {:ok, data} ->
        task = find_task(data, task_id)

        case task["status"] do
          "completed" ->
            {:ok, task}

          "failed" ->
            {:error, {:mockup_failed, task["failure_reasons"]}}

          _pending ->
            IO.puts("    Polling... (#{attempt + 1}/#{@max_poll_attempts})")
            poll_mockup_task(task_id, attempt + 1)
        end

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

  defp find_task(data, task_id) when is_list(data) do
    Enum.find(data, fn t -> t["id"] == task_id end) || hd(data)
  end

  defp find_task(data, _task_id) when is_map(data), do: data

  defp extract_mockup_urls(task) do
    (task["catalog_variant_mockups"] || [])
    |> Enum.flat_map(fn cvm -> cvm["mockups"] || [] end)
    |> Enum.map(fn m -> m["mockup_url"] end)
    |> Enum.reject(&is_nil/1)
    |> Enum.uniq()
  end

  # ============================================================================
  # Image download (shared logic with Printify generator)
  # ============================================================================

  defp download_mockups(product_slug, mockup_urls) do
    File.mkdir_p!(@output_dir)

    results =
      mockup_urls
      |> Enum.with_index(1)
      |> Enum.map(fn {url, index} ->
        basename = "#{product_slug}-#{index}"
        source_path = Path.join(@output_dir, "#{basename}.webp")
        IO.puts("    Processing mockup #{index}...")

        temp_path = Path.join(System.tmp_dir!(), "#{basename}-pf-temp.jpg")

        with {:ok, _} <- download_file(url, temp_path),
             {:ok, image_data} <- File.read(temp_path),
             {:ok, webp_data, source_width, _} <- Optimizer.to_optimized_webp(image_data),
             :ok <- File.write(source_path, webp_data),
             {:ok, _} <- Optimizer.process_file(webp_data, basename, @output_dir) do
          File.rm(temp_path)
          IO.puts("      Saved #{basename} (#{source_width}px)")
          {:ok, basename, source_width}
        else
          {:error, reason} ->
            File.rm(temp_path)
            {:error, {url, reason}}
        end
      end)

    successful = Enum.filter(results, &match?({:ok, _, _}, &1))
    {:ok, Enum.map(successful, fn {:ok, basename, _} -> basename end)}
  end

  # Download without auth headers (mockup URLs are pre-signed S3 URLs)
  defp download_file(url, output_path) do
    case Req.get(url, into: File.stream!(output_path), receive_timeout: 60_000) do
      {:ok, %Req.Response{status: status}} when status in 200..299 ->
        {:ok, output_path}

      {:ok, %Req.Response{status: status}} ->
        File.rm(output_path)
        {:error, {:http_error, status}}

      {:error, reason} ->
        File.rm(output_path)
        {:error, reason}
    end
  end

  # ============================================================================
  # Sync product creation (v1 store products)
  # ============================================================================

  defp create_products(definitions) do
    IO.puts("Creating sync products...")
    IO.puts("")

    definitions
    |> Enum.with_index()
    |> Enum.map(fn {product_def, index} ->
      if index > 0, do: Process.sleep(@api_delay_ms)
      IO.puts("  Product: #{product_def.name}")

      case create_single_product(product_def) do
        {:ok, product_id} ->
          IO.puts("    ✓ Created (ID: #{product_id})")
          {:ok, product_def.slug, product_id}

        {:error, reason} ->
          IO.puts("    ✗ #{inspect(reason)}")
          {:error, product_def.slug, reason}
      end
    end)
  end

  defp create_single_product(product_def) do
    config = catalog_config()[product_def.product_type]

    price =
      product_def.price
      |> Decimal.new()
      |> Decimal.div(100)
      |> Decimal.to_string()

    sync_variants =
      Enum.map(config.variant_ids, fn {variant_id, _label} ->
        %{
          variant_id: variant_id,
          retail_price: price,
          files: [%{url: product_def.artwork_url, type: "default"}]
        }
      end)

    product_data = %{
      sync_product: %{name: product_def.name},
      sync_variants: sync_variants
    }

    with_retry(fn -> Client.create_sync_product(product_data) end)
    |> case do
      {:ok, result} -> {:ok, result["id"]}
      {:error, reason} -> {:error, reason}
    end
  end

  # ============================================================================
  # Retry logic for rate limits
  # ============================================================================

  defp with_retry(fun, attempt \\ 1) do
    case fun.() do
      {:error, {429, _}} when attempt <= @max_retries ->
        wait = attempt * 60_000

        IO.puts(
          "    Rate limited, waiting #{div(wait, 1000)}s (retry #{attempt}/#{@max_retries})..."
        )

        Process.sleep(wait)
        with_retry(fun, attempt + 1)

      result ->
        result
    end
  end
end
