Fix picking commit as PR head

Fix picking commit as PR head

require Logger

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 poll(pid) do
    send(pid, {:poll, :once})
  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, project_id) do
    case Repo.get(Patch, patch_id) do
      nil -> nil
      %{priority: ^priority} -> nil
      patch ->
        patch.id
        |> Batch.all_for_patch(:incomplete)
        |> Repo.one()
        |> raise_batch_priority(priority)
        patch
        |> Patch.changeset(%{priority: priority})
        |> Repo.update!()
    end

    {:reply, :ok, project_id}
  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], :already_running_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)
        repo_conn = get_repo_conn(project)
        case patch_preflight(repo_conn, patch) do
          :ok ->
            {batch, is_new_batch} = get_new_batch(
              project_id,
              patch.into_branch,
              patch.priority
            )
            %LinkPatchBatch{}
            |> LinkPatchBatch.changeset(%{
              batch_id: batch.id,
              patch_id: patch.id,
              reviewer: reviewer})
            |> Repo.insert!()
            if is_new_batch do
              put_incomplete_on_hold(get_repo_conn(project), batch)
            end
            poll_after_delay(project)
            send_status(repo_conn, batch.id, [patch], :waiting)
          {:error, message} ->
            send_message(repo_conn, [patch], {:preflight, message})
        end
    end
  end

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

  def do_handle_cast({:cancel, patch_id}, _project_id) do
    patch_id
    |> Batch.all_for_patch(:incomplete)
    |> Repo.one()
    |> cancel_patch(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][skip ci][skip netlify]",
          committer: nil})}
    do_merge_patch = fn %{patch: patch}, branch ->
      case branch do
        :conflict -> :conflict
        :canceled -> :canceled
        _ -> GitHub.merge_branch!(
          repo_conn,
          %{
            from: patch.commit,
            to: stmp,
            commit_message: "[ci skip][skip ci][skip netlify] -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(), :second)

    GitHub.delete_branch!(repo_conn, stmp)
    send_status(repo_conn, batch, status)
    batch
    |> Batch.changeset(%{state: status, commit: commit, last_polled: now})
    |> Repo.update!()

          stmp = "#{batch.project.staging_branch}.tmp2"
          GitHub.force_push!(repo_conn, base.commit, stmp)

          new_head = Enum.reduce(patch_links, base.commit, fn patch_link, prev_head ->

            Logger.debug("Patch Link #{inspect(patch_link)}")
            Logger.debug("Patch #{inspect(patch_link.patch)}")

            {:ok, commits} = GitHub.get_pr_commits(repo_conn, patch_link.patch.pr_xref)
            {:ok, pr} = GitHub.get_pr(repo_conn, patch_link.patch.pr_xref)

            {token, _} = repo_conn
             user = GitHub.get_user_by_login!(token, pr.user.login)

            Logger.debug("PR #{inspect(pr)}")
            Logger.debug("User #{inspect(user)}")

            # If a user doesn't have a public email address in their GH profile
            # then get the email from the first commit to the PR
            user_email = if user.email != nil do
              user.email
            else
              Enum.at(commits, 0).author_email
            end

            # The head sha is the final commit in the PR.
            source_sha =  pr.head_sha
            Logger.info("Staging branch #{stmp}")
            Logger.info("Commit sha #{source_sha}")

            # Create a merge commit for each PR
            # because each PR is merged on top of each other in stmp, we can verify against any merge conflicts
            merge_commit = GitHub.merge_branch!(repo_conn,
               %{
               from: source_sha,
               to: stmp,
                 commit_message: "[ci skip][skip ci][skip netlify] -bors-staging-tmp-#{source_sha}"}
            )

            Logger.info("Merge Commit #{inspect(merge_commit)}")

            Logger.info("Previous Head #{inspect(prev_head)}")
            # Then compress the merge commit into tree into a single commit
            # appent it to the previous commit
            # Because the merges are iterative the contain *only* the changes from the PR vs the previous PR(or head)
            cpt = GitHub.create_commit!(
              repo_conn,
              %{
                tree: merge_commit.tree,
                parents: [prev_head],
                commit_message: "#{pr.title} (##{pr.number})\n#{pr.body}",
                committer: %{name: user.login, email: user_email}})

            Logger.info("Commit Sha #{inspect(cpt)}")
              cpt

          end)
          GitHub.delete_branch!(repo_conn, stmp)
          [new_head]
        else
          parents = [base.commit | Enum.map(patch_links, &(&1.patch.commit))]
          parents
        end
        commit_message = Batcher.Message.generate_commit_message(
          patch_links,
          toml.cut_body_after,
          gather_co_authors(batch, patch_links))

        head = if toml.use_squash_merge do
            head = Enum.at(parents, 0)
            GitHub.force_push!(repo_conn, head, batch.project.staging_branch)
            head
        else
          GitHub.synthesize_commit!(
            repo_conn,
            %{
              branch: batch.project.staging_branch,
              tree: tree,
              parents: parents,
              commit_message: commit_message,
              committer: toml.committer})
        end

        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(), :second)
    batch
    |> Batch.changeset(%{timeout_at: now + toml.timeout_sec})
    |> Repo.update!()
  end

  defp poll_running_batch(batch) do
    batch.project
    |> get_repo_conn()
    |> GitHub.get_commit_status!(batch.commit)
    |> Enum.each(fn {identifier, state} ->
      batch.id
      |> Status.get_for_batch(identifier)
      |> Repo.update_all([set: [state: state, identifier: identifier]])
    end)
    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(), :second)
    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

  @spec complete_batch(Status.state, Batch.t, [Status.t]) :: :ok
  defp complete_batch(:ok, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    {res,toml} = case Batcher.GetBorsToml.get(repo_conn, "#{batch.project.staging_branch}") do
      {:error, :fetch_failed} -> Batcher.GetBorsToml.get(repo_conn, "#{batch.project.staging_branch}.tmp")
      {:ok, x} -> {:ok, x}
    end

    {:ok, _} = push_with_retry(
      repo_conn,
      batch.commit,
      batch.into_branch)

    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()

    if toml.use_squash_merge do
      Enum.each(patches, fn patch ->
        pr = GitHub.get_pr!(repo_conn, patch.pr_xref)
        pr = %BorsNG.GitHub.Pr{pr | state: :closed, title: "[Merged by Bors] - #{pr.title}"}
        pr = GitHub.update_pr!(repo_conn, pr)
        GitHub.post_comment!(repo_conn, patch.pr_xref, "# Pull request successfully merged into master.")
      end)
    end

    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

  # A delay has been observed between Bors sending the Status change
  # and GitHub allowing a Status-bearing commit to be pushed to master.
  # As a workaround, retry with exponential backoff.
  # This should retry *nine times*, by the way.
  defp push_with_retry(repo_conn, commit, into_branch, timeout \\ 1) do
    Process.sleep(timeout)
    result = GitHub.push(
      repo_conn,
      commit,
      into_branch)
    case result do
      {:ok, _} -> result
      _ when timeout >= 512 -> result
      _ -> push_with_retry(repo_conn, commit, into_branch, timeout * 2)
    end
  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
    if Patch.ci_skip?(patch) do
      {:error, :ci_skip}
    else
      toml = Batcher.GetBorsToml.get(
        repo_conn,
        patch.commit)
      patch_preflight(repo_conn, patch, toml)
    end
  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)
    |> reviews_status(toml)
    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

  @spec reviews_status(map, Batcher.BorsToml.t) :: :sufficient | :failed | :insufficient
  defp reviews_status(reviews, toml) do
    %{"CHANGES_REQUESTED" => failed, "APPROVED" => approvals} = reviews
    review_required? = is_integer(toml.required_approvals)
    approvals_needed = review_required? && toml.required_approvals || 0
    approved? = approvals >= approvals_needed
    failed? = failed > 0
    cond do
      # NOTE: A way to disable the code reviewing behaviour was requested on #587.
      #   As such, we only apply the reviewing rules if, on bors, the config
      #   `required_approvals` is present and an integer.
      not review_required? ->
        :sufficient
      failed? ->
        :failed
      approved? ->
        :sufficient
      review_required? ->
        :insufficient
    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 raise_batch_priority(%Batch{priority: old_priority} = batch, priority) when old_priority < priority do
    project = Repo.get!(Project, batch.project_id)
    batch = batch
    |> Batch.changeset_raise_priority(%{priority: priority})
    |> Repo.update!()
    put_incomplete_on_hold(get_repo_conn(project), batch)
  end
  defp raise_batch_priority(_, _) do
    :ok
  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 = batch.project_id
    |> Batch.all_for_project(:running)
    |> where([b], b.id != ^batch.id and b.priority < ^batch.priority)
    |> Repo.all()

    ids = Enum.map(batches, &(&1.id))

    Status
    |> where([s], s.batch_id in ^ids)
    |> Repo.delete_all()

    Enum.each(batches, &send_status(repo_conn, &1, :delayed))
    Batch
    |> where([b], b.id in ^ids)
    |> Repo.update_all(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

require Logger

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 poll(pid) do
    send(pid, {:poll, :once})
  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, project_id) do
    case Repo.get(Patch, patch_id) do
      nil -> nil
      %{priority: ^priority} -> nil
      patch ->
        patch.id
        |> Batch.all_for_patch(:incomplete)
        |> Repo.one()
        |> raise_batch_priority(priority)
        patch
        |> Patch.changeset(%{priority: priority})
        |> Repo.update!()
    end

    {:reply, :ok, project_id}
  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], :already_running_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)
        repo_conn = get_repo_conn(project)
        case patch_preflight(repo_conn, patch) do
          :ok ->
            {batch, is_new_batch} = get_new_batch(
              project_id,
              patch.into_branch,
              patch.priority
            )
            %LinkPatchBatch{}
            |> LinkPatchBatch.changeset(%{
              batch_id: batch.id,
              patch_id: patch.id,
              reviewer: reviewer})
            |> Repo.insert!()
            if is_new_batch do
              put_incomplete_on_hold(get_repo_conn(project), batch)
            end
            poll_after_delay(project)
            send_status(repo_conn, batch.id, [patch], :waiting)
          {:error, message} ->
            send_message(repo_conn, [patch], {:preflight, message})
        end
    end
  end

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

  def do_handle_cast({:cancel, patch_id}, _project_id) do
    patch_id
    |> Batch.all_for_patch(:incomplete)
    |> Repo.one()
    |> cancel_patch(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][skip ci][skip netlify]",
          committer: nil})}
    do_merge_patch = fn %{patch: patch}, branch ->
      case branch do
        :conflict -> :conflict
        :canceled -> :canceled
        _ -> GitHub.merge_branch!(
          repo_conn,
          %{
            from: patch.commit,
            to: stmp,
            commit_message: "[ci skip][skip ci][skip netlify] -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(), :second)

    GitHub.delete_branch!(repo_conn, stmp)
    send_status(repo_conn, batch, status)
    batch
    |> Batch.changeset(%{state: status, commit: commit, last_polled: now})
    |> Repo.update!()

          stmp = "#{batch.project.staging_branch}.tmp2"
          GitHub.force_push!(repo_conn, base.commit, stmp)

          new_head = Enum.reduce(patch_links, base.commit, fn patch_link, prev_head ->

            Logger.debug("Patch Link #{inspect(patch_link)}")
            Logger.debug("Patch #{inspect(patch_link.patch)}")

            {:ok, commits} = GitHub.get_pr_commits(repo_conn, patch_link.patch.pr_xref)
            {:ok, pr} = GitHub.get_pr(repo_conn, patch_link.patch.pr_xref)

            {token, _} = repo_conn
             user = GitHub.get_user_by_login!(token, pr.user.login)

            Logger.debug("PR #{inspect(pr)}")
            Logger.debug("User #{inspect(user)}")

            # If a user doesn't have a public email address in their GH profile
            # then get the email from the first commit to the PR
            user_email = if user.email != nil do
              user.email
            else
              Enum.at(commits, 0).author_email
            end

            source_sha = Enum.at(commits, length(commits)-1).sha
            Logger.info("Staging branch #{stmp}")
            Logger.info("Commit sha #{source_sha}")

            # Create a merge commit for each PR
            # because each PR is merged on top of each other in stmp, we can verify against any merge conflicts
            merge_commit = GitHub.merge_branch!(repo_conn,
               %{
               from: Enum.at(commits, length(commits)-1).sha,
               to: stmp,
                 commit_message: "[ci skip][skip ci][skip netlify] -bors-staging-tmp-#{source_sha}"}
            )

            Logger.info("Merge Commit #{inspect(merge_commit)}")

            Logger.info("Previous Head #{inspect(prev_head)}")
            # Then compress the merge commit into tree into a single commit
            # appent it to the previous commit
            # Because the merges are iterative the contain *only* the changes from the PR vs the previous PR(or head)
            cpt = GitHub.create_commit!(
              repo_conn,
              %{
                tree: merge_commit.tree,
                parents: [prev_head],
                commit_message: "#{pr.title} (##{pr.number})\n#{pr.body}",
                committer: %{name: user.login, email: user_email}})

            Logger.info("Commit Sha #{inspect(cpt)}")
              cpt

          end)
          GitHub.delete_branch!(repo_conn, stmp)
          [new_head]
        else
          parents = [base.commit | Enum.map(patch_links, &(&1.patch.commit))]
          parents
        end
        commit_message = Batcher.Message.generate_commit_message(
          patch_links,
          toml.cut_body_after,
          gather_co_authors(batch, patch_links))

        head = if toml.use_squash_merge do
                                        head = Enum.at(parents, 0)
            GitHub.force_push!(repo_conn, head, batch.project.staging_branch)
            head
        else
          GitHub.synthesize_commit!(
            repo_conn,
            %{
              branch: batch.project.staging_branch,
              tree: tree,
              parents: parents,
              commit_message: commit_message,
              committer: toml.committer})
        end

        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(), :second)
    batch
    |> Batch.changeset(%{timeout_at: now + toml.timeout_sec})
    |> Repo.update!()
  end

  defp poll_running_batch(batch) do
    batch.project
    |> get_repo_conn()
    |> GitHub.get_commit_status!(batch.commit)
    |> Enum.each(fn {identifier, state} ->
      batch.id
      |> Status.get_for_batch(identifier)
      |> Repo.update_all([set: [state: state, identifier: identifier]])
    end)
    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(), :second)
    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

  @spec complete_batch(Status.state, Batch.t, [Status.t]) :: :ok
  defp complete_batch(:ok, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    {res,toml} = case Batcher.GetBorsToml.get(repo_conn, "#{batch.project.staging_branch}") do
      {:error, :fetch_failed} -> Batcher.GetBorsToml.get(repo_conn, "#{batch.project.staging_branch}.tmp")
      {:ok, x} -> {:ok, x}
    end

    {:ok, _} = push_with_retry(
      repo_conn,
      batch.commit,
      batch.into_branch)

    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()

    if toml.use_squash_merge do
      Enum.each(patches, fn patch ->
        pr = GitHub.get_pr!(repo_conn, patch.pr_xref)
        pr = %BorsNG.GitHub.Pr{pr | state: :closed, title: "[Merged by Bors] - #{pr.title}"}
        pr = GitHub.update_pr!(repo_conn, pr)
        GitHub.post_comment!(repo_conn, patch.pr_xref, "# Pull request successfully merged into master.")
      end)
    end

    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

  # A delay has been observed between Bors sending the Status change
  # and GitHub allowing a Status-bearing commit to be pushed to master.
  # As a workaround, retry with exponential backoff.
  # This should retry *nine times*, by the way.
  defp push_with_retry(repo_conn, commit, into_branch, timeout \\ 1) do
    Process.sleep(timeout)
    result = GitHub.push(
      repo_conn,
      commit,
      into_branch)
    case result do
      {:ok, _} -> result
      _ when timeout >= 512 -> result
      _ -> push_with_retry(repo_conn, commit, into_branch, timeout * 2)
    end
  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
    if Patch.ci_skip?(patch) do
      {:error, :ci_skip}
    else
      toml = Batcher.GetBorsToml.get(
        repo_conn,
        patch.commit)
      patch_preflight(repo_conn, patch, toml)
    end
  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)
    |> reviews_status(toml)
    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

  @spec reviews_status(map, Batcher.BorsToml.t) :: :sufficient | :failed | :insufficient
  defp reviews_status(reviews, toml) do
    %{"CHANGES_REQUESTED" => failed, "APPROVED" => approvals} = reviews
    review_required? = is_integer(toml.required_approvals)
    approvals_needed = review_required? && toml.required_approvals || 0
    approved? = approvals >= approvals_needed
    failed? = failed > 0
    cond do
      # NOTE: A way to disable the code reviewing behaviour was requested on #587.
      #   As such, we only apply the reviewing rules if, on bors, the config
      #   `required_approvals` is present and an integer.
      not review_required? ->
        :sufficient
      failed? ->
        :failed
      approved? ->
        :sufficient
      review_required? ->
        :insufficient
    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 raise_batch_priority(%Batch{priority: old_priority} = batch, priority) when old_priority < priority do
    project = Repo.get!(Project, batch.project_id)
    batch = batch
    |> Batch.changeset_raise_priority(%{priority: priority})
    |> Repo.update!()
    put_incomplete_on_hold(get_repo_conn(project), batch)
  end
  defp raise_batch_priority(_, _) do
    :ok
  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 = batch.project_id
    |> Batch.all_for_project(:running)
    |> where([b], b.id != ^batch.id and b.priority < ^batch.priority)
    |> Repo.all()

    ids = Enum.map(batches, &(&1.id))

    Status
    |> where([s], s.batch_id in ^ids)
    |> Repo.delete_all()

    Enum.each(batches, &send_status(repo_conn, &1, :delayed))
    Batch
    |> where([b], b.id in ^ids)
    |> Repo.update_all(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


remove miner tx from total count

remove miner tx from total count

defmodule Neoscan.Transactions do

  @moduledoc false

  @moduledoc """
  The boundary for the Transactions system.
  """

  import Ecto.Query, warn: false
  alias Neoscan.Repo
  alias NeoscanMonitor.Api
  alias Neoscan.Transactions.Transaction
  alias Neoscan.Vouts.Vout
  alias Neoscan.ChainAssets
  alias Neoscan.Addresses
  alias Neoscan.Vouts

  require Logger

  @doc """
  Returns the list of transactions.

  ## Examples

      iex> list_transactions()
      [%Transaction{}, ...]

  """
  def list_transactions do
    Repo.all(Transaction)
  end

  @doc """
  Count total transactions in DB.

  ## Examples

      iex> count_transactions()
      50

  """
  def count_transactions do
    query = from t in Transaction,
            where: t.type != "MinerTransaction"

    Repo.aggregate(query, :count, :id)
  end

  @doc """
  Count total transactions in DB for an especific asset.

  ## Examples

      iex> count_transactions_for_asset(asset_hash)
      20

  """
  def count_transactions_for_asset(asset_hash) do
    query = from t in Transaction,
            where: t.asset_moved == ^asset_hash

    Repo.aggregate(query, :count, :id)
  end

  @doc """
  Returns the list of transactions in the home page.

  ## Examples

      iex> home_transactions()
      [%Transaction{}, ...]

  """
  def home_transactions do
    transaction_query = from e in Transaction,
                             order_by: [
                               desc: e.inserted_at
                             ],
                             where: e.inserted_at > ago(
                               1,
                               "hour"
                             ) and e.type != "MinerTransaction",
                             select: %{
                               :id => e.id,
                               :type => e.type,
                               :time => e.time,
                               :txid => e.txid,
                               :block_height => e.block_height,
                               :block_hash => e.block_hash,
                               :vin => e.vin,
                               :claims => e.claims,
                               :sys_fee => e.sys_fee,
                               :net_fee => e.net_fee,
                               :size => e.size,
                             },
                             limit: 15

    Repo.all(transaction_query)
  end

  @doc """
  Returns the list of the last transactions for an asset.

  ## Examples

      iex> home_transactions()
      [%Transaction{}, ...]

  """
  def get_last_transactions_for_asset(hash) do
    transaction_query = from e in Transaction,
                             order_by: [
                               desc: e.inserted_at
                             ],
                             where: e.asset_moved == ^hash and e.type != "MinerTransaction",
                             select: %{
                               :id => e.id,
                               :type => e.type,
                               :time => e.time,
                               :txid => e.txid,
                               :block_height => e.block_height,
                               :block_hash => e.block_hash,
                               :vin => e.vin,
                               :claims => e.claims,
                               :sys_fee => e.sys_fee,
                               :net_fee => e.net_fee,
                               :size => e.size,
                             },
                             limit: 5

    transactions = Repo.all(transaction_query)

    vouts = Enum.map(transactions, fn tx -> tx.id end)
              |> get_transactions_vouts

    transactions
    |> Enum.map(fn tx ->
         Map.put(tx, :vouts, Enum.filter(vouts, fn vout ->
           vout.transaction_id == tx.id
         end))
       end)
  end

  @doc """
  Returns the list of paginated transactions.

  ## Examples

      iex> paginate_transactions(page)
      [%Transaction{}, ...]

  """
  def paginate_transactions(pag) do
    transaction_query = from e in Transaction,
                        order_by: [
                          desc: e.block_height
                        ],
                        where: e.type != "MinerTransaction",
                        select: %{
                         :id => e.id,
                         :type => e.type,
                         :time => e.time,
                         :txid => e.txid,
                         :block_height => e.block_height,
                         :block_hash => e.block_hash,
                         :vin => e.vin,
                         :claims => e.claims,
                         :sys_fee => e.sys_fee,
                         :net_fee => e.net_fee,
                         :size => e.size,
                        }

    transactions = Repo.paginate(transaction_query, page: pag, page_size: 15)
    vouts = Enum.map(transactions, fn tx -> tx.id end)
              |> get_transactions_vouts

    transactions
    |> Enum.map(fn tx ->
         Map.put(tx, :vouts, Enum.filter(vouts, fn vout ->
           vout.transaction_id == tx.id
         end))
       end)
  end

  @doc """
  Returns the list of paginated transactions.

  ## Examples

      iex> paginate_transactions(page)
      [%Transaction{}, ...]

  """
  def paginate_transactions_for_block(hash, pag) do
    transaction_query = from e in Transaction,
                        order_by: [
                          desc: e.block_height
                        ],
                        where: e.block_hash == ^hash,
                        select: %{
                         :id => e.id,
                         :type => e.type,
                         :time => e.time,
                         :txid => e.txid,
                         :block_height => e.block_height,
                         :block_hash => e.block_hash,
                         :vin => e.vin,
                         :claims => e.claims,
                         :sys_fee => e.sys_fee,
                         :net_fee => e.net_fee,
                         :size => e.size,
                        }

    transactions = Repo.paginate(transaction_query, page: pag, page_size: 15)
    vouts = Enum.map(transactions, fn tx -> tx.id end)
              |> get_transactions_vouts

    transactions
    |> Enum.map(fn tx ->
         Map.put(tx, :vouts, Enum.filter(vouts, fn vout ->
           vout.transaction_id == tx.id
         end))
       end)
  end

  @doc """
  Returns the list of vouts for a transaction.

  ## Examples

      iex> get_transaction_vouts(id)
      [%Vout{}, ...]

  """
  def get_transaction_vouts(id) do
    vout_query = from e in Vout,
                             order_by: [
                               asc: e.n
                             ],
                             where: e.transaction_id == ^id,
                             select: %{
                               :asset => e.asset,
                               :address_hash => e.address_hash,
                               :value => e.value,
                             }

    Repo.all(vout_query)
  end

  @doc """
  Returns the list of vouts for many transaction.

  ## Examples

      iex> get_transaction_vouts(id)
      [%Vout{}, ...]

  """
  def get_transactions_vouts(id_list) do
    vout_query = from e in Vout,
                             order_by: [
                               asc: e.n
                             ],
                             where: e.transaction_id in ^id_list,
                             select: %{
                               :transaction_id => e.transaction_id,
                               :asset => e.asset,
                               :address_hash => e.address_hash,
                               :value => e.value,
                             }

    Repo.all(vout_query)
  end

  @doc """
  Returns the list of contract transactions.

  ## Examples

      iex> list_contracts()
      [%Transaction{}, ...]

  """
  def list_contracts do
    transaction_query = from e in Transaction,
                             order_by: [
                               desc: e.inserted_at
                             ],
                             where: e.type == "PublishTransaction"
                                    or e.type == "InvocationTransaction",
                             select: %{
                               :type => e.type,
                               :time => e.time,
                               :txid => e.txid
                             }

    Repo.all(transaction_query)
  end

  @doc """
  Gets a single transaction.

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

  ## Examples

      iex> get_transaction!(123)
      %Transaction{}

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

  """
  def get_transaction!(id), do: Repo.get!(Transaction, id)

  @doc """
  Gets a single transaction by its hash value

  ## Examples

      iex> get_block_by_hash(123)
      %Block{}

      iex> get_block_by_hash(456)
      nil

  """
  def get_transaction_by_hash(hash) do
    query = from e in Transaction,
                 where: e.txid == ^hash

    Repo.all(query)
    |> List.first
  end

  @doc """
  Gets a single transaction by its hash and send it as a map

  ## Examples

      iex> get_block_by_hash_for_view(123)
      %{}

      iex> get_block_by_hash_for_view(456)
      nil

  """
  def get_transaction_by_hash_for_view(hash) do
    vout_query = from v in Vout,
                      select: %{
                        asset: v.asset,
                        address_hash: v.address_hash,
                        value: v.value
                      }
    query = from e in Transaction,
                 where: e.txid == ^hash,
                 preload: [
                   vouts: ^vout_query
                 ],
                 select: e

    Repo.all(query)
    |> List.first
  end

  @doc """
  Creates a transaction.

  ## Examples

      iex> create_transaction(%{field: value})
      {:ok, %Transaction{}}

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

  """
  def create_transaction(
        %{:time => time, :hash => hash, :index => height} = block,
        %{"vout" => vouts, "vin" => vin, "txid" => txid, "type" => type} = attrs
      ) do

    #get inputs from db
    new_vin = get_vins(vin, height)

    #get claims from db
    new_claim = get_claims(attrs["claims"])

    #fetch all addresses involved in the transaction
    address_list = Task.async(
      fn -> Addresses.get_transaction_addresses(new_vin, vouts, time, attrs["asset"])
            |> Addresses.update_all_addresses(
                 new_vin,
                 new_claim,
                 vouts,
                 String.slice(to_string(txid), -64..-1),
                 height,
                 time
               )
      end
    ) #updates addresses with vin and claims,
      # vouts are just for record in claims,
      # the balance is updated in the insert vout function called in create_vout

    #create asset if register Transaction
    ChainAssets.create(
      attrs["asset"],
      String.slice(to_string(txid), -64..-1),
      time
    )

    #create asset if issue Transaction
    ChainAssets.issue(type, vouts)

    #prepare and create transaction
    transaction = Map.merge(
                    attrs,
                    %{
                      "txid" => String.slice(to_string(txid), -64..-1),
                      "time" => time,
                      "vin" => new_vin,
                      "claims" => new_claim,
                      "block_hash" => hash,
                      "block_height" => height,
                    }
                  )
                  |> set_transaction_asset(vouts)
                  |> Map.delete("vout")

    Transaction.changeset_with_block(block, transaction)
    |> Repo.insert!()
    |> update_transaction_state(vouts)
    |> Vouts.create_vouts(vouts, Task.await(address_list, 60_000))
  end

  #set transaction asset if it has vouts
  def set_transaction_asset(attrs, []) do
    attrs
  end
  def set_transaction_asset(attrs, vouts) do
    vout = List.first(vouts)
    Map.put(attrs, "asset_moved", String.slice(to_string(vout["asset"]), -64..-1))
  end

  #add transaction to monitor cache
  def update_transaction_state(%{:type => type} = transaction, vouts)
      when type != "MinerTransaction" do
    Api.add_transaction(transaction, vouts)
    transaction
  end
  def update_transaction_state(%{:type => type} = transaction, vouts)
      when type == "PublishTransaction" or type == "InvocationTransaction" do
    Api.add_transaction(transaction, vouts)
    Api.add_contract(transaction, vouts)
    transaction
  end
  def update_transaction_state(transaction, _vouts) do
    transaction
  end

  #get vins and add to addresses
  defp get_vins([] = vin, _height) do
    vin
  end
  defp get_vins(vin, height) do
    lookups = Enum.map(
      vin,
      &"#{String.slice(to_string(&1["txid"]), -64..-1)}#{&1["vout"]}"
    ) #sometimes "0x" is prepended to hashes

    query = from e in Vout,
                 where: e.query in ^lookups,
                 select: struct(
                   e,
                   [:asset, :address_hash, :n, :value, :txid, :query, :id]
                 )

    Repo.all(query)
    |> Vouts.verify_vouts(lookups, vin)
    |> Vouts.end_vouts_and_return(height)
  end

  #get claimed vouts and add to addresses
  defp get_claims(nil = claims) do
    claims
  end
  defp get_claims(claims) do

    lookups = Enum.map(
      claims,
      &"#{String.slice(to_string(&1["txid"]), -64..-1)}#{&1["vout"]}"
    ) #sometimes "0x" is prepended to hashes

    query = from e in Vout,
                 where: e.query in ^lookups,
                 select: struct(
                   e,
                   [:asset, :address_hash, :n, :value, :txid, :query, :id]
                 )

    Repo.all(query)
    |> Vouts.verify_vouts(lookups, claims)
    |> Vouts.set_claimed_and_return()
  end

  @doc """
  Updates a transaction.

  ## Examples

      iex> update_transaction(transaction, %{field: new_value})
      {:ok, %Transaction{}}

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

  """
  def update_transaction(%Transaction{} = transaction, attrs) do
    transaction
    |> Transaction.update_changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Transaction.

  ## Examples

      iex> delete_transaction(transaction)
      {:ok, %Transaction{}}

      iex> delete_transaction(transaction)
      {:error, %Ecto.Changeset{}}

  """
  def delete_transaction(%Transaction{} = transaction) do
    Repo.delete(transaction)
  end

  @doc """
  Creates many transactions.

  ## Examples

      iex> create_transactions([%{field: value}, ...])
      {:ok, "Created"}

      iex> create_transactions([%{field: value}, ...])
      {:error, %Ecto.Changeset{}}

  """
  def create_transactions(block, transactions) do
    case Enum.each(
           transactions,
           fn transaction -> create_transaction(block, transaction) end
         ) do
      :ok ->
        {:ok, "Created"}
      _ ->
        {:error, "failed to create transactions"}
    end
  end

end

defmodule Neoscan.Transactions do

  @moduledoc false

  @moduledoc """
  The boundary for the Transactions system.
  """

  import Ecto.Query, warn: false
  alias Neoscan.Repo
  alias NeoscanMonitor.Api
  alias Neoscan.Transactions.Transaction
  alias Neoscan.Vouts.Vout
  alias Neoscan.ChainAssets
  alias Neoscan.Addresses
  alias Neoscan.Vouts

  require Logger

  @doc """
  Returns the list of transactions.

  ## Examples

      iex> list_transactions()
      [%Transaction{}, ...]

  """
  def list_transactions do
    Repo.all(Transaction)
  end

  @doc """
  Count total transactions in DB.

  ## Examples

      iex> count_transactions()
      50

  """
  def count_transactions do
    Repo.aggregate(Transaction, :count, :id)
  end

  @doc """
  Count total transactions in DB for an especific asset.

  ## Examples

      iex> count_transactions_for_asset(asset_hash)
      20

  """
  def count_transactions_for_asset(asset_hash) do
    query = from t in Transaction,
            where: t.asset_moved == ^asset_hash

    Repo.aggregate(query, :count, :id)
  end

  @doc """
  Returns the list of transactions in the home page.

  ## Examples

      iex> home_transactions()
      [%Transaction{}, ...]

  """
  def home_transactions do
    transaction_query = from e in Transaction,
                             order_by: [
                               desc: e.inserted_at
                             ],
                             where: e.inserted_at > ago(
                               1,
                               "hour"
                             ) and e.type != "MinerTransaction",
                             select: %{
                               :id => e.id,
                               :type => e.type,
                               :time => e.time,
                               :txid => e.txid,
                               :block_height => e.block_height,
                               :block_hash => e.block_hash,
                               :vin => e.vin,
                               :claims => e.claims,
                               :sys_fee => e.sys_fee,
                               :net_fee => e.net_fee,
                               :size => e.size,
                             },
                             limit: 15

    Repo.all(transaction_query)
  end

  @doc """
  Returns the list of the last transactions for an asset.

  ## Examples

      iex> home_transactions()
      [%Transaction{}, ...]

  """
  def get_last_transactions_for_asset(hash) do
    transaction_query = from e in Transaction,
                             order_by: [
                               desc: e.inserted_at
                             ],
                             where: e.asset_moved == ^hash and e.type != "MinerTransaction",
                             select: %{
                               :id => e.id,
                               :type => e.type,
                               :time => e.time,
                               :txid => e.txid,
                               :block_height => e.block_height,
                               :block_hash => e.block_hash,
                               :vin => e.vin,
                               :claims => e.claims,
                               :sys_fee => e.sys_fee,
                               :net_fee => e.net_fee,
                               :size => e.size,
                             },
                             limit: 5

    transactions = Repo.all(transaction_query)

    vouts = Enum.map(transactions, fn tx -> tx.id end)
              |> get_transactions_vouts

    transactions
    |> Enum.map(fn tx ->
         Map.put(tx, :vouts, Enum.filter(vouts, fn vout ->
           vout.transaction_id == tx.id
         end))
       end)
  end

  @doc """
  Returns the list of paginated transactions.

  ## Examples

      iex> paginate_transactions(page)
      [%Transaction{}, ...]

  """
  def paginate_transactions(pag) do
    transaction_query = from e in Transaction,
                        order_by: [
                          desc: e.block_height
                        ],
                        where: e.type != "MinerTransaction",
                        select: %{
                         :id => e.id,
                         :type => e.type,
                         :time => e.time,
                         :txid => e.txid,
                         :block_height => e.block_height,
                         :block_hash => e.block_hash,
                         :vin => e.vin,
                         :claims => e.claims,
                         :sys_fee => e.sys_fee,
                         :net_fee => e.net_fee,
                         :size => e.size,
                        }

    transactions = Repo.paginate(transaction_query, page: pag, page_size: 15)
    vouts = Enum.map(transactions, fn tx -> tx.id end)
              |> get_transactions_vouts

    transactions
    |> Enum.map(fn tx ->
         Map.put(tx, :vouts, Enum.filter(vouts, fn vout ->
           vout.transaction_id == tx.id
         end))
       end)
  end

  @doc """
  Returns the list of paginated transactions.

  ## Examples

      iex> paginate_transactions(page)
      [%Transaction{}, ...]

  """
  def paginate_transactions_for_block(hash, pag) do
    transaction_query = from e in Transaction,
                        order_by: [
                          desc: e.block_height
                        ],
                        where: e.block_hash == ^hash,
                        select: %{
                         :id => e.id,
                         :type => e.type,
                         :time => e.time,
                         :txid => e.txid,
                         :block_height => e.block_height,
                         :block_hash => e.block_hash,
                         :vin => e.vin,
                         :claims => e.claims,
                         :sys_fee => e.sys_fee,
                         :net_fee => e.net_fee,
                         :size => e.size,
                        }

    transactions = Repo.paginate(transaction_query, page: pag, page_size: 15)
    vouts = Enum.map(transactions, fn tx -> tx.id end)
              |> get_transactions_vouts

    transactions
    |> Enum.map(fn tx ->
         Map.put(tx, :vouts, Enum.filter(vouts, fn vout ->
           vout.transaction_id == tx.id
         end))
       end)
  end

  @doc """
  Returns the list of vouts for a transaction.

  ## Examples

      iex> get_transaction_vouts(id)
      [%Vout{}, ...]

  """
  def get_transaction_vouts(id) do
    vout_query = from e in Vout,
                             order_by: [
                               asc: e.n
                             ],
                             where: e.transaction_id == ^id,
                             select: %{
                               :asset => e.asset,
                               :address_hash => e.address_hash,
                               :value => e.value,
                             }

    Repo.all(vout_query)
  end

  @doc """
  Returns the list of vouts for many transaction.

  ## Examples

      iex> get_transaction_vouts(id)
      [%Vout{}, ...]

  """
  def get_transactions_vouts(id_list) do
    vout_query = from e in Vout,
                             order_by: [
                               asc: e.n
                             ],
                             where: e.transaction_id in ^id_list,
                             select: %{
                               :transaction_id => e.transaction_id,
                               :asset => e.asset,
                               :address_hash => e.address_hash,
                               :value => e.value,
                             }

    Repo.all(vout_query)
  end

  @doc """
  Returns the list of contract transactions.

  ## Examples

      iex> list_contracts()
      [%Transaction{}, ...]

  """
  def list_contracts do
    transaction_query = from e in Transaction,
                             order_by: [
                               desc: e.inserted_at
                             ],
                             where: e.type == "PublishTransaction"
                                    or e.type == "InvocationTransaction",
                             select: %{
                               :type => e.type,
                               :time => e.time,
                               :txid => e.txid
                             }

    Repo.all(transaction_query)
  end

  @doc """
  Gets a single transaction.

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

  ## Examples

      iex> get_transaction!(123)
      %Transaction{}

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

  """
  def get_transaction!(id), do: Repo.get!(Transaction, id)

  @doc """
  Gets a single transaction by its hash value

  ## Examples

      iex> get_block_by_hash(123)
      %Block{}

      iex> get_block_by_hash(456)
      nil

  """
  def get_transaction_by_hash(hash) do
    query = from e in Transaction,
                 where: e.txid == ^hash

    Repo.all(query)
    |> List.first
  end

  @doc """
  Gets a single transaction by its hash and send it as a map

  ## Examples

      iex> get_block_by_hash_for_view(123)
      %{}

      iex> get_block_by_hash_for_view(456)
      nil

  """
  def get_transaction_by_hash_for_view(hash) do
    vout_query = from v in Vout,
                      select: %{
                        asset: v.asset,
                        address_hash: v.address_hash,
                        value: v.value
                      }
    query = from e in Transaction,
                 where: e.txid == ^hash,
                 preload: [
                   vouts: ^vout_query
                 ],
                 select: e

    Repo.all(query)
    |> List.first
  end

  @doc """
  Creates a transaction.

  ## Examples

      iex> create_transaction(%{field: value})
      {:ok, %Transaction{}}

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

  """
  def create_transaction(
        %{:time => time, :hash => hash, :index => height} = block,
        %{"vout" => vouts, "vin" => vin, "txid" => txid, "type" => type} = attrs
      ) do

    #get inputs from db
    new_vin = get_vins(vin, height)

    #get claims from db
    new_claim = get_claims(attrs["claims"])

    #fetch all addresses involved in the transaction
    address_list = Task.async(
      fn -> Addresses.get_transaction_addresses(new_vin, vouts, time, attrs["asset"])
            |> Addresses.update_all_addresses(
                 new_vin,
                 new_claim,
                 vouts,
                 String.slice(to_string(txid), -64..-1),
                 height,
                 time
               )
      end
    ) #updates addresses with vin and claims,
      # vouts are just for record in claims,
      # the balance is updated in the insert vout function called in create_vout

    #create asset if register Transaction
    ChainAssets.create(
      attrs["asset"],
      String.slice(to_string(txid), -64..-1),
      time
    )

    #create asset if issue Transaction
    ChainAssets.issue(type, vouts)

    #prepare and create transaction
    transaction = Map.merge(
                    attrs,
                    %{
                      "txid" => String.slice(to_string(txid), -64..-1),
                      "time" => time,
                      "vin" => new_vin,
                      "claims" => new_claim,
                      "block_hash" => hash,
                      "block_height" => height,
                    }
                  )
                  |> set_transaction_asset(vouts)
                  |> Map.delete("vout")

    Transaction.changeset_with_block(block, transaction)
    |> Repo.insert!()
    |> update_transaction_state(vouts)
    |> Vouts.create_vouts(vouts, Task.await(address_list, 60_000))
  end

  #set transaction asset if it has vouts
  def set_transaction_asset(attrs, []) do
    attrs
  end
  def set_transaction_asset(attrs, vouts) do
    vout = List.first(vouts)
    Map.put(attrs, "asset_moved", String.slice(to_string(vout["asset"]), -64..-1))
  end

  #add transaction to monitor cache
  def update_transaction_state(%{:type => type} = transaction, vouts)
      when type != "MinerTransaction" do
    Api.add_transaction(transaction, vouts)
    transaction
  end
  def update_transaction_state(%{:type => type} = transaction, vouts)
      when type == "PublishTransaction" or type == "InvocationTransaction" do
    Api.add_transaction(transaction, vouts)
    Api.add_contract(transaction, vouts)
    transaction
  end
  def update_transaction_state(transaction, _vouts) do
    transaction
  end

  #get vins and add to addresses
  defp get_vins([] = vin, _height) do
    vin
  end
  defp get_vins(vin, height) do
    lookups = Enum.map(
      vin,
      &"#{String.slice(to_string(&1["txid"]), -64..-1)}#{&1["vout"]}"
    ) #sometimes "0x" is prepended to hashes

    query = from e in Vout,
                 where: e.query in ^lookups,
                 select: struct(
                   e,
                   [:asset, :address_hash, :n, :value, :txid, :query, :id]
                 )

    Repo.all(query)
    |> Vouts.verify_vouts(lookups, vin)
    |> Vouts.end_vouts_and_return(height)
  end

  #get claimed vouts and add to addresses
  defp get_claims(nil = claims) do
    claims
  end
  defp get_claims(claims) do

    lookups = Enum.map(
      claims,
      &"#{String.slice(to_string(&1["txid"]), -64..-1)}#{&1["vout"]}"
    ) #sometimes "0x" is prepended to hashes

    query = from e in Vout,
                 where: e.query in ^lookups,
                 select: struct(
                   e,
                   [:asset, :address_hash, :n, :value, :txid, :query, :id]
                 )

    Repo.all(query)
    |> Vouts.verify_vouts(lookups, claims)
    |> Vouts.set_claimed_and_return()
  end

  @doc """
  Updates a transaction.

  ## Examples

      iex> update_transaction(transaction, %{field: new_value})
      {:ok, %Transaction{}}

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

  """
  def update_transaction(%Transaction{} = transaction, attrs) do
    transaction
    |> Transaction.update_changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Transaction.

  ## Examples

      iex> delete_transaction(transaction)
      {:ok, %Transaction{}}

      iex> delete_transaction(transaction)
      {:error, %Ecto.Changeset{}}

  """
  def delete_transaction(%Transaction{} = transaction) do
    Repo.delete(transaction)
  end

  @doc """
  Creates many transactions.

  ## Examples

      iex> create_transactions([%{field: value}, ...])
      {:ok, "Created"}

      iex> create_transactions([%{field: value}, ...])
      {:error, %Ecto.Changeset{}}

  """
  def create_transactions(block, transactions) do
    case Enum.each(
           transactions,
           fn transaction -> create_transaction(block, transaction) end
         ) do
      :ok ->
        {:ok, "Created"}
      _ ->
        {:error, "failed to create transactions"}
    end
  end

end


Fix up

Fix up

defmodule Phoenix.Tranports.LongPollSerializerTest do
  use ExUnit.Case, async: true

  alias Phoenix.Transports.LongPollSerializer
  alias Phoenix.Socket.{Message, Broadcast, Reply}

  @msg_json [123, [[34, ["topic"], 34], 58, [34, ["t"], 34], 44, [34, ["ref"], 34], 58, "null", 44, [34, ["payload"], 34], 58, [34, ["m"], 34], 44, [34, ["event"], 34], 58, [34, ["e"], 34]], 125]

  test "fastlane!/1 translates `Phoenix.Socket.Broadcast` into 'Phoenix.Socket.Message'" do
    broadcast = %Broadcast{topic: "t", event: "e", payload: "m"}
    assert LongPollSerializer.fastlane!(broadcast) ==
      %Message{topic: broadcast.topic, event: broadcast.event, payload: broadcast.payload}
  end

  test "encode!/1 encodes `Phoenix.Socket.Message` as JSON" do
    reply = %Reply{topic: "t", payload: "m", ref: "foo", status: "bar"}
    assert LongPollSerializer.encode!(reply) ==
      %Message{topic: reply.topic, event: "phx_reply", ref: reply.ref, payload: %{status: reply.status, response: reply.payload}}
  end

  test "decode!/2 decodes `Phoenix.Socket.Message` from JSON" do
    assert %Message{topic: "t", event: "e", payload: "m"} ==
      LongPollSerializer.decode!(@msg_json, opcode: :text)
  end
end

defmodule Phoenix.Tranports.LongPollSerializerTest do
  use ExUnit.Case, async: true

  alias Phoenix.Transports.LongPollSerializer
  alias Phoenix.Socket.Message
  alias Phoenix.Socket.Broadcast
  alias Phoenix.Socket.Reply

  @msg_json [123, [[34, ["topic"], 34], 58, [34, ["t"], 34], 44, [34, ["ref"], 34], 58, "null", 44, [34, ["payload"], 34], 58, [34, ["m"], 34], 44, [34, ["event"], 34], 58, [34, ["e"], 34]], 125]

  test "fastlane!/1 translates `Phoenix.Socket.Broadcast` into 'Phoenix.Socket.Message'" do
    broadcast = %Broadcast{topic: "t", event: "e", payload: "m"}
    assert LongPollSerializer.fastlane!(broadcast) == %Message{topic: broadcast.topic, event: broadcast.event, payload: broadcast.payload}
  end

  test "encode!/1 encodes `Phoenix.Socket.Message` as JSON" do
    reply = %Reply{topic: "t", payload: "m", ref: "foo", status: "bar"}
    assert LongPollSerializer.encode!(reply) == %Message{topic: reply.topic, event: "phx_reply", ref: reply.ref, payload: %{status: reply.status, response: reply.payload}}
  end

  test "decode!/2 decodes `Phoenix.Socket.Message` from JSON" do
    assert %Message{topic: "t", event: "e", payload: "m"} ==
      LongPollSerializer.decode!(@msg_json, opcode: :text)
  end
end


:recycle: Remove private method.

:recycle: Remove private method.

defmodule MyList00 do
    def sum([]), do: 0
    def sum([head | tail]), do: head + sum(tail)
end

defmodule MyList00 do
    def sum(list), do: _sum(list)

    defp _sum([]), do: 0
    defp _sum([head | tail]), do: head + _sum(tail)
end


Fix false positive for AliasUsage

Fix false positive for AliasUsage

Fixes #54

defmodule Credo.Check.Design.AliasUsage do
  @moduledoc """
  Functions from other modules should be used via an alias if the module's
  namespace is not top-level.

  While this is completely fine:

      defmodule MyApp.Web.Search do
        def twitter_mentions do
          MyApp.External.TwitterAPI.search(...)
        end
      end

  ... you might want to refactor it to look like this:

      defmodule MyApp.Web.Search do
        alias MyApp.External.TwitterAPI

        def twitter_mentions do
          TwitterAPI.search(...)
        end
      end

  The thinking behind this is that you can see the dependencies of your module
  at a glance. So if you are attempting to build a medium to large project,
  this can help you to get your boundaries/layers/contracts right.

  Like all `Software Design` issues, this is just advice and might not be
  applicable to your project/situation.
  """

  @explanation [check: @moduledoc]
  @default_params [
      excluded_namespaces: [
        "File",
        "IO",
        "Inspect",
        "Kernel",
        "Macro",
        "Supervisor",
        "Task",
        "Version"
      ],
      excluded_lastnames:  ~w[Access Agent Application Atom Base Behaviour
                              Bitwise Code Dict Enum Exception File Float
                              GenEvent GenServer HashDict HashSet IO Integer
                              Kernel Keyword List Macro Map MapSet Module Node
                              OptionParser Path Port Process Protocol Range
                              Record Regex Set Stream String StringIO Supervisor
                              System Task Tuple URI Version]
    ]

  use Credo.Check, base_priority: :normal

  @doc false
  def run(%SourceFile{ast: ast} = source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)
    excluded_namespaces = params |> Params.get(:excluded_namespaces, @default_params)
    excluded_lastnames = params |> Params.get(:excluded_lastnames, @default_params)

    Credo.Code.traverse(ast, &traverse(&1, &2, issue_meta, excluded_namespaces, excluded_lastnames))
  end

  # Ignore multi alias call
  defp traverse({:., _, [{:__aliases__, meta, mod_list}, :{}]} = ast, issues, _source_file, _excluded_namespaces, _excluded_lastnames) do
    {ast, issues}
  end
  defp traverse({:., _, [{:__aliases__, meta, mod_list}, fun_atom]} = ast, issues, issue_meta, excluded_namespaces, excluded_lastnames) when is_list(mod_list) and is_atom(fun_atom) do
    if Enum.count(mod_list) > 1 && !Enum.any?(mod_list, &tuple?/1) do
      first_name = mod_list |> List.first |> to_string
      last_name = mod_list |> List.last |> to_string
      if !Enum.member?(excluded_namespaces, first_name) && !Enum.member?(excluded_lastnames, last_name) do
        trigger = mod_list |> Enum.join(".")
        IO.inspect fun_atom
        {ast, issues ++ [issue_for(issue_meta, meta[:line], trigger)]}
      else
        {ast, issues}
      end
    else
      {ast, issues}
    end
  end
  defp traverse(ast, issues, _source_file, _excluded_namespaces, _excluded_lastnames) do
    {ast, issues}
  end

  def tuple?(t) when is_tuple(t), do: true
  def tuple?(_), do: false

  defp issue_for(issue_meta, line_no, trigger) do
    format_issue issue_meta,
      message: "Nested modules could be aliased at the top of the invoking module.",
      trigger: trigger,
      line_no: line_no
  end
end

defmodule Credo.Check.Design.AliasUsage do
  @moduledoc """
  Functions from other modules should be used via an alias if the module's
  namespace is not top-level.

  While this is completely fine:

      defmodule MyApp.Web.Search do
        def twitter_mentions do
          MyApp.External.TwitterAPI.search(...)
        end
      end

  ... you might want to refactor it to look like this:

      defmodule MyApp.Web.Search do
        alias MyApp.External.TwitterAPI

        def twitter_mentions do
          TwitterAPI.search(...)
        end
      end

  The thinking behind this is that you can see the dependencies of your module
  at a glance. So if you are attempting to build a medium to large project,
  this can help you to get your boundaries/layers/contracts right.

  Like all `Software Design` issues, this is just advice and might not be
  applicable to your project/situation.
  """

  @explanation [check: @moduledoc]
  @default_params [
      excluded_namespaces: [
        "File",
        "IO",
        "Inspect",
        "Kernel",
        "Macro",
        "Supervisor",
        "Task",
        "Version"
      ],
      excluded_lastnames:  ~w[Access Agent Application Atom Base Behaviour
                              Bitwise Code Dict Enum Exception File Float
                              GenEvent GenServer HashDict HashSet IO Integer
                              Kernel Keyword List Macro Map MapSet Module Node
                              OptionParser Path Port Process Protocol Range
                              Record Regex Set Stream String StringIO Supervisor
                              System Task Tuple URI Version]
    ]

  use Credo.Check, base_priority: :normal

  @doc false
  def run(%SourceFile{ast: ast} = source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)
    excluded_namespaces = params |> Params.get(:excluded_namespaces, @default_params)
    excluded_lastnames = params |> Params.get(:excluded_lastnames, @default_params)

    Credo.Code.traverse(ast, &traverse(&1, &2, issue_meta, excluded_namespaces, excluded_lastnames))
  end

  defp traverse({:., _, [{:__aliases__, meta, mod_list}, fun_atom]} = ast, issues, issue_meta, excluded_namespaces, excluded_lastnames) when is_list(mod_list) and is_atom(fun_atom) do
    if Enum.count(mod_list) > 1 && !Enum.any?(mod_list, &tuple?/1) do
      first_name = mod_list |> List.first |> to_string
      last_name = mod_list |> List.last |> to_string
      if !Enum.member?(excluded_namespaces, first_name) && !Enum.member?(excluded_lastnames, last_name) do
        trigger = mod_list |> Enum.join(".")
        {ast, issues ++ [issue_for(issue_meta, meta[:line], trigger)]}
      else
        {ast, issues}
      end
    else
      {ast, issues}
    end
  end
  defp traverse(ast, issues, _source_file, _excluded_namespaces, _excluded_lastnames) do
    {ast, issues}
  end

  def tuple?(t) when is_tuple(t), do: true
  def tuple?(_), do: false

  defp issue_for(issue_meta, line_no, trigger) do
    format_issue issue_meta,
      message: "Nested modules could be aliased at the top of the invoking module.",
      trigger: trigger,
      line_no: line_no
  end
end