const express = require('express');
const blockService = require('../services/blockService');
const callbackService = require('../services/callbackService');
const chainTransactionService = require('../services/chainTransactionService');
const nodeParticipantService = require('../services/nodeParticipantService');
const proofLedgerService = require('../services/proofLedgerService');
const rewardAuditService = require('../services/rewardAuditService');
const rewardLedgerService = require('../services/rewardLedgerService');
const rewardPolicyService = require('../services/rewardPolicyService');
const rewardReviewService = require('../services/rewardReviewService');
const testnetService = require('../services/testnetService');
const trxCollateralService = require('../services/trxCollateralService');
const walletService = require('../services/walletService');
const { toDisplayAmount } = require('../config/coinConfig');
const { attachWalletUser } = require('../middleware/walletUserAuth');

const router = express.Router();
router.use(attachWalletUser);

const EXPLORER_TEXT = {
  ko: {
    htmlLang: 'ko', switchLabel: 'English', modeLabel: 'PLAI CHAIN TESTNET', title: 'PLAI 체인 익스플로러',
    subtitle: '노드, 담보, 보상, 증명 원장을 한 곳에서 확인합니다.', searchPlaceholder: '주소, txHash, 블록 높이/해시, proof id/hash 검색', search: '검색', wallet: '지갑', admin: '관리자', port: '포트',
    latestBlock: '최신 블록', chainVerify: '체인 검증', readiness: '준비도', network: '네트워크', notFound: '검색 결과가 없습니다',
    metrics: { wallets: '지갑', circulating: '유통량', blocks: '블록', finalized: '확정', recentTx: '최근 TX', proofs: '최근 Proof', rewards: '최근 보상', audits: '보상 감사', reviews: '보상 검토', nodes: '검증자 / 노드' },
    participantDashboard: '내 노드 현황', networkDashboard: '전체 노드 현황',
    dashboardNote: '관리자와 공개 화면은 전체 네트워크를 보여주고, 로그인한 참여자는 본인 노드와 담보 상태만 확인합니다.',
    activeNodes: '활성 노드', totalCapacity: '총 용량', requiredCollateral: '필요 담보', confirmedCollateral: '확정 담보', pendingCollateral: '대기 담보', coverage: '담보 충족률',
    policyTitle: '참여 정책', policyText: '현재 정책은 1TB당 %TRX% TRX입니다. 담보가 확인된 노드만 참여 보상 대상입니다.',
    policyWarning: '전송 전 TRX 주소와 네트워크를 확인하세요. 잘못 보낸 전송은 자동 복구되지 않을 수 있습니다.',
    recentCollateral: '최근 담보 확인', noEvents: '아직 담보 확인 기록이 없습니다.', nodeLedger: '노드 담보 원장', node: '노드', status: '상태', capacity: '용량', required: '필요', confirmedPending: '확정 / 대기', lastHeartbeat: '마지막 하트비트', collateralMemo: '담보 메모', noNodes: '표시할 노드가 없습니다.',
    recentBlocks: '최신 블록', testnet: '테스트넷', symbol: '심볼', issuance: '발행 상태', treasury: '트레저리', genesis: '제네시스', recentChainTx: '최근 체인 TX', recentProofs: '최근 Proof', recentRewards: '최근 보상', recentAudits: '최근 보상 감사', recentReviews: '최근 보상 검토', recentWalletTx: '최근 지갑 전송', walletList: '지갑 목록', callbacks: '콜백', validators: '검증자 / 노드', heightFinality: '높이 / 확정성', noRecords: '기록이 없습니다.', fullRecentList: '최근 기록',
    explorerTitle: 'PLAI 체인 익스플로러',
    networkLabel: 'PLAI CHAIN TESTNET',
    langToggle: 'English',
    heroTitle: '노드, 담보, 보상, 증명 원장을 한 곳에서 추적합니다.',
    heroSubtitle: '관리자는 전체 네트워크를 보고, 로그인한 참여자는 본인 노드와 담보 상태를 확인합니다.',
    searchButton: '검색',
    notFoundLabel: '검색 결과가 없습니다',
    metricsLabel: '익스플로러 지표',
    wallets: '지갑',
    circulating: '유통량',
    blocks: '블록',
    finalized: '확정',
    recentTx: '최근 TX',
    rewardAudits: '보상 감사',
    rewardReviews: '보상 검토',
    participantLedger: '참여자 노드 원장',
    policySubtext: '각 노드는 고유 메모로 추적되므로 같은 지갑에서 여러 노드 담보를 보내도 노드별로 배정할 수 있습니다.',
    noCollateral: '아직 담보 확인 기록이 없습니다.',
    uptimeVerify: '가동 / 검증',
    usedPrefix: '사용',
    recentTransactions: '최근 체인 TX',
    statusMap: { active: '활성', waiting_deposit: '담보 대기', pending: '대기', inactive: '비활성', online: '온라인', offline: '오프라인' }
  },
  en: {
    htmlLang: 'en', switchLabel: 'Korean', modeLabel: 'PLAI CHAIN TESTNET', title: 'PLAI Chain Explorer',
    subtitle: 'Track nodes, collateral, rewards, and proof ledger activity in one place.', searchPlaceholder: 'Search address, txHash, block height/hash, proof id/hash', search: 'Search', wallet: 'Wallet', admin: 'Admin', port: 'Port',
    latestBlock: 'Latest Block', chainVerify: 'Chain Verify', readiness: 'Readiness', network: 'Network', notFound: 'No result found',
    metrics: { wallets: 'Wallets', circulating: 'Circulating', blocks: 'Blocks', finalized: 'Finalized', recentTx: 'Recent Tx', proofs: 'Recent Proofs', rewards: 'Recent Rewards', audits: 'Reward Audits', reviews: 'Reward Reviews', nodes: 'Validators / Nodes' },
    participantDashboard: 'My Node Dashboard', networkDashboard: 'Network Node Dashboard',
    dashboardNote: 'Admin and public views show the whole network. Signed-in participants see only their own node and collateral status.',
    activeNodes: 'Active Nodes', totalCapacity: 'Total Capacity', requiredCollateral: 'Required Collateral', confirmedCollateral: 'Confirmed Collateral', pendingCollateral: 'Pending Collateral', coverage: 'Collateral Coverage',
    policyTitle: 'Participation Policy', policyText: 'Current policy is %TRX% TRX per 1TB. Only collateral-confirmed nodes are eligible for participant rewards.',
    policyWarning: 'Check the TRX address and network before sending. Incorrect transfers may not be recoverable automatically.',
    recentCollateral: 'Recent Collateral Confirmations', noEvents: 'No collateral confirmations yet.', nodeLedger: 'Node Collateral Ledger', node: 'Node', status: 'Status', capacity: 'Capacity', required: 'Required', confirmedPending: 'Confirmed / Pending', lastHeartbeat: 'Last Heartbeat', collateralMemo: 'Collateral Memo', noNodes: 'No nodes to display.',
    recentBlocks: 'Latest Blocks', testnet: 'Testnet', symbol: 'Symbol', issuance: 'Issuance', treasury: 'Treasury', genesis: 'Genesis', recentChainTx: 'Recent Chain Tx', recentProofs: 'Recent Proofs', recentRewards: 'Recent Rewards', recentAudits: 'Recent Reward Audits', recentReviews: 'Recent Reward Reviews', recentWalletTx: 'Recent Wallet Transfers', walletList: 'Wallet List', callbacks: 'Callbacks', validators: 'Validators / Nodes', heightFinality: 'height / finality', noRecords: 'No records.', fullRecentList: 'Recent Records',
    explorerTitle: 'PLAI Chain Explorer',
    networkLabel: 'PLAI CHAIN TESTNET',
    langToggle: 'Korean',
    heroTitle: 'Track nodes, collateral, rewards, and proof ledger activity in one place.',
    heroSubtitle: 'Admins see the whole network. Signed-in participants see only their own nodes and collateral status.',
    searchButton: 'Search',
    notFoundLabel: 'No result found',
    metricsLabel: 'Explorer metrics',
    wallets: 'Wallets',
    circulating: 'Circulating',
    blocks: 'Blocks',
    finalized: 'Finalized',
    recentTx: 'Recent Tx',
    rewardAudits: 'Reward Audits',
    rewardReviews: 'Reward Reviews',
    participantLedger: 'PARTICIPANT NODE LEDGER',
    policySubtext: 'Each node is tracked by a unique memo, so multiple node deposits from the same wallet can still be assigned per node.',
    noCollateral: 'No collateral confirmations yet.',
    uptimeVerify: 'Uptime / Verify',
    usedPrefix: 'used',
    recentTransactions: 'Recent Chain Tx',
    statusMap: { active: 'Active', waiting_deposit: 'Waiting Deposit', pending: 'Pending', inactive: 'Inactive', online: 'Online', offline: 'Offline' }
  }
};

