/**
 * Wallet Migration Compatibility
 *
 * Provides utilities to handle wallet address changes after app migration.
 * When an app's DID changes, the derived addresses (delegation, staking) also change,
 * but user authorizations were made with the old addresses.
 *
 * This module provides fallback query mechanisms using migratedFrom list.
 */

import { toDelegateAddress, toStakeAddress } from '@arcblock/did-util';
import type OcapClient from '@ocap/client';

import { wallet } from './auth';
import logger from './logger';
import { Subscription } from '../store/models';

// Cache for migratedFrom list (keyed by wallet.address + chain host)
let cachedMigratedFrom: string[] | null = null;
let cachedWalletAddress: string | null = null;
let cachedChainHost: string | null = null;

/**
 * Get the migratedFrom list for the current app wallet (with caching)
 * The cache is invalidated when wallet.address or chain host changes
 */
export async function getMigratedFromList(client: OcapClient): Promise<string[]> {
  // @ts-ignore - OcapClient has host property
  const chainHost = client?.config?.httpEndpoint || 'unknown';
  // If wallet address and chain host haven't changed, use cached value
  if (
    wallet.address === cachedWalletAddress &&
    chainHost === cachedChainHost &&
    cachedMigratedFrom &&
    cachedMigratedFrom.length > 0
  ) {
    return cachedMigratedFrom;
  }

  // Query chain for migratedFrom list
  try {
    const { state } = await client.getAccountState({ address: wallet.address });
    cachedMigratedFrom = state?.migratedFrom || [];
    cachedWalletAddress = wallet.address;
    cachedChainHost = chainHost;

    if (cachedMigratedFrom.length > 0) {
      logger.info('wallet-migration: loaded migratedFrom list', {
        walletAddress: wallet.address,
        chainHost,
        migratedFrom: cachedMigratedFrom,
      });
    }

    return cachedMigratedFrom;
  } catch (err) {
    logger.error('wallet-migration: failed to get migratedFrom list', {
      walletAddress: wallet.address,
      chainHost,
      error: err,
    });
    return [];
  }
}

/**
 * Clear the migratedFrom cache (for testing purposes)
 */
export function clearMigratedFromCache(): void {
  cachedMigratedFrom = null;
  cachedChainHost = null;
  cachedWalletAddress = null;
}

export type AddressWithFallbackResult = {
  address: string;
  needsBackfill: boolean;
  source: 'stored' | 'current' | 'migrated';
};

export type GetDelegationAddressParams = {
  /** Stored delegation address from payment_details (if available) */
  storedAddress?: string;
  /** The delegator's DID (user's wallet DID) */
  delegator: string;
  /** OCAP client for chain queries */
  client: OcapClient;
};

/**
 * Get the effective delegation address with migration fallback
 *
 * Priority:
 * 1. Use stored delegation_address if provided
 * 2. Try current wallet.address
 * 3. Fallback to migratedFrom addresses
 */
export async function getDelegationAddressWithFallback({
  storedAddress,
  delegator,
  client,
}: GetDelegationAddressParams): Promise<AddressWithFallbackResult | null> {
  // @ts-ignore - OcapClient has host property
  const chainHost = client?.config?.httpEndpoint || 'unknown';

  // 1. Check stored delegation_address first - but verify it has valid state
  if (storedAddress) {
    try {
      const { state: storedState } = await client.getDelegateState({ address: storedAddress });
      if (storedState?.ops?.length > 0) {
        return { address: storedAddress, needsBackfill: false, source: 'stored' };
      }
      logger.warn('wallet-migration: stored delegation address has no valid state, falling back', {
        storedAddress,
        delegator,
        hasState: !!storedState,
        opsCount: storedState?.ops?.length || 0,
      });
    } catch (err) {
      logger.warn('wallet-migration: failed to query stored delegation address, falling back', {
        storedAddress,
        delegator,
        error: err,
      });
    }
    // Continue to fallback instead of returning early
  }

  // 2. Try current wallet.address
  const currentAddress = toDelegateAddress(delegator, wallet.address);
  let currentStateResult: { hasState: boolean; opsCount: number; error?: string } = {
    hasState: false,
    opsCount: 0,
  };
  try {
    const { state: currentState } = await client.getDelegateState({ address: currentAddress });
    currentStateResult = {
      hasState: !!currentState,
      opsCount: currentState?.ops?.length || 0,
    };
    if (currentState?.ops?.length > 0) {
      return { address: currentAddress, needsBackfill: true, source: 'current' };
    }
  } catch (err) {
    currentStateResult.error = String(err);
    logger.warn('wallet-migration: failed to query current delegation state', {
      address: currentAddress,
      error: err,
    });
  }

  // 3. Fallback to migratedFrom addresses
  const migratedFrom = await getMigratedFromList(client);
  const migratedResults: Array<{
    appDid: string;
    address: string;
    hasState: boolean;
    opsCount: number;
    error?: string;
  }> = [];
  for (const oldAppDid of migratedFrom) {
    const oldAddress = toDelegateAddress(delegator, oldAppDid);
    try {
      // eslint-disable-next-line no-await-in-loop
      const { state: oldState } = await client.getDelegateState({ address: oldAddress });
      migratedResults.push({
        appDid: oldAppDid,
        address: oldAddress,
        hasState: !!oldState,
        opsCount: oldState?.ops?.length || 0,
      });
      if (oldState?.ops?.length > 0) {
        logger.info('wallet-migration: found delegation in migratedFrom', {
          delegator,
          oldAppDid,
          oldAddress,
        });
        return { address: oldAddress, needsBackfill: true, source: 'migrated' };
      }
    } catch (err) {
      migratedResults.push({
        appDid: oldAppDid,
        address: oldAddress,
        hasState: false,
        opsCount: 0,
        error: String(err),
      });
      logger.warn('wallet-migration: failed to query migrated delegation state', {
        address: oldAddress,
        oldAppDid,
        error: err,
      });
    }
  }

  // Not found - log detailed search results
  logger.error('wallet-migration: no delegation found after full search', {
    delegator,
    chainHost,
    currentWalletAddress: wallet.address,
    currentDelegationAddress: currentAddress,
    currentStateResult,
    migratedFromCount: migratedFrom.length,
    migratedResults,
  });
  return null;
}

