defmodule Explorer.Repo.Migrations.CreateTransactions do
  use Ecto.Migration

  def change do
    create table(:transactions, primary_key: false) do
      # `null` when a pending transaction
      add(:cumulative_gas_used, :numeric, precision: 100, null: true)

      # `null` before internal transactions are fetched or if no error in those internal transactions
      add(:error, :string, null: true)

      add(:gas, :numeric, precision: 100, null: false)
      add(:gas_price, :numeric, precision: 100, null: false)
      add(:effective_gas_price, :numeric, precision: 100, null: false)

      # `null` when a pending transaction
      add(:gas_used, :numeric, precision: 100, null: true)
      add(:gas_used_for_l1, :numeric, precision: 100, null: false)

      add(:hash, :bytea, null: false, primary_key: true)

      # `null` when a pending transaction
      add(:index, :integer, null: true)

      add(:input, :bytea, null: false)

      # `null` when `internal_transactions` has never been fetched
      add(:internal_transactions_indexed_at, :utc_datetime_usec, null: true)

      add(:nonce, :integer, null: false)
      add(:r, :numeric, precision: 100, null: false)
      add(:s, :numeric, precision: 100, null: false)

      # `null` when a pending transaction
      add(:status, :integer, null: true)

      add(:v, :integer, null: false)
      add(:value, :numeric, precision: 100, null: false)

      timestamps(null: false, type: :utc_datetime_usec)

      # `null` when a pending transaction
      add(:block_hash, references(:blocks, column: :hash, on_delete: :delete_all, type: :bytea), null: true)

      # `null` when a pending transaction
      # denormalized from `blocks.number` to improve `Explorer.Chain.recent_collated_transactions/0` performance
      add(:block_number, :integer, null: true)

      add(:from_address_hash, references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), null: false)
      # `null` when it is a contract creation transaction
      add(:to_address_hash, references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), null: true)
      add(:created_contract_address_hash, references(:addresses, column: :hash, type: :bytea), null: true)
    end

    create(
      constraint(
        :transactions,
        :collated_block_number,
        check: "block_hash IS NULL OR block_number IS NOT NULL"
      )
    )

    create(
      constraint(
        :transactions,
        :collated_cumalative_gas_used,
        check: "block_hash IS NULL OR cumulative_gas_used IS NOT NULL"
      )
    )

    create(
      constraint(
        :transactions,
        :collated_gas_used,
        check: "block_hash IS NULL OR gas_used IS NOT NULL"
      )
    )

    create(
      constraint(
        :transactions,
        :collated_index,
        check: "block_hash IS NULL OR index IS NOT NULL"
      )
    )

    create(
      constraint(
        :transactions,
        :status,
        # 0 - NULL
        # 1 - NOT NULL
        #
        # | block_hash | internal_transactions_indexed_at | status | OK | description
        # |------------|----------------------------------|--------|----|------------
        # | 0          | 0                                | 0      | 1  | pending
        # | 0          | 0                                | 1      | 0  | pending with status
        # | 0          | 1                                | 0      | 0  | pending with internal transactions
        # | 0          | 1                                | 1      | 0  | pending with internal transactions and status
        # | 1          | 0                                | 0      | 1  | pre-byzantium collated transaction without internal transactions
        # | 1          | 0                                | 1      | 1  | post-byzantium collated transaction without internal transactions
        # | 1          | 1                                | 0      | 0  | pre-byzantium collated transaction with internal transaction without status
        # | 1          | 1                                | 1      | 1  | pre- or post-byzantium collated transaction with internal transactions and status
        #
        # [Karnaugh map](https://en.wikipedia.org/wiki/Karnaugh_map)
        # b \ is | 00 | 01 | 11 | 10 |
        # -------|----|----|----|----|
        #      0 | 1  | 0  | 0  | 0  |
        #      1 | 1  | 1  | 1  | 0  |
        #
        # Simplification: ¬i·¬s + b·¬i + b·s
        check: """
        (internal_transactions_indexed_at IS NULL AND status IS NULL) OR
        (block_hash IS NOT NULL AND internal_transactions_indexed_at IS NULL) OR
        (block_hash IS NOT NULL AND status IS NOT NULL)
        """
      )
    )

    create(
      constraint(
        :transactions,
        :error,
        # | status | internal_transactions_indexed_at | error    | OK         | description
        # |--------|----------------------------------|----------|------------|------------
        # | NULL   | NULL                             | NULL     | TRUE       | pending or pre-byzantium collated
        # | NULL   | NULL                             | NOT NULL | FALSE      | error cannot be known before internal transactions are indexed
        # | NULL   | NOT NULL                         | NULL     | DON'T CARE | handled by `status` check
        # | NULL   | NOT NULL                         | NOT NULL | FALSE      | error cannot be set unless status is known to be error (`0`)
        # | 0      | NULL                             | NULL     | TRUE       | post-byzantium before internal transactions indexed
        # | 0      | NULL                             | NOT NULL | FALSE      | error cannot be set unless internal transactions are indexed
        # | 0      | NOT NULL                         | NULL     | FALSE      | error MUST be set when status is error
        # | 0      | NOT NULL                         | NOT NULL | TRUE       | error is set when status is error
        # | 1      | NULL                             | NULL     | TRUE       | post-byzantium before internal transactions indexed
        # | 1      | NULL                             | NOT NULL | FALSE      | error cannot be set when status is ok
        # | 1      | NOT NULL                         | NULL     | TRUE       | error is not set when status is ok
        # | 1      | NOT NULL                         | NOT NULL | FALSE      | error cannot be set when status is ok
        #
        # Karnaugh map
        # s \ ie | NULL, NULL | NULL, NOT NULL | NOT NULL, NOT NULL | NOT NULL, NULL |
        # -------|------------|----------------|--------------------|----------------|
        # NULL   | TRUE       | FALSE          | FALSE              | DON'T CARE     |
        # 0      | TRUE       | FALSE          | TRUE               | FALSE          |
        # 1      | TRUE       | FALSE          | FALSE              | TRUE           |
        #
        check: """
        (internal_transactions_indexed_at IS NULL AND error IS NULL) OR
        (status = 0 AND internal_transactions_indexed_at IS NOT NULL AND error IS NOT NULL) OR
        (status != 0 AND internal_transactions_indexed_at IS NOT NULL AND error IS NULL)
        """
      )
    )

    create(
      constraint(
        :transactions,
        :pending_block_number,
        check: "block_hash IS NOT NULL OR block_number IS NULL"
      )
    )

    create(
      constraint(
        :transactions,
        :pending_cumalative_gas_used,
        check: "block_hash IS NOT NULL OR cumulative_gas_used IS NULL"
      )
    )

    create(
      constraint(
        :transactions,
        :pending_gas_used,
        check: "block_hash IS NOT NULL OR gas_used IS NULL"
      )
    )

    create(
      constraint(
        :transactions,
        :pending_index,
        check: "block_hash IS NOT NULL OR index IS NULL"
      )
    )

    create(index(:transactions, :block_hash))

    create(index(:transactions, :inserted_at))
    create(index(:transactions, :updated_at))

    create(index(:transactions, :status))

    create(
      index(
        :transactions,
        ["block_number DESC NULLS FIRST", "index DESC NULLS FIRST"],
        name: "transactions_recent_collated_index"
      )
    )

    create(
      index(
        :transactions,
        [:from_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", :hash],
        name: "transactions_from_address_hash_recent_collated_index"
      )
    )

    create(
      index(
        :transactions,
        [:to_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", :hash],
        name: "transactions_to_address_hash_recent_collated_index"
      )
    )

    create(
      index(
        :transactions,
        [:created_contract_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", :hash],
        name: "transactions_created_contract_address_hash_recent_collated_index"
      )
    )

    create(unique_index(:transactions, [:block_hash, :index]))
  end
end