function getExplorerLang(req) {
  return String(req.query.lang || '').toLowerCase() === 'en' ? 'en' : 'ko';
}

function withLang(path, lang) {
  if (!lang || lang === 'ko') return path;
  const joiner = path.includes('?') ? '&' : '?';
  return `${path}${joiner}lang=${encodeURIComponent(lang)}`;
}

function buildLangUrl(req, nextLang) {
  const basePath = req.baseUrl || '/explorer';
  const params = new URLSearchParams(req.query || {});
  if (nextLang === 'ko') params.delete('lang'); else params.set('lang', nextLang);
  const query = params.toString();
  return `${basePath}${query ? `?${query}` : ''}`;
}

function normalizeKey(value) {
  return String(value || '').trim().toLowerCase();
}

function getSessionUser(req) {
  const session = req.session || {};
  return req.walletUser || req.user || session.user || session.aisforUser || session.walletUser || null;
}

function getSessionUserKeys(user) {
  if (!user) return [];
  const metadata = user.metadata || {};
  return [
    user.userId,
    user.userid,
    user.username,
    user.loginId,
    user.mongoId,
    user.id,
    user._id,
    metadata.userId,
    metadata.userid,
    metadata.username,
    metadata.loginId,
    metadata.mongoId
  ].map(normalizeKey).filter(Boolean);
}

function getSessionUserId(user) {
  return getSessionUserKeys(user)[0] || '';
}

function hasSessionUser(user) {
  return getSessionUserKeys(user).length > 0;
}

function isAdminUser(user) {
  const role = normalizeKey(user && (user.role || user.userRole || user.type));
  return Boolean(user && (user.isAdmin === true || user.admin === true || role === 'admin' || role === 'manager'));
}

