Avoid passing a float to send_after

Avoid passing a float to send_after

defmodule BorsNG.Worker.Batcher do
  @moduledoc """
  A "Batcher" manages the backlog of batches a project has.
  It implements this set of rules:

    * When a patch is reviewed ("r+'ed"),
      it gets added to the project's non-running batch.
      If no such batch exists, it creates it.
    * After a short delay, if there is no currently running batch,
      the project's non-running batch is started.
    * The project's CI is occasionally polled,
      if a batch is currently running.
      After polling, the completion logic is run.
    * If a notification related to the underlying CI is received,
      the completion logic is run.
    * When the completion logic is run, the batch is either
      bisected (if it failed and there are two or more patches in it),
      blocked (if it failed and there is only one patch in it),
      pushed to master (if it passed),
      or (if there are still CI jobs with no results) it is left alone.
  """

  use GenServer
  alias BorsNG.Worker.Batcher
  alias BorsNG.Database.Repo
  alias BorsNG.Database.Batch
  alias BorsNG.Database.BatchState
  alias BorsNG.Database.Patch
  alias BorsNG.Database.Project
  alias BorsNG.Database.Status
  alias BorsNG.Database.LinkPatchBatch
  alias BorsNG.GitHub
  alias BorsNG.Endpoint
  import BorsNG.Router.Helpers
  import Ecto.Query

  # Every half-hour
  @poll_period 30 * 60 * 1000

  # Public API

  def start_link(project_id) do
    GenServer.start_link(__MODULE__, project_id)
  end

  def reviewed(pid, patch_id, reviewer) when is_integer(patch_id) do
    GenServer.cast(pid, {:reviewed, patch_id, reviewer})
  end

  def set_priority(pid, patch_id, priority) when is_integer(patch_id) do
    GenServer.call(pid, {:set_priority, patch_id, priority})
  end

  def status(pid, stat) do
    GenServer.cast(pid, {:status, stat})
  end

  def cancel(pid, patch_id) when is_integer(patch_id) do
    GenServer.cast(pid, {:cancel, patch_id})
  end

  def cancel_all(pid) do
    GenServer.cast(pid, {:cancel_all})
  end

  # Server callbacks

  def init(project_id) do
    Process.send_after(
      self(),
      {:poll, :repeat},
      trunc(@poll_period * :rand.uniform(2) * 0.5))
    {:ok, project_id}
  end

  def handle_cast(args, project_id) do
    do_handle_cast(args, project_id)
    {:noreply, project_id}
  end

  def handle_call({:set_priority, patch_id, priority}, _from, state) do
    case Repo.get(Patch.all(:awaiting_review), patch_id) do
      nil -> nil
      patch ->
        if patch.priority != priority do
          patch
          |> Patch.changeset(%{priority: priority})
          |> Repo.update!()
        end
    end

    {:reply, :ok, state}
  end

  def do_handle_cast({:reviewed, patch_id, reviewer}, project_id) do
    case Repo.get(Patch.all(:awaiting_review), patch_id) do
      nil ->
        # Patch exists (otherwise, no ID), but is not awaiting review
        patch = Repo.get!(Patch, patch_id)
        project = Repo.get!(Project, patch.project_id)
        project
        |> get_repo_conn()
        |> send_message([patch], :not_awaiting_review)
      patch ->
        # Patch exists and is awaiting review
        # This will cause the PR to start after the patch's scheduled delay
        project = Repo.get!(Project, patch.project_id)
        if Patch.ci_skip?(patch) do
          project
          |> get_repo_conn()
          |> send_message([patch], :ci_skip)
        else

          {batch, is_new_batch} = get_new_batch(
            project_id,
            patch.into_branch,
            patch.priority
          )

          if is_new_batch do
            put_incomplete_on_hold(get_repo_conn(project), batch)
          end

          repo_conn = get_repo_conn(project)
          case patch_preflight(repo_conn, patch) do
            :ok ->
              params = %{
                batch_id: batch.id,
                patch_id: patch.id,
                reviewer: reviewer}
              Repo.insert!(LinkPatchBatch.changeset(%LinkPatchBatch{}, params))
              poll_after_delay(project)
              send_status(repo_conn, batch.id, [patch], :waiting)
            {:error, message} ->
              send_message(repo_conn, [patch], {:preflight, message})
              send_status(repo_conn, batch.id, [patch], :error)
          end
        end
    end
  end

  def do_handle_cast({:status, {commit, identifier, state, url}}, project_id) do
    batch = Repo.all(Batch.get_assoc_by_commit(commit))
    case batch do
      [batch] ->
        ^project_id = batch.project_id
        batch.id
        |> Status.get_for_batch(identifier)
        |> Repo.update_all([set: [state: state, url: url]])
        if batch.state == :running do
          maybe_complete_batch(batch)
        end
      [] -> :ok
    end
  end

  def do_handle_cast({:cancel, patch_id}, _project_id) do
    batch = patch_id
    |> Batch.all_for_patch(:incomplete)
    |> Repo.one()
    cancel_patch(batch, patch_id)
  end

  def do_handle_cast({:cancel_all}, project_id) do
    waiting = project_id
    |> Batch.all_for_project(:waiting)
    |> Repo.all()
    Enum.each(waiting, &Repo.delete!/1)
    running = project_id
    |> Batch.all_for_project(:running)
    |> Repo.all()
    Enum.map(running, &Batch.changeset(&1, %{state: :canceled}))
    |> Enum.each(&Repo.update!/1)
    repo_conn = Project
    |> Repo.get!(project_id)
    |> get_repo_conn()
    Enum.each(running, &send_status(repo_conn, &1, :canceled))
    Enum.each(waiting, &send_status(repo_conn, &1, :canceled))
  end

  def handle_info({:poll, repetition}, project_id) do
    if repetition != :once do
      Process.send_after(self(), {:poll, repetition}, @poll_period)
    end
    case poll(project_id) do
      :stop ->
        {:stop, :normal, project_id}
      :again ->
        {:noreply, project_id}
    end
  end

  # Private implementation details

  defp poll(project_id) do
    project = Repo.get(Project, project_id)
    incomplete = project_id
    |> Batch.all_for_project(:incomplete)
    |> Repo.all()
    incomplete
    |> Enum.map(&%Batch{&1 | project: project})
    |> sort_batches()
    |> poll_batches()
    if Enum.empty?(incomplete) do
      :stop
    else
      :again
    end
  end

  def sort_batches(batches) do
    sorted_batches = Enum.sort_by(batches, &{
      -BatchState.numberize(&1.state),
      -&1.priority,
      &1.last_polled
    })
    new_batches = Enum.dedup_by(sorted_batches, &(&1.id))
    state = if new_batches != [] and hd(new_batches).state == :running do
      :running
    else
      Enum.each(new_batches, fn batch -> :waiting =  batch.state end)
      :waiting
    end
    {state, new_batches}
  end

  defp poll_batches({:waiting, batches}) do
    case Enum.filter(batches, &Batch.next_poll_is_past/1) do
      [] -> :ok
      [batch | _] -> start_waiting_batch(batch)
    end
  end

  defp poll_batches({:running, batches}) do
    batch = hd(batches)
    cond do
      Batch.timeout_is_past(batch) ->
        timeout_batch(batch)
      Batch.next_poll_is_past(batch) ->
        poll_running_batch(batch)
      true -> :ok
    end
  end

  defp start_waiting_batch(batch) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    patch_links = Repo.all(LinkPatchBatch.from_batch(batch.id))
    |> Enum.sort_by(&(&1.patch.pr_xref))
    stmp = "#{project.staging_branch}.tmp"
    base = GitHub.get_branch!(
      repo_conn,
      batch.into_branch)
    tbase = %{
      tree: base.tree,
      commit: GitHub.synthesize_commit!(
        repo_conn,
        %{
          branch: stmp,
          tree: base.tree,
          parents: [base.commit],
          commit_message: "[ci skip]",
          committer: nil})}
    do_merge_patch = fn %{patch: patch}, branch ->
      case branch do
        :conflict -> :conflict
        _ -> GitHub.merge_branch!(
          repo_conn,
          %{
            from: patch.commit,
            to: stmp,
            commit_message: "[ci skip] -bors-staging-tmp-#{patch.pr_xref}"})
      end
    end
    merge = Enum.reduce(patch_links, tbase, do_merge_patch)
    {status, commit} = start_waiting_merged_batch(
      batch,
      patch_links,
      base,
      merge)
    now = DateTime.to_unix(DateTime.utc_now(), :seconds)
    GitHub.delete_branch!(repo_conn, stmp)
    send_status(repo_conn, batch, status)
    batch
    |> Batch.changeset(%{state: status, commit: commit, last_polled: now})
    |> Repo.update!()
    Project.ping!(batch.project_id)
    status
  end

  defp start_waiting_merged_batch(batch, patch_links, base, %{tree: tree}) do
    repo_conn = get_repo_conn(batch.project)
    patches = Enum.map(patch_links, &(&1.patch))
    repo_conn
    |> Batcher.GetBorsToml.get("#{batch.project.staging_branch}.tmp")
    |> case do
      {:ok, toml} ->
        parents = [base.commit | Enum.map(patch_links, &(&1.patch.commit))]
        commit_message = Batcher.Message.generate_commit_message(
          patch_links,
          toml.cut_body_after,
          gather_co_authors(batch, patch_links))
        head = GitHub.synthesize_commit!(
          repo_conn,
          %{
            branch: batch.project.staging_branch,
            tree: tree,
            parents: parents,
            commit_message: commit_message,
            committer: toml.committer})
        setup_statuses(batch, toml)
        {:running, head}
      {:error, message} ->
        message = Batcher.Message.generate_bors_toml_error(message)
        send_message(repo_conn, patches, {:config, message})
        {:error, nil}
    end
  end

  defp start_waiting_merged_batch(batch, patch_links, _base, :conflict) do
    repo_conn = get_repo_conn(batch.project)
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    send_message(repo_conn, patches, {:conflict, state})
    {:conflict, nil}
  end

  def gather_co_authors(batch, patch_links) do
    repo_conn = get_repo_conn(batch.project)
    patch_links
    |> Enum.map(&(&1.patch.pr_xref))
    |> Enum.flat_map(&GitHub.get_pr_commits!(repo_conn, &1))
    |> Enum.map(&("#{&1.author_name} <#{&1.author_email}>"))
    |> Enum.uniq
  end

  defp setup_statuses(batch, toml) do
    toml.status
    |> Enum.map(&%Status{
        batch_id: batch.id,
        identifier: &1,
        url: nil,
        state: :running})
    |> Enum.each(&Repo.insert!/1)
    now = DateTime.to_unix(DateTime.utc_now(), :seconds)
    batch
    |> Batch.changeset(%{timeout_at: now + toml.timeout_sec})
    |> Repo.update!()
  end

  defp poll_running_batch(batch) do
    project = batch.project
    gh_statuses = project
    |> get_repo_conn()
    |> GitHub.get_commit_status!(batch.commit)
    |> Enum.map(&{elem(&1, 0), elem(&1, 1)})
    |> Map.new()
    batch.id
    |> Status.all_for_batch()
    |> Repo.all()
    |> Enum.filter(&Map.has_key?(gh_statuses, &1.identifier))
    |> Enum.map(&{&1, %{state: Map.fetch!(gh_statuses, &1.identifier)}})
    |> Enum.map(&Status.changeset(elem(&1, 0), elem(&1, 1)))
    |> Enum.each(&Repo.update!/1)
    maybe_complete_batch(batch)
  end

  defp maybe_complete_batch(batch) do
    statuses = Repo.all(Status.all_for_batch(batch.id))
    status = Batcher.State.summary_database_statuses(statuses)
    now = DateTime.to_unix(DateTime.utc_now(), :seconds)
    if status != :running do
      batch.project
      |> get_repo_conn()
      |> send_status(batch, status)
      Project.ping!(batch.project_id)
      complete_batch(status, batch, statuses)
    end
    batch
    |> Batch.changeset(%{state: status, last_polled: now})
    |> Repo.update!()
    if status != :running do
      poll(batch.project_id)
    end
  end

  defp complete_batch(:ok, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    GitHub.push!(
      repo_conn,
      batch.commit,
      batch.into_branch)
    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()

    send_message(repo_conn, patches, {:succeeded, statuses})
  end

  defp complete_batch(:error, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    erred = Enum.filter(statuses, &(&1.state == :error))
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    send_message(repo_conn, patches, {state, erred})
  end

  defp timeout_batch(batch) do
    project = batch.project
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    project
    |> get_repo_conn()
    |> send_message(patches, {:timeout, state})
    batch
    |> Batch.changeset(%{state: :error})
    |> Repo.update!()
    Project.ping!(project.id)
    project
    |> get_repo_conn()
    |> send_status(batch, :timeout)
  end

  defp cancel_patch(nil, _), do: :ok

  defp cancel_patch(batch, patch_id) do
    cancel_patch(batch, patch_id, batch.state)
    Project.ping!(batch.project_id)
  end

  defp cancel_patch(batch, patch_id, :running) do
    project = batch.project
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = case tl(patch_links) do
      [] -> :failed
      _ -> :retrying
    end
    batch
    |> Batch.changeset(%{state: :canceled})
    |> Repo.update!()
    repo_conn = get_repo_conn(project)
    if state == :retrying do
      uncanceled_patch_links = Enum.filter(
        patch_links,
        &(&1.patch_id != patch_id))
      clone_batch(uncanceled_patch_links, project.id, batch.into_branch)
      canceled_patches = Enum.filter(
        patches,
        &(&1.id == patch_id))
      uncanceled_patches = Enum.filter(
        patches,
        &(&1.id != patch_id))
      send_message(repo_conn, canceled_patches, {:canceled, :failed})
      send_message(repo_conn, uncanceled_patches, {:canceled, :retrying})
    else
      send_message(repo_conn, patches, {:canceled, :failed})
    end
    send_status(repo_conn, batch, :canceled)
  end

  defp cancel_patch(batch, patch_id, _state) do
    project = batch.project
    LinkPatchBatch
    |> Repo.get_by!(batch_id: batch.id, patch_id: patch_id)
    |> Repo.delete!()
    if Batch.is_empty(batch.id, Repo) do
      Repo.delete!(batch)
    end
    patch = Repo.get!(Patch, patch_id)
    repo_conn = get_repo_conn(project)
    send_status(repo_conn, batch.id, [patch], :canceled)
    send_message(repo_conn, [patch], {:canceled, :failed})
  end

  defp bisect(patch_links, %Batch{project: project, into_branch: into}) do
    count = Enum.count(patch_links)
    if count > 1 do
      {lo, hi} = Enum.split(patch_links, div(count, 2))
      clone_batch(lo, project.id, into)
      clone_batch(hi, project.id, into)
      poll_after_delay(project)
      :retrying
    else
      :failed
    end
  end

  defp patch_preflight(repo_conn, patch) do
    toml = Batcher.GetBorsToml.get(
      repo_conn,
      patch.commit)
    patch_preflight(repo_conn, patch, toml)
  end

  defp patch_preflight(_repo_conn, _patch, {:error, _}) do
    :ok
  end

  defp patch_preflight(repo_conn, patch, {:ok, toml}) do
    passed_label = repo_conn
    |> GitHub.get_labels!(patch.pr_xref)
    |> MapSet.new()
    |> MapSet.disjoint?(MapSet.new(toml.block_labels))
    passed_status = repo_conn
    |> GitHub.get_commit_status!(patch.commit)
    |> Enum.filter(fn {_, status} -> status != :ok end)
    |> Enum.map(fn {context, _} -> context end)
    |> MapSet.new()
    |> MapSet.disjoint?(MapSet.new(toml.pr_status))
    passed_review = repo_conn
    |> GitHub.get_reviews!(patch.pr_xref)
    |> are_reviews_passing(toml.required_approvals)

    case {passed_label, passed_status, passed_review} do
      {true, true, :sufficient} -> :ok
      {false, _, _}             -> {:error, :blocked_labels}
      {_, false, _}             -> {:error, :pr_status}
      {_, _, :insufficient}     -> {:error, :insufficient_approvals}
      {_, _, :failed}           -> {:error, :blocked_review}
    end
  end

  defp are_reviews_passing(reviews, required) do
    %{"CHANGES_REQUESTED" => failed, "APPROVED" => passed} = reviews

    case {failed, passed} do
      {0, approved} when approved >= required -> :sufficient
      {0, approved} when approved < required -> :insufficient
      _ -> :failed
    end
  end

  defp clone_batch(patch_links, project_id, into_branch) do
    batch = Repo.insert!(Batch.new(project_id, into_branch))
    patch_links
    |> Enum.map(&%{
      batch_id: batch.id,
      patch_id: &1.patch_id,
      reviewer: &1.reviewer})
    |> Enum.map(&LinkPatchBatch.changeset(%LinkPatchBatch{}, &1))
    |> Enum.each(&Repo.insert!/1)
    batch
  end

  def get_new_batch(project_id, into_branch, priority) do
    Batch
    |> where([b], b.project_id == ^project_id)
    |> where([b], b.state == ^(:waiting))
    |> where([b], b.into_branch == ^into_branch)
    |> where([b], b.priority == ^priority)
    |> order_by([b], [desc: b.updated_at])
    |> limit(1)
    |> Repo.all()
    |> case do
      [batch] -> {batch, false}
      _ -> {Repo.insert!(Batch.new(project_id, into_branch, priority)), true}
    end
  end

  defp send_message(repo_conn, patches, message) do
    body = Batcher.Message.generate_message(message)
    Enum.each(patches, &GitHub.post_comment!(
      repo_conn,
      &1.pr_xref,
      body))
  end

  defp send_status(
         repo_conn,
         %Batch{id: id, commit: commit, project_id: project_id},
         message
       ) do
    patches = id
    |> Patch.all_for_batch()
    |> Repo.all()
    send_status(repo_conn, id, patches, message)
    unless is_nil commit do
      {msg, status} = Batcher.Message.generate_status(message)
      repo_conn
      |> GitHub.post_commit_status!({
        commit,
        status,
        msg,
        project_url(Endpoint, :log, project_id) <> "#batch-#{id}"})
    end
  end
  defp send_status(repo_conn, batch_id,  patches, message) do
    {msg, status} = Batcher.Message.generate_status(message)
    Enum.each(patches, &GitHub.post_commit_status!(
      repo_conn,
      {
        &1.commit,
        status,
        msg,
        project_url(Endpoint, :log, &1.project_id) <> "#batch-#{batch_id}"}))
  end

  @spec get_repo_conn(%Project{}) :: {{:installation, number}, number}
  defp get_repo_conn(project) do
    Project.installation_connection(project.repo_xref, Repo)
  end

  defp put_incomplete_on_hold(repo_conn, batch) do
    batches_query = batch.project_id
    |> Batch.all_for_project(:running)
    |> where([b], b.id != ^batch.id and b.priority < ^batch.priority)
    Status
    |> join(:inner, [s], b in ^batches_query, s.batch_id == b.id)
    |> Repo.delete_all()
    batches_query
    |> Repo.all()
    |> Enum.each(&send_status(repo_conn, &1, :delayed))
    Repo.update_all(batches_query,
      set: [state: :waiting])
  end

  defp poll_after_delay(project) do
    poll_at = (project.batch_delay_sec + 1) * 1000
    Process.send_after(self(), {:poll, :once}, poll_at)
  end
end

defmodule BorsNG.Worker.Batcher do
  @moduledoc """
  A "Batcher" manages the backlog of batches a project has.
  It implements this set of rules:

    * When a patch is reviewed ("r+'ed"),
      it gets added to the project's non-running batch.
      If no such batch exists, it creates it.
    * After a short delay, if there is no currently running batch,
      the project's non-running batch is started.
    * The project's CI is occasionally polled,
      if a batch is currently running.
      After polling, the completion logic is run.
    * If a notification related to the underlying CI is received,
      the completion logic is run.
    * When the completion logic is run, the batch is either
      bisected (if it failed and there are two or more patches in it),
      blocked (if it failed and there is only one patch in it),
      pushed to master (if it passed),
      or (if there are still CI jobs with no results) it is left alone.
  """

  use GenServer
  alias BorsNG.Worker.Batcher
  alias BorsNG.Database.Repo
  alias BorsNG.Database.Batch
  alias BorsNG.Database.BatchState
  alias BorsNG.Database.Patch
  alias BorsNG.Database.Project
  alias BorsNG.Database.Status
  alias BorsNG.Database.LinkPatchBatch
  alias BorsNG.GitHub
  alias BorsNG.Endpoint
  import BorsNG.Router.Helpers
  import Ecto.Query

  # Every half-hour
  @poll_period 30 * 60 * 1000

  # Public API

  def start_link(project_id) do
    GenServer.start_link(__MODULE__, project_id)
  end

  def reviewed(pid, patch_id, reviewer) when is_integer(patch_id) do
    GenServer.cast(pid, {:reviewed, patch_id, reviewer})
  end

  def set_priority(pid, patch_id, priority) when is_integer(patch_id) do
    GenServer.call(pid, {:set_priority, patch_id, priority})
  end

  def status(pid, stat) do
    GenServer.cast(pid, {:status, stat})
  end

  def cancel(pid, patch_id) when is_integer(patch_id) do
    GenServer.cast(pid, {:cancel, patch_id})
  end

  def cancel_all(pid) do
    GenServer.cast(pid, {:cancel_all})
  end

  # Server callbacks

  def init(project_id) do
    Process.send_after(
      self(),
      {:poll, :repeat},
      @poll_period * :rand.uniform(2) * 0.5)
    {:ok, project_id}
  end

  def handle_cast(args, project_id) do
    do_handle_cast(args, project_id)
    {:noreply, project_id}
  end

  def handle_call({:set_priority, patch_id, priority}, _from, state) do
    case Repo.get(Patch.all(:awaiting_review), patch_id) do
      nil -> nil
      patch ->
        if patch.priority != priority do
          patch
          |> Patch.changeset(%{priority: priority})
          |> Repo.update!()
        end
    end

    {:reply, :ok, state}
  end

  def do_handle_cast({:reviewed, patch_id, reviewer}, project_id) do
    case Repo.get(Patch.all(:awaiting_review), patch_id) do
      nil ->
        # Patch exists (otherwise, no ID), but is not awaiting review
        patch = Repo.get!(Patch, patch_id)
        project = Repo.get!(Project, patch.project_id)
        project
        |> get_repo_conn()
        |> send_message([patch], :not_awaiting_review)
      patch ->
        # Patch exists and is awaiting review
        # This will cause the PR to start after the patch's scheduled delay
        project = Repo.get!(Project, patch.project_id)
        if Patch.ci_skip?(patch) do
          project
          |> get_repo_conn()
          |> send_message([patch], :ci_skip)
        else

          {batch, is_new_batch} = get_new_batch(
            project_id,
            patch.into_branch,
            patch.priority
          )

          if is_new_batch do
            put_incomplete_on_hold(get_repo_conn(project), batch)
          end

          repo_conn = get_repo_conn(project)
          case patch_preflight(repo_conn, patch) do
            :ok ->
              params = %{
                batch_id: batch.id,
                patch_id: patch.id,
                reviewer: reviewer}
              Repo.insert!(LinkPatchBatch.changeset(%LinkPatchBatch{}, params))
              poll_after_delay(project)
              send_status(repo_conn, batch.id, [patch], :waiting)
            {:error, message} ->
              send_message(repo_conn, [patch], {:preflight, message})
              send_status(repo_conn, batch.id, [patch], :error)
          end
        end
    end
  end

  def do_handle_cast({:status, {commit, identifier, state, url}}, project_id) do
    batch = Repo.all(Batch.get_assoc_by_commit(commit))
    case batch do
      [batch] ->
        ^project_id = batch.project_id
        batch.id
        |> Status.get_for_batch(identifier)
        |> Repo.update_all([set: [state: state, url: url]])
        if batch.state == :running do
          maybe_complete_batch(batch)
        end
      [] -> :ok
    end
  end

  def do_handle_cast({:cancel, patch_id}, _project_id) do
    batch = patch_id
    |> Batch.all_for_patch(:incomplete)
    |> Repo.one()
    cancel_patch(batch, patch_id)
  end

  def do_handle_cast({:cancel_all}, project_id) do
    waiting = project_id
    |> Batch.all_for_project(:waiting)
    |> Repo.all()
    Enum.each(waiting, &Repo.delete!/1)
    running = project_id
    |> Batch.all_for_project(:running)
    |> Repo.all()
    Enum.map(running, &Batch.changeset(&1, %{state: :canceled}))
    |> Enum.each(&Repo.update!/1)
    repo_conn = Project
    |> Repo.get!(project_id)
    |> get_repo_conn()
    Enum.each(running, &send_status(repo_conn, &1, :canceled))
    Enum.each(waiting, &send_status(repo_conn, &1, :canceled))
  end

  def handle_info({:poll, repetition}, project_id) do
    if repetition != :once do
      Process.send_after(self(), {:poll, repetition}, @poll_period)
    end
    case poll(project_id) do
      :stop ->
        {:stop, :normal, project_id}
      :again ->
        {:noreply, project_id}
    end
  end

  # Private implementation details

  defp poll(project_id) do
    project = Repo.get(Project, project_id)
    incomplete = project_id
    |> Batch.all_for_project(:incomplete)
    |> Repo.all()
    incomplete
    |> Enum.map(&%Batch{&1 | project: project})
    |> sort_batches()
    |> poll_batches()
    if Enum.empty?(incomplete) do
      :stop
    else
      :again
    end
  end

  def sort_batches(batches) do
    sorted_batches = Enum.sort_by(batches, &{
      -BatchState.numberize(&1.state),
      -&1.priority,
      &1.last_polled
    })
    new_batches = Enum.dedup_by(sorted_batches, &(&1.id))
    state = if new_batches != [] and hd(new_batches).state == :running do
      :running
    else
      Enum.each(new_batches, fn batch -> :waiting =  batch.state end)
      :waiting
    end
    {state, new_batches}
  end

  defp poll_batches({:waiting, batches}) do
    case Enum.filter(batches, &Batch.next_poll_is_past/1) do
      [] -> :ok
      [batch | _] -> start_waiting_batch(batch)
    end
  end

  defp poll_batches({:running, batches}) do
    batch = hd(batches)
    cond do
      Batch.timeout_is_past(batch) ->
        timeout_batch(batch)
      Batch.next_poll_is_past(batch) ->
        poll_running_batch(batch)
      true -> :ok
    end
  end

  defp start_waiting_batch(batch) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    patch_links = Repo.all(LinkPatchBatch.from_batch(batch.id))
    |> Enum.sort_by(&(&1.patch.pr_xref))
    stmp = "#{project.staging_branch}.tmp"
    base = GitHub.get_branch!(
      repo_conn,
      batch.into_branch)
    tbase = %{
      tree: base.tree,
      commit: GitHub.synthesize_commit!(
        repo_conn,
        %{
          branch: stmp,
          tree: base.tree,
          parents: [base.commit],
          commit_message: "[ci skip]",
          committer: nil})}
    do_merge_patch = fn %{patch: patch}, branch ->
      case branch do
        :conflict -> :conflict
        _ -> GitHub.merge_branch!(
          repo_conn,
          %{
            from: patch.commit,
            to: stmp,
            commit_message: "[ci skip] -bors-staging-tmp-#{patch.pr_xref}"})
      end
    end
    merge = Enum.reduce(patch_links, tbase, do_merge_patch)
    {status, commit} = start_waiting_merged_batch(
      batch,
      patch_links,
      base,
      merge)
    now = DateTime.to_unix(DateTime.utc_now(), :seconds)
    GitHub.delete_branch!(repo_conn, stmp)
    send_status(repo_conn, batch, status)
    batch
    |> Batch.changeset(%{state: status, commit: commit, last_polled: now})
    |> Repo.update!()
    Project.ping!(batch.project_id)
    status
  end

  defp start_waiting_merged_batch(batch, patch_links, base, %{tree: tree}) do
    repo_conn = get_repo_conn(batch.project)
    patches = Enum.map(patch_links, &(&1.patch))
    repo_conn
    |> Batcher.GetBorsToml.get("#{batch.project.staging_branch}.tmp")
    |> case do
      {:ok, toml} ->
        parents = [base.commit | Enum.map(patch_links, &(&1.patch.commit))]
        commit_message = Batcher.Message.generate_commit_message(
          patch_links,
          toml.cut_body_after,
          gather_co_authors(batch, patch_links))
        head = GitHub.synthesize_commit!(
          repo_conn,
          %{
            branch: batch.project.staging_branch,
            tree: tree,
            parents: parents,
            commit_message: commit_message,
            committer: toml.committer})
        setup_statuses(batch, toml)
        {:running, head}
      {:error, message} ->
        message = Batcher.Message.generate_bors_toml_error(message)
        send_message(repo_conn, patches, {:config, message})
        {:error, nil}
    end
  end

  defp start_waiting_merged_batch(batch, patch_links, _base, :conflict) do
    repo_conn = get_repo_conn(batch.project)
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    send_message(repo_conn, patches, {:conflict, state})
    {:conflict, nil}
  end

  def gather_co_authors(batch, patch_links) do
    repo_conn = get_repo_conn(batch.project)
    patch_links
    |> Enum.map(&(&1.patch.pr_xref))
    |> Enum.flat_map(&GitHub.get_pr_commits!(repo_conn, &1))
    |> Enum.map(&("#{&1.author_name} <#{&1.author_email}>"))
    |> Enum.uniq
  end

  defp setup_statuses(batch, toml) do
    toml.status
    |> Enum.map(&%Status{
        batch_id: batch.id,
        identifier: &1,
        url: nil,
        state: :running})
    |> Enum.each(&Repo.insert!/1)
    now = DateTime.to_unix(DateTime.utc_now(), :seconds)
    batch
    |> Batch.changeset(%{timeout_at: now + toml.timeout_sec})
    |> Repo.update!()
  end

  defp poll_running_batch(batch) do
    project = batch.project
    gh_statuses = project
    |> get_repo_conn()
    |> GitHub.get_commit_status!(batch.commit)
    |> Enum.map(&{elem(&1, 0), elem(&1, 1)})
    |> Map.new()
    batch.id
    |> Status.all_for_batch()
    |> Repo.all()
    |> Enum.filter(&Map.has_key?(gh_statuses, &1.identifier))
    |> Enum.map(&{&1, %{state: Map.fetch!(gh_statuses, &1.identifier)}})
    |> Enum.map(&Status.changeset(elem(&1, 0), elem(&1, 1)))
    |> Enum.each(&Repo.update!/1)
    maybe_complete_batch(batch)
  end

  defp maybe_complete_batch(batch) do
    statuses = Repo.all(Status.all_for_batch(batch.id))
    status = Batcher.State.summary_database_statuses(statuses)
    now = DateTime.to_unix(DateTime.utc_now(), :seconds)
    if status != :running do
      batch.project
      |> get_repo_conn()
      |> send_status(batch, status)
      Project.ping!(batch.project_id)
      complete_batch(status, batch, statuses)
    end
    batch
    |> Batch.changeset(%{state: status, last_polled: now})
    |> Repo.update!()
    if status != :running do
      poll(batch.project_id)
    end
  end

  defp complete_batch(:ok, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    GitHub.push!(
      repo_conn,
      batch.commit,
      batch.into_branch)
    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()

    send_message(repo_conn, patches, {:succeeded, statuses})
  end

  defp complete_batch(:error, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    erred = Enum.filter(statuses, &(&1.state == :error))
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    send_message(repo_conn, patches, {state, erred})
  end

  defp timeout_batch(batch) do
    project = batch.project
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    project
    |> get_repo_conn()
    |> send_message(patches, {:timeout, state})
    batch
    |> Batch.changeset(%{state: :error})
    |> Repo.update!()
    Project.ping!(project.id)
    project
    |> get_repo_conn()
    |> send_status(batch, :timeout)
  end

  defp cancel_patch(nil, _), do: :ok

  defp cancel_patch(batch, patch_id) do
    cancel_patch(batch, patch_id, batch.state)
    Project.ping!(batch.project_id)
  end

  defp cancel_patch(batch, patch_id, :running) do
    project = batch.project
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = case tl(patch_links) do
      [] -> :failed
      _ -> :retrying
    end
    batch
    |> Batch.changeset(%{state: :canceled})
    |> Repo.update!()
    repo_conn = get_repo_conn(project)
    if state == :retrying do
      uncanceled_patch_links = Enum.filter(
        patch_links,
        &(&1.patch_id != patch_id))
      clone_batch(uncanceled_patch_links, project.id, batch.into_branch)
      canceled_patches = Enum.filter(
        patches,
        &(&1.id == patch_id))
      uncanceled_patches = Enum.filter(
        patches,
        &(&1.id != patch_id))
      send_message(repo_conn, canceled_patches, {:canceled, :failed})
      send_message(repo_conn, uncanceled_patches, {:canceled, :retrying})
    else
      send_message(repo_conn, patches, {:canceled, :failed})
    end
    send_status(repo_conn, batch, :canceled)
  end

  defp cancel_patch(batch, patch_id, _state) do
    project = batch.project
    LinkPatchBatch
    |> Repo.get_by!(batch_id: batch.id, patch_id: patch_id)
    |> Repo.delete!()
    if Batch.is_empty(batch.id, Repo) do
      Repo.delete!(batch)
    end
    patch = Repo.get!(Patch, patch_id)
    repo_conn = get_repo_conn(project)
    send_status(repo_conn, batch.id, [patch], :canceled)
    send_message(repo_conn, [patch], {:canceled, :failed})
  end

  defp bisect(patch_links, %Batch{project: project, into_branch: into}) do
    count = Enum.count(patch_links)
    if count > 1 do
      {lo, hi} = Enum.split(patch_links, div(count, 2))
      clone_batch(lo, project.id, into)
      clone_batch(hi, project.id, into)
      poll_after_delay(project)
      :retrying
    else
      :failed
    end
  end

  defp patch_preflight(repo_conn, patch) do
    toml = Batcher.GetBorsToml.get(
      repo_conn,
      patch.commit)
    patch_preflight(repo_conn, patch, toml)
  end

  defp patch_preflight(_repo_conn, _patch, {:error, _}) do
    :ok
  end

  defp patch_preflight(repo_conn, patch, {:ok, toml}) do
    passed_label = repo_conn
    |> GitHub.get_labels!(patch.pr_xref)
    |> MapSet.new()
    |> MapSet.disjoint?(MapSet.new(toml.block_labels))
    passed_status = repo_conn
    |> GitHub.get_commit_status!(patch.commit)
    |> Enum.filter(fn {_, status} -> status != :ok end)
    |> Enum.map(fn {context, _} -> context end)
    |> MapSet.new()
    |> MapSet.disjoint?(MapSet.new(toml.pr_status))
    passed_review = repo_conn
    |> GitHub.get_reviews!(patch.pr_xref)
    |> are_reviews_passing(toml.required_approvals)

    case {passed_label, passed_status, passed_review} do
      {true, true, :sufficient} -> :ok
      {false, _, _}             -> {:error, :blocked_labels}
      {_, false, _}             -> {:error, :pr_status}
      {_, _, :insufficient}     -> {:error, :insufficient_approvals}
      {_, _, :failed}           -> {:error, :blocked_review}
    end
  end

  defp are_reviews_passing(reviews, required) do
    %{"CHANGES_REQUESTED" => failed, "APPROVED" => passed} = reviews

    case {failed, passed} do
      {0, approved} when approved >= required -> :sufficient
      {0, approved} when approved < required -> :insufficient
      _ -> :failed
    end
  end

  defp clone_batch(patch_links, project_id, into_branch) do
    batch = Repo.insert!(Batch.new(project_id, into_branch))
    patch_links
    |> Enum.map(&%{
      batch_id: batch.id,
      patch_id: &1.patch_id,
      reviewer: &1.reviewer})
    |> Enum.map(&LinkPatchBatch.changeset(%LinkPatchBatch{}, &1))
    |> Enum.each(&Repo.insert!/1)
    batch
  end

  def get_new_batch(project_id, into_branch, priority) do
    Batch
    |> where([b], b.project_id == ^project_id)
    |> where([b], b.state == ^(:waiting))
    |> where([b], b.into_branch == ^into_branch)
    |> where([b], b.priority == ^priority)
    |> order_by([b], [desc: b.updated_at])
    |> limit(1)
    |> Repo.all()
    |> case do
      [batch] -> {batch, false}
      _ -> {Repo.insert!(Batch.new(project_id, into_branch, priority)), true}
    end
  end

  defp send_message(repo_conn, patches, message) do
    body = Batcher.Message.generate_message(message)
    Enum.each(patches, &GitHub.post_comment!(
      repo_conn,
      &1.pr_xref,
      body))
  end

  defp send_status(
         repo_conn,
         %Batch{id: id, commit: commit, project_id: project_id},
         message
       ) do
    patches = id
    |> Patch.all_for_batch()
    |> Repo.all()
    send_status(repo_conn, id, patches, message)
    unless is_nil commit do
      {msg, status} = Batcher.Message.generate_status(message)
      repo_conn
      |> GitHub.post_commit_status!({
        commit,
        status,
        msg,
        project_url(Endpoint, :log, project_id) <> "#batch-#{id}"})
    end
  end
  defp send_status(repo_conn, batch_id,  patches, message) do
    {msg, status} = Batcher.Message.generate_status(message)
    Enum.each(patches, &GitHub.post_commit_status!(
      repo_conn,
      {
        &1.commit,
        status,
        msg,
        project_url(Endpoint, :log, &1.project_id) <> "#batch-#{batch_id}"}))
  end

  @spec get_repo_conn(%Project{}) :: {{:installation, number}, number}
  defp get_repo_conn(project) do
    Project.installation_connection(project.repo_xref, Repo)
  end

  defp put_incomplete_on_hold(repo_conn, batch) do
    batches_query = batch.project_id
    |> Batch.all_for_project(:running)
    |> where([b], b.id != ^batch.id and b.priority < ^batch.priority)
    Status
    |> join(:inner, [s], b in ^batches_query, s.batch_id == b.id)
    |> Repo.delete_all()
    batches_query
    |> Repo.all()
    |> Enum.each(&send_status(repo_conn, &1, :delayed))
    Repo.update_all(batches_query,
      set: [state: :waiting])
  end

  defp poll_after_delay(project) do
    poll_at = (project.batch_delay_sec + 1) * 1000
    Process.send_after(self(), {:poll, :once}, poll_at)
  end
end


Update validator

Update validator

defmodule Xema.Validator do
  @moduledoc false

  @spec validate(Xema.t, any) :: :ok | {:error, map}
  def validate(%Xema{} = xema, value) do
    validate(xema.type, value)
  end

  @spec validate(Xema.types, any) :: :ok | {:error, map}
  def validate(%Xema.Nil{} = type, value) do
    case value == nil do
      true -> :ok
      false -> error type
    end
  end
  def validate(%Xema.Number{} = type, value) do
    validate_number(type, value)
  end
  def validate(%Xema.Integer{} = type, value) do
    validate_number(type, value)
  end
  def validate(%Xema.Float{} = type, value) do
    validate_number(type, value)
  end
  def validate(%Xema.Any{} = type, value) do
    with :ok <- enum(type, value) do
      :ok
    end
  end
  def validate(%Xema.Boolean{} = type, value) do
    case is_boolean(value) do
      true -> :ok
      false -> error type
    end
  end
  def validate(%Xema.String{} = type, value) do
    with :ok <- type(type, value),
         length <- String.length(value),
         :ok <- min_length(type, length),
         :ok <- max_length(type, length),
         :ok <- pattern(type, value),
         :ok <- enum(type, value),
      do: :ok
  end
  def validate(%Xema.List{} = type, value) do
    with :ok <- type(type, value),
         :ok <- min_items(type, value),
         :ok <- max_items(type, value),
         :ok <- items(type, value),
         :ok <- unique(type, value),
      do: :ok
  end
  def validate(%Xema.Map{} = type, value) do
    with :ok <- type(type, value),
         :ok <- size(type, value),
         :ok <- keys(type, value),
         :ok <- required(type, value),
         :ok <- dependencies(type, value),
         {:ok, value} <- properties(type, value),
         {:ok, value} <- patterns(type, value),
         :ok <- additionals(type, value),
      do: :ok
  end

  @spec validate_number(Xema.types, any) :: :ok | {:error, map}
  defp validate_number(type, value) do
    with :ok <- type(type, value),
         :ok <- minimum(type, value),
         :ok <- maximum(type, value),
         :ok <- multiple_of(type, value),
         :ok <- enum(type, value) do
      :ok
    end
  end

  @spec type(Xema.types, any) :: :ok | {:error, map}
  defp type(%Xema.Integer{}, value) when is_integer(value), do: :ok
  defp type(%Xema.Float{}, value) when is_float(value), do: :ok
  defp type(%Xema.Number{}, value) when is_number(value), do: :ok
  defp type(%Xema.String{}, value) when is_binary(value), do: :ok
  defp type(%Xema.List{}, value) when is_list(value), do: :ok
  defp type(%Xema.Map{}, value) when is_map(value), do: :ok
  defp type(type, _value), do: error type

  @spec enum(Xema.types, any) :: :ok | {:error, map}
  defp enum(%{enum: nil}, _element), do: :ok
  defp enum(%{enum: enum}, value) do
    case Enum.member?(enum, value) do
      true -> :ok
      false -> error :not_in_enum, enum: enum, element: value
    end
  end

  @spec minimum(Xema.types, any) :: :ok | {:error, map}
  defp minimum(%{minimum: nil}, _value), do: :ok
  defp minimum(
    %{minimum: minimum, exclusive_minimum: exclusive_minimum},
    value
  ), do: minimum(minimum, exclusive_minimum, value)

  @spec minimum(number, boolean, number) :: :ok | {:error, map}
  defp minimum(minimum, _exclusive, value) when value > minimum, do: :ok
  defp minimum(minimum, nil, value) when value == minimum, do: :ok
  defp minimum(minimum, false, value) when value == minimum, do: :ok
  defp minimum(minimum, _exclusive, value) do
    cond do
      value != minimum -> error :too_small, minimum: minimum
      true -> error :too_small, minimum: minimum, exclusive_minimum: true
    end
  end

  @spec maximum(Xema.types, any) :: :ok | {:error, map}
  defp maximum(%{maximum: nil}, _value), do: :ok
  defp maximum(
    %{maximum: maximum, exclusive_maximum: exclusive_maximum},
    value
  ), do: maximum(maximum, exclusive_maximum, value)

  @spec maximum(number, boolean, number) :: :ok | {:error, map}
  defp maximum(maximum, _exclusive, value) when value < maximum, do: :ok
  defp maximum(maximum, nil, value) when value == maximum, do: :ok
  defp maximum(maximum, false, value) when value == maximum, do: :ok
  defp maximum(maximum, _exclusive, value) do
    cond do
      value != maximum -> error :too_big, maximum: maximum
      true -> error :too_big, maximum: maximum, exclusive_maximum: true
    end
  end

  @spec multiple_of(Xema.types, number) :: :ok | {:error, map}
  defp multiple_of(%{multiple_of: nil} = _keywords, _value), do: :ok
  defp multiple_of(%{multiple_of: multiple_of}, value) do
    x = value / multiple_of
    if x - Float.floor(x) == 0,
      do: :ok,
      else: error :not_multiple, multiple_of: multiple_of
  end

  defp min_length(%{min_length: nil}, _), do: :ok
  defp min_length(%{min_length: min}, len) when len >= min, do: :ok
  defp min_length(%{min_length: min}, _length) do
    error :too_short, min_length: min
  end

  defp max_length(%{max_length: nil}, _length), do: :ok
  defp max_length(%{max_length: max}, len) when len <= max, do: :ok
  defp max_length(%{max_length: max}, _length) do
    error :too_long, max_length: max
  end

  defp pattern(%{pattern: nil}, _string), do: :ok
  defp pattern(%{pattern: pattern}, string) do
    case Regex.match?(pattern, string) do
      true -> :ok
      false -> error :no_match, pattern: pattern
    end
  end

  defp min_items(%Xema.List{min_items: nil}, _list), do: :ok
  defp min_items(%Xema.List{min_items: min}, list) when length(list) >= min do
    :ok
  end
  defp min_items(%Xema.List{min_items: min}, _list) do
    error :too_less_items, min_items: min
  end

  defp max_items(%Xema.List{max_items: nil}, _list), do: :ok
  defp max_items(%Xema.List{max_items: max}, list) when length(list) <= max do
    :ok
  end
  defp max_items(%Xema.List{max_items: max}, _list) do
    error :too_many_items, max_items: max
  end

  defp unique(%Xema.List{unique_items: nil}, _list), do: :ok
  defp unique(%Xema.List{unique_items: true}, list) do
    case is_unique?(list) do
      true -> :ok
      false -> error :not_unique
    end
  end

  defp is_unique?(list, set \\ %{})
  defp is_unique?([], _), do: true
  defp is_unique?([h|t], set) do
    case set do
      %{^h => true} -> false
      _ -> is_unique?(t, Map.put(set, h, true))
    end
  end

  defp items(%Xema.List{items: nil}, _list), do: :ok
  defp items(
    %Xema.List{items: items, additional_items: additional_items},
    list
  ) when is_list(items), do: items_tuple(items, additional_items, list, 0)
  defp items(%Xema.List{items: items}, list), do: items_list(items, list, 0)

  defp items_list(_schema, [], _at), do: :ok
  defp items_list(schema, [item|list], at) do
    case Xema.validate(schema, item) do
      :ok -> items_list(schema, list, at + 1)
      {:error, reason} -> error :invalid_item, at: at, error: reason
    end
  end

  defp items_tuple([], _additonal_items, [], _at), do: :ok
  defp items_tuple(_schemas, _additonal_items, [], at) do
    error :missing_item, at: at
  end
  defp items_tuple([], false, _list, at), do: error :additional_item, at: at
  defp items_tuple([], true, _list, _at), do: :ok
  defp items_tuple([schema|schemas], additional_items, [item|list], at) do
    case Xema.validate(schema, item) do
      :ok -> items_tuple(schemas, additional_items, list, at + 1)
      {:error, reason} -> error :invalid_item, at: at, error: reason
    end
  end

  defp keys(%Xema.Map{keys: nil}, _value), do: :ok
  defp keys(%Xema.Map{keys: :atom}, map) do
    case map |> Map.keys |> Enum.all?(&is_atom/1) do
      true -> :ok
      false -> error :invalid_keys, keys: :atom
    end
  end
  defp keys(%Xema.Map{keys: :string}, map) do
    case map |> Map.keys |> Enum.all?(&is_binary/1) do
      true -> :ok
      false -> error :invalid_keys, keys: :string
    end
  end

  defp properties(%Xema.Map{properties: nil}, map), do: {:ok, map}
  defp properties(%Xema.Map{properties: props}, map) do
    do_properties(Map.to_list(props), map)
  end

  defp do_properties([], map), do: {:ok, map}
  defp do_properties([{prop, schema}|props], map) do
    case do_property(schema, get_value(map, prop)) do
      :ok -> do_properties(props, Map.delete(map, prop))
      {:error, reason} ->
        {:error, Map.merge(reason, %{property: get_key(map, prop)})}
    end
  end

  defp do_property(_schema, nil), do: :ok
  defp do_property(schema, value) do
    case Xema.validate(schema, value) do
      :ok -> :ok
      {:error, reason} -> error :invalid_property, error: reason
    end
  end

  defp get_value(map, key) when is_atom(key) do
    do_get_value(map, to_string(key), key)
  end
  defp get_value(map, key) do
    do_get_value(map, key, String.to_atom(key))
  end
  defp do_get_value(map, key_string, key_atom) do
    case {Map.get(map, key_string), Map.get(map, key_atom)} do
      {nil, nil} -> nil
      {nil, value} -> value
      {value, nil} -> value
      _ ->
        error :mixed_map
    end
  end

  defp get_key(map, key) when is_atom(key) do
    if Map.has_key?(map, key), do: key, else: to_string key
  end
  defp get_key(map, key) do
    if Map.has_key?(map, key), do: key, else: String.to_existing_atom(key)
  end

  defp required(%Xema.Map{required: nil}, _map), do: :ok
  defp required(%Xema.Map{required: required}, map) do
    props = map |> Map.keys |> MapSet.new

    if MapSet.subset?(required, props) do
      :ok
    else
      error(
        :missing_properties,
        missing: required |> MapSet.difference(props) |> MapSet.to_list,
        required: MapSet.to_list(required)
      )
    end
  end

  defp size(%Xema.Map{min_properties: nil, max_properties: nil}, _map), do: :ok
  defp size(%Xema.Map{min_properties: min, max_properties: max}, map) do
    do_size(length(Map.keys(map)), min, max)
  end

  defp do_size(len, min, _max) when not is_nil(min) and len < min do
    error :too_less_properties, min_properties: min
  end
  defp do_size(len, _min, max)
    when not is_nil(max) and len > max,
    do: error :too_many_properties, max_properties: max
  defp do_size(_len, _min, _max), do: :ok

  defp patterns(%Xema.Map{pattern_properties: nil}, map), do: {:ok, map}
  defp patterns(%Xema.Map{pattern_properties: patterns}, map) do
    props =
      for {pattern, schema} <- Map.to_list(patterns),
          key <- Map.keys(map),
          key_match?(pattern, key),
          do: {key, schema}

    do_properties(props, map)
  end

  defp key_match?(regex, atom) when is_atom(atom),
    do: key_match?(regex, to_string(atom))
  defp key_match?(regex, string), do: Regex.match?(regex, string)

  defp additionals(%Xema.Map{additional_properties: false}, map) do
    if Map.equal?(map, %{}) do
      :ok
    else
      error(:no_additional_properties_allowed,
            additional_properties: Map.keys(map))
    end
  end
  defp additionals(_schema, _map), do: :ok

  defp dependencies(%Xema.Map{dependencies: nil}, _map), do: :ok
  defp dependencies(%Xema.Map{dependencies: dependencies}, map) do
    dependencies
    |> Map.to_list
    |> Enum.filter(fn {key, _} -> Map.has_key?(map, key) end)
    |> do_dependencies(map)
  end

  defp do_dependencies([], _map), do: :ok
  defp do_dependencies([{key, list}|tail], map) when is_list(list) do
    with :ok <- do_dependencies_list(key, list, map) do
      do_dependencies(tail, map)
    end
  end
  defp do_dependencies([{key, schema}|tail], map) do
    with :ok <- Xema.validate(schema, map) do
      do_dependencies(tail, map)
    else
      {:error, error} ->
        {:error, %{reason: :invalid_dependency, for: key, error: error}}
    end
  end

  defp do_dependencies_list(_key, [], _map), do: :ok
  defp do_dependencies_list(key, [dependency|dependencies], map) do
    if Map.has_key?(map, dependency) do
      do_dependencies_list(key, dependencies, map)
    else
      {:error, %{reason: :missing_dependency, for: key, dependency: dependency}}
    end
  end

  @spec error(atom | Xema.types) :: {:error, map}
  defp error(atom) when is_atom(atom), do: error atom, []
  defp error(type), do: error :wrong_type, type: type.as

  @spec error(atom, keyword) :: {:error, map}
  defp error(reason, info) when is_atom(reason) do
    info =
      info
      |> Enum.into(%{})
      |> Map.merge(%{reason: reason})

    {:error, info}
  end
end

defmodule Xema.Validator do
  @moduledoc false

  @spec validate(Xema.t, any) :: :ok | {:error, map}
  def validate(%Xema{} = xema, value) do
    validate(xema.type, value)
  end

  @spec validate(Xema.types, any) :: :ok | {:error, map}
  def validate(%Xema.Nil{} = type, value) do
    case value == nil do
      true -> :ok
      false -> error type
    end
  end
  def validate(%Xema.Number{} = type, value) do
    validate_number(type, value)
  end
  def validate(%Xema.Integer{} = type, value) do
    validate_number(type, value)
  end
  def validate(%Xema.Float{} = type, value) do
    validate_number(type, value)
  end
  def validate(%Xema.Any{} = type, value) do
    with :ok <- enum(type, value) do
      :ok
    end
  end
  def validate(%Xema.Boolean{} = type, value) do
    case is_boolean(value) do
      true -> :ok
      false -> error type
    end
  end
  def validate(%Xema.String{} = type, value) do
    with :ok <- type(type, value),
         length <- String.length(value),
         :ok <- min_length(type, length),
         :ok <- max_length(type, length),
         :ok <- pattern(type, value),
         :ok <- enum(type, value),
      do: :ok
  end
  def validate(%Xema.List{} = type, value) do
    with :ok <- type(type, value),
         :ok <- min_items(type, value),
         :ok <- max_items(type, value),
         :ok <- items(type, value),
         :ok <- unique(type, value),
      do: :ok
  end
  def validate(%Xema.Map{} = type, value) do
    with :ok <- type(type, value),
         :ok <- size(type, value),
         :ok <- keys(type, value),
         :ok <- required(type, value),
         :ok <- dependencies(type, value),
         {:ok, value} <- properties(type, value),
         {:ok, value} <- patterns(type, value),
         :ok <- additionals(type, value),
      do: :ok
  end

  @spec validate_number(Xema.types, any) :: :ok | {:error, map}
  defp validate_number(type, value) do
    with :ok <- type(type, value),
         :ok <- minimum(type, value),
         :ok <- maximum(type, value),
         :ok <- multiple_of(type, value),
         :ok <- enum(type, value) do
      :ok
    end
  end

  @spec type(Xema.types, any) :: :ok | {:error, map}
  defp type(%Xema.Integer{}, value) when is_integer(value), do: :ok
  defp type(%Xema.Float{}, value) when is_float(value), do: :ok
  defp type(%Xema.Number{}, value) when is_number(value), do: :ok
  defp type(%Xema.String{}, value) when is_binary(value), do: :ok
  defp type(%Xema.List{}, value) when is_list(value), do: :ok
  defp type(%Xema.Map{}, value) when is_map(value), do: :ok
  defp type(type, _value), do: error type

  @spec enum(Xema.types, any) :: :ok | {:error, map}
  defp enum(%{enum: nil}, _element), do: :ok
  defp enum(%{enum: enum}, value) do
    case Enum.member?(enum, value) do
      true -> :ok
      false -> error :not_in_enum, enum: enum, element: value
    end
  end

  @spec minimum(Xema.types, any) :: :ok | {:error, map}
  defp minimum(%{minimum: nil}, _value), do: :ok
  defp minimum(
    %{minimum: minimum, exclusive_minimum: exclusive_minimum},
    value
  ), do: minimum(minimum, exclusive_minimum, value)

  @spec maximum(Xema.types, any) :: :ok | {:error, map}
  defp maximum(%{maximum: nil}, _value), do: :ok
  defp maximum(
    %{maximum: maximum, exclusive_maximum: exclusive_maximum},
    value
  ), do: maximum(maximum, exclusive_maximum, value)

  @spec multiple_of(Xema.types, number) :: :ok | {:error, map}
  defp multiple_of(%{multiple_of: nil} = _keywords, _value), do: :ok
  defp multiple_of(%{multiple_of: multiple_of}, value) do
    x = value / multiple_of
    if x - Float.floor(x) == 0,
      do: :ok,
      else: error :not_multiple, multiple_of: multiple_of
  end

  @spec minimum(number, boolean, number) :: :ok | {:error, map}
  defp minimum(minimum, _exclusive, value) when value > minimum, do: :ok
  defp minimum(minimum, true, value) when value == minimum do
    error :too_small, minimum: minimum, exclusive_minimum: true
  end
  defp minimum(minimum, _exclusive, value) when value == minimum, do: :ok
  defp minimum(minimum, _exclusive, _value) do
    error :too_small, minimum: minimum
  end

  @spec maximum(number, boolean, number) :: :ok | {:error, map}
  defp maximum(maximum, _exclusive, value) when value < maximum, do: :ok
  defp maximum(maximum, true, value) when value == maximum do
    error :too_big, maximum: maximum, exclusive_maximum: true
  end
  defp maximum(maximum, _exclusive, value) when value == maximum, do: :ok
  defp maximum(maximum, _exclusive, _value) do
    error :too_big, maximum: maximum
  end

  @spec error(atom | Xema.types) :: {:error, map}
  defp error(atom) when is_atom(atom), do: error atom, []
  defp error(type), do: error :wrong_type, type: type.as

  @spec error(atom, keyword) :: {:error, map}
  defp error(reason, info) when is_atom(reason) do
    info =
      info
      |> Enum.into(%{})
      |> Map.merge(%{reason: reason})

    {:error, info}
  end

  defp min_length(%{min_length: nil}, _), do: :ok
  defp min_length(%{min_length: min_length}, length)
       when length >= min_length do
    :ok
  end
  defp min_length(%{min_length: min_length}, _length) do
    error :too_short, min_length: min_length
  end

  defp max_length(%{max_length: nil}, _length), do: :ok
  defp max_length(%{max_length: max_length}, length)
       when length <= max_length do
    :ok
  end
  defp max_length(%{max_length: max_length}, _length) do
    error :too_long, max_length: max_length
  end

  defp pattern(%{pattern: nil}, _string), do: :ok
  defp pattern(%{pattern: pattern}, string) do
    if Regex.match?(pattern, string),
    do: :ok,
    else: error :no_match, pattern: pattern
  end

  defp min_items(%Xema.List{min_items: nil}, _list), do: :ok
  defp min_items(%Xema.List{min_items: min_items}, list)
    when length(list) < min_items,
    do: error :too_less_items, min_items: min_items
  defp min_items(_keywords, _list), do: :ok

  defp max_items(%Xema.List{max_items: nil}, _list), do: :ok
  defp max_items(%Xema.List{max_items: max_items}, list)
    when length(list) > max_items,
    do: error :too_many_items, max_items: max_items
  defp max_items(_keywords, _list), do: :ok

  defp unique(%Xema.List{unique_items: nil}, _list), do: :ok
  defp unique(%Xema.List{unique_items: true}, list) do
    if is_unique?(list),
      do: :ok,
      else: error :not_unique
  end

  defp is_unique?(list, set \\ %{})
  defp is_unique?([], _), do: true
  defp is_unique?([h|t], set) do
    case set do
      %{^h => true} -> false
      _ -> is_unique?(t, Map.put(set, h, true))
    end
  end

  defp items(%Xema.List{items: nil}, _list), do: :ok
  defp items(%Xema.List{items: items, additional_items: additional_items}, list)
    when is_list(items),
    do: items_tuple(items, additional_items, list, 0)
  defp items(%Xema.List{items: items}, list) do
    items_list(items, list, 0)
  end

  defp items_list(_schema, [], _at), do: :ok
  defp items_list(schema, [item|list], at) do
    case Xema.validate(schema, item) do
      :ok -> items_list(schema, list, at + 1)
      {:error, reason} -> error :invalid_item, at: at, error: reason
    end
  end

  defp items_tuple([], _additonal_items, [], _at), do: :ok
  defp items_tuple(_schemas, _additonal_items, [], at),
    do: error :missing_item, at: at
  defp items_tuple([], false, _list, at),
    do: error :additional_item, at: at
  defp items_tuple([], true, _list, _at), do: :ok
  defp items_tuple([schema|schemas], additional_items, [item|list], at) do
    case Xema.validate(schema, item) do
      :ok -> items_tuple(schemas, additional_items, list, at + 1)
      {:error, reason} -> error :invalid_item, at: at, error: reason
    end
  end

  defp keys(%Xema.Map{keys: nil}, _value), do: :ok
  defp keys(%Xema.Map{keys: :atom}, map) do
    if map |> Map.keys |> Enum.all?(&is_atom/1),
      do: :ok,
      else: {:error, %{reason: :invalid_keys, keys: :atom}}
  end
  defp keys(%Xema.Map{keys: :string}, map) do
    if map |> Map.keys |> Enum.all?(&is_binary/1),
      do: :ok,
      else: {:error, %{reason: :invalid_keys, keys: :string}}
  end

  defp properties(%Xema.Map{properties: nil}, map), do: {:ok, map}
  defp properties(%Xema.Map{properties: props}, map) do
    do_properties(Map.to_list(props), map)
  end

  defp do_properties([], map), do: {:ok, map}
  defp do_properties([{prop, schema}|props], map) do
    case do_property(schema, get_value(map, prop)) do
      :ok -> do_properties(props, Map.delete(map, prop))
      {:error, reason} ->
        {:error, Map.merge(reason, %{property: get_key(map, prop)})}
    end
  end

  defp do_property(_schema, nil), do: :ok
  defp do_property(schema, value) do
    case Xema.validate(schema, value) do
      :ok -> :ok
      {:error, reason} -> error(:invalid_property, error: reason)
    end
  end

  defp get_value(map, key) when is_atom(key) do
    case {Map.get(map, key), Map.get(map, to_string key)} do
      {nil, nil} -> nil
      {nil, value} -> value
      {value, nil} -> value
      _ -> {:erro, :mixed_map}
    end
  end
  defp get_value(map, key) do
    case {Map.get(map, key), Map.get(map, String.to_atom key)} do
      {nil, nil} -> nil
      {nil, value} -> value
      {value, nil} -> value
      _ -> {:erro, :mixed_map}
    end
  end

  defp get_key(map, key) when is_atom(key) do
    if Map.has_key?(map, key), do: key, else: to_string key
  end
  defp get_key(map, key) do
    if Map.has_key?(map, key), do: key, else: String.to_existing_atom(key)
  end

  defp required(%Xema.Map{required: nil}, _map), do: :ok
  defp required(%Xema.Map{required: required}, map) do
    props = map |> Map.keys |> MapSet.new

    if MapSet.subset?(required, props) do
      :ok
    else
      error(
        :missing_properties,
        missing: required |> MapSet.difference(props) |> MapSet.to_list,
        required: MapSet.to_list(required)
      )
    end
  end

  defp size(%Xema.Map{min_properties: nil, max_properties: nil}, _map), do: :ok
  defp size(%Xema.Map{min_properties: min, max_properties: max}, map) do
    do_size(length(Map.keys(map)), min, max)
  end

  defp do_size(len, min, _max) when not is_nil(min) and len < min do
    error :too_less_properties, min_properties: min
  end
  defp do_size(len, _min, max)
    when not is_nil(max) and len > max,
    do: error :too_many_properties, max_properties: max
  defp do_size(_len, _min, _max), do: :ok

  defp patterns(%Xema.Map{pattern_properties: nil}, map), do: {:ok, map}
  defp patterns(%Xema.Map{pattern_properties: patterns}, map) do
    props =
      for {pattern, schema} <- Map.to_list(patterns),
          key <- Map.keys(map),
          key_match?(pattern, key),
          do: {key, schema}

    do_properties(props, map)
  end

  defp key_match?(regex, atom) when is_atom(atom),
    do: key_match?(regex, to_string(atom))
  defp key_match?(regex, string), do: Regex.match?(regex, string)

  defp additionals(%Xema.Map{additional_properties: false}, map) do
    if Map.equal?(map, %{}) do
      :ok
    else
      error(:no_additional_properties_allowed,
            additional_properties: Map.keys(map))
    end
  end
  defp additionals(_schema, _map), do: :ok

  defp dependencies(%Xema.Map{dependencies: nil}, _map), do: :ok
  defp dependencies(%Xema.Map{dependencies: dependencies}, map) do
    dependencies
    |> Map.to_list
    |> Enum.filter(fn {key, _} -> Map.has_key?(map, key) end)
    |> do_dependencies(map)
  end

  defp do_dependencies([], _map), do: :ok
  defp do_dependencies([{key, list}|tail], map) when is_list(list) do
    with :ok <- do_dependencies_list(key, list, map) do
      do_dependencies(tail, map)
    end
  end
  defp do_dependencies([{key, schema}|tail], map) do
    with :ok <- Xema.validate(schema, map) do
      do_dependencies(tail, map)
    else
      {:error, error} ->
        {:error, %{reason: :invalid_dependency, for: key, error: error}}
    end
  end

  defp do_dependencies_list(_key, [], _map), do: :ok
  defp do_dependencies_list(key, [dependency|dependencies], map) do
    if Map.has_key?(map, dependency) do
      do_dependencies_list(key, dependencies, map)
    else
      {:error, %{reason: :missing_dependency, for: key, dependency: dependency}}
    end
  end
end