import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
import { IWalletAccountRepository } from '@/domain/repositories';
import { WalletAccount } from '@/domain/aggregates';
import { UserId, WalletStatus } from '@/domain/value-objects';
import { OptimisticLockError } from '@/shared/exceptions/domain.exception';
import { RepositorySaveOptions, PrismaTransactionClient } from '@/infrastructure/persistence/unit-of-work';
import Decimal from 'decimal.js';

@Injectable()
export class WalletAccountRepositoryImpl implements IWalletAccountRepository {
  private readonly logger = new Logger(WalletAccountRepositoryImpl.name);

  constructor(private readonly prisma: PrismaService) {}

  private getClient(options?: RepositorySaveOptions): PrismaService | PrismaTransactionClient {
    return options?.tx || this.prisma;
  }

  async save(wallet: WalletAccount, options?: RepositorySaveOptions): Promise<WalletAccount> {
    const client = this.getClient(options);
    const data = {
      accountSequence: wallet.accountSequence,
      userId: wallet.userId.value,
      usdtAvailable: wallet.balances.usdt.available.toDecimal(),
      usdtFrozen: wallet.balances.usdt.frozen.toDecimal(),
      dstAvailable: wallet.balances.dst.available.toDecimal(),
      dstFrozen: wallet.balances.dst.frozen.toDecimal(),
      bnbAvailable: wallet.balances.bnb.available.toDecimal(),
      bnbFrozen: wallet.balances.bnb.frozen.toDecimal(),
      ogAvailable: wallet.balances.og.available.toDecimal(),
      ogFrozen: wallet.balances.og.frozen.toDecimal(),
      rwadAvailable: wallet.balances.rwad.available.toDecimal(),
      rwadFrozen: wallet.balances.rwad.frozen.toDecimal(),
      hashpower: wallet.hashpower.decimal,
      pendingUsdt: wallet.rewards.pendingUsdt.toDecimal(),
      pendingHashpower: wallet.rewards.pendingHashpower.decimal,
      pendingExpireAt: wallet.rewards.pendingExpireAt,
      settleableUsdt: wallet.rewards.settleableUsdt.toDecimal(),
      settleableHashpower: wallet.rewards.settleableHashpower.decimal,
      settledTotalUsdt: wallet.rewards.settledTotalUsdt.toDecimal(),
      settledTotalHashpower: wallet.rewards.settledTotalHashpower.decimal,
      expiredTotalUsdt: wallet.rewards.expiredTotalUsdt.toDecimal(),
      expiredTotalHashpower: wallet.rewards.expiredTotalHashpower.decimal,
      status: wallet.status,
      hasPlanted: wallet.hasPlanted,
    };

    if (wallet.walletId.value === BigInt(0)) {
      // Create new wallet with version = 0
      const created = await client.walletAccount.create({
        data: {
          ...data,
          version: 0,
        },
      });
      return this.toDomain(created);
    } else {
      // Update existing with optimistic lock
      const currentVersion = wallet.version;
      const newVersion = currentVersion + 1;

      const result = await client.walletAccount.updateMany({
        where: {
          id: wallet.walletId.value,
          version: currentVersion, // Optimistic lock: only update if version matches
        },
        data: {
          ...data,
          version: newVersion,
          updatedAt: new Date(),
        },
      });

      if (result.count === 0) {
        // Version mismatch - concurrent modification detected
        this.logger.warn(
          `[OPTIMISTIC_LOCK] Conflict detected for wallet ${wallet.accountSequence} (id=${wallet.walletId.value}, version=${currentVersion})`,
        );
        throw new OptimisticLockError(
          `Wallet ${wallet.accountSequence} was modified by another transaction (version=${currentVersion})`,
        );
      }

      this.logger.debug(
        `[SAVE] Wallet ${wallet.accountSequence} updated (version: ${currentVersion} -> ${newVersion})`,
      );

      // Fetch the updated record to return
      const updated = await client.walletAccount.findUnique({
        where: { id: wallet.walletId.value },
      });

      if (!updated) {
        throw new Error(`Wallet ${wallet.walletId.value} not found after update`);
      }

      return this.toDomain(updated);
    }
  }

  async findById(walletId: bigint, options?: RepositorySaveOptions): Promise<WalletAccount | null> {
    const client = this.getClient(options);
    const record = await client.walletAccount.findUnique({
      where: { id: walletId },
    });
    return record ? this.toDomain(record) : null;
  }

  async findByUserId(userId: bigint, options?: RepositorySaveOptions): Promise<WalletAccount | null> {
    const client = this.getClient(options);
    const record = await client.walletAccount.findUnique({
      where: { userId },
    });
    return record ? this.toDomain(record) : null;
  }

  async findByAccountSequence(accountSequence: string, options?: RepositorySaveOptions): Promise<WalletAccount | null> {
    const client = this.getClient(options);
    const record = await client.walletAccount.findUnique({
      where: { accountSequence },
    });
    return record ? this.toDomain(record) : null;
  }

  async getOrCreate(accountSequence: string, userId: bigint, options?: RepositorySaveOptions): Promise<WalletAccount> {
    const existing = await this.findByAccountSequence(accountSequence, options);
    if (existing) {
      return existing;
    }

    const newWallet = WalletAccount.createNew(accountSequence, UserId.create(userId));
    return this.save(newWallet, options);
  }

  async findByUserIds(userIds: bigint[]): Promise<Map<string, WalletAccount>> {
    const records = await this.prisma.walletAccount.findMany({
      where: { userId: { in: userIds } },
    });

    const map = new Map<string, WalletAccount>();
    for (const record of records) {
      map.set(record.userId.toString(), this.toDomain(record));
    }
    return map;
  }

  private toDomain(record: {
    id: bigint;
    accountSequence: string;
    userId: bigint;
    usdtAvailable: Decimal;
    usdtFrozen: Decimal;
    dstAvailable: Decimal;
    dstFrozen: Decimal;
    bnbAvailable: Decimal;
    bnbFrozen: Decimal;
    ogAvailable: Decimal;
    ogFrozen: Decimal;
    rwadAvailable: Decimal;
    rwadFrozen: Decimal;
    hashpower: Decimal;
    pendingUsdt: Decimal;
    pendingHashpower: Decimal;
    pendingExpireAt: Date | null;
    settleableUsdt: Decimal;
    settleableHashpower: Decimal;
    settledTotalUsdt: Decimal;
    settledTotalHashpower: Decimal;
    expiredTotalUsdt: Decimal;
    expiredTotalHashpower: Decimal;
    status: string;
    hasPlanted: boolean;
    version: number;
    createdAt: Date;
    updatedAt: Date;
  }): WalletAccount {
    return WalletAccount.reconstruct({
      walletId: record.id,
      accountSequence: record.accountSequence,
      userId: record.userId,
      usdtAvailable: new Decimal(record.usdtAvailable.toString()),
      usdtFrozen: new Decimal(record.usdtFrozen.toString()),
      dstAvailable: new Decimal(record.dstAvailable.toString()),
      dstFrozen: new Decimal(record.dstFrozen.toString()),
      bnbAvailable: new Decimal(record.bnbAvailable.toString()),
      bnbFrozen: new Decimal(record.bnbFrozen.toString()),
      ogAvailable: new Decimal(record.ogAvailable.toString()),
      ogFrozen: new Decimal(record.ogFrozen.toString()),
      rwadAvailable: new Decimal(record.rwadAvailable.toString()),
      rwadFrozen: new Decimal(record.rwadFrozen.toString()),
      hashpower: new Decimal(record.hashpower.toString()),
      pendingUsdt: new Decimal(record.pendingUsdt.toString()),
      pendingHashpower: new Decimal(record.pendingHashpower.toString()),
      pendingExpireAt: record.pendingExpireAt,
      settleableUsdt: new Decimal(record.settleableUsdt.toString()),
      settleableHashpower: new Decimal(record.settleableHashpower.toString()),
      settledTotalUsdt: new Decimal(record.settledTotalUsdt.toString()),
      settledTotalHashpower: new Decimal(record.settledTotalHashpower.toString()),
      expiredTotalUsdt: new Decimal(record.expiredTotalUsdt.toString()),
      expiredTotalHashpower: new Decimal(record.expiredTotalHashpower.toString()),
      status: record.status,
      hasPlanted: record.hasPlanted,
      version: record.version,
      createdAt: record.createdAt,
      updatedAt: record.updatedAt,
    });
  }
}