function isAdminRequest(req, user) {
  const session = req.session || {};
  return Boolean(session.chainAdminAuthed || session.adminAuthed || session.adminAuthenticated || session.chainAdmin || session.isAdmin || session.user?.isAdmin || isAdminUser(user));
}

function latestRows(rows, limit = 20) {
  return (rows || []).slice().sort((a, b) => {
    const at = new Date(a.confirmedAt || a.createdAt || a.updatedAt || a.timestamp || 0).getTime();
    const bt = new Date(b.confirmedAt || b.createdAt || b.updatedAt || b.timestamp || 0).getTime();
    return bt - at;
  }).slice(0, limit);
}

function shortHash(value, left = 10, right = 8) {
  const text = String(value || '');
  if (text.length <= left + right + 3) return text;
  return `${text.slice(0, left)}...${text.slice(-right)}`;
}

function firstFiniteNumber(...values) {
  for (const value of values) {
    const number = Number(value);
    if (Number.isFinite(number) && number > 0) return number;
  }
  return 0;
}

function getNodeCapacityTb(node, config = {}) {
  const metadata = node.metadata || {};
  const directTb = firstFiniteNumber(node.capacityTb, node.storageTb, node.providedTb, metadata.capacityTb, metadata.storageTb, metadata.providedTb);
  if (directTb) return directTb;
  const directGb = firstFiniteNumber(node.capacityGb, node.storageGb, node.providedGb, metadata.capacityGb, metadata.storageGb, metadata.providedGb);
  if (directGb) return directGb / 1000;
  const bytes = firstFiniteNumber(node.storageLimitBytes, node.maxStorageBytes, node.availableStorageBytes, metadata.storageLimitBytes, metadata.maxStorageBytes);
  if (bytes) return bytes / (1024 * 1024 * 1024 * 1024);
  return Number(config.minimumCapacityTb || config.baseCapacityTb || 1) || 1;
}

function formatNodeCapacity(capacityTb) {
  const value = Number(capacityTb || 0);
  if (!Number.isFinite(value) || value <= 0) return '0 TB';
  if (value < 1) return `${Math.round(value * 1000).toLocaleString('en-US')} GB`;
  if (Number.isInteger(value)) return `${value.toLocaleString('en-US')} TB`;
  return `${value.toLocaleString('en-US', { maximumFractionDigits: 2 })} TB`;
}

function bytesLabel(bytes) {
  const value = Number(bytes || 0);
  if (!Number.isFinite(value) || value <= 0) return '0 B';
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  let size = value;
  let unit = 0;
  while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1; }
  return `${size.toLocaleString('en-US', { maximumFractionDigits: unit === 0 ? 0 : 1 })} ${units[unit]}`;
}

function findNodeAccount(node, accounts = []) {
  const nodeId = String(node.nodeId || node.id || node.deviceId || '');
  return accounts.find((account) => String(account.ownerType || '') === 'node' && String(account.ownerId || account.nodeId || account.id || '') === nodeId) || null;
}

function isExplorerParticipantNode(node) {
  const metadata = node.metadata || {};
  const endpoint = String(node.endpoint || '');
  const deviceName = String(node.deviceName || node.name || '');
  if (metadata.seeded === true) return false;
  if (endpoint.startsWith('mock://')) return false;
  if (/mock participant/i.test(deviceName)) return false;
  return true;
}

function collectNodeUserKeys(node, account) {
  const metadata = node.metadata || {};
  const accountMetadata = account?.metadata || {};
  return [
    node.userId,
    node.userid,
    node.username,
    node.loginId,
    node.userIdHash,
    node.ownerId,
    node.ownerUserId,
    node.ownerMongoId,
    node.accountId,
    node.aisforUserId,
    node.createdBy,
    metadata.userId,
    metadata.userid,
    metadata.username,
    metadata.loginId,
    metadata.userIdHash,
    metadata.ownerId,
    metadata.ownerUserId,
    metadata.ownerMongoId,
    metadata.ownerUserIdHash,
    metadata.accountId,
    metadata.aisforUserId,
    metadata.createdBy,
    account?.userId,
    account?.userid,
    account?.username,
    account?.loginId,
    account?.userIdHash,
    account?.ownerId,
    account?.ownerUserId,
    account?.ownerMongoId,
    account?.accountId,
    accountMetadata.userId,
    accountMetadata.userid,
    accountMetadata.username,
    accountMetadata.loginId,
    accountMetadata.userIdHash,
    accountMetadata.ownerId,
    accountMetadata.ownerUserId,
    accountMetadata.ownerMongoId,
    accountMetadata.ownerUserIdHash,
    accountMetadata.accountId
  ]
    .map(normalizeKey).filter(Boolean);
}

function nodeMatchesSessionUser(node, account, sessionUser) {
  const userIds = getSessionUserKeys(sessionUser);
  if (!userIds.length) return false;
  const nodeKeys = collectNodeUserKeys(node, account);
  return userIds.some((userId) => nodeKeys.includes(userId));
}

function getCollateralRequired(capacityTb, config) {
  const rate = Number(config.trxPerTb || config.trxPerUnit || 0);
  const base = Number(config.baseCapacityTb || config.minimumCapacityTb || 1) || 1;
  if (!rate) return 0;
  return Math.max(rate * (Number(capacityTb || 0) / base), Number(config.minimumNodeCollateralTrx || 0));
}

