defmodule TarakanWeb.BountyLive.New do
  @moduledoc """
  Post a new contract (a bounty internally).

  This route lives in the public live session so `/bounties/:public_id` can
  stay public; it enforces authentication in mount and redirects anonymous
  visitors to the login page.

  Targets are picked, not typed: repositories come from a live search and
  infestations from a select, so `target_ref` always holds a reference the
  market can resolve. `?target_type=&target_ref=` prefills the form, which is
  how repository and infestation pages link into it.
  """
  use TarakanWeb, :live_view

  alias Tarakan.Credits
  alias Tarakan.Infestations
  alias Tarakan.Market
  alias Tarakan.Repositories
  alias Tarakan.Repositories.Repository

  @target_types [
    {"Repository", "repository"},
    {"Infestation", "infestation"},
    {"Finding", "finding"}
  ]

  @fundings [{"Credits", "credits"}, {"Card (fiat escrow)", "fiat"}]

  @search_limit 8
  @infestation_limit 50

  @impl true
  def mount(params, _session, socket) do
    scope = socket.assigns.current_scope

    if scope && scope.account do
      form_params = prefill(params)

      {:ok,
       socket
       |> assign(:page_title, "Post a contract")
       |> assign(:target_types, @target_types)
       |> assign(:fundings, @fundings)
       |> assign(:credit_balance, Credits.balance(scope.account))
       |> assign(:infestation_options, infestation_options())
       |> assign(:repo_results, [])
       |> assign_form(form_params)}
    else
      {:ok,
       socket
       |> put_flash(:error, "You must log in to access this page.")
       |> redirect(to: ~p"/accounts/log-in")}
    end
  end

  defp prefill(params) do
    %{
      "target_type" => present_or(params["target_type"], "repository"),
      "target_ref" => present_or(params["target_ref"], ""),
      "funding" => present_or(params["funding"], "credits")
    }
  end

  defp present_or(value, default) when is_binary(value) do
    if String.trim(value) == "", do: default, else: value
  end

  defp present_or(_value, default), do: default

  @impl true
  def handle_event("validate", %{"bounty" => params}, socket) do
    {:noreply,
     socket
     |> assign_form(params)
     |> search_repositories(params)}
  end

  def handle_event("pick_repository", %{"ref" => ref}, socket) do
    params =
      socket.assigns.form.params
      |> Map.put("target_ref", ref)
      |> Map.put("repo_query", "")

    {:noreply,
     socket
     |> assign_form(params)
     |> assign(:repo_results, [])}
  end

  def handle_event("clear_repository", _params, socket) do
    {:noreply,
     socket
     |> assign_form(Map.put(socket.assigns.form.params, "target_ref", ""))
     |> assign(:repo_results, [])}
  end

  def handle_event("create", %{"bounty" => params}, socket) do
    attrs = build_attrs(params)

    case Market.create_bounty(socket.assigns.current_scope, attrs) do
      {:ok, bounty} ->
        {:noreply,
         socket
         |> put_flash(:info, "Contract posted - credits are held in escrow until settlement.")
         |> push_navigate(to: ~p"/bounties/#{bounty.public_id}")}

      {:ok, _bounty, checkout_url} ->
        {:noreply, redirect(socket, external: checkout_url)}

      {:error, reason} ->
        {:noreply, put_flash(socket, :error, create_error_message(reason))}
    end
  end

  # The repository search runs only while the repository target is selected and
  # nothing has been picked yet - once a ref is set the results list is noise.
  defp search_repositories(socket, params) do
    query = params["repo_query"] || ""

    results =
      if params["target_type"] == "repository" and blank?(params["target_ref"]) do
        Repositories.search_repositories(query, @search_limit)
      else
        []
      end

    assign(socket, :repo_results, results)
  end

  defp assign_form(socket, params) do
    params = Map.put_new(params, "target_type", "repository")

    socket
    |> assign(:form, to_form(params, as: :bounty))
    |> assign(:target_type, params["target_type"])
    |> assign(:funding, if(params["funding"] == "fiat", do: "fiat", else: "credits"))
    |> assign(:selected_repository, selected_repository(params))
  end

  defp selected_repository(%{"target_type" => "repository", "target_ref" => ref})
       when is_binary(ref) do
    if String.trim(ref) == "", do: nil, else: ref
  end

  defp selected_repository(_params), do: nil

  defp blank?(nil), do: true
  defp blank?(value) when is_binary(value), do: String.trim(value) == ""

  defp infestation_options do
    Infestations.list_infestations(limit: @infestation_limit)
    |> Enum.map(fn infestation ->
      {"#{infestation.title} (#{infestation.repo_count} repos)", infestation.pattern_key}
    end)
  end

  # Hosted repositories are addressed without a host; remote ones need theirs.
  defp repository_ref(%Repository{} = repository) do
    if Repository.hosted?(repository) do
      "#{repository.owner}/#{repository.name}"
    else
      "#{repository.host}/#{repository.owner}/#{repository.name}"
    end
  end

  defp build_attrs(params) do
    funding = if params["funding"] == "fiat", do: "fiat", else: "credits"

    base = %{
      "target_type" => params["target_type"],
      "target_ref" => params["target_ref"],
      "title" => params["title"],
      "description" => params["description"],
      "funding" => funding,
      "expires_days" => params["expires_days"]
    }

    case funding do
      "fiat" -> Map.put(base, "amount_cents", dollars_to_cents(params["amount"]))
      "credits" -> Map.put(base, "credit_amount", parse_integer(params["amount"]))
    end
  end

  defp dollars_to_cents(nil), do: nil

  defp dollars_to_cents(value) when is_binary(value) do
    case Float.parse(String.trim(value)) do
      {dollars, ""} -> round(dollars * 100)
      _invalid -> nil
    end
  end

  defp parse_integer(nil), do: nil

  defp parse_integer(value) when is_binary(value) do
    case Integer.parse(String.trim(value)) do
      {integer, ""} -> integer
      _invalid -> nil
    end
  end

  defp create_error_message(:insufficient_credits),
    do: "Not enough credits to fund this contract."

  defp create_error_message(:target_not_found),
    do: "That target was not found. Check the reference and try again."

  defp create_error_message(:target_not_public),
    do: "Contracts can only target publicly visible repositories and findings."

  defp create_error_message(:unauthorized),
    do: "Only active accounts can post contracts."

  defp create_error_message(%Ecto.Changeset{} = changeset) do
    details =
      changeset
      |> Ecto.Changeset.traverse_errors(fn {message, _meta} -> message end)
      |> Enum.map_join("; ", fn {field, messages} -> "#{field} #{Enum.join(messages, ", ")}" end)

    "The contract could not be created: #{details}."
  end

  defp create_error_message(_reason),
    do: "The contract could not be created. Try again shortly."

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
      <Layouts.page width={:focused}>
        <div class="border-b-2 border-strong pb-6">
          <h1 class="font-display text-3xl font-medium uppercase leading-none tracking-[0.02em] text-ink">
            Post a contract
          </h1>
        </div>

        <.form
          for={@form}
          id="bounty-form"
          phx-change="validate"
          phx-submit="create"
          class="mt-6 space-y-5"
        >
          <.input
            field={@form[:target_type]}
            type="select"
            label="Target type"
            options={@target_types}
          />

          <div :if={@target_type == "repository"}>
            <div :if={@selected_repository} class="flex items-center gap-3">
              <span class="font-mono text-xs uppercase tracking-[0.12em] text-ink-faint">Target</span>
              <span
                id="bounty-selected-repository"
                class="min-w-0 flex-1 truncate border-2 border-strong bg-panel px-3 py-2 font-mono text-xs text-ink"
              >
                {@selected_repository}
              </span>
              <button
                type="button"
                phx-click="clear_repository"
                class="cursor-pointer font-mono text-[11px] uppercase tracking-[0.12em] text-signal hover:underline"
              >
                Change
              </button>
            </div>

            <div :if={is_nil(@selected_repository)}>
              <.input
                field={@form[:repo_query]}
                type="text"
                label="Repository"
                placeholder="Search listed repositories"
                phx-debounce="200"
                autocomplete="off"
              />
              <ul
                :if={@repo_results != []}
                id="bounty-repo-results"
                class="mt-2 divide-y divide-rule border-2 border-strong bg-ground"
              >
                <li :for={repository <- @repo_results}>
                  <button
                    type="button"
                    phx-click="pick_repository"
                    phx-value-ref={repository_ref(repository)}
                    class="flex w-full cursor-pointer items-baseline gap-2 px-3 py-2 text-left transition hover:bg-panel"
                  >
                    <span class="min-w-0 flex-1 truncate font-mono text-xs text-ink">
                      {repository.owner}/{repository.name}
                    </span>
                    <span class="shrink-0 font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
                      {repository.host}
                    </span>
                  </button>
                </li>
              </ul>
              <p
                :if={@repo_results == [] and not blank?(@form.params["repo_query"])}
                class="mt-2 font-mono text-[11px] text-ink-faint"
              >
                No listed repository matches.
              </p>
            </div>
          </div>

          <.input
            :if={@target_type == "infestation"}
            field={@form[:target_ref]}
            type="select"
            label="Infestation"
            options={@infestation_options}
            prompt="Choose an infestation"
          />

          <div :if={@target_type == "finding"}>
            <.input
              field={@form[:target_ref]}
              type="text"
              label="Finding"
              placeholder="Public id from the finding page"
              autocomplete="off"
            />
            <p class="mt-2 font-mono text-[11px] leading-5 text-ink-faint">
              The id in the finding's URL, <code>/findings/&lt;id&gt;</code>.
            </p>
          </div>

          <input
            :if={@target_type == "repository"}
            type="hidden"
            name="bounty[target_ref]"
            value={@form.params["target_ref"] || ""}
          />

          <.input field={@form[:title]} type="text" label="Title" required maxlength="200" />
          <.input
            field={@form[:description]}
            type="textarea"
            label="What should the hunter do?"
            rows="5"
            required
          />
          <.input
            field={@form[:funding]}
            type="select"
            label="Fund with"
            options={@fundings}
          />
          <.input
            field={@form[:amount]}
            type="text"
            label={if @funding == "fiat", do: "Amount (USD)", else: "Amount (credits)"}
            placeholder={if @funding == "fiat", do: "250", else: "500"}
            required
          />
          <p
            :if={@funding == "credits"}
            id="bounty-credit-balance"
            class="-mt-3 font-mono text-[11px] leading-5 text-ink-faint"
          >
            Your balance: <span class="text-ink">{@credit_balance} credits</span>
            · platform take rate applies at settlement.
          </p>
          <p
            :if={@funding == "fiat"}
            class="-mt-3 font-mono text-[11px] leading-5 text-ink-faint"
          >
            Held by Stripe until the work is accepted · platform take rate applies at settlement.
          </p>

          <button
            id="bounty-create-button"
            type="submit"
            class="cursor-pointer border-2 border-signal px-5 py-2 font-display text-xs uppercase tracking-[0.12em] text-signal transition hover:bg-signal hover:text-ground"
          >
            Post contract
          </button>
        </.form>
      </Layouts.page>
    </Layouts.app>
    """
  end
end
