import { v4 as uuidv4 } from 'uuid';
import { Transaction } from '../../../domain/entities/transaction';
import { Address } from '../../../domain/entities/address';
import { NotFoundError } from '../../../domain/errors/not-found-error';
import { ValidationError } from '../../../domain/errors/validation-error';
import { ITransactionRepository } from '../../../domain/repositories/transaction-repository';
import { INetworkRepository } from '../../../domain/repositories/network-repository';
import { IAddressRepository } from '../../../domain/repositories/address-repository';
import { ITokenRepository } from '../../../domain/repositories/token-repository';
import { CreateTransactionInput } from '../../dto/transaction.dto';
import { IDomainEventPublisher } from '../../ports/domain-event-publisher';
import { TransactionCreatedEvent } from '../../../domain/events/transaction-created.event';
import {
  isContractTokenTransferPath,
  TransactionData,
} from '../../../domain/value-objects/transaction-data';

export class CreateTransactionUseCase {
  constructor(
    private readonly transactionRepository: ITransactionRepository,
    private readonly networkRepository: INetworkRepository,
    private readonly addressRepository: IAddressRepository,
    private readonly tokenRepository: ITokenRepository,
    private readonly eventPublisher: IDomainEventPublisher,
  ) {}

  async execute(input: CreateTransactionInput): Promise<Transaction> {
    const network = await this.networkRepository.findById(input.networkId);
    if (!network) {
      throw new NotFoundError('Network', input.networkId);
    }

    const normalizedFrom = await this.resolveAddress(input.from);
    const normalizedTo = await this.resolveAddress(input.to);
    const normalizedData = await this.normalizeData(input.data);
    const dataToAddress = await this.findKnownRecipientInData(normalizedData);

    const involvedAddress = normalizedFrom ?? normalizedTo ?? dataToAddress;

    if (!involvedAddress) {
      throw new ValidationError(
        'At least one of from, to, or data.to must be a known address in the system',
        {
          from: ['Address not found'],
          to: ['Address not found'],
          data: ['No known recipient address found in data'],
        },
      );
    }

    if (involvedAddress.type !== network.addressType) {
      throw new ValidationError(
        `Involved address has type ${involvedAddress.type}, but network ${network.name} expects type ${network.addressType}`,
        {
          networkId: ['Transaction network does not match the involved address type'],
        },
      );
    }

    if (input.interactedWith) {
      const token = await this.tokenRepository.findById(input.interactedWith);
      if (!token) {
        throw new NotFoundError('Token', input.interactedWith);
      }
      if (token.networkId !== network.id) {
        throw new ValidationError(
          `Token ${token.symbol} belongs to network ${token.networkId}, but transaction network is ${network.id}`,
          {
            interactedWith: ['Token network does not match transaction network'],
          },
        );
      }
    }

    const fee = await this.resolveFee(input, network.id);

    const transaction = Transaction.create(
      uuidv4(),
      input.hash,
      input.status,
      input.block ?? null,
      input.networkId,
      input.timestamp,
      normalizedFrom?.id ?? input.from ?? null,
      normalizedTo?.id ?? input.to ?? null,
      input.interactedWith ?? null,
      input.value,
      fee,
      input.gasPrice ?? null,
      input.gasLimit ?? null,
      input.method,
      normalizedData,
    );

    const saved = await this.transactionRepository.save(transaction);
    this.eventPublisher.publish(new TransactionCreatedEvent(saved));
    return saved;
  }

  /**
   * Resolves an address value to a known Address record.
   *
   * Third-party integrations often send the raw blockchain address, while the
   * rest of the system (balance calculation, notifications) expects the
   * internal address record id. This helper tries the id first, then falls
   * back to a lookup by raw address.
   */
  private async resolveAddress(value: string | null | undefined): Promise<Address | null> {
    if (!value) {
      return null;
    }

    const byId = await this.addressRepository.findById(value);
    if (byId) {
      return byId;
    }

    return this.addressRepository.findByAddress(value);
  }

  private async normalizeData(data: TransactionData[]): Promise<TransactionData[]> {
    return Promise.all(data.map((path) => this.normalizeDataPath(path)));
  }

  private async normalizeDataPath(path: TransactionData): Promise<TransactionData> {
    if (isContractTokenTransferPath(path) && path.to) {
      const address = await this.resolveAddress(path.to);
      if (address) {
        return { ...path, to: address.id };
      }
    }
    return path;
  }

  private async findKnownRecipientInData(
    data: TransactionData[],
  ): Promise<Address | null> {
    for (const path of data) {
      if (isContractTokenTransferPath(path) && path.to) {
        const address = await this.resolveAddress(path.to);
        if (address) {
          return address;
        }
      }
    }
    return null;
  }

  private async resolveFee(
    input: CreateTransactionInput,
    networkId: string,
  ): Promise<string | null> {
    if (input.fee) {
      return input.fee;
    }

    if (input.method.toLowerCase() !== 'swap') {
      return null;
    }

    const networkTokens = await this.tokenRepository.findByNetworkId(networkId);
    const nativeToken = networkTokens.find((token) => token.isNative);

    if (!nativeToken || nativeToken.decimals <= 0) {
      return null;
    }

    const exponent = Math.max(0, nativeToken.decimals - 3);
    return (10n ** BigInt(exponent)).toString();
  }
}
