defmodule BDS.PostLinks do
  @moduledoc false

  import Ecto.Query

  alias BDS.Persistence
  alias BDS.Posts.Link
  alias BDS.Posts.Post
  alias BDS.Projects
  alias BDS.Repo

  @spec sync_post_links(Post.t()) :: :ok
  def sync_post_links(%Post{} = post) do
    links =
      post
      |> post_body()
      |> extract_links()
      |> Enum.map(&resolve_post_link(post.project_id, &1))
      |> Enum.reject(&is_nil/1)
      |> Enum.uniq_by(fn %{target_post_id: target_post_id, link_text: link_text} ->
        {target_post_id, link_text}
      end)

    Repo.transaction(fn ->
      Repo.delete_all(from link in Link, where: link.source_post_id == ^post.id)

      now = Persistence.now_ms()

      Enum.each(links, fn %{target_post_id: target_post_id, link_text: link_text} ->
        %Link{}
        |> Link.changeset(%{
          id: Ecto.UUID.generate(),
          source_post_id: post.id,
          target_post_id: target_post_id,
          link_text: link_text,
          created_at: now
        })
        |> Repo.insert!()
      end)
    end)

    :ok
  end

  @spec delete_post_links(String.t()) :: :ok
  def delete_post_links(post_id) when is_binary(post_id) do
    Repo.delete_all(
      from link in Link,
        where: link.source_post_id == ^post_id or link.target_post_id == ^post_id
    )

    :ok
  end

  @spec list_outgoing_links(String.t()) :: [Link.t()]
  def list_outgoing_links(post_id) when is_binary(post_id) do
    Repo.all(
      from link in Link, where: link.source_post_id == ^post_id, order_by: [asc: link.created_at]
    )
  end

  @spec list_incoming_links(String.t()) :: [Link.t()]
  def list_incoming_links(post_id) when is_binary(post_id) do
    Repo.all(
      from link in Link, where: link.target_post_id == ^post_id, order_by: [asc: link.created_at]
    )
  end

  defp post_body(%Post{content: content}) when is_binary(content), do: content

  defp post_body(%Post{project_id: project_id, file_path: file_path}) do
    if file_path in [nil, ""] do
      ""
    else
      project = Projects.get_project!(project_id)
      full_path = Path.join(Projects.project_data_dir(project), file_path)

      case File.read(full_path) do
        {:ok, contents} ->
          case String.split(contents, "\n---\n", parts: 2) do
            [_frontmatter, body] -> String.trim_trailing(body, "\n")
            _parts -> contents
          end

        {:error, _reason} ->
          ""
      end
    end
  end

  defp extract_links(body) when is_binary(body) do
    markdown_links =
      Regex.scan(~r/\[([^\]]+)\]\(([^)]+)\)/, body)
      |> Enum.map(fn [_full, link_text, href] ->
        %{link_text: normalize_link_text(link_text), href: href}
      end)

    html_links =
      Regex.scan(~r/<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/is, body)
      |> Enum.map(fn [_full, href, link_text] ->
        %{link_text: normalize_link_text(link_text), href: href}
      end)

    markdown_links ++ html_links
  end

  defp resolve_post_link(project_id, %{href: href, link_text: link_text}) do
    path =
      href
      |> to_string()
      |> String.trim()
      |> URI.parse()
      |> Map.get(:path)

    with path when is_binary(path) <- path,
         slug when is_binary(slug) <- extract_slug(path),
         %Post{id: target_post_id} <- Repo.get_by(Post, project_id: project_id, slug: slug) do
      %{target_post_id: target_post_id, link_text: link_text}
    else
      _ -> nil
    end
  end

  defp extract_slug(path) do
    segments = path |> String.split("/", trim: true)

    case segments do
      [year, month, day, slug] ->
        if numeric_year?(year) and numeric_month_or_day?(month) and numeric_month_or_day?(day),
          do: slug,
          else: nil

      [language, year, month, day, slug] ->
        if language_code?(language) and numeric_year?(year) and numeric_month_or_day?(month) and
             numeric_month_or_day?(day),
           do: slug,
           else: nil

      [slug] ->
        slug

      [language, slug] ->
        if(language_code?(language), do: slug, else: nil)

      _other ->
        nil
    end
  end

  defp numeric_year?(value), do: String.match?(value, ~r/^\d{4}$/)
  defp numeric_month_or_day?(value), do: String.match?(value, ~r/^\d{2}$/)
  defp language_code?(value), do: String.match?(value, ~r/^[a-z]{2}$/)

  defp normalize_link_text(value) do
    value
    |> to_string()
    |> String.replace(~r/<[^>]+>/, "")
    |> String.trim()
    |> case do
      "" -> nil
      trimmed -> trimmed
    end
  end
end
