import { ethers } from 'ethers';
import {
  type SupportedChain,
  type DiscoverOptions,
  type DiscoveredAgent,
  type OnChainAgentData,
  CHAIN_CONFIGS,
  IDENTITY_ABI,
  PARTNERSHIP_ABI,
  REPUTATION_ABI,
  BRIDGE_ABI,
  getStreetCredTier,
} from './types';
import { calculateStreetCred, fetchOnChainData } from './enrich';

// ─── Provider Cache ────────────────────────────────────────────────────────────

const providerCache = new Map<SupportedChain, ethers.JsonRpcProvider>();

function getProvider(chain: SupportedChain): ethers.JsonRpcProvider {
  if (!providerCache.has(chain)) {
    const config = CHAIN_CONFIGS[chain];
    providerCache.set(chain, new ethers.JsonRpcProvider(config.rpcUrl));
  }
  return providerCache.get(chain)!;
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

/**
 * Fetch all registered agent addresses from the Identity contract by
 * iterating bond participants and known addresses.
 * Uses getTotalAgents() to determine scope, then queries via events or index.
 */
async function fetchAllAgentAddresses(chain: SupportedChain): Promise<string[]> {
  const provider = getProvider(chain);
  const config = CHAIN_CONFIGS[chain];

  const identityContract = new ethers.Contract(config.contracts.identity, IDENTITY_ABI, provider);

  // Try to get total count
  let total = 0;
  try {
    const totalBig: bigint = await identityContract.getTotalAgents();
    total = Number(totalBig);
  } catch {
    // Contract may not support this — use fallback
  }

  if (total === 0) {
    return [];
  }

  // Query AgentRegistered events to get all addresses
  // Limit to last 50000 blocks (~7 days on base) for performance
  const latestBlock = await provider.getBlockNumber();
  const fromBlock = Math.max(0, latestBlock - 50000);

  const agentRegisteredTopic = ethers.id('AgentRegistered(address,string,string,uint256)');

  let addresses: string[] = [];
  try {
    const logs = await provider.getLogs({
      address: config.contracts.identity,
      topics: [agentRegisteredTopic],
      fromBlock,
      toBlock: latestBlock,
    });

    // Extract agent addresses from logs (first indexed topic is the address)
    addresses = logs
      .map((log) => {
        try {
          // Agent address is typically the first indexed parameter
          if (log.topics[1]) {
            return ethers.getAddress('0x' + log.topics[1].slice(26));
          }
        } catch {
          // ignore malformed log
        }
        return null;
      })
      .filter((addr): addr is string => addr !== null);

    // Deduplicate
    addresses = [...new Set(addresses)];
  } catch {
    // Fallback: return empty — discovery requires events
    addresses = [];
  }

  return addresses;
}

// ─── Main Export ──────────────────────────────────────────────────────────────

/**
 * Discover trusted agents from the Vaultfire registry on a given chain.
 *
 * Filters agents by Street Cred score, bond count, and active status.
 * Results are sorted by Street Cred score descending.
 *
 * @example
 * ```typescript
 * const agents = await discoverTrustedAgents({
 *   chain: 'base',
 *   minStreetCred: 40, // Silver tier minimum
 * });
 * ```
 */
export async function discoverTrustedAgents(
  options: DiscoverOptions
): Promise<DiscoveredAgent[]> {
  const {
    chain,
    minStreetCred = 0,
    minBonds = 0,
    requireActive = true,
    agentType,
    limit = 50,
  } = options;

  if (!CHAIN_CONFIGS[chain]) {
    throw new Error(`Unsupported chain: ${chain}`);
  }

  // Get all agent addresses
  const addresses = await fetchAllAgentAddresses(chain);

  if (addresses.length === 0) {
    return [];
  }

  // Fetch on-chain data for each agent (parallel, capped at 20 concurrent)
  const CONCURRENCY = 20;
  const results: DiscoveredAgent[] = [];

  for (let i = 0; i < addresses.length; i += CONCURRENCY) {
    const batch = addresses.slice(i, i + CONCURRENCY);
    const batchResults = await Promise.allSettled(
      batch.map(async (address): Promise<DiscoveredAgent | null> => {
        let onChain: OnChainAgentData;
        try {
          onChain = await fetchOnChainData(chain, address);
        } catch {
          return null;
        }

        // Apply filters
        if (requireActive && !onChain.identity.active) {
          return null;
        }

        if (agentType && onChain.identity.agentType !== agentType) {
          return null;
        }

        if (onChain.bonds.active < minBonds) {
          return null;
        }

        const score = calculateStreetCred(onChain);

        if (score < minStreetCred) {
          return null;
        }

        const avgRating =
          onChain.reputation.totalFeedbacks > 0n
            ? Number(onChain.reputation.averageRating) / 100
            : 0;

        return {
          address,
          chain,
          agentURI: onChain.identity.agentURI,
          agentType: onChain.identity.agentType || 'unknown',
          streetCred: {
            score,
            tier: getStreetCredTier(score),
            maxPossible: 95,
          },
          bonds: {
            total: onChain.bonds.total,
            active: onChain.bonds.active,
          },
          reputation: {
            averageRating: Math.round(avgRating * 100) / 100,
            totalFeedbacks: Number(onChain.reputation.totalFeedbacks),
          },
          active: onChain.identity.active,
        };
      })
    );

    for (const result of batchResults) {
      if (result.status === 'fulfilled' && result.value !== null) {
        results.push(result.value);
      }
    }

    // Stop early if we have enough
    if (results.length >= limit) {
      break;
    }
  }

  // Sort by Street Cred score descending
  results.sort((a, b) => b.streetCred.score - a.streetCred.score);

  return results.slice(0, limit);
}

/**
 * Get the total number of registered agents on a chain.
 */
export async function getTotalAgentCount(chain: SupportedChain): Promise<number> {
  const provider = getProvider(chain);
  const config = CHAIN_CONFIGS[chain];

  const identityContract = new ethers.Contract(config.contracts.identity, IDENTITY_ABI, provider);

  try {
    const total: bigint = await identityContract.getTotalAgents();
    return Number(total);
  } catch {
    return 0;
  }
}

/**
 * Check if an agent address is recognized on a chain's bridge contract.
 */
export async function isAgentRecognized(
  chain: SupportedChain,
  agentAddress: string
): Promise<boolean> {
  const provider = getProvider(chain);
  const config = CHAIN_CONFIGS[chain];

  const bridgeContract = new ethers.Contract(config.contracts.bridge, BRIDGE_ABI, provider);

  try {
    return await bridgeContract.isAgentRecognized(agentAddress);
  } catch {
    return false;
  }
}
