defmodule Core.Syndication do
  alias __MODULE__

  ## mastodon

  @mastodon_instance Application.compile_env!(:sloanely_but_surely, [
                       Core.Syndication,
                       :mastodon_instance
                     ])

  def get_mastodon_account(user) do
    Core.Repo.get_by(Schema.MastodonAccount, user_id: user.id)
  end

  def save_mastodon_account(user, attrs) do
    user
    |> Ecto.build_assoc(:mastodon_account)
    |> Syndication.MastodonAccount.changeset(attrs)
    |> Core.Repo.insert()
  end

  def syndicate_to_mastodon(%Schema.Post{} = post) do
    conn = build_mastodon_client_conn()

    {:ok, resp} = MastodonClient.post(conn, "/api/v1/statuses", %{status: post.body})

    post
    |> Ecto.build_assoc(:mastodon_post)
    |> Syndication.MastodonPost.changeset(%{
      status_id: resp.body["id"],
      url: resp.body["url"]
    })
    |> Core.Repo.insert()

    {:ok, post}
  end

  defp get_mastodon_access_token! do
    mastodon_account = Core.Repo.one!(Schema.MastodonAccount)
    mastodon_account.access_token
  end

  defp build_mastodon_client_conn do
    %MastodonClient.Conn{
      instance: @mastodon_instance,
      access_token: get_mastodon_access_token!()
    }
  end

  ## bluesky

  def get_bluesky_account(%Schema.User{} = user) do
    Core.Repo.get_by(Schema.BlueskyAccount, user_id: user.id)
  end

  def save_bluesky_account(user, identifier, password) do
    with {:ok, session_resp} <-
           Syndication.BlueskyClient.create_session(identifier, password),
         {:ok, attrs} <- parse_bluesky_token_resp(session_resp) do
      user
      |> Ecto.build_assoc(:bluesky_account)
      |> Syndication.BlueskyAccount.create_changeset(attrs)
      |> Core.Repo.insert(
        conflict_target: [:user_id],
        on_conflict: {:replace_all_except, [:id, :inserted_at]}
      )
    end
  end

  def refresh_bluesky_account(%Schema.BlueskyAccount{} = bluesky_account) do
    if DateTime.compare(DateTime.utc_now(), bluesky_account.access_jwt_exp) == :lt do
      {:ok, bluesky_account}
    else
      with {:ok, refresh_resp} <-
             Syndication.BlueskyClient.refresh_session(bluesky_account),
           {:ok, attrs} <- parse_bluesky_token_resp(refresh_resp) do
        %{"bluesky_account_id" => bluesky_account.id}
        |> Core.Syndication.BlueskyRefreshWorker.new(
          scheduled_at: DateTime.add(bluesky_account.refresh_jwt_exp, -1, :day)
        )
        |> Oban.insert()

        bluesky_account
        |> Syndication.BlueskyAccount.refresh_changeset(attrs)
        |> Core.Repo.update()
      end
    end
  end

  def syndicate_to_bluesky(%Schema.Post{} = post) do
    {:ok, bluesky_account} = get_bluesky_account!() |> refresh_bluesky_account()

    with {:ok, resp} <- Syndication.BlueskyClient.post_status(bluesky_account, post.body) do
      post
      |> Ecto.build_assoc(:bluesky_post)
      |> Syndication.BlueskyPost.changeset(resp.body)
      |> Core.Repo.insert()

      {:ok, post}
    end
  end

  def bsky_app_url(%Schema.BlueskyPost{uri: uri}) do
    [did, id] = Regex.run(~r|at://(did:.*)/app.bsky.feed.post/(.*)|, uri, capture: :all_but_first)

    "https://bsky.app/profile/#{did}/post/#{id}"
  end

  defp parse_bluesky_token_resp(resp) do
    with {:ok, %{iat: access_jwt_iat, exp: access_jwt_exp}} <-
           parse_bluesky_jwt(resp.body["accessJwt"]),
         {:ok, %{iat: refresh_jwt_iat, exp: refresh_jwt_exp}} <-
           parse_bluesky_jwt(resp.body["refreshJwt"]) do
      {:ok,
       %{
         handle: resp.body["handle"],
         did: resp.body["did"],
         access_jwt: resp.body["accessJwt"],
         access_jwt_iat: access_jwt_iat,
         access_jwt_exp: access_jwt_exp,
         refresh_jwt: resp.body["refreshJwt"],
         refresh_jwt_iat: refresh_jwt_iat,
         refresh_jwt_exp: refresh_jwt_exp
       }}
    end
  end

  defp parse_bluesky_jwt(jwt) do
    with {:ok, claims} <- Joken.peek_claims(jwt),
         {:ok, iat} <- DateTime.from_unix(claims["iat"]),
         {:ok, exp} <- DateTime.from_unix(claims["exp"]) do
      {:ok, %{iat: iat, exp: exp}}
    end
  end

  defp get_bluesky_account! do
    Core.Repo.one!(Schema.BlueskyAccount)
  end
end