defmodule Berrypod.Sync.ProductSyncWorker do
  @moduledoc """
  Oban worker for syncing products from POD providers.

  This worker fetches products from a provider, normalizes them,
  and upserts them into the local database.

  ## Usage

      # Enqueue a sync for a provider connection
      ProductSyncWorker.enqueue(provider_connection_id)

  ## Job Args

    * `provider_connection_id` - The ID of the provider connection to sync
  """

  use Oban.Worker, queue: :sync, max_attempts: 3, unique: [period: 60]

  alias Berrypod.{ActivityLog, Products}
  alias Berrypod.Products.ProviderConnection
  alias Berrypod.Providers.Provider
  alias Berrypod.Sync.ImageDownloadWorker
  alias Berrypod.Sync.MockupEnricher

  require Logger

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"provider_connection_id" => conn_id}}) do
    case Products.get_provider_connection(conn_id) do
      nil ->
        {:cancel, :connection_not_found}

      %ProviderConnection{enabled: false} ->
        {:cancel, :connection_disabled}

      conn ->
        sync_products(conn)
    end
  end

  @doc """
  Enqueue a product sync for a provider connection.
  """
  def enqueue(provider_connection_id) do
    %{provider_connection_id: provider_connection_id}
    |> new()
    |> Oban.insert()
  end

  @doc """
  Enqueue a product sync with a delay.
  """
  def enqueue(provider_connection_id, delay_seconds) when is_integer(delay_seconds) do
    %{provider_connection_id: provider_connection_id}
    |> new(scheduled_at: DateTime.add(DateTime.utc_now(), delay_seconds, :second))
    |> Oban.insert()
  end

  # =============================================================================
  # Private
  # =============================================================================

  # Number of concurrent product syncs (DB operations only, not API calls)
  @max_concurrency 5

  defp sync_products(conn) do
    Logger.info("Starting product sync for #{conn.provider_type} (#{conn.id})")

    ActivityLog.log_event("sync.started", "Product sync started (#{conn.provider_type})",
      payload: %{provider_type: conn.provider_type, connection_id: conn.id}
    )

    Products.update_sync_status(conn, "syncing")
    broadcast_sync(conn.id, {:sync_status, "syncing"})

    try do
      do_sync_products(conn)
    rescue
      e ->
        Logger.error("Product sync crashed for #{conn.provider_type}: #{Exception.message(e)}")
        Products.update_sync_status(conn, "failed")
        broadcast_sync(conn.id, {:sync_status, "failed"})
        {:error, :sync_crashed}
    end
  end

  defp do_sync_products(conn) do
    with {:ok, provider} <- Provider.for_connection(conn),
         {:ok, products} <- provider.fetch_products(conn) do
      Logger.info("Fetched #{length(products)} products from #{conn.provider_type}")

      results = sync_all_products(conn, products)

      # Mark products not returned by provider as discontinued
      discontinued = mark_discontinued_products(conn, products)

      created = Enum.count(results, &match?({:ok, _, :created}, &1))
      updated = Enum.count(results, &match?({:ok, _, :updated}, &1))
      unchanged = Enum.count(results, &match?({:ok, _, :unchanged}, &1))
      errors = Enum.count(results, &match?({:error, _}, &1))

      Logger.info(
        "Product sync complete for #{conn.provider_type}: " <>
          "#{created} created, #{updated} updated, #{unchanged} unchanged, " <>
          "#{discontinued} discontinued, #{errors} errors"
      )

      ActivityLog.log_event(
        "sync.completed",
        "Product sync complete — #{created + updated + unchanged} products, #{created} created, #{updated} updated" <>
          if(discontinued > 0, do: ", #{discontinued} discontinued", else: ""),
        payload: %{
          provider_type: conn.provider_type,
          created: created,
          updated: updated,
          unchanged: unchanged,
          discontinued: discontinued,
          errors: errors
        }
      )

      # Enqueue mockup enrichment for Printful products (extra angle images)
      if conn.provider_type == "printful" do
        enqueue_mockup_enrichment(conn, results)
      end

      # Sync shipping rates (non-fatal — logged and skipped on failure)
      sync_shipping_rates(conn, provider, products)

      Products.update_sync_status(conn, "completed", DateTime.utc_now())
      product_count = Products.count_products_for_connection(conn.id)
      broadcast_sync(conn.id, {:sync_status, "completed", product_count})

      # Rebuild search index after successful sync
      Berrypod.Search.rebuild_index()

      # Invalidate cached categories and countries (products/rates may have changed)
      Berrypod.Settings.SettingsCache.invalidate_cached(:categories)
      Berrypod.Settings.SettingsCache.invalidate_cached(:countries_with_names)

      :ok
    else
      {:error, reason} = error ->
        Logger.error("Product sync failed for #{conn.provider_type}: #{inspect(reason)}")

        ActivityLog.log_event("sync.failed", "Product sync failed: #{inspect(reason)}",
          level: "error",
          payload: %{
            provider_type: conn.provider_type,
            connection_id: conn.id,
            error: inspect(reason)
          }
        )

        Products.update_sync_status(conn, "failed")
        broadcast_sync(conn.id, {:sync_status, "failed"})
        error
    end
  end

  defp sync_all_products(conn, products) do
    products
    |> Task.async_stream(
      fn product_data -> sync_single_product(conn, product_data) end,
      max_concurrency: @max_concurrency,
      timeout: 30_000,
      on_timeout: :kill_task
    )
    |> Enum.map(fn
      {:ok, result} -> result
      {:exit, :timeout} -> {:error, :timeout}
      {:exit, reason} -> {:error, reason}
    end)
  end

  # Mark products that exist locally but weren't returned by the provider as discontinued.
  # Returns the count of products that were discontinued.
  defp mark_discontinued_products(conn, products) do
    # Get provider_product_ids from the fetched products
    fetched_ids = MapSet.new(products, & &1[:provider_product_id])

    # Find local active products not in the fetched list
    Products.list_active_products_for_connection(conn.id)
    |> Enum.reject(fn product -> MapSet.member?(fetched_ids, product.provider_product_id) end)
    |> Enum.map(fn product ->
      Logger.info("Marking product #{product.title} as discontinued (no longer in provider)")

      ActivityLog.log_event(
        "sync.product_discontinued",
        "#{product.title} no longer available from provider",
        level: "warning",
        payload: %{product_id: product.id, title: product.title}
      )

      Products.update_product(product, %{status: "discontinued"})
    end)
    |> length()
  end

  defp sync_single_product(conn, product_data) do
    case sync_product(conn, product_data) do
      {:ok, product, status} ->
        sync_product_associations(product, product_data)
        {:ok, product, status}

      error ->
        Logger.warning(
          "Failed to sync product #{product_data[:provider_product_id]}: #{inspect(error)}"
        )

        error
    end
  end

  defp sync_product(conn, product_data) do
    attrs = %{
      provider_product_id: product_data[:provider_product_id],
      title: product_data[:title],
      description: product_data[:description],
      category: product_data[:category],
      provider_data: product_data[:provider_data]
    }

    Products.upsert_product(conn, attrs)
  end

  defp broadcast_sync(conn_id, message) do
    Phoenix.PubSub.broadcast(Berrypod.PubSub, "sync:#{conn_id}", message)
  end

  defp sync_product_associations(product, product_data) do
    # Sync images
    images =
      (product_data[:images] || [])
      |> Enum.map(fn img ->
        %{
          src: img[:src],
          position: img[:position],
          alt: img[:alt],
          color: img[:color]
        }
      end)

    image_results = Products.sync_product_images(product, images)

    # Enqueue downloads for images without image_id
    Enum.each(image_results, fn
      {:ok, %{image_id: nil, id: id}} -> ImageDownloadWorker.enqueue(id)
      _ -> :ok
    end)

    # Sync variants
    variants =
      (product_data[:variants] || [])
      |> Enum.map(fn var ->
        %{
          provider_variant_id: var[:provider_variant_id],
          title: var[:title],
          sku: var[:sku],
          price: var[:price],
          cost: var[:cost],
          options: var[:options],
          is_enabled: var[:is_enabled],
          is_available: var[:is_available]
        }
      end)

    Products.sync_product_variants(product, variants)

    # Recompute denormalized fields (cheapest_price, in_stock, on_sale) from variants
    Products.recompute_cached_fields(product)
  end

  defp sync_shipping_rates(conn, provider, products) do
    if function_exported?(provider, :fetch_shipping_rates, 2) do
      # Fetch live exchange rates so shipping costs are stored in GBP
      {:ok, exchange_rates} = Berrypod.ExchangeRate.fetch_and_cache()

      case provider.fetch_shipping_rates(conn, products) do
        {:ok, rates} when rates != [] ->
          Berrypod.Shipping.upsert_rates(conn.id, rates, exchange_rates)

        {:ok, []} ->
          Logger.info("No shipping rates returned for #{conn.provider_type}")

        {:error, reason} ->
          Logger.warning(
            "Shipping rate sync failed for #{conn.provider_type}: #{inspect(reason)}"
          )
      end
    end
  rescue
    e ->
      Logger.error(
        "Shipping rate sync crashed for #{conn.provider_type}: #{Exception.message(e)}"
      )
  end

  # Enqueue MockupEnricher jobs for created/updated Printful products
  defp enqueue_mockup_enrichment(conn, results) do
    products_to_enrich =
      results
      |> Enum.filter(&match?({:ok, _, status} when status in [:created, :updated], &1))
      |> Enum.map(fn {:ok, product, _status} -> product end)

    if products_to_enrich != [] do
      Logger.info(
        "Enqueueing mockup enrichment for #{length(products_to_enrich)} Printful product(s)"
      )

      products_to_enrich
      |> Enum.with_index()
      |> Enum.each(fn {product, index} ->
        MockupEnricher.enqueue(conn.id, product.id, index)
      end)
    end
  rescue
    e ->
      Logger.error("Mockup enrichment enqueue failed: #{Exception.message(e)}")
  end
end
