//
// Copyright 2021 Vulcanize, Inc.
//

import assert from 'assert';
import { Connection, ConnectionOptions, DeepPartial, FindConditions, QueryRunner, FindManyOptions, EntityTarget } from 'typeorm';
import path from 'path';

import {
  ENTITY_QUERY_TYPE,
  Database as BaseDatabase,
  DatabaseInterface,
  QueryOptions,
  StateKind,
  Where
} from '@cerc-io/util';

import { Contract } from './entity/Contract';
import { Event } from './entity/Event';
import { SyncStatus } from './entity/SyncStatus';
import { StateSyncStatus } from './entity/StateSyncStatus';
import { BlockProgress } from './entity/BlockProgress';
import { State } from './entity/State';
import { Token } from './entity/Token';
import { PoolFactory } from './entity/PoolFactory';
import { Pool } from './entity/Pool';
import { Bucket } from './entity/Bucket';
import { Lend } from './entity/Lend';
import { Loan } from './entity/Loan';
import { Account } from './entity/Account';
import { LiquidationAuction } from './entity/LiquidationAuction';
import { ReserveAuction } from './entity/ReserveAuction';
import { LPTransferorList } from './entity/LPTransferorList';
import { LPAllowance } from './entity/LPAllowance';
import { LPAllowanceList } from './entity/LPAllowanceList';
import { AddCollateral } from './entity/AddCollateral';
import { AddQuoteToken } from './entity/AddQuoteToken';
import { AuctionSettle } from './entity/AuctionSettle';
import { BondWithdrawn } from './entity/BondWithdrawn';
import { BucketBankruptcy } from './entity/BucketBankruptcy';
import { BucketTake } from './entity/BucketTake';
import { BucketTakeLPAwarded } from './entity/BucketTakeLPAwarded';
import { DrawDebt } from './entity/DrawDebt';
import { Flashloan } from './entity/Flashloan';
import { Kick } from './entity/Kick';
import { LoanStamped } from './entity/LoanStamped';
import { MoveQuoteToken } from './entity/MoveQuoteToken';
import { RemoveCollateral } from './entity/RemoveCollateral';
import { RemoveQuoteToken } from './entity/RemoveQuoteToken';
import { RepayDebt } from './entity/RepayDebt';
import { ReserveAuctionKick } from './entity/ReserveAuctionKick';
import { ReserveAuctionTake } from './entity/ReserveAuctionTake';
import { ResetInterestRate } from './entity/ResetInterestRate';
import { Settle } from './entity/Settle';
import { Take } from './entity/Take';
import { TransferLP } from './entity/TransferLP';
import { UpdateInterestRate } from './entity/UpdateInterestRate';
import { Approval } from './entity/Approval';
import { ApprovalForAll } from './entity/ApprovalForAll';
import { MoveLiquidity } from './entity/MoveLiquidity';
import { Transfer } from './entity/Transfer';
import { PoolCreated } from './entity/PoolCreated';
import { AddCollateralNFT } from './entity/AddCollateralNFT';
import { AuctionNFTSettle } from './entity/AuctionNFTSettle';
import { DrawDebtNFT } from './entity/DrawDebtNFT';
import { MergeOrRemoveCollateralNFT } from './entity/MergeOrRemoveCollateralNFT';
import { Position } from './entity/Position';
import { PositionLend } from './entity/PositionLend';
import { Burn } from './entity/Burn';
import { MemorializePosition } from './entity/MemorializePosition';
import { Mint } from './entity/Mint';
import { RedeemPosition } from './entity/RedeemPosition';
import { GrantFund } from './entity/GrantFund';
import { DistributionPeriod } from './entity/DistributionPeriod';
import { Proposal } from './entity/Proposal';
import { ProposalParams } from './entity/ProposalParams';
import { DistributionPeriodVote } from './entity/DistributionPeriodVote';
import { ScreeningVote } from './entity/ScreeningVote';
import { FundingVote } from './entity/FundingVote';
import { FundedSlate } from './entity/FundedSlate';
import { DelegateRewardClaimed } from './entity/DelegateRewardClaimed';
import { FundTreasury } from './entity/FundTreasury';
import { FundedSlateUpdated } from './entity/FundedSlateUpdated';
import { ProposalCreated } from './entity/ProposalCreated';
import { ProposalExecuted } from './entity/ProposalExecuted';
import { DistributionPeriodStarted } from './entity/DistributionPeriodStarted';
import { VoteCast } from './entity/VoteCast';
import { DelegateChanged } from './entity/DelegateChanged';
import { DelegateVotesChanged } from './entity/DelegateVotesChanged';
import { BurnWrap } from './entity/BurnWrap';

