defmodule Mix.Tasks.CreatePaddleProdPlans do
  @moduledoc """
  ## Utility for creating Paddle plans for production use.

  Takes a single `filename` argument which should be of format
  `input_plans_v*.json`. That file should live in the `/priv` directory next
  to all other plans and it should contain the necessary information about
  the production plans to be created.

  In order to create the input file:

  * Copy an existing `plans_v*.json` (latest recommended) into the new
    `input_plans_v*.json` file.
  * For every plan object:
    * Adjust the generation, limits, features, etc as desired
    * Replace `monthly_product_id` with a `monthly_price` (integer)
    * Replace `yearly_product_id` with a `yearly_price` (integer)

  After this task is finished successfully, the plans will be created in Paddle
  with the prices given in the input file. With the creation, every plan gets an
  autoincremented ID in Paddle. We will then fetch those exact plans from Paddle
  in an API call and use their monthly and yearly product_id's to write
  `plans_v*.json`. It will be written taking the input file as the "template"
  and replacing the monthly/yearly prices with monthly/yearly product_id's.

  The prices will be written into `/priv/plan_prices.json` (instead of the
  prod plans output file). Note that this separation is intentional - we only
  store prices locally to not rely on Paddle in the dev environment. Otherwise,
  Paddle is considered the "source of truth" of plan prices.

  ## Usage example:

  ```
  mix create_paddle_prod_plans input_plans_v5.json
  ```

  ## Requirement 1: Replace the curl command

  Unfortunately, there's no API in Paddle that would allow "bulk creating"
  plans - it has to be done through the UI. As a hack though, we can automate
  the process by copying the curl request with the help of browser devtools.

  Therefore, this Mix.Task **does not work out of the box** and the actual curl
  command that it executes must be replaced by the developer. Here's how:

  0) Access required to the production Paddle account
  1) Navigate to https://vendors.paddle.com/subscriptions/plans. Chrome or
     Firefox recommended (need to copy a POST request as cURL in a later step)
  2) Click the "+ New Plan" button (top right of the screen) to open the form
  3) Open browser devtools, fill in the required fields and submit the form.
     No need to worry about the form fields since they're provided in this task
     (except `_token`) and they *should work* as long as nothing has changed.
  4) Find the POST request from the "Network" tab and copy it as cURL
  5) Come back here and paste it into the `create_paddle_plan` function
  6) Replace the params within the string with the real params (these should
     be available in the function already)

  ## Requirement 2: Paddle production credentials

  You also need to obtain the Paddle credentials from prod environment and
  replace them into the module attributes. See `@paddle_vendor_id` and
  `@paddle_vendor_auth_code`. Those are needed to fetch the plans via an
  actual API call after the plans have been created in Paddle.
  """

  use Mix.Task

  @requirements ["app.config"]

  @paddle_vendor_id "REPLACE ME"
  @paddle_vendor_auth_code "REPLACE ME"

  def run([filename]) do
    {:ok, _} = Application.ensure_all_started(:telemetry)
    Finch.start_link(name: MyFinch)

    if not Regex.match?(~r/^input_plans_v(\d+)\.json$/, filename) do
      raise ArgumentError,
            "Invalid filename argument. Note the strict format - e.g.: \"input_plans_v5.json\""
    end

    input_plans =
      Application.app_dir(:plausible, ["priv", filename])
      |> File.read!()
      |> JSON.decode!()

    to_be_created_in_paddle =
      input_plans
      |> Enum.flat_map(fn plan ->
        [
          create_paddle_plan_attrs(plan, "monthly"),
          create_paddle_plan_attrs(plan, "yearly")
        ]
      end)

    user_input =
      """
      \n
      ##########################################################################
      #                                                                        #
      #                              !WARNING!                                 #
      #                                                                        #
      #      You're about to create production plans in Paddle. Multiple       #
      #      consecutive executions will create the same plans again.          #
      #      Please make sure to not leave duplicates behind!                  #
      #                                                                        #
      ##########################################################################

      * 'y' - proceed and create all plans
      * 't' - test only with two plans
      * 'h' - halt

      What would you like to do?
      """
      |> IO.gets()
      |> String.trim()
      |> String.upcase()

    test_run? =
      case user_input do
        "Y" ->
          IO.puts("Creating all plans...")
          false

        "T" ->
          IO.puts("Creating 2 plans just for testing. Make sure to delete them manually!")
          true

        _ ->
          IO.puts("Halting execution per user request.")
          System.halt()
      end

    {paddle_create_count, create_count} =
      if test_run? do
        {2, 1}
      else
        {length(to_be_created_in_paddle), length(input_plans)}
      end

    to_be_created_in_paddle
    |> Enum.take(paddle_create_count)
    |> Enum.each(&create_paddle_plan/1)

    IO.puts("⏳ waiting 3s before fetching the newly created plans...")
    Process.sleep(3000)
    IO.puts("Fetching the #{create_count} plans created a moment ago...")

    created_paddle_plans =
      fetch_all_prod_plans()
      |> Enum.sort_by(& &1["id"])
      |> Enum.take(-paddle_create_count)

    file_path_to_write = Path.join("priv", String.replace(filename, "input_", ""))

    prod_plans_with_ids_and_prices =
      input_plans
      |> Enum.take(create_count)
      |> write_prod_plans_json_file(file_path_to_write, created_paddle_plans)

    IO.puts("✅ Wrote #{create_count} new plans into #{file_path_to_write}!")

    if not test_run? do
      write_prices(prod_plans_with_ids_and_prices)
      IO.puts("✅ Updated `plan_prices.json`.")
    end

    IO.puts("✅ All done!")
  end

  defp create_paddle_plan(%{name: name, price: price, type: type, interval_index: interval_index}) do
    your_unique_token = "abc"

    # Replace this curl command. You should be able to reuse
    # the request body after replacing your unique token.
    curl_command = """
    ... REPLACE ME
    --data-raw '_token=#{your_unique_token}&plan-id=&default-curr=USD&tmpicon=false&name=#{name}&checkout_custom_message=&taxable_type=standard&interval=#{interval_index}&period=1&type=#{type}&trial_length=&price_USD=#{price}&active_EUR=on&price_EUR=#{price}&active_GBP=on&price_GBP=#{price}'
    """

    case curl_quietly(curl_command) do
      :ok ->
        IO.puts("✅ Created #{name}")

      {:error, reason} ->
        IO.puts("❌ Halting. The plan #{name} could not be created. Error: #{reason}")
        System.halt(1)
    end
  end

  @paddle_interval_indexes %{"monthly" => 2, "yearly" => 5}

  defp create_paddle_plan_attrs(plan_with_price, type) do
    %{
      name: plan_name(plan_with_price, type),
      price: plan_with_price["#{type}_price"],
      type: type,
      interval_index: @paddle_interval_indexes[type]
    }
  end

  defp fetch_all_prod_plans() do
    "https://vendors.paddle.com/api/2.0/subscription/plans"
    |> fetch_all_paddle_plans(%{
      vendor_id: @paddle_vendor_id,
      vendor_auth_code: @paddle_vendor_auth_code
    })
  end

  @paddle_plans_api_pagination_limit 500
  def fetch_all_paddle_plans(url, paddle_credentials, page \\ 0, fetched \\ 0) do
    body =
      paddle_credentials
      |> Map.merge(%{
        limit: @paddle_plans_api_pagination_limit,
        offset: page * @paddle_plans_api_pagination_limit
      })
      |> JSON.encode!()

    headers = [
      {"Content-type", "application/json"},
      {"Accept", "application/json"}
    ]

    request = Finch.build(:post, url, headers, body)

    with {:ok, response} <- Finch.request(request, MyFinch),
         {:ok, %{"success" => true, "response" => plans} = body} <- JSON.decode(response.body) do
      fetched = body["count"] + fetched
      total = body["total"]

      IO.puts("✅ Successfully fetched #{fetched}/#{body["total"]} plans")

      if fetched == total do
        plans
      else
        plans ++ fetch_all_paddle_plans(url, paddle_credentials, page + 1, fetched)
      end
    else
      error ->
        IO.puts("❌ Failed to fetch plans from Paddle - #{inspect(error)}")
        System.halt(1)
    end
  end

  defp write_prod_plans_json_file(input_plans, filepath, paddle_plans) do
    prod_plans_with_prices =
      input_plans
      |> Enum.map(fn input_plan ->
        monthly_plan_name = plan_name(input_plan, "monthly")
        yearly_plan_name = plan_name(input_plan, "yearly")

        %{"id" => monthly_product_id} =
          Enum.find(paddle_plans, &(&1["name"] == monthly_plan_name))

        %{"id" => yearly_product_id} =
          Enum.find(paddle_plans, &(&1["name"] == yearly_plan_name))

        input_plan
        |> Map.merge(%{
          "monthly_product_id" => to_string(monthly_product_id),
          "yearly_product_id" => to_string(yearly_product_id)
        })
      end)

    content =
      prod_plans_with_prices
      |> Enum.map(fn plan ->
        plan
        |> Map.drop(["monthly_price", "yearly_price"])
        |> order_keys()
      end)
      |> Jason.encode!(pretty: true)

    File.write!(filepath, content)

    prod_plans_with_prices
  end

  @plan_prices_filepath Application.app_dir(:plausible, ["priv", "plan_prices.json"])
  defp write_prices(prod_plans_with_ids_and_prices) do
    current_prices = File.read!(@plan_prices_filepath) |> JSON.decode!()

    new_prices =
      prod_plans_with_ids_and_prices
      |> Enum.reduce(current_prices, fn plan, prices ->
        prices
        |> Map.put_new(plan["monthly_product_id"], plan["monthly_price"])
        |> Map.put_new(plan["yearly_product_id"], plan["yearly_price"])
      end)
      |> Enum.sort()
      |> Jason.OrderedObject.new()
      |> Jason.encode!(pretty: true)

    File.write(@plan_prices_filepath, new_prices)
  end

  @plan_key_order [
    "kind",
    "generation",
    "monthly_pageview_limit",
    "monthly_product_id",
    "yearly_product_id",
    "site_limit",
    "team_member_limit",
    "features"
  ]
  def order_keys(plan) do
    plan
    |> Map.to_list()
    |> Enum.sort_by(fn {key, _value} ->
      Enum.find_index(@plan_key_order, fn ordered_key -> ordered_key == key end) || 99
    end)
    |> Jason.OrderedObject.new()
  end

  defp plan_name(plan, type) do
    kind = plan["kind"] |> String.capitalize()
    type = type |> String.capitalize()

    volume =
      plan["monthly_pageview_limit"]
      |> PlausibleWeb.StatsView.large_number_format(capitalize_k?: true)

    "Plausible #{kind} #{type} Plan (#{volume})"
  end

  def curl_quietly(cmd) do
    cmd = String.replace(cmd, "curl", ~s|curl -s -o /dev/null -w "%{http_code}"|)

    case System.cmd("sh", ["-c", cmd], stderr_to_stdout: true) do
      {"302", 0} ->
        :ok

      {http_status, 0} ->
        {:error, "unexpected HTTP response status (#{http_status}). Expected 302."}

      {_, exit_code} ->
        {:error, "curl command exited with exit code #{exit_code}"}
    end
  end
end