function firstStringValue(source, keys) {
  if (!source) return '';
  const metadata = source.metadata || {};
  for (const key of keys) {
    const value = source[key] ?? metadata[key];
    if (value && typeof value === 'object') {
      const nested = value.address || value.walletAddress || value.publicAddress || value.id || value._id;
      if (nested !== undefined && nested !== null && String(nested).trim()) return String(nested).trim();
      continue;
    }
    if (value !== undefined && value !== null && String(value).trim()) return String(value).trim();
  }
  return '';
}

function walletAddressValue(wallet) {
  return firstStringValue(wallet, ['address', 'walletAddress', 'publicAddress', 'id']);
}

function walletAddressKey(wallet) {
  return normalizeKey(walletAddressValue(wallet));
}

function collectWalletUserKeys(wallet) {
  const metadata = wallet?.metadata || {};
  return [
    wallet?.userId, wallet?.userid, wallet?.username, wallet?.loginId,
    wallet?.userIdHash, wallet?.ownerId, wallet?.ownerUserId,
    wallet?.ownerMongoId, wallet?.accountId, wallet?.aisforUserId, wallet?.createdBy,
    metadata.userId, metadata.userid, metadata.username, metadata.loginId,
    metadata.userIdHash, metadata.ownerId, metadata.ownerUserId,
    metadata.ownerMongoId, metadata.ownerUserIdHash, metadata.accountId,
    metadata.aisforUserId, metadata.createdBy
  ].map(normalizeKey).filter(Boolean);
}

function walletMatchesSessionUser(wallet, sessionUser) {
  const userIds = getSessionUserKeys(sessionUser);
  if (!userIds.length) return false;
  const walletKeys = collectWalletUserKeys(wallet);
  return userIds.some((userId) => walletKeys.includes(userId));
}

function collectTransactionUserKeys(tx) {
  const metadata = tx?.metadata || {};
  return [
    tx?.userId, tx?.userid, tx?.username, tx?.loginId,
    tx?.fromUserId, tx?.toUserId, tx?.ownerUserId, tx?.accountId, tx?.createdBy,
    metadata.userId, metadata.userid, metadata.username, metadata.loginId,
    metadata.fromUserId, metadata.toUserId, metadata.ownerUserId,
    metadata.accountId, metadata.createdBy
  ].map(normalizeKey).filter(Boolean);
}

function transactionAddress(tx, side) {
  const fromKeys = ['fromAddress', 'from', 'senderAddress', 'sender', 'sourceAddress', 'source', 'fromWalletAddress', 'fromWallet'];
  const toKeys = ['toAddress', 'to', 'recipientAddress', 'recipient', 'targetAddress', 'target', 'toWalletAddress', 'toWallet'];
  return firstStringValue(tx, side === 'from' ? fromKeys : toKeys);
}

function walletTxAmount(tx) {
  const metadata = tx?.metadata || {};
  const values = [tx?.amount, tx?.amountPla, tx?.amountPLAI, tx?.value, tx?.quantity, metadata.amount, metadata.value];
  for (const value of values) {
    const number = Number(value);
    if (Number.isFinite(number) && number !== 0) return Math.abs(number);
  }
  return 0;
}