export const SUBGRAPH_ENTITIES = new Set([Token, PoolFactory, Pool, Bucket, Lend, Loan, Account, LiquidationAuction, ReserveAuction, LPTransferorList, LPAllowance, LPAllowanceList, AddCollateral, AddQuoteToken, AuctionSettle, BondWithdrawn, BucketBankruptcy, BucketTake, BucketTakeLPAwarded, DrawDebt, Flashloan, Kick, LoanStamped, MoveQuoteToken, RemoveCollateral, RemoveQuoteToken, RepayDebt, ReserveAuctionKick, ReserveAuctionTake, ResetInterestRate, Settle, Take, TransferLP, UpdateInterestRate, Approval, ApprovalForAll, MoveLiquidity, Transfer, PoolCreated, AddCollateralNFT, AuctionNFTSettle, DrawDebtNFT, MergeOrRemoveCollateralNFT, Position, PositionLend, Burn, MemorializePosition, Mint, RedeemPosition, GrantFund, DistributionPeriod, Proposal, ProposalParams, DistributionPeriodVote, ScreeningVote, FundingVote, FundedSlate, DelegateRewardClaimed, FundTreasury, FundedSlateUpdated, ProposalCreated, ProposalExecuted, DistributionPeriodStarted, VoteCast, DelegateChanged, DelegateVotesChanged, BurnWrap]);
export const ENTITIES = [...SUBGRAPH_ENTITIES];
// Map: Entity to suitable query type.
export const ENTITY_QUERY_TYPE_MAP = new Map<new() => any, ENTITY_QUERY_TYPE>([]);

export const ENTITY_TO_LATEST_ENTITY_MAP: Map<any, any> = new Map();

export class Database implements DatabaseInterface {
  _config: ConnectionOptions;
  _conn!: Connection;
  _baseDatabase: BaseDatabase;
  _propColMaps: { [key: string]: Map<string, string>; };

  constructor (config: ConnectionOptions) {
    assert(config);

    this._config = {
      ...config,
      subscribers: [path.join(__dirname, 'entity/Subscriber.*')],
      entities: [path.join(__dirname, 'entity/*')]
    };

    this._baseDatabase = new BaseDatabase(this._config);
    this._propColMaps = {};
  }

  get baseDatabase (): BaseDatabase {
    return this._baseDatabase;
  }

  async init (): Promise<void> {
    this._conn = await this._baseDatabase.init();
  }

  async close (): Promise<void> {
    return this._baseDatabase.close();
  }

  getNewState (): State {
    return new State();
  }

  async getStates (where: FindConditions<State>): Promise<State[]> {
    const repo = this._conn.getRepository(State);

    return this._baseDatabase.getStates(repo, where);
  }

  async getLatestState (contractAddress: string, kind: StateKind | null, blockNumber?: number): Promise<State | undefined> {
    const repo = this._conn.getRepository(State);

    return this._baseDatabase.getLatestState(repo, contractAddress, kind, blockNumber);
  }

  async getPrevState (blockHash: string, contractAddress: string, kind?: string): Promise<State | undefined> {
    const repo = this._conn.getRepository(State);

    return this._baseDatabase.getPrevState(repo, blockHash, contractAddress, kind);
  }

