# lib/moldb_web/live/home_live.html.heex
<.form for={@form} phx-change="change" id="search-form">
  <.live_select
    id="compound-search"
    field={@form[:compound]}
    label="search-form"
    options={@options}
    update_min_len={1}
    allow_clear
    placeholder="Search for compounds"
  />
</.form>

# lib/moldb_web/live/home_live.ex
defmodule MoldbWeb.HomeLive do
  use MoldbWeb, :live_view
  alias Moldb.Search

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket,
                  form: to_form(%{"compound" => ""}),
                  options: []
           )}
  end

  @impl true
  def handle_event("live_select_change", %{"id" => "compound-search", "text" => search_text}, socket) do
    options = Search.search_options(search_text)

    send_update(LiveSelect.Component, id: "compound-search", options: options)

    {:noreply, assign(socket,
                      options: options,
                      form: to_form(%{"compound" => search_text})
             )}
  end

  @impl true
  def handle_event("change", %{"compound" => compound}, socket) do
    {:noreply, assign(socket, form: to_form(%{"compound" => compound}))}
  end

  @impl true
  def handle_event("blur", %{"id" => "compound-search"}, socket) do
    send_update(LiveSelect.Component, id: "compound-search", options: [])

    {:noreply, socket}
  end

  @impl true
  def handle_event("clear", %{"id" => "compound-search"}, socket) do
    send_update(LiveSelect.Component, id: "compound-search", options: [])

    {:noreply, assign(socket,
                      options: [],
                      form: to_form(%{"compound" => ""})
             )}
  end
end

# lib/moldb/search.ex
defmodule Moldb.Search do
  @moduledoc """
  The Search context.
  """

  import Ecto.Query
  alias Moldb.Repo
  alias Moldb.Compounds.{Compound, Substance}

  @search_limit 10

  @spec search_options(String.t()) :: [map()]
  def search_options(""), do: []
  def search_options(query) do
    query
    |> search_query()
    |> Repo.all()
    |> Enum.map(&to_live_select_option/1)
    |> Enum.sort_by(& &1.label)
  end

  defp search_query(query) do
    compound_query = from(c in Compound, where: ilike(c.iupac_name, ^"%#{query}%"), select: %{label: c.iupac_name, value: c.iupac_name})
    substance_query = from(s in Substance, where: ilike(s.substance_name, ^"%#{query}%"), select: %{label: s.substance_name, value: s.substance_name})

    compound_query
    |> union_all(^substance_query)
    |> limit(@search_limit)
  end

  defp to_live_select_option(%{label: label, value: value}) do
    %{label: label, value: value}
  end
end

# lib/moldb/compounds/compound.ex
defmodule Moldb.Compounds.Compound do
  @moduledoc """
  Module for handling compounds in Moldb.
  """
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, UUIDv7, autogenerate: true}
  schema "compounds" do
    field :iupac_name, :string
    field :molecular_formula, :string
    field :molecular_weight, :decimal
    field :canonical_smiles, :string
    field :inchi, :string
    field :inchi_key, :string

    has_many :substances, Moldb.Compounds.Substance
    has_many :properties, Moldb.Compounds.CompoundProperty
    has_many :synonyms, Moldb.Compounds.CompoundSynonym

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(compound, attrs) do
    compound
    |> cast(attrs, [
      :iupac_name,
      :molecular_formula,
      :molecular_weight,
      :canonical_smiles,
      :inchi,
      :inchi_key
    ])
    |> validate_required([:iupac_name, :molecular_formula, :canonical_smiles, :inchi, :inchi_key])
    |> unique_constraint(:canonical_smiles)
    |> unique_constraint(:inchi)
    |> unique_constraint(:inchi_key)
    |> cast_assoc(:properties, with: &Moldb.Compounds.CompoundProperty.changeset/2)
    |> cast_assoc(:synonyms, with: &Moldb.Compounds.CompoundSynonym.changeset/2)
  end
end

# lib/moldb/compounds/substance.ex
defmodule Moldb.Compounds.Substance do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, UUIDv7, autogenerate: true}
  schema "substances" do
    field :substance_name, :string
    field :physical_state, :string
    field :salt_form, :string
    field :stereochemistry, :string

    belongs_to :compound, Moldb.Compounds.Compound, type: UUIDv7

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(substance, attrs) do
    substance
    |> cast(attrs, [:substance_name, :physical_state, :salt_form, :stereochemistry, :compound_id])
    |> validate_required([:substance_name, :compound_id])
    |> foreign_key_constraint(:compound_id)
  end
end

