defmodule Berrypod.Workers.DeadLinkCheckerWorker do
  @moduledoc """
  Daily Oban cron job that scans all content for broken links.

  Can also be run on-demand for a single page via `new(%{"page_slug" => slug})`.
  """

  use Oban.Worker, queue: :default, max_attempts: 1

  require Logger

  alias Berrypod.Redirects
  alias Berrypod.Redirects.{LinkScanner, LinkChecker}

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"page_slug" => slug}}) do
    check_page(slug)
    broadcast_changed()
    :ok
  end

  def perform(%Oban.Job{args: %{"check_url" => url}}) do
    check_url(url)
    broadcast_changed()
    :ok
  end

  def perform(_job) do
    check_all()
    broadcast_changed()
    :ok
  end

  @doc """
  Runs a full scan of all content links.
  """
  def check_all do
    links = LinkScanner.scan_all()
    all_urls = Enum.map(links, & &1.url)

    {broken, healthy} =
      links
      |> Enum.uniq_by(& &1.url)
      |> Enum.split_with(&broken?/1)

    # Record broken links
    for link <- broken do
      {http_status, error} = check_result(link)

      Redirects.upsert_dead_link(%{
        url: link.url,
        url_type: to_string(link.type),
        status: "broken",
        http_status: http_status,
        error: error
      })
    end

    # Clear any previously-broken links that are now healthy
    for link <- healthy do
      Redirects.clear_healthy_link(link.url)
    end

    # Remove dead_links for URLs no longer in content
    Redirects.prune_orphan_dead_links(all_urls)

    broken_count = length(broken)

    if broken_count > 0 do
      Logger.warning("Dead link check: #{broken_count} broken link(s) found")
    end

    :ok
  end

  @doc """
  Checks links for a single page.
  """
  def check_page(slug) do
    case Berrypod.Pages.get_page(slug) do
      nil ->
        :ok

      page ->
        links = LinkScanner.scan_page(page)

        for link <- Enum.uniq_by(links, & &1.url) do
          if broken?(link) do
            {http_status, error} = check_result(link)

            Redirects.upsert_dead_link(%{
              url: link.url,
              url_type: to_string(link.type),
              status: "broken",
              http_status: http_status,
              error: error
            })
          else
            Redirects.clear_healthy_link(link.url)
          end
        end

        :ok
    end
  end

  @doc """
  Re-checks a single URL and updates its dead_link record.
  """
  def check_url(url) do
    type = if String.starts_with?(url, "/"), do: :internal, else: :external
    link = %{url: url, type: type}

    if broken?(link) do
      {http_status, error} = check_result(link)

      Redirects.upsert_dead_link(%{
        url: url,
        url_type: to_string(type),
        status: "broken",
        http_status: http_status,
        error: error
      })
    else
      Redirects.clear_healthy_link(url)
    end

    :ok
  end

  defp broadcast_changed do
    Phoenix.PubSub.broadcast(Berrypod.PubSub, "redirects", :dead_links_changed)
  end

  defp broken?(%{type: :internal, url: url}) do
    case LinkChecker.check_internal(url) do
      :ok -> false
      {:error, _} -> true
    end
  end

  defp broken?(%{type: :external, url: url}) do
    case LinkChecker.check_external(url) do
      :ok -> false
      {:error, _} -> true
      {:error, _, _} -> true
    end
  end

  defp check_result(%{type: :internal, url: url}) do
    case LinkChecker.check_internal(url) do
      {:error, reason} -> {nil, reason}
      :ok -> {nil, nil}
    end
  end

  defp check_result(%{type: :external, url: url}) do
    case LinkChecker.check_external(url) do
      {:error, status, reason} -> {status, reason}
      {:error, reason} -> {nil, reason}
      :ok -> {nil, nil}
    end
  end
end