  // Fetch all diff States after the specified block number.
  async getDiffStatesInRange (contractAddress: string, startblock: number, endBlock: number): Promise<State[]> {
    const repo = this._conn.getRepository(State);

    return this._baseDatabase.getDiffStatesInRange(repo, contractAddress, startblock, endBlock);
  }

  async saveOrUpdateState (dbTx: QueryRunner, state: State): Promise<State> {
    const repo = dbTx.manager.getRepository(State);

    return this._baseDatabase.saveOrUpdateState(repo, state);
  }

  async removeStates (dbTx: QueryRunner, blockNumber: number, kind: string): Promise<void> {
    const repo = dbTx.manager.getRepository(State);

    await this._baseDatabase.removeStates(repo, blockNumber, kind);
  }

  async removeStatesAfterBlock (dbTx: QueryRunner, blockNumber: number): Promise<void> {
    const repo = dbTx.manager.getRepository(State);

    await this._baseDatabase.removeStatesAfterBlock(repo, blockNumber);
  }

  async getStateSyncStatus (): Promise<StateSyncStatus | undefined> {
    const repo = this._conn.getRepository(StateSyncStatus);

    return this._baseDatabase.getStateSyncStatus(repo);
  }

  async updateStateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
    const repo = queryRunner.manager.getRepository(StateSyncStatus);

    return this._baseDatabase.updateStateSyncStatusIndexedBlock(repo, blockNumber, force);
  }

  async updateStateSyncStatusCheckpointBlock (queryRunner: QueryRunner, blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
    const repo = queryRunner.manager.getRepository(StateSyncStatus);

    return this._baseDatabase.updateStateSyncStatusCheckpointBlock(repo, blockNumber, force);
  }

  async getContracts (): Promise<Contract[]> {
    const repo = this._conn.getRepository(Contract);

    return this._baseDatabase.getContracts(repo);
  }

  async createTransactionRunner (): Promise<QueryRunner> {
    return this._baseDatabase.createTransactionRunner();
  }

  async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> {
    const repo = this._conn.getRepository(BlockProgress);

    return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber);
  }

  async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
    const repo = this._conn.getRepository(Event);

    return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
  }

  async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
    const repo = queryRunner.manager.getRepository(Event);
    return this._baseDatabase.saveEventEntity(repo, entity);
  }

  async getBlockEvents (blockHash: string, where: Where, queryOptions: QueryOptions): Promise<Event[]> {
    const repo = this._conn.getRepository(Event);

    return this._baseDatabase.getBlockEvents(repo, blockHash, where, queryOptions);
  }

  async saveBlockWithEvents (queryRunner: QueryRunner, block: DeepPartial<BlockProgress>, events: DeepPartial<Event>[]): Promise<BlockProgress> {
    const blockRepo = queryRunner.manager.getRepository(BlockProgress);
    const eventRepo = queryRunner.manager.getRepository(Event);

    return this._baseDatabase.saveBlockWithEvents(blockRepo, eventRepo, block, events);
  }

  async saveEvents (queryRunner: QueryRunner, events: Event[]): Promise<void> {
    const eventRepo = queryRunner.manager.getRepository(Event);

    return this._baseDatabase.saveEvents(eventRepo, events);
  }

  async saveBlockProgress (queryRunner: QueryRunner, block: DeepPartial<BlockProgress>): Promise<BlockProgress> {
    const repo = queryRunner.manager.getRepository(BlockProgress);

    return this._baseDatabase.saveBlockProgress(repo, block);
  }

  async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<Contract> {
    const repo = queryRunner.manager.getRepository(Contract);

    return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock, context);
  }

  async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    const repo = queryRunner.manager.getRepository(SyncStatus);

    return this._baseDatabase.updateSyncStatusIndexedBlock(repo, blockHash, blockNumber, force);
  }

  async updateSyncStatusCanonicalBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    const repo = queryRunner.manager.getRepository(SyncStatus);

    return this._baseDatabase.updateSyncStatusCanonicalBlock(repo, blockHash, blockNumber, force);
  }

  async updateSyncStatusChainHead (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    const repo = queryRunner.manager.getRepository(SyncStatus);

    return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber, force);
  }

  async updateSyncStatusProcessedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    const repo = queryRunner.manager.getRepository(SyncStatus);

    return this._baseDatabase.updateSyncStatusProcessedBlock(repo, blockHash, blockNumber, force);
  }

  async updateSyncStatusIndexingError (queryRunner: QueryRunner, hasIndexingError: boolean): Promise<SyncStatus | undefined> {
    const repo = queryRunner.manager.getRepository(SyncStatus);

    return this._baseDatabase.updateSyncStatusIndexingError(repo, hasIndexingError);
  }

  async updateSyncStatus (queryRunner: QueryRunner, syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
    const repo = queryRunner.manager.getRepository(SyncStatus);

    return this._baseDatabase.updateSyncStatus(repo, syncStatus);
  }

  async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
    const repo = queryRunner.manager.getRepository(SyncStatus);

    return this._baseDatabase.getSyncStatus(repo);
  }

  async getEvent (id: string): Promise<Event | undefined> {
    const repo = this._conn.getRepository(Event);

    return this._baseDatabase.getEvent(repo, id);
  }

  async getBlocksAtHeight (height: number, isPruned: boolean): Promise<BlockProgress[]> {
    const repo = this._conn.getRepository(BlockProgress);

    return this._baseDatabase.getBlocksAtHeight(repo, height, isPruned);
  }

  async markBlocksAsPruned (queryRunner: QueryRunner, blocks: BlockProgress[]): Promise<void> {
    const repo = queryRunner.manager.getRepository(BlockProgress);

    return this._baseDatabase.markBlocksAsPruned(repo, blocks);
  }

  async getBlockProgress (blockHash: string): Promise<BlockProgress | undefined> {
    const repo = this._conn.getRepository(BlockProgress);
    return this._baseDatabase.getBlockProgress(repo, blockHash);
  }

  async getBlockProgressEntities (where: FindConditions<BlockProgress>, options: FindManyOptions<BlockProgress>): Promise<BlockProgress[]> {
    const repo = this._conn.getRepository(BlockProgress);

    return this._baseDatabase.getBlockProgressEntities(repo, where, options);
  }

  async getEntitiesForBlock (blockHash: string, tableName: string): Promise<any[]> {
    return this._baseDatabase.getEntitiesForBlock(blockHash, tableName);
  }

  async updateBlockProgress (queryRunner: QueryRunner, block: BlockProgress, lastProcessedEventIndex: number): Promise<BlockProgress> {
    const repo = queryRunner.manager.getRepository(BlockProgress);

    return this._baseDatabase.updateBlockProgress(repo, block, lastProcessedEventIndex);
  }

  async removeEntities<Entity> (queryRunner: QueryRunner, entity: new () => Entity, findConditions?: FindManyOptions<Entity> | FindConditions<Entity>): Promise<void> {
    return this._baseDatabase.removeEntities(queryRunner, entity, findConditions);
  }

  async deleteEntitiesByConditions<Entity> (queryRunner: QueryRunner, entity: EntityTarget<Entity>, findConditions: FindConditions<Entity>): Promise<void> {
    await this._baseDatabase.deleteEntitiesByConditions(queryRunner, entity, findConditions);
  }

  async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
    return this._baseDatabase.getAncestorAtHeight(blockHash, height);
  }

  _getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
    return this._conn.getMetadata(entityName).ownColumns.reduce((acc, curr) => {
      return acc.set(curr.propertyName, curr.databaseName);
    }, new Map<string, string>());
  }
}