function buildWalletTransferExplorer(wallets = [], walletTransactions = [], sessionUser = null) {
  const personalMode = hasSessionUser(sessionUser);
  const ownedWallets = personalMode ? wallets.filter((wallet) => walletMatchesSessionUser(wallet, sessionUser)) : wallets;
  const ownedAddresses = new Set(ownedWallets.map(walletAddressKey).filter(Boolean));
  const sessionKeys = getSessionUserKeys(sessionUser);
  const txRows = latestRows(walletTransactions, 240).map((tx) => {
    const fromAddress = transactionAddress(tx, 'from');
    const toAddress = transactionAddress(tx, 'to');
    const fromMine = ownedAddresses.has(normalizeKey(fromAddress));
    const toMine = ownedAddresses.has(normalizeKey(toAddress));
    let direction = String(tx.direction || tx.metadata?.direction || '').toUpperCase();
    if (personalMode) {
      if (fromMine && toMine) direction = 'SELF';
      else if (fromMine) direction = 'OUT';
      else if (toMine) direction = 'IN';
      else if (!direction) direction = 'TRANSFER';
    } else if (!['IN', 'OUT', 'SELF'].includes(direction)) {
      direction = 'TRANSFER';
    }
    return {
      id: tx.id || tx.txId || tx.txHash || '',
      txHash: tx.txHash || tx.hash || tx.id || tx.txId || '',
      chainHash: tx.chainTxHash || tx.chainTransactionHash || tx.metadata?.chainTxHash || tx.metadata?.chainHash || '',
      ledgerHeight: tx.ledgerHeight || tx.blockHeight || tx.metadata?.ledgerHeight || tx.metadata?.blockHeight || '',
      type: tx.type || tx.kind || 'TRANSFER',
      direction,
      amount: walletTxAmount(tx),
      memo: tx.memo || tx.metadata?.memo || '',
      status: tx.status || tx.metadata?.status || '',
      fromAddress,
      toAddress,
      createdAt: tx.createdAt || tx.timestamp || tx.updatedAt,
      matchesUser: personalMode ? (fromMine || toMine || collectTransactionUserKeys(tx).some((key) => sessionKeys.includes(key))) : true
    };
  }).filter((row) => !personalMode || row.matchesUser).slice(0, 30);
  const summary = txRows.reduce((acc, row) => {
    acc.txCount += 1;
    if (row.direction === 'IN') acc.received += row.amount;
    else if (row.direction === 'OUT') acc.sent += row.amount;
    else if (row.direction === 'SELF') acc.self += row.amount;
    return acc;
  }, { txCount: 0, walletCount: ownedWallets.length, received: 0, sent: 0, self: 0 });
  summary.net = summary.received - summary.sent;
  return { rows: txRows, isPersonal: personalMode, summary };
}
function buildNodeExplorer(nodes = [], sessionUser = null) {
  const config = trxCollateralService.refreshCollateralConfig ? trxCollateralService.refreshCollateralConfig() : (trxCollateralService.getCollateralConfig ? trxCollateralService.getCollateralConfig() : {});
  const accounts = trxCollateralService.listAccounts ? trxCollateralService.listAccounts() : [];
  const events = trxCollateralService.listEvents ? trxCollateralService.listEvents({ limit: 160 }) : [];
  const rows = (nodes || []).filter(isExplorerParticipantNode).map((node) => {
    const account = findNodeAccount(node, accounts);
    const capacityTb = getNodeCapacityTb(node, config);
    const requiredCollateral = getCollateralRequired(capacityTb, config);
    const confirmed = Number(account?.confirmedTrx || account?.confirmedAmount || account?.confirmed || 0);
    const pending = Number(account?.pendingTrx || account?.pendingAmount || account?.pending || 0);
    const coverage = requiredCollateral > 0 ? Math.min(100, Math.round((confirmed / requiredCollateral) * 100)) : 0;
    const status = confirmed >= requiredCollateral && requiredCollateral > 0 ? 'active' : (account?.status || node.status || 'waiting_deposit');
    const nodeId = String(node.nodeId || node.id || node.deviceId || account?.ownerId || '');
    return {
      node,
      account,
      nodeId,
      name: node.deviceName || node.name || nodeId || 'Node',
      service: node.service || node.ownerService || node.metadata?.service || 'PLAI',
      status,
      capacityTb,
      capacityLabel: formatNodeCapacity(capacityTb),
      usedLabel: bytesLabel(node.usedBytes || node.usedStorageBytes || node.metadata?.usedBytes || 0),
      requiredCollateral,
      confirmedCollateral: confirmed,
      pendingCollateral: pending,
      confirmed,
      pending,
      coverage,
      lastHeartbeatAt: node.lastHeartbeatAt || node.heartbeatAt || node.updatedAt || node.createdAt,
      memo: account?.memo || account?.metadata?.memo || (account?.id ? `PLAI-${account.id}` : ''),
      isMine: nodeMatchesSessionUser(node, account, sessionUser)
    };
  });
  const personalMode = hasSessionUser(sessionUser);
  const personalRows = rows.filter(row => row.isMine);
  const visibleRows = personalMode ? personalRows : rows;
  const visibleIds = new Set(visibleRows.flatMap(row => [row.nodeId, row.account?.id, row.account?.ownerId].filter(Boolean).map(String)));
  const recentEvents = latestRows(events.filter(event => visibleIds.has(String(event.accountId || event.ownerId || event.nodeId || event.metadata?.nodeId || ''))), 8);
  const summary = visibleRows.reduce((acc, row) => {
    acc.activeNodes += row.status === 'active' ? 1 : 0;
    acc.totalCapacityTb += row.capacityTb;
    acc.requiredCollateral += row.requiredCollateral;
    acc.confirmedCollateral += row.confirmed;
    acc.pendingCollateral += row.pending;
    return acc;
  }, { activeNodes: 0, totalCapacityTb: 0, requiredCollateral: 0, confirmedCollateral: 0, pendingCollateral: 0 });
  summary.totalNodes = visibleRows.length;
  summary.coverage = summary.requiredCollateral > 0 ? Math.round((summary.confirmedCollateral / summary.requiredCollateral) * 100) : 0;
  summary.totalCapacityLabel = formatNodeCapacity(summary.totalCapacityTb);
  return { config, rows: visibleRows, allRows: rows, recentEvents, isPersonal: personalMode, summary };
}

function getRewardClass(entry = {}) {
  const category = normalizeKey(entry.category);
  const actionType = normalizeKey(entry.actionType);
  const sourceService = normalizeKey(entry.sourceService);
  const refType = normalizeKey(entry.refType);
  const note = normalizeKey(entry.metadata?.note);
  const haystack = [category, actionType, sourceService, refType, note].join(' ');

  if (haystack.includes('slash') || haystack.includes('cancel') || Number(entry.rewardAmount || 0) < 0) {
    return { key: 'correction', labelKo: '차감 / 정정', labelEn: 'Correction' };
  }
  if (category === 'storage' || haystack.includes('storage') || haystack.includes('chunk') || haystack.includes('proof_bundle')) {
    return { key: 'storage', labelKo: '스토리지 보상', labelEn: 'Storage Reward' };
  }
  if (category === 'uptime' || haystack.includes('node_online') || haystack.includes('uptime') || haystack.includes('validator')) {
    return { key: 'node', labelKo: '노드 active 보상', labelEn: 'Node Active Reward' };
  }
  if (haystack.includes('activity') || haystack.includes('message') || haystack.includes('mall') || haystack.includes('group') || haystack.includes('cloud') || haystack.includes('pet') || category === 'service' || category === 'content' || category === 'commerce' || category === 'emoticon' || category === 'ecosystem') {
    return { key: 'app', labelKo: '앱 사용 active 보상', labelEn: 'App Active Reward' };
  }
  return { key: 'other', labelKo: '기타 보상', labelEn: 'Other Reward' };
}