export type GetStakingAddressParams = {
  /** Stored staking address from payment_details (if available) */
  storedAddress?: string;
  /** The user's DID */
  userDid: string;
  /** The nonce for stake address derivation (usually subscription ID) */
  nonce: string;
  /** OCAP client for chain queries */
  client: OcapClient;
};

/**
 * Get the effective staking address with migration fallback
 *
 * Priority:
 * 1. Use stored staking.address if provided
 * 2. Try current wallet.address
 * 3. Fallback to migratedFrom addresses
 */
export async function getStakingAddressWithFallback({
  storedAddress,
  userDid,
  nonce,
  client,
}: GetStakingAddressParams): Promise<AddressWithFallbackResult | null> {
  // 1. Check stored staking.address first
  if (storedAddress) {
    return { address: storedAddress, needsBackfill: false, source: 'stored' };
  }

  // 2. Try current wallet.address
  const currentAddress = toStakeAddress(userDid, wallet.address, nonce);
  try {
    const { state: currentState } = await client.getStakeState({ address: currentAddress });
    if (currentState) {
      return { address: currentAddress, needsBackfill: true, source: 'current' };
    }
  } catch (err) {
    logger.warn('wallet-migration: failed to query current stake state', {
      address: currentAddress,
      error: err,
    });
  }

  // 3. Fallback to migratedFrom addresses
  const migratedFrom = await getMigratedFromList(client);
  for (const oldAppDid of migratedFrom) {
    const oldAddress = toStakeAddress(userDid, oldAppDid, nonce);
    try {
      // eslint-disable-next-line no-await-in-loop
      const { state: oldState } = await client.getStakeState({ address: oldAddress });
      if (oldState) {
        logger.info('wallet-migration: found stake in migratedFrom', {
          userDid,
          nonce,
          oldAppDid,
          oldAddress,
        });
        return { address: oldAddress, needsBackfill: true, source: 'migrated' };
      }
    } catch (err) {
      logger.warn('wallet-migration: failed to query migrated stake state', {
        address: oldAddress,
        oldAppDid,
        error: err,
      });
    }
  }

  // Not found
  return null;
}

export type GetAppStakeAddressParams = {
  /** OCAP client for chain queries */
  client: OcapClient;
};

/**
 * Get the effective staking address for gas with migration fallback
 * This is for app's own stake (sender === receiver === wallet.address)
 */
export async function getAppStakeAddressWithFallback({
  client,
}: GetAppStakeAddressParams): Promise<AddressWithFallbackResult | null> {
  // 1. Try current wallet.address
  const currentAddress = toStakeAddress(wallet.address, wallet.address);
  try {
    const { state: currentState } = await client.getStakeState({ address: currentAddress });
    if (currentState) {
      return { address: currentAddress, needsBackfill: false, source: 'current' };
    }
  } catch (err) {
    logger.warn('wallet-migration: failed to query current app stake state', {
      address: currentAddress,
      error: err,
    });
  }

  // 2. Fallback to migratedFrom addresses
  const migratedFrom = await getMigratedFromList(client);
  for (const oldAppDid of migratedFrom) {
    const oldAddress = toStakeAddress(oldAppDid, oldAppDid);
    try {
      // eslint-disable-next-line no-await-in-loop
      const { state: oldState } = await client.getStakeState({ address: oldAddress });
      if (oldState) {
        logger.info('wallet-migration: found app stake in migratedFrom', {
          oldAppDid,
          oldAddress,
        });
        return { address: oldAddress, needsBackfill: false, source: 'migrated' };
      }
    } catch (err) {
      logger.warn('wallet-migration: failed to query migrated app stake state', {
        address: oldAddress,
        oldAppDid,
        error: err,
      });
    }
  }

  // Not found
  return null;
}

/**
 * Backfill delegation_address to subscription's payment_details (with optimistic locking)
 *
 * @param subscription - The subscription to update
 * @param address - The delegation address to store
 * @returns true if backfilled, false if already had a value
 */
export async function backfillDelegationAddress(subId: string, address: string): Promise<boolean> {
  // Optimistic lock: check if already has a value
  // Use fresh read to avoid race conditions
  if (!subId) {
    return false;
  }
  try {
    const subscription = await Subscription.findByPk(subId);
    if (!subscription) {
      return false;
    }
    if (subscription.payment_details?.arcblock?.delegation_address) {
      // Already backfilled by another request
      return false;
    }

    // Backfill the address (only if arcblock payment_details exists with required fields)
    const existingArcblock = subscription.payment_details?.arcblock;
    if (!existingArcblock) {
      logger.info('migration: cannot backfill delegation_address - missing arcblock payment_details', {
        subscriptionId: subscription.id,
      });
      return false;
    }

    await subscription.update({
      payment_details: {
        ...subscription.payment_details,
        arcblock: {
          ...existingArcblock,
          delegation_address: address,
        },
      },
    });

    logger.info('wallet-migration: backfilled delegation_address', {
      subscriptionId: subscription.id,
      address,
    });

    return true;
  } catch (err) {
    logger.error('wallet-migration: failed to backfill delegation_address', {
      subscriptionId: subId,
      address,
      error: err,
    });
    return false;
  }
}
