defmodule BDS.Posts.Translations do
  @moduledoc false

  import Ecto.Query
  import BDS.MapUtils, only: [attr: 2, maybe_put: 3]

  alias BDS.Persistence
  alias BDS.Posts
  alias BDS.Posts.FileSync
  alias BDS.Posts.Post
  alias BDS.Posts.RebuildFromFiles
  alias BDS.Posts.Translation
  alias BDS.Projects
  alias BDS.Repo
  alias BDS.Search

  @type attrs :: %{optional(atom()) => term(), optional(String.t()) => term()}

  @spec publish_post_translation(String.t(), String.t() | atom()) ::
          {:ok, Translation.t()} | {:error, :not_found | term()}
  def publish_post_translation(post_id, language) do
    normalized_language = language |> to_string() |> String.trim() |> String.downcase()

    case Repo.get_by(Translation, translation_for: post_id, language: normalized_language) do
      nil ->
        {:error, :not_found}

      %Translation{} ->
        with {:ok, _post} <- Posts.publish_post(post_id),
             %Translation{} = translation <-
               Repo.get_by(Translation, translation_for: post_id, language: normalized_language) do
          {:ok, translation}
        else
          nil -> {:error, :not_found}
          error -> error
        end
    end
  end

  @spec list_post_translations(String.t()) :: {:ok, [Translation.t()]}
  def list_post_translations(post_id) do
    {:ok,
     Repo.all(
       from(translation in Translation,
         where: translation.translation_for == ^post_id,
         order_by: [asc: translation.language]
       )
     )}
  end

  @spec upsert_post_translation(String.t(), String.t() | atom(), attrs()) ::
          {:ok, Translation.t()} | {:error, :not_found | Ecto.Changeset.t()}
  def upsert_post_translation(post_id, language, attrs) do
    case Repo.get(Post, post_id) do
      nil ->
        {:error, :not_found}

      %Post{do_not_translate: true} = post ->
        {:error,
         post
         |> Post.changeset(%{})
         |> Ecto.Changeset.add_error(
           :do_not_translate,
           "cannot add translations when do_not_translate is true"
         )}

      %Post{} = post ->
        now = Persistence.now_ms()
        normalized_language = normalize_language(language)

        translation =
          Repo.get_by(Translation, translation_for: post.id, language: normalized_language) ||
            %Translation{}

        updates =
          normalize_translation_updates(post, translation, normalized_language, attrs, now)

        translation
        |> Translation.changeset(updates)
        |> Repo.insert_or_update()
        |> case do
          {:ok, saved_translation} ->
            {:ok, _post} = maybe_reopen_source_post_for_manual_translation(post, attrs)
            :ok = Search.sync_post(post.id)
            {:ok, saved_translation}

          error ->
            error
        end
    end
  end

  @spec delete_post_translation(String.t()) :: {:ok, :deleted} | {:error, :not_found}
  def delete_post_translation(translation_id) do
    case Repo.get(Translation, translation_id) do
      nil ->
        {:error, :not_found}

      %Translation{} = translation ->
        case Repo.delete(translation) do
          {:ok, deleted_translation} ->
            FileSync.delete_translation_file(deleted_translation)
            Search.sync_post(deleted_translation.translation_for)
            {:ok, :deleted}

          {:error, changeset} ->
            {:error, changeset}
        end
    end
  end

  @spec sync_post_translation_from_file(String.t()) ::
          {:ok, Translation.t()} | {:error, :not_found}
  def sync_post_translation_from_file(translation_id) do
    case Repo.get(Translation, translation_id) do
      nil ->
        {:error, :not_found}

      %Translation{file_path: file_path} when file_path in [nil, ""] ->
        {:error, :not_found}

      %Translation{} = translation ->
        project = Projects.get_project!(translation.project_id)
        full_path = Path.join(Projects.project_data_dir(project), translation.file_path)

        with true <- File.exists?(full_path),
             {:ok, rebuild_file} <- RebuildFromFiles.parse_rebuild_file(project, full_path) do
          {:ok,
           RebuildFromFiles.upsert_post_translation_from_rebuild_file(
             translation.project_id,
             rebuild_file,
             sync_search: true
           )}
        else
          false -> {:error, :not_found}
          {:error, reason} -> {:error, reason}
        end
    end
  end

  @spec rewrite_published_post_translation(String.t()) ::
          {:ok, Translation.t()} | {:error, :not_found}
  def rewrite_published_post_translation(translation_id) do
    case Repo.get(Translation, translation_id) do
      nil ->
        {:error, :not_found}

      %Translation{file_path: file_path, status: status} = translation
      when file_path not in [nil, ""] and status == :published ->
        post = Repo.get!(Post, translation.translation_for)
        :ok = publish_translation(post, translation)
        {:ok, Repo.get!(Translation, translation_id)}

      %Translation{} ->
        {:error, :not_found}
    end
  end

  @doc false
  def publish_post_translations(%Post{} = post) do
    Repo.all(from(translation in Translation, where: translation.translation_for == ^post.id))
    |> Enum.each(fn translation ->
      if translation.status == :draft do
        publish_translation(post, translation)
      end
    end)

    :ok
  end

  @doc false
  def publish_translation(%Post{} = post, %Translation{} = translation) do
    project = Projects.get_project!(post.project_id)
    published_at = translation.published_at || Persistence.now_ms()
    relative_path = FileSync.translation_relative_path(post, translation.language)
    full_path = Path.join(Projects.project_data_dir(project), relative_path)
    updated_at = Persistence.now_ms()
    body = FileSync.publishable_translation_body(translation, full_path)

    :ok =
      Persistence.atomic_write(
        full_path,
        FileSync.serialize_translation_file(
          %{translation | updated_at: updated_at, content: body},
          published_at
        )
      )

    translation
    |> Translation.changeset(%{
      status: :published,
      published_at: published_at,
      file_path: relative_path,
      content: nil,
      updated_at: updated_at
    })
    |> Repo.update!()

    :ok
  end

  defp normalize_translation_updates(post, %Translation{} = translation, language, attrs, now) do
    requested_status =
      case attr(attrs, :status) do
        nil -> nil
        status -> RebuildFromFiles.parse_translation_status(status)
      end

    updates =
      %{}
      |> maybe_put(:title, attr(attrs, :title))
      |> maybe_put(:excerpt, attr(attrs, :excerpt))
      |> maybe_put(:content, attr(attrs, :content))

    reopened? =
      translation.status == :published and translation_content_change?(translation, updates)

    status = if(reopened?, do: :draft, else: requested_status || translation.status || :draft)

    %{
      id: translation.id || Ecto.UUID.generate(),
      project_id: post.project_id,
      translation_for: post.id,
      language: language,
      title: Map.get(updates, :title, translation.title),
      excerpt: Map.get(updates, :excerpt, translation.excerpt),
      content: Map.get(updates, :content, translation.content),
      status: status,
      created_at: translation.created_at || now,
      updated_at: now,
      published_at: translation.published_at || if(status == :published, do: now, else: nil),
      file_path: translation.file_path || "",
      checksum: translation.checksum
    }
  end

  defp translation_content_change?(translation, updates) do
    Enum.any?([:title, :excerpt, :content], fn field ->
      case Map.fetch(updates, field) do
        {:ok, value} -> value != Map.get(translation, field)
        :error -> false
      end
    end)
  end

  defp maybe_reopen_source_post_for_manual_translation(%Post{} = post, attrs) do
    if attr(attrs, :auto_generated) == true or post.status != :published or
         post.file_path in [nil, ""] do
      {:ok, post}
    else
      project = Projects.get_project!(post.project_id)
      full_path = Path.join(Projects.project_data_dir(project), post.file_path)
      restored_content = FileSync.published_post_body(post, full_path)

      post
      |> Post.changeset(%{
        status: :draft,
        content: restored_content,
        updated_at: Persistence.now_ms()
      })
      |> Repo.update()
    end
  end

  defp normalize_language(nil), do: ""

  defp normalize_language(language) do
    language
    |> to_string()
    |> String.downcase()
    |> String.split("-", parts: 2)
    |> hd()
  end
end