# lib/moldb/compounds.ex
defmodule Moldb.Compounds do
  @moduledoc """
  The Compounds context handles all interactions with compounds and substances in the database.
  """
  import Ecto.Query, warn: false
  alias Moldb.Repo
  alias Moldb.Compounds.{Compound, Substance}

  # Compound Functions

  @doc """
  Returns the list of compounds.
  """
  def list_compounds, do: Repo.all(Compound)

  @doc """
  Gets a single compound.

  Raises `Ecto.NoResultsError` if the Compound does not exist.
  """
  def get_compound!(id), do: Repo.get!(Compound, id)

  @doc """
  Creates a compound.
  """
  def create_compound(attrs \\ %{}),
    do: %Compound{} |> Compound.changeset(attrs) |> Repo.insert()

  @doc """
  Updates a compound.
  """
  def update_compound(%Compound{} = compound, attrs),
    do: compound |> Compound.changeset(attrs) |> Repo.update()

  @doc """
  Deletes a compound.
  """
  def delete_compound(%Compound{} = compound), do: Repo.delete(compound)

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking compound changes.
  """
  def change_compound(%Compound{} = compound, attrs \\ %{}),
    do: Compound.changeset(compound, attrs)

  # Substance Functions

  @doc """
  Returns the list of substances.
  """
  def list_substances, do: Repo.all(Substance)

  @doc """
  Gets a single substance.

  Raises `Ecto.NoResultsError` if the Substance does not exist.
  """
  def get_substance!(id), do: Repo.get!(Substance, id)

  @doc """
  Creates a substance.
  """
  def create_substance(attrs \\ %{}),
    do: %Substance{} |> Substance.changeset(attrs) |> Repo.insert()

  @doc """
  Updates a substance.
  """
  def update_substance(%Substance{} = substance, attrs),
    do: substance |> Substance.changeset(attrs) |> Repo.update()

  @doc """
  Deletes a substance.
  """
  def delete_substance(%Substance{} = substance), do: Repo.delete(substance)

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking substance changes.
  """
  def change_substance(%Substance{} = substance, attrs \\ %{}),
    do: Substance.changeset(substance, attrs)

  # Helpers

  def compound_name(%Compound{} = compound),
    do: compound.iupac_name || "Unknown"

  def substance_name(%Substance{} = substance),
    do: substance.substance_name || "Unknown"

  def list_compounds_with_assocs,
    do: Repo.all(from(c in Compound, preload: [:properties, :synonyms]))

  def get_compound_with_assocs!(id),
    do: Repo.get!(Compound, id) |> Repo.preload([:properties, :synonyms])
end

