defmodule StarknetExplorer.Data do
  require Logger

  alias StarknetExplorer.{
    Rpc,
    Transaction,
    Block,
    TransactionReceipt,
    Calldata
  }

  @doc """
  Fetch `block_amount` blocks (defaults to 15) with txs
  """
  def many_blocks_with_txs(network, block_amount \\ 15) do
    Block.latest_blocks_with_txs(block_amount, network)
  end

  @doc """
  Fetch `block_amount` blocks (defaults to 15) without txs
  """
  def many_blocks(network, block_amount \\ 15) do
    Block.latest_blocks(block_amount, network)
  end

  def first_n_blocks(network, block_amount \\ 15) do
    case Block.latest_blocks_with_txs(block_amount, network) do
      no_blocks when no_blocks == nil or no_blocks == [] ->
        with {:ok, latest_block} <- Rpc.get_latest_block(network),
             # Create a list getting all blocks from the latest block to the
             # latest - block_amount discards any error, returns error if
             # the list is empty
             blocks = [_ | _] <-
               for(
                 n <- 1..block_amount,
                 block = Rpc.get_block_by_number(latest_block.number - n, network),
                 match?({:ok, _}, block),
                 do: block
               ) do
          {:ok, blocks}
        else
          _ -> {:error, "list is empty"}
        end

      blocks ->
        {:ok, blocks}
    end
  end

  @doc """
  Fetch a block by its hash, first look up in
  the db, if not found, fetch from the RPC provider
  """
  def block_by_hash(hash, network, preload_transactions \\ true) do
    case Block.get_by_hash(hash, network, preload_transactions) do
      nil ->
        case Rpc.get_block_by_hash(hash, network) do
          {:ok, block} ->
            StarknetExplorer.BlockUtils.store_block(block, network)

            block = Block.from_rpc_block(block, network)
            {:ok, block}

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

      block ->
        {:ok, block}
    end
  end

  def block_by_partial_hash(hash, network) do
    case Block.get_by_partial_hash(hash, network) do
      no_blocks when no_blocks == nil or no_blocks == [] ->
        with {:ok, blocks} <- first_n_blocks(network, 50) do
          {:ok,
           for(
             nth_block <- blocks,
             String.contains?(nth_block.hash, hash),
             do: nth_block
           )}
        else
          err -> {:error, err}
        end

      blocks ->
        {:ok, blocks}
    end
  end

  @doc """
  Fetch a block by number, first look up in
  the db, if not found, fetch from the RPC provider
  """
  def block_by_number(number, network, preload_transactions \\ true) do
    case Block.get_by_num(number, network, preload_transactions) do
      nil ->
        case Rpc.get_block_by_number(number, network) do
          {:ok, block} ->
            StarknetExplorer.BlockUtils.store_block(block, network)

            block = Block.from_rpc_block(block, network)
            {:ok, block}

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

      block ->
        {:ok, block}
    end
  end

  def block_by_partial_number(number, network) do
    case Block.get_by_partial_num(number, network) do
      no_blocks when no_blocks == nil or no_blocks == [] ->
        with {:ok, blocks} <- first_n_blocks(network, 50) do
          {:ok,
           for(
             nth_block <- blocks,
             String.contains?(Integer.to_string(nth_block.number), Integer.to_string(number)),
             do: nth_block
           )}
        else
          err -> {:error, err}
        end

      blocks ->
        {:ok, blocks}
    end
  end

  def transactions_by_block_number(block_number, network) do
    Transaction.get_by_block_number(block_number, network)
  end

  @doc """
  Fetch transactions receipts by block
  """
  def receipts_by_block(block, network) do
    case TransactionReceipt.get_by_block_hash(block.hash) do
      # TODO: remove case; it should not happen anymore because on block retrieval we are also storing related data on db
      # if receipts are not found in the db, split the txs in chunks and get receipts by RPC
      [] ->
        Logger.warning(
          "The block should have been saved with all related txs and receipts earlier"
        )

        all_receipts =
          block.transactions
          |> Enum.chunk_every(50)
          |> Enum.flat_map(fn chunk ->
            tasks =
              Enum.map(chunk, fn x ->
                Task.async(fn ->
                  {:ok, receipt} = Rpc.get_transaction_receipt(x.hash, network)

                  # if we try to create a %StarknetExplorer.TransactionReceipt, this fails because it will not be linked to any transaction struct
                  receipt
                  |> StarknetExplorerWeb.Utils.atomize_keys()
                end)
              end)

            Enum.map(tasks, &Task.await(&1, 10000))
          end)

        {:ok, Map.new(all_receipts)}

      receipts ->
        {:ok, receipts}
    end
  end

  def latest_block_number(network) do
    {:ok, _latest_block = %{"block_number" => block_number}} =
      Rpc.get_latest_block_no_cache(network)

    block_number
  end

  def latest_block_with_transactions(network) do
    blocks = Block.latest_blocks_with_txs(1, network)

    blocks
  end

  def transaction(tx_hash, network) do
    case Transaction.get_by_hash_with_receipt(tx_hash, network) do
      nil ->
        with {:ok, %{"transaction_hash" => _transaction} = tx} <-
               Rpc.get_transaction(tx_hash, network),
             {:ok, receipt} <- Rpc.get_transaction_receipt(tx_hash, network) do
          {:ok,
           tx
           |> Transaction.from_rpc_tx()
           |> Map.put(:receipt, receipt |> StarknetExplorerWeb.Utils.atomize_keys())}
        else
          err -> {:error, err}
        end

      tx ->
        {:ok, tx}
    end
  end

  def transaction_by_partial_hash(tx_hash, network) do
    case Transaction.get_by_partial_hash(tx_hash) do
      nil ->
        {:error, "No match"}

      tx ->
        {:ok, tx}

        case Rpc.get_transaction(tx_hash, network) do
          {:ok, tx} ->
            {:ok, receipt} = Rpc.get_transaction_receipt(tx_hash, network)

            tx =
              tx
              |> Transaction.from_rpc_tx()
              |> Map.put(:receipt, receipt |> StarknetExplorerWeb.Utils.atomize_keys())

            {:ok, tx}

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

  def get_block_events_paginated(block_hash, pagination, network) do
    {:ok, events} = Rpc.get_block_events_paginated(block_hash, pagination, network)

    events
  end

  def get_events(params, network) do
    Rpc.get_events(params, network)
  end

  def full_transaction(tx_hash, network) do
    {:ok, tx} = transaction(tx_hash, network)
    receipt = tx.receipt

    block_id =
      if Map.has_key?(receipt, :block_number),
        do: %{"block_number" => receipt.block_number},
        else: "latest"

    contract =
      case Rpc.get_class_at(block_id, tx.sender_address, network) do
        {:ok, contract} ->
          contract

        _ ->
          nil
      end

    tx = tx |> Map.put(:contract, contract)

    input_data =
      try do
        Calldata.parse_calldata(tx, block_id, network)
      rescue
        _ -> nil
      end

    {:ok, tx |> Map.put(:input_data, input_data)}
  end

  def get_class_at(block_number, contract_address, network) do
    {:ok, class_hash} =
      Rpc.get_class_at(%{"block_number" => block_number}, contract_address, network)

    class_hash
  end

  def get_entity_count(network) do
    counts = StarknetExplorer.Counts.get(network)

    if counts do
      Map.new()
      |> Map.put(:messages_count, counts.messages)
      |> Map.put(:events_count, counts.events)
      |> Map.put(:transaction_count, counts.transactions)
      |> Map.put(:block_count, counts.blocks)
    else
      Map.new()
      |> Map.put(:messages_count, 0)
      |> Map.put(:events_count, 0)
      |> Map.put(:transaction_count, 0)
      |> Map.put(:block_count, 0)
    end
  end
end
