import { Brackets, Repository } from 'typeorm';
import {
  ITransactionRepository,
  TransactionFilters,
} from '../../../domain/repositories/transaction-repository';
import { PaginationOptions } from '../../../domain/repositories/pagination-options';
import { Transaction } from '../../../domain/entities/transaction';
import { TransactionEntity } from '../entities/transaction.entity';
import { AppDataSource } from '../data-source';
import { transactionMapper } from './mappers/transaction.mapper';

export class TypeOrmTransactionRepository implements ITransactionRepository {
  private get repository(): Repository<TransactionEntity> {
    return AppDataSource.getRepository(TransactionEntity);
  }

  async findById(id: string): Promise<Transaction | null> {
    const entity = await this.repository.findOneBy({ id });
    return entity ? transactionMapper.toDomain(entity) : null;
  }

  async findByHash(hash: string): Promise<Transaction | null> {
    const entity = await this.repository.findOneBy({ hash });
    return entity ? transactionMapper.toDomain(entity) : null;
  }

  async findByAddressIds(
    addressIds: string[],
    options?: PaginationOptions
  ): Promise<Transaction[]> {
    if (addressIds.length === 0) {
      return [];
    }

    const dataMatchClauses = addressIds
      .map(
        (_, index) =>
          `(
          JSON_SEARCH(transaction.data, 'one', :addressId_${index}, NULL, '$[*].to') IS NOT NULL
          OR
          JSON_SEARCH(transaction.data, 'one', :addressId_${index}, NULL, '$[*].from') IS NOT NULL
        )`,
      )
      .join(' OR ');

    const dataMatchParams = addressIds.reduce<Record<string, string>>(
      (params, id, index) => {
        params[`addressId_${index}`] = id;
        return params;
      },
      {},
    );

    const entities = await this.repository
      .createQueryBuilder('transaction')
      .where(
        new Brackets((qb) => {
          qb.where('transaction.fromAddressId IN (:...addressIds)', {
            addressIds,
          }).orWhere('transaction.toAddressId IN (:...addressIds)', {
            addressIds,
          });

          if (dataMatchClauses) {
            qb.orWhere(dataMatchClauses, dataMatchParams);
          }
        }),
      )
      .orderBy('transaction.timestamp', 'DESC')
      .take(options?.limit)
      .skip(options?.offset)
      .getMany();

    return entities.map(transactionMapper.toDomain);
  }

  async findByFilters(filters: TransactionFilters, options?: PaginationOptions): Promise<Transaction[]> {
    const qb = this.repository.createQueryBuilder('transaction');

    if (filters.walletId) {
      qb.innerJoin(
        'addresses',
        'address',
        'address.walletId = :walletId AND (address.id = transaction.fromAddressId OR address.id = transaction.toAddressId OR JSON_CONTAINS(transaction.data, JSON_QUOTE(address.id)) = 1)',
        { walletId: filters.walletId },
      );
    }

    if (filters.networkId) {
      qb.andWhere('transaction.networkId = :networkId', { networkId: filters.networkId });
    }

    if (filters.tokenId) {
      qb.andWhere('transaction.interactedWith = :tokenId', { tokenId: filters.tokenId });
    }

    if (filters.status) {
      qb.andWhere('transaction.status = :status', { status: filters.status });
    }

    if (filters.method) {
      qb.andWhere('transaction.method = :method', { method: filters.method });
    }

    const entities = await qb
      .orderBy('transaction.timestamp', 'DESC')
      .take(options?.limit)
      .skip(options?.offset)
      .getMany();

    return entities.map(transactionMapper.toDomain);
  }

  async save(transaction: Transaction): Promise<Transaction> {
    const entity = await this.repository.save(transactionMapper.toEntity(transaction));
    return transactionMapper.toDomain(entity);
  }

  async update(transaction: Transaction): Promise<Transaction> {
    const entity = await this.repository.save(transactionMapper.toEntity(transaction));
    return transactionMapper.toDomain(entity);
  }

  async delete(id: string): Promise<void> {
    await this.repository.delete(id);
  }
}