# mix.exs
defmodule Moldb.MixProject do
  use Mix.Project

  def project do
    [
      app: :moldb,
      version: "0.1.0",
      elixir: "~> 1.14",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps()
    ]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [
      mod: {Moldb.Application, []},
      extra_applications: [:logger, :runtime_tools, :os_mon]
    ]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      {:argon2_elixir, "~> 3.0"},
      {:phoenix, "~> 1.7.14"},
      {:phoenix_ecto, "~> 4.5"},
      {:ecto_sql, "~> 3.10"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 4.1"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
      {:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
      {:floki, ">= 0.30.0", only: :test},
      {:phoenix_live_dashboard, "~> 0.8.3"},
      {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
      {:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
      {:heroicons,
       github: "tailwindlabs/heroicons",
       tag: "v2.1.1",
       sparse: "optimized",
       app: false,
       compile: false,
       depth: 1},
      {:swoosh, "~> 1.5"},
      {:finch, "~> 0.13"},
      {:telemetry_metrics, "~> 1.0"},
      {:telemetry_poller, "~> 1.0"},
      {:gettext, "~> 0.20"},
      {:jason, "~> 1.2"},
      {:dns_cluster, "~> 0.1.1"},
      {:bandit, "~> 1.5"},
      {:uuidv7, "~> 0.2.1"},
      {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false},
      {:ex_doc, "~> 0.31", only: :dev, runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:ecto_psql_extras, "~> 0.7"},
      {:live_select, "~> 1.4"}
    ]
  end

  # Aliases are shortcuts or tasks specific to the current project.
  # For example, to install project dependencies and perform other setup tasks, run:
  #
  #     $ mix setup
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    [
      setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
      "ecto.reset": ["ecto.drop", "ecto.setup"],
      test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
      "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
      "assets.build": ["tailwind moldb", "esbuild moldb"],
      "assets.deploy": [
        "tailwind moldb --minify",
        "esbuild moldb --minify",
        "phx.digest"
      ]
    ]
  end
end

# lib/moldb/compounds/compound_property.ex
defmodule Moldb.Compounds.CompoundProperty do
  @moduledoc """
  Defines the CompoundProperty schema and changeset functions.
  """
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, UUIDv7, autogenerate: true}
  @foreign_key_type :binary_id

  schema "compound_properties" do
    field :property_name, :string
    field :property_value, :decimal
    field :property_unit, :string
    field :property_category, :string

    belongs_to :compound, Moldb.Compounds.Compound

    timestamps()
  end

  @doc false
  def changeset(property, attrs) do
    property
    |> cast(attrs, [
      :property_name,
      :property_value,
      :property_unit,
      :property_category,
      :compound_id
    ])
    |> validate_required([:property_name, :property_value, :property_category])
    |> validate_inclusion(:property_category, [
      "physical",
      "chemical",
      "thermodynamic",
      "mechanical"
    ])
    |> assoc_constraint(:compound)
  end
end

# lib/moldb/compounds/compound_synonym.ex
defmodule Moldb.Compounds.CompoundSynonym do
  @moduledoc """
  Defines the CompoundSynonym schema and changeset functions.
  """
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, UUIDv7, autogenerate: true}
  @foreign_key_type :binary_id

  schema "compound_synonyms" do
    field :synonym, :string

    belongs_to :compound, Moldb.Compounds.Compound

    timestamps()
  end

  @doc false
  def changeset(synonym, attrs) do
    synonym
    |> cast(attrs, [:synonym, :compound_id])
    |> validate_required([:synonym])
    |> assoc_constraint(:compound)
  end
end

# lib/moldb/accounts.ex
defmodule Moldb.Accounts do
  @moduledoc """
  The Accounts context.
  """

  import Ecto.Query, warn: false
  alias Moldb.Repo

  alias Moldb.Accounts.{User, UserToken, UserNotifier}

  ## Database getters

  @doc """
  Gets a user by email.

  ## Examples

      iex> get_user_by_email("foo@example.com")
      %User{}

      iex> get_user_by_email("unknown@example.com")
      nil

  """
  def get_user_by_email(email) when is_binary(email) do
    Repo.get_by(User, email: email)
  end

  @doc """
  Gets a user by email and password.

  ## Examples

      iex> get_user_by_email_and_password("foo@example.com", "correct_password")
      %User{}

      iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
      nil

  """
  def get_user_by_email_and_password(email, password)
      when is_binary(email) and is_binary(password) do
    user = Repo.get_by(User, email: email)
    if User.valid_password?(user, password), do: user
  end

  @doc """
  Gets a single user.

  Raises `Ecto.NoResultsError` if the User does not exist.

  ## Examples

      iex> get_user!(123)
      %User{}

      iex> get_user!(456)
      ** (Ecto.NoResultsError)

  """
  def get_user!(id), do: Repo.get!(User, id)

  ## User registration

  @doc """
  Registers a user.

  ## Examples

      iex> register_user(%{field: value})
      {:ok, %User{}}

      iex> register_user(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def register_user(attrs) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking user changes.

  ## Examples

      iex> change_user_registration(user)
      %Ecto.Changeset{data: %User{}}

  """
  def change_user_registration(%User{} = user, attrs \\ %{}) do
    User.registration_changeset(user, attrs, hash_password: false, validate_email: false)
  end

  ## Settings

  @doc """
  Returns an `%Ecto.Changeset{}` for changing the user email.

  ## Examples

      iex> change_user_email(user)
      %Ecto.Changeset{data: %User{}}

  """
  def change_user_email(user, attrs \\ %{}) do
    User.email_changeset(user, attrs, validate_email: false)
  end

  @doc """
  Emulates that the email will change without actually changing
  it in the database.

  ## Examples

      iex> apply_user_email(user, "valid password", %{email: ...})
      {:ok, %User{}}

      iex> apply_user_email(user, "invalid password", %{email: ...})
      {:error, %Ecto.Changeset{}}

  """
  def apply_user_email(user, password, attrs) do
    user
    |> User.email_changeset(attrs)
    |> User.validate_current_password(password)
    |> Ecto.Changeset.apply_action(:update)
  end

  @doc """
  Updates the user email using the given token.

  If the token matches, the user email is updated and the token is deleted.
  The confirmed_at date is also updated to the current time.
  """
  def update_user_email(user, token) do
    context = "change:#{user.email}"

    with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
         %UserToken{sent_to: email} <- Repo.one(query),
         {:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
      :ok
    else
      _ -> :error
    end
  end

  defp user_email_multi(user, email, context) do
    changeset =
      user
      |> User.email_changeset(%{email: email})
      |> User.confirm_changeset()

    Ecto.Multi.new()
    |> Ecto.Multi.update(:user, changeset)
    |> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, [context]))
  end

  @doc ~S"""
  Delivers the update email instructions to the given user.

  ## Examples

      iex> deliver_user_update_email_instructions(user, current_email, &url(~p"/users/settings/confirm_email/#{&1}"))
      {:ok, %{to: ..., body: ...}}

  """
  def deliver_user_update_email_instructions(%User{} = user, current_email, update_email_url_fun)
      when is_function(update_email_url_fun, 1) do
    {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")

    Repo.insert!(user_token)
    UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for changing the user password.

  ## Examples

      iex> change_user_password(user)
      %Ecto.Changeset{data: %User{}}

  """
  def change_user_password(user, attrs \\ %{}) do
    User.password_changeset(user, attrs, hash_password: false)
  end

  @doc """
  Updates the user password.

  ## Examples

      iex> update_user_password(user, "valid password", %{password: ...})
      {:ok, %User{}}

      iex> update_user_password(user, "invalid password", %{password: ...})
      {:error, %Ecto.Changeset{}}

  """
  def update_user_password(user, password, attrs) do
    changeset =
      user
      |> User.password_changeset(attrs)
      |> User.validate_current_password(password)

    Ecto.Multi.new()
    |> Ecto.Multi.update(:user, changeset)
    |> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, :all))
    |> Repo.transaction()
    |> case do
      {:ok, %{user: user}} -> {:ok, user}
      {:error, :user, changeset, _} -> {:error, changeset}
    end
  end

  ## Session

  @doc """
  Generates a session token.
  """
  def generate_user_session_token(user) do
    {token, user_token} = UserToken.build_session_token(user)
    Repo.insert!(user_token)
    token
  end

  @doc """
  Gets the user with the given signed token.
  """
  def get_user_by_session_token(token) do
    {:ok, query} = UserToken.verify_session_token_query(token)
    Repo.one(query)
  end

  @doc """
  Deletes the signed token with the given context.
  """
  def delete_user_session_token(token) do
    Repo.delete_all(UserToken.by_token_and_context_query(token, "session"))
    :ok
  end

  ## Confirmation

  @doc ~S"""
  Delivers the confirmation email instructions to the given user.

  ## Examples

      iex> deliver_user_confirmation_instructions(user, &url(~p"/users/confirm/#{&1}"))
      {:ok, %{to: ..., body: ...}}

      iex> deliver_user_confirmation_instructions(confirmed_user, &url(~p"/users/confirm/#{&1}"))
      {:error, :already_confirmed}

  """
  def deliver_user_confirmation_instructions(%User{} = user, confirmation_url_fun)
      when is_function(confirmation_url_fun, 1) do
    if user.confirmed_at do
      {:error, :already_confirmed}
    else
      {encoded_token, user_token} = UserToken.build_email_token(user, "confirm")
      Repo.insert!(user_token)
      UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token))
    end
  end

  @doc """
  Confirms a user by the given token.

  If the token matches, the user account is marked as confirmed
  and the token is deleted.
  """
  def confirm_user(token) do
    with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
         %User{} = user <- Repo.one(query),
         {:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do
      {:ok, user}
    else
      _ -> :error
    end
  end

  defp confirm_user_multi(user) do
    Ecto.Multi.new()
    |> Ecto.Multi.update(:user, User.confirm_changeset(user))
    |> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, ["confirm"]))
  end

  ## Reset password

  @doc ~S"""
  Delivers the reset password email to the given user.

  ## Examples

      iex> deliver_user_reset_password_instructions(user, &url(~p"/users/reset_password/#{&1}"))
      {:ok, %{to: ..., body: ...}}

  """
  def deliver_user_reset_password_instructions(%User{} = user, reset_password_url_fun)
      when is_function(reset_password_url_fun, 1) do
    {encoded_token, user_token} = UserToken.build_email_token(user, "reset_password")
    Repo.insert!(user_token)
    UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token))
  end

  @doc """
  Gets the user by reset password token.

  ## Examples

      iex> get_user_by_reset_password_token("validtoken")
      %User{}

      iex> get_user_by_reset_password_token("invalidtoken")
      nil

  """
  def get_user_by_reset_password_token(token) do
    with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
         %User{} = user <- Repo.one(query) do
      user
    else
      _ -> nil
    end
  end

  @doc """
  Resets the user password.

  ## Examples

      iex> reset_user_password(user, %{password: "new long password", password_confirmation: "new long password"})
      {:ok, %User{}}

      iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"})
      {:error, %Ecto.Changeset{}}

  """
  def reset_user_password(user, attrs) do
    Ecto.Multi.new()
    |> Ecto.Multi.update(:user, User.password_changeset(user, attrs))
    |> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, :all))
    |> Repo.transaction()
    |> case do
      {:ok, %{user: user}} -> {:ok, user}
      {:error, :user, changeset, _} -> {:error, changeset}
    end
  end
end
Review and analyze