defmodule Berrypod.Providers.Printify do
  @moduledoc """
  Printify provider implementation.

  Handles product sync and order submission for Printify.
  """

  @behaviour Berrypod.Providers.Provider

  alias Berrypod.Clients.Printify, as: Client
  alias Berrypod.Products.ProviderConnection

  require Logger

  @impl true
  def provider_type, do: "printify"

  @impl true
  def test_connection(%ProviderConnection{} = conn) do
    with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn),
         :ok <- set_api_key(api_key),
         {:ok, shops} <- Client.get_shops() do
      shop = List.first(shops)

      {:ok,
       %{
         shop_id: shop["id"],
         shop_name: shop["title"],
         shop_count: length(shops)
       }}
    else
      nil -> {:error, :no_api_key}
      {:error, _} = error -> error
    end
  end

  @impl true
  def fetch_products(%ProviderConnection{config: config} = conn) do
    shop_id = config["shop_id"]

    if is_nil(shop_id) do
      {:error, :no_shop_id}
    else
      with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn),
           :ok <- set_api_key(api_key),
           {:ok, products} <- fetch_all_products(shop_id) do
        {:ok, products}
      else
        nil -> {:error, :no_api_key}
        {:error, _} = error -> error
      end
    end
  end

  # Fetches all products by paginating through the API
  defp fetch_all_products(shop_id) do
    fetch_products_page(shop_id, 1, [])
  end

  defp fetch_products_page(shop_id, page, acc) do
    case Client.list_products(shop_id, page: page) do
      {:ok, response} ->
        products = Enum.map(response["data"] || [], &normalize_product/1)
        all_products = acc ++ products

        current_page = response["current_page"] || page
        last_page = response["last_page"] || 1

        if current_page < last_page do
          # Small delay to be nice to rate limits (600/min = 10/sec)
          Process.sleep(100)
          fetch_products_page(shop_id, page + 1, all_products)
        else
          {:ok, all_products}
        end

      {:error, _} = error ->
        error
    end
  end

  @impl true
  def submit_order(%ProviderConnection{config: config} = conn, order) do
    shop_id = config["shop_id"]

    if is_nil(shop_id) do
      {:error, :no_shop_id}
    else
      with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn),
           :ok <- set_api_key(api_key),
           order_data <- build_order_payload(order),
           {:ok, response} <- Client.create_order(shop_id, order_data) do
        {:ok, %{provider_order_id: response["id"]}}
      else
        nil -> {:error, :no_api_key}
        {:error, _} = error -> error
      end
    end
  end

  @impl true
  def get_order_status(%ProviderConnection{config: config} = conn, provider_order_id) do
    shop_id = config["shop_id"]

    if is_nil(shop_id) do
      {:error, :no_shop_id}
    else
      with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn),
           :ok <- set_api_key(api_key),
           {:ok, response} <- Client.get_order(shop_id, provider_order_id) do
        {:ok, normalize_order_status(response)}
      else
        nil -> {:error, :no_api_key}
        {:error, _} = error -> error
      end
    end
  end

  # =============================================================================
  # Shipping Rates
  # =============================================================================

  @impl true
  def fetch_shipping_rates(%ProviderConnection{} = conn, products) when is_list(products) do
    with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn),
         :ok <- set_api_key(api_key) do
      pairs = extract_blueprint_provider_pairs(products)
      Logger.info("Fetching shipping rates for #{length(pairs)} blueprint/provider pairs")

      rates =
        pairs
        |> Enum.with_index()
        |> Enum.flat_map(fn {pair, index} ->
          # Rate limit: 100ms between requests
          if index > 0, do: Process.sleep(100)
          fetch_shipping_for_pair(pair)
        end)

      {:ok, rates}
    else
      nil -> {:error, :no_api_key}
    end
  end

  defp extract_blueprint_provider_pairs(products) do
    products
    |> Enum.flat_map(fn product ->
      provider_data = product[:provider_data] || %{}
      blueprint_id = provider_data[:blueprint_id] || provider_data["blueprint_id"]
      print_provider_id = provider_data[:print_provider_id] || provider_data["print_provider_id"]

      if blueprint_id && print_provider_id do
        [{blueprint_id, print_provider_id}]
      else
        []
      end
    end)
    |> Enum.uniq()
  end

  defp fetch_shipping_for_pair({blueprint_id, print_provider_id}) do
    case Client.get_shipping(blueprint_id, print_provider_id) do
      {:ok, response} ->
        normalize_shipping_response(blueprint_id, print_provider_id, response)

      {:error, reason} ->
        Logger.warning(
          "Failed to fetch shipping for blueprint #{blueprint_id}, " <>
            "provider #{print_provider_id}: #{inspect(reason)}"
        )

        []
    end
  end

  defp normalize_shipping_response(blueprint_id, print_provider_id, response) do
    handling_time_days =
      case response["handling_time"] do
        %{"value" => value, "unit" => "day"} -> value
        _ -> nil
      end

    profiles = response["profiles"] || []

    # For each profile, expand countries into individual rate maps.
    # Then group by country and take the max first_item_cost across profiles
    # (conservative estimate across variant groups).
    profiles
    |> Enum.flat_map(fn profile ->
      countries = profile["countries"] || []
      first_cost = get_in(profile, ["first_item", "cost"]) || 0
      additional_cost = get_in(profile, ["additional_items", "cost"]) || 0
      currency = get_in(profile, ["first_item", "currency"]) || "USD"

      Enum.map(countries, fn country ->
        %{
          blueprint_id: blueprint_id,
          print_provider_id: print_provider_id,
          country_code: country,
          first_item_cost: first_cost,
          additional_item_cost: additional_cost,
          currency: currency,
          handling_time_days: handling_time_days
        }
      end)
    end)
    |> Enum.group_by(& &1.country_code)
    |> Enum.map(fn {_country, country_rates} ->
      # Take the max first_item_cost across variant groups for this country
      Enum.max_by(country_rates, & &1.first_item_cost)
    end)
  end

  # =============================================================================
  # Webhook Registration
  # =============================================================================

  @webhook_events [
    "product:updated",
    "product:deleted",
    "product:publish:started",
    "order:sent-to-production",
    "order:shipment:created",
    "order:shipment:delivered"
  ]

  @doc """
  Registers webhooks for product events with Printify.

  Returns {:ok, results} or {:error, reason}.
  """
  def register_webhooks(%ProviderConnection{config: config} = conn, webhook_url) do
    shop_id = config["shop_id"]
    secret = config["webhook_secret"]

    cond do
      is_nil(shop_id) ->
        {:error, :no_shop_id}

      is_nil(secret) or secret == "" ->
        {:error, :no_webhook_secret}

      true ->
        with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn),
             :ok <- set_api_key(api_key) do
          {:ok, create_all_webhooks(shop_id, webhook_url, secret)}
        else
          nil -> {:error, :no_api_key}
        end
    end
  end

  defp create_all_webhooks(shop_id, webhook_url, secret) do
    Enum.map(@webhook_events, fn event ->
      case Client.create_webhook(shop_id, webhook_url, event, secret) do
        {:ok, response} -> {:ok, event, response}
        {:error, reason} -> {:error, event, reason}
      end
    end)
  end

  @doc """
  Lists registered webhooks for the shop.
  """
  def list_webhooks(%ProviderConnection{config: config} = conn) do
    shop_id = config["shop_id"]

    if is_nil(shop_id) do
      {:error, :no_shop_id}
    else
      with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn),
           :ok <- set_api_key(api_key),
           {:ok, webhooks} <- Client.list_webhooks(shop_id) do
        {:ok, webhooks}
      else
        nil -> {:error, :no_api_key}
        {:error, _} = error -> error
      end
    end
  end

  # =============================================================================
  # Option Types Extraction (for frontend)
  # =============================================================================

  @doc """
  Extracts option types from Printify provider_data for frontend display.

  Returns a list of option type maps with normalized names, types, and values
  including hex color codes for color-type options.

  ## Examples

      iex> extract_option_types(%{"options" => [
      ...>   %{"name" => "Colors", "type" => "color", "values" => [
      ...>     %{"id" => 1, "title" => "Black", "colors" => ["#000000"]}
      ...>   ]}
      ...> ]})
      [%{name: "Color", type: :color, values: [%{id: 1, title: "Black", hex: "#000000"}]}]
  """
  def extract_option_types(%{"options" => options}) when is_list(options) do
    Enum.map(options, fn opt ->
      %{
        name: singularize_option_name(opt["name"]),
        type: option_type_atom(opt["type"]),
        values: extract_option_values(opt)
      }
    end)
  end

  def extract_option_types(_), do: []

  defp option_type_atom("color"), do: :color
  defp option_type_atom("size"), do: :size
  defp option_type_atom(_), do: :other

  defp extract_option_values(%{"values" => values, "type" => "color"}) do
    Enum.map(values, fn val ->
      %{
        id: val["id"],
        title: val["title"],
        hex: List.first(val["colors"])
      }
    end)
  end

  defp extract_option_values(%{"values" => values}) do
    Enum.map(values, fn val ->
      %{id: val["id"], title: val["title"]}
    end)
  end

  # =============================================================================
  # Data Normalization
  # =============================================================================

  defp normalize_product(raw) do
    options = raw["options"] || []
    raw_variants = raw["variants"] || []
    color_lookup = build_image_color_lookup(raw_variants, options)
    filtered_options = filter_options_by_variants(options, raw_variants)

    %{
      provider_product_id: to_string(raw["id"]),
      title: raw["title"],
      description: raw["description"],
      category: extract_category(raw),
      images: normalize_images(raw["images"] || [], color_lookup),
      variants: normalize_variants(raw_variants, options),
      provider_data: %{
        blueprint_id: raw["blueprint_id"],
        print_provider_id: raw["print_provider_id"],
        tags: raw["tags"] || [],
        options: filtered_options,
        raw: raw
      }
    }
  end

  defp normalize_images(images, color_lookup) do
    images
    |> Enum.map(fn img ->
      color = resolve_image_color(img["variant_ids"] || [], color_lookup)
      %{src: img["src"], alt: img["position"], color: color, raw_position: img["position"]}
    end)
    |> select_images_per_color()
    |> Enum.with_index()
    |> Enum.map(fn {img, index} ->
      %{src: img.src, position: index, alt: img.alt, color: img.color}
    end)
  end

  # Hero colour (first seen) keeps all images.
  # Other colours keep only front + back views to avoid bloating the DB.
  defp select_images_per_color(images) do
    hero_color =
      images
      |> Enum.find_value(fn img -> img.color end)

    Enum.filter(images, fn img ->
      img.color == hero_color || is_nil(img.color) ||
        img.raw_position in ["front", "back"]
    end)
  end

  # Filter blueprint options to only include values present in enabled variants,
  # ordered to match variant ordering (so the default variant's colour comes first).
  defp filter_options_by_variants(options, raw_variants) do
    enabled_order = enabled_option_value_order(options, raw_variants)

    Enum.map(options, fn opt ->
      name = opt["name"]

      case Map.get(enabled_order, name) do
        nil ->
          opt

        ordered_values ->
          values_by_title = Map.new(opt["values"] || [], fn v -> {v["title"], v} end)

          sorted =
            ordered_values
            |> Enum.map(&Map.get(values_by_title, &1))
            |> Enum.reject(&is_nil/1)

          Map.put(opt, "values", sorted)
      end
    end)
  end

  # Returns %{"Colors" => ["Dark Heather", "Navy", ...], "Sizes" => ["S", "2XL"]}
  # preserving first-seen order from enabled variants.
  defp enabled_option_value_order(options, raw_variants) do
    option_names = Enum.map(options, & &1["name"])

    raw_variants
    |> Enum.filter(& &1["is_enabled"])
    |> Enum.reduce(%{}, fn var, acc ->
      parts = String.split(var["title"] || "", " / ")

      option_names
      |> Enum.with_index()
      |> Enum.reduce(acc, fn {name, idx}, inner_acc ->
        case Enum.at(parts, idx) do
          nil ->
            inner_acc

          val ->
            existing = Map.get(inner_acc, name, [])

            if val in existing,
              do: inner_acc,
              else: Map.put(inner_acc, name, existing ++ [val])
        end
      end)
    end)
  end

  defp build_image_color_lookup(raw_variants, options) do
    option_names = Enum.map(options, & &1["name"])
    color_index = Enum.find_index(option_names, &(&1 in ["Colors", "Color"]))

    if color_index do
      Map.new(raw_variants, fn var ->
        parts = String.split(var["title"] || "", " / ")
        color = Enum.at(parts, color_index)
        {var["id"], color}
      end)
    else
      %{}
    end
  end

  defp resolve_image_color(variant_ids, color_lookup) do
    Enum.find_value(variant_ids, fn vid -> Map.get(color_lookup, vid) end)
  end

  defp normalize_variants(variants, options) do
    option_names = extract_option_names(options)

    Enum.map(variants, fn var ->
      %{
        provider_variant_id: to_string(var["id"]),
        title: var["title"],
        sku: var["sku"],
        price: var["price"],
        cost: var["cost"],
        options: normalize_variant_options(var, option_names),
        is_enabled: var["is_enabled"] == true,
        is_available: var["is_available"] == true
      }
    end)
  end

  # Extract option names from product options, singularizing common plurals
  defp extract_option_names(options) do
    Enum.map(options, fn opt ->
      singularize_option_name(opt["name"])
    end)
  end

  defp singularize_option_name("Colors"), do: "Color"
  defp singularize_option_name("Sizes"), do: "Size"
  defp singularize_option_name(name), do: name

  defp normalize_variant_options(variant, option_names) do
    # Build human-readable map from variant title
    # Title format matches product options order: "Navy / S" for [Colors, Sizes]
    title = variant["title"] || ""
    parts = String.split(title, " / ")

    parts
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {value, index}, acc ->
      key = Enum.at(option_names, index) || "Option #{index + 1}"
      Map.put(acc, key, value)
    end)
  end

  defp extract_category(raw) do
    # Try to extract category from tags (case-insensitive)
    tags =
      (raw["tags"] || [])
      |> Enum.map(&String.downcase/1)

    cond do
      has_tag?(tags, ~w[t-shirt tshirt shirt hoodie sweatshirt apparel clothing]) -> "Apparel"
      has_tag?(tags, ~w[canvas]) -> "Canvas Prints"
      has_tag?(tags, ~w[mug cup blanket pillow cushion homeware homewares home]) -> "Homewares"
      has_tag?(tags, ~w[notebook journal stationery]) -> "Stationery"
      has_tag?(tags, ~w[phone case bag tote accessories]) -> "Accessories"
      has_tag?(tags, ~w[art print poster wall]) -> "Art Prints"
      true -> List.first(raw["tags"])
    end
  end

  defp has_tag?(tags, keywords) do
    Enum.any?(tags, fn tag ->
      Enum.any?(keywords, fn keyword -> String.contains?(tag, keyword) end)
    end)
  end

  defp normalize_order_status(raw) do
    %{
      status: map_order_status(raw["status"]),
      provider_status: raw["status"],
      tracking_number: extract_tracking(raw),
      tracking_url: extract_tracking_url(raw),
      shipments: raw["shipments"] || []
    }
  end

  defp map_order_status("pending"), do: "submitted"
  defp map_order_status("on-hold"), do: "submitted"
  defp map_order_status("payment-not-received"), do: "submitted"
  defp map_order_status("cost-calculation"), do: "submitted"
  defp map_order_status("in-production"), do: "processing"
  defp map_order_status("partially-shipped"), do: "processing"
  defp map_order_status("shipped"), do: "shipped"
  defp map_order_status("delivered"), do: "delivered"
  defp map_order_status("canceled"), do: "cancelled"
  defp map_order_status(_), do: "submitted"

  defp extract_tracking(raw) do
    case raw["shipments"] do
      [shipment | _] -> shipment["tracking_number"]
      _ -> nil
    end
  end

  defp extract_tracking_url(raw) do
    case raw["shipments"] do
      [shipment | _] -> shipment["tracking_url"]
      _ -> nil
    end
  end

  # =============================================================================
  # Order Building
  # =============================================================================

  defp build_order_payload(order_data) do
    %{
      external_id: order_data.order_number,
      label: order_data.order_number,
      line_items:
        Enum.map(order_data.line_items, fn item ->
          %{
            product_id: item.provider_product_id,
            variant_id: parse_variant_id(item.provider_variant_id),
            quantity: item.quantity
          }
        end),
      shipping_method: 1,
      address_to: build_address(order_data.shipping_address, order_data.customer_email)
    }
  end

  # Maps Stripe shipping_details address fields to Printify's expected format.
  # Stripe gives us: name, line1, line2, city, postal_code, state, country
  # Printify wants: first_name, last_name, address1, address2, city, zip, region, country
  defp build_address(address, email) when is_map(address) do
    {first, last} = split_name(address["name"])

    %{
      first_name: first,
      last_name: last,
      email: email,
      phone: address["phone"] || "",
      country: address["country"] || "",
      region: address["state"] || address["region"] || "",
      address1: address["line1"] || address["address1"] || "",
      address2: address["line2"] || address["address2"] || "",
      city: address["city"] || "",
      zip: address["postal_code"] || address["zip"] || ""
    }
  end

  defp build_address(_address, email) do
    %{
      first_name: "",
      last_name: "",
      email: email,
      phone: "",
      country: "",
      region: "",
      address1: "",
      address2: "",
      city: "",
      zip: ""
    }
  end

  defp split_name(nil), do: {"", ""}
  defp split_name(""), do: {"", ""}

  defp split_name(name) do
    case String.split(name, " ", parts: 2) do
      [first] -> {first, ""}
      [first, last] -> {first, last}
    end
  end

  # Printify variant IDs are integers, but we store them as strings
  defp parse_variant_id(id) when is_integer(id), do: id
  defp parse_variant_id(id) when is_binary(id), do: String.to_integer(id)

  # =============================================================================
  # API Key Management
  # =============================================================================

  # Temporarily sets the API key for the request
  # In a production system, this would use a connection pool or request context
  defp set_api_key(api_key) do
    Process.put(:printify_api_key, api_key)
    :ok
  end
end