function rewardMatchesSessionUser(entry = {}, sessionUser, nodeExplorer = {}) {
  const userKeys = getSessionUserKeys(sessionUser);
  if (!userKeys.length) return false;
  const actor = normalizeKey(entry.actorId);
  const metadata = entry.metadata || {};
  const capReview = metadata.capReview || {};
  const rewardKeys = [
    actor,
    metadata.userId,
    metadata.userid,
    metadata.username,
    metadata.loginId,
    metadata.ownerId,
    metadata.ownerUserId,
    metadata.nodeId,
    metadata.actorId,
    capReview.actorId
  ].map(normalizeKey).filter(Boolean);
  const myNodeIds = new Set((nodeExplorer.allRows || [])
    .filter(row => row.isMine)
    .map(row => normalizeKey(row.nodeId)));
  return userKeys.some(key => rewardKeys.includes(key)) || myNodeIds.has(actor);
}

function getCurrentPolicyFlags(entry = {}) {
  const validation = rewardPolicyService.validateRewardSubmission({
    sourceService: entry.sourceService,
    category: entry.category,
    actionType: entry.actionType,
    idempotencyKey: entry.metadata?.idempotencyKey || ''
  });
  return new Set([...(validation.flags || []), ...(validation.warnings || [])]);
}

function getActiveRewardFlags(entry = {}) {
  const currentPolicyFlags = getCurrentPolicyFlags(entry);
  return (entry.flags || []).filter((flag) => {
    if (flag === 'SOURCE_CATEGORY_NOT_ALLOWED' || flag === 'SOURCE_EVENT_NOT_LISTED') {
      return currentPolicyFlags.has(flag);
    }
    return true;
  });
}

function getRewardStatus(entry = {}) {
  if (entry.metadata?.reviewDecision?.decision === 'REJECT') return 'rejected';
  if (getActiveRewardFlags(entry).length) return 'review';
  if (entry.blockHeight) return 'anchored';
  return 'pending';
}

function buildRewardExplorer(rewards = [], sessionUser = null, nodeExplorer = {}, lang = 'ko', isAdmin = false) {
  const personalMode = hasSessionUser(sessionUser) && !isAdmin;
  const visibleRewards = personalMode
    ? rewards.filter(entry => rewardMatchesSessionUser(entry, sessionUser, nodeExplorer))
    : rewards;
  const rows = latestRows(visibleRewards, Math.max(visibleRewards.length, 1)).map((entry) => {
    const rewardClass = getRewardClass(entry);
    return {
      entry,
      id: entry.id,
      label: lang === 'en' ? rewardClass.labelEn : rewardClass.labelKo,
      classKey: rewardClass.key,
      sourceService: entry.sourceService || '-',
      actionType: entry.actionType || '-',
      actorId: entry.actorId || '-',
      amount: Number(entry.rewardAmount || 0),
      score: Number(entry.rewardScore || 0),
      symbol: entry.symbol || 'PLAI',
      status: getRewardStatus(entry),
      blockHeight: entry.blockHeight || null,
      blockHash: entry.blockHash || '',
      createdAt: entry.createdAt || entry.metadata?.sourceCreatedAt || null,
      note: entry.metadata?.note || '',
      flags: getActiveRewardFlags(entry),
      refId: entry.refId || entry.metadata?.externalEventKey || entry.metadata?.idempotencyKey || ''
    };
  });
  const summary = rows.reduce((acc, row) => {
    acc.totalAmount += row.amount;
    acc.totalCount += 1;
    if (!acc.byClass[row.classKey]) {
      acc.byClass[row.classKey] = { amount: 0, count: 0, label: row.label };
    }
    acc.byClass[row.classKey].amount += row.amount;
    acc.byClass[row.classKey].count += 1;
    acc.status[row.status] = (acc.status[row.status] || 0) + 1;
    return acc;
  }, {
    totalAmount: 0,
    totalCount: 0,
    byClass: {},
    status: {}
  });
  return {
    isPersonal: personalMode,
    rows,
    summary,
    recentRows: rows.slice(0, 14)
  };
}

function explorerService(testnet) {
  return {
    name: 'PLAI Chain Explorer',
    mode: testnet.config.chainName,
    port: process.env.PORT || 4317
  };
}

function findProof(value) {
  const key = String(value || '').trim();
  if (!key) return null;
  return proofLedgerService.getProof(key) || proofLedgerService.listProofs().find((proof) => (
    proof.proofHash === key ||
    proof.txHash === key ||
    proof.chainTransactionId === key ||
    proof.externalProofId === key ||
    proof.contentHash === key ||
    proof.chunkHash === key ||
    proof.payloadHash === key ||
    proof.bundleHash === key ||
    proof.merkleRoot === key
  )) || null;
}

function findReward(value) {
  const key = String(value || '').trim();
  if (!key) return null;
  return rewardLedgerService.listEntries().find((reward) => (
    reward.id === key ||
    reward.refId === key ||
    reward.metadata?.idempotencyKey === key ||
    reward.metadata?.externalEventKey === key ||
    reward.metadata?.externalProofHash === key
  )) || null;
}

function findRewardAudit(value) {
  return rewardAuditService.findEntry(value);
}

function findRewardReviewDecision(value) {
  const key = String(value || '').trim();
  if (!key) return null;
  return rewardReviewService.listDecisions().find((decision) => (
    decision.id === key ||
    decision.rewardId === key ||
    decision.chainTransactionId === key ||
    decision.txHash === key
  )) || null;
}

function getReviewDecisionTransaction(reviewDecision) {
  if (!reviewDecision) return null;
  return chainTransactionService.getTransaction(reviewDecision.chainTransactionId || reviewDecision.txHash);
}

function getReviewDecisionForTransaction(tx) {
  if (!tx) return null;
  const metadataDecisionId = tx.metadata?.reviewDecisionId || '';
  return findRewardReviewDecision(metadataDecisionId || tx.id || tx.txHash);
}

function redirectSearch(res, q, lang = 'ko') {
  const value = String(q || '').trim();
  if (!value) return res.redirect(withLang('/explorer', lang));

  const wallet = walletService.getWallet(value);
  if (wallet) return res.redirect(withLang(`/explorer/address/${encodeURIComponent(wallet.address)}`, lang));

  const walletTx = walletService.getWalletTransaction(value);
  if (walletTx) return res.redirect(withLang(`/explorer/tx/${encodeURIComponent(walletTx.txHash)}`, lang));

  const chainTx = chainTransactionService.getTransaction(value);
  if (chainTx) return res.redirect(withLang(`/explorer/tx/${encodeURIComponent(chainTx.txHash || chainTx.id)}`, lang));

  const block = blockService.getBlock(value);
  if (block) return res.redirect(withLang(`/explorer/block/${encodeURIComponent(block.height || block.blockHash || block.id)}`, lang));

  const proof = findProof(value);
  if (proof) return res.redirect(withLang(`/explorer/proof/${encodeURIComponent(proof.id || proof.proofHash)}`, lang));

  const reward = findReward(value);
  if (reward) return res.redirect(withLang(`/explorer/reward/${encodeURIComponent(reward.id)}`, lang));

  const rewardAudit = findRewardAudit(value);
  if (rewardAudit) return res.redirect(withLang(`/explorer/reward-audit/${encodeURIComponent(rewardAudit.id)}`, lang));

  const rewardReviewDecision = findRewardReviewDecision(value);
  if (rewardReviewDecision) return res.redirect(withLang(`/explorer/reward-review/${encodeURIComponent(rewardReviewDecision.id)}`, lang));

  return res.redirect(withLang(`/explorer?notFound=${encodeURIComponent(value)}`, lang));
}

router.get('/', (req, res) => {
  const lang = getExplorerLang(req);
  if (req.query.q) return redirectSearch(res, req.query.q, lang);

  const text = EXPLORER_TEXT[lang] || EXPLORER_TEXT.ko;
  const sessionUser = getSessionUser(req);
  const isAdmin = isAdminRequest(req, sessionUser);
  const hasWalletSsoUser = Boolean(req.walletUser && req.walletUser.userId);
  const showAdminNetworkView = isAdmin && !hasWalletSsoUser;
  const explorerUser = showAdminNetworkView ? null : sessionUser;
  const blocks = blockService.listBlocks();
  const proofs = proofLedgerService.listProofs();
  const transactions = chainTransactionService.listTransactions();
  const walletTransactions = walletService.listWalletTransactions();
  const callbacks = callbackService.listCallbacks();
  const rewards = rewardLedgerService.listEntries();
  const rewardAudits = rewardAuditService.listEntries();
  const rewardReviewDecisions = rewardReviewService.listDecisions();
  const nodes = nodeParticipantService.listNodes();
  const testnet = testnetService.getStatus();
  const wallets = walletService.listWallets();
  const blockStats = blockService.getBlockStats();
  const blockChain = blockService.verifyBlockChain();
  const orderedBlocks = blocks.slice().sort((a, b) => (b.height || 0) - (a.height || 0));
  const latestBlock = orderedBlocks[0] || null;
  const nodeExplorer = buildNodeExplorer(nodes, explorerUser);
  const rewardExplorer = buildRewardExplorer(rewards, explorerUser, nodeExplorer, lang, showAdminNetworkView);

  return res.render('explorer', {
    service: explorerService(testnet),
    testnet,
    lang,
    text,
    sessionUser,
    isAdmin: showAdminNetworkView,
    langToggleUrl: buildLangUrl(req, lang === 'ko' ? 'en' : 'ko'),
    urlWithLang: (path) => withLang(path, lang),
    walletStats: walletService.getStats(),
    wallets,
    latestWalletTransactions: latestRows(walletTransactions),
    blockStats,
    blockChain,
    latestBlock,
    blocks: orderedBlocks,
    latestProofs: latestRows(proofs),
    latestRewards: latestRows(rewards),
    latestRewardAudits: latestRows(rewardAudits),
    latestRewardReviewDecisions: latestRows(rewardReviewDecisions, 10),
    latestTransactions: latestRows(transactions),
    latestCallbacks: latestRows(callbacks),
    nodes,
    nodeExplorer,
    rewardExplorer,
    notFound: req.query.notFound || '',
    toDisplayAmount,
    shortHash
  });
});
router.get('/reward-audit/:auditId', (req, res) => {
  const testnet = testnetService.getStatus();
  const audit = findRewardAudit(req.params.auditId);
  const reward = audit?.rewardId ? findReward(audit.rewardId) : null;
  const reviewDecision = reward ? rewardReviewService.getDecisionForReward(reward.id) : null;
  const reviewDecisionTx = getReviewDecisionTransaction(reviewDecision);

  res.render('reward-audit-detail', {
    service: explorerService(testnet),
    testnet,
    audit,
    reward,
    reviewDecision,
    reviewDecisionTx,
    shortHash
  });
});

router.get('/reward/:rewardId', (req, res) => {
  const testnet = testnetService.getStatus();
  const reward = findReward(req.params.rewardId);
  const audit = reward ? findRewardAudit(reward.id) : null;
  const reviewDecision = reward ? rewardReviewService.getDecisionForReward(reward.id) : null;
  const reviewDecisionTx = getReviewDecisionTransaction(reviewDecision);

  res.render('reward-detail', {
    service: explorerService(testnet),
    testnet,
    reward,
    audit,
    reviewDecision,
    reviewDecisionTx,
    shortHash
  });
});

router.get('/reward-review/:decisionId', (req, res) => {
  const testnet = testnetService.getStatus();
  const reviewDecision = findRewardReviewDecision(req.params.decisionId);
  const reward = reviewDecision ? findReward(reviewDecision.rewardId) : null;
  const audit = reward ? findRewardAudit(reward.id) : null;
  const reviewDecisionTx = getReviewDecisionTransaction(reviewDecision);

  res.render('reward-review-detail', {
    service: explorerService(testnet),
    testnet,
    reviewDecision,
    reviewDecisionTx,
    reward,
    audit,
    shortHash
  });
});

router.get('/address/:address', (req, res) => {
  const wallet = walletService.getWallet(req.params.address);
  const testnet = testnetService.getStatus();
  if (!wallet) return res.status(404).render('address', {
    service: explorerService(testnet),
    testnet,
    wallet: null,
    transactions: [],
    walletStats: walletService.getStats(),
    toDisplayAmount,
    shortHash
  });

  res.render('address', {
    service: explorerService(testnet),
    testnet,
    wallet,
    transactions: walletService.getWalletTransactions(wallet.address),
    walletStats: walletService.getStats(),
    toDisplayAmount,
    shortHash
  });
});

router.get('/tx/:txId', (req, res) => {
  const testnet = testnetService.getStatus();
  const walletTx = walletService.getWalletTransaction(req.params.txId);
  const chainTx = chainTransactionService.getTransaction(req.params.txId);
  const tx = walletTx || chainTx || null;
  const reviewDecision = getReviewDecisionForTransaction(chainTx);
  const reward = reviewDecision ? findReward(reviewDecision.rewardId) : null;
  const audit = reward ? findRewardAudit(reward.id) : null;

  res.render('tx-detail', {
    service: explorerService(testnet),
    testnet,
    tx,
    txKind: walletTx ? 'wallet' : (chainTx ? 'chain' : 'missing'),
    reviewDecision,
    reward,
    audit,
    toDisplayAmount,
    shortHash
  });
});

router.get('/block/:blockId', (req, res) => {
  const testnet = testnetService.getStatus();
  const block = blockService.getBlock(req.params.blockId);
  if (!block) return res.status(404).render('block-detail', {
    service: explorerService(testnet),
    testnet,
    block: null,
    chainTransactions: [],
    chainTransactionTraces: [],
    proofs: [],
    rewards: [],
    votes: [],
    shortHash
  });

  const chainTransactions = chainTransactionService.listTransactions()
    .filter(tx => (block.transactionIds || []).includes(tx.id));
  const chainTransactionTraces = chainTransactions.map((tx) => {
    const reviewDecision = getReviewDecisionForTransaction(tx);
    const reward = reviewDecision ? findReward(reviewDecision.rewardId) : null;
    const audit = reward ? findRewardAudit(reward.id) : null;
    return {
      txId: tx.id,
      reviewDecision,
      reward,
      audit
    };
  });
  const proofs = proofLedgerService.listProofs()
    .filter(proof => (block.proofIds || []).includes(proof.id));
  const rewards = rewardLedgerService.listEntries()
    .filter(reward => (block.rewardIds || []).includes(reward.id));
  const votes = blockService.getVotesForBlock(block.id);

  return res.render('block-detail', {
    service: explorerService(testnet),
    testnet,
    block,
    chainTransactions,
    chainTransactionTraces,
    proofs,
    rewards,
    votes,
    shortHash
  });
});

router.get('/proof/:proofId', (req, res) => {
  const testnet = testnetService.getStatus();
  const proof = findProof(req.params.proofId);
  const chainTx = proof?.chainTransactionId ? chainTransactionService.getTransaction(proof.chainTransactionId) : null;
  const block = proof?.blockId ? blockService.getBlock(proof.blockId) : null;

  res.render('proof-detail', {
    service: explorerService(testnet),
    testnet,
    proof,
    chainTx,
    block,
    shortHash
  });
});

module.exports = router;


