import { Router, Request, Response } from 'express';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { getNetworks, getConfigByChain, API_VERSION, type NetworkEntry } from '../../config/networks';
import { toWalletAddEthereumChainParams } from '../../lib/wallet-add-ethereum-chain';
import { dedupeWatchAssetEntriesBySymbol, isWalletWatchAssetEligible } from '../../lib/wallet-watch-eligible';
import { getCanonicalTokensByChain, getLogoUriForSpec, getTokenRegistryFamily } from '../../config/canonical-tokens';
import { cacheMiddleware } from '../middleware/cache';
import { fetchRemoteJson } from '../utils/fetch-remote-json';
import { logger } from '../../utils/logger';

const router: Router = Router();
const DEFAULT_PUBLIC_BASE_URL = 'https://explorer.d-bis.org';
const DEFAULT_WALLET_METADATA_VERSION = '20260510';

function resolvePublicBaseUrl(req: Request): string {
  const configured = (
    process.env.TOKEN_AGGREGATION_PUBLIC_BASE_URL ??
    process.env.PUBLIC_API_BASE_URL ??
    process.env.PUBLIC_BASE_URL ??
    ''
  ).trim();
  if (configured) return configured.replace(/\/+$/, '');

  const host = String(req.get('x-forwarded-host') || req.get('host') || '').split(',')[0].trim();
  if (host) {
    let proto = String(req.get('x-forwarded-proto') || 'https').split(',')[0].trim() || 'https';
    if (proto === 'http' && !/^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(host)) {
      proto = 'https';
    }
    return `${proto}://${host}`.replace(/\/+$/, '');
  }

  return DEFAULT_PUBLIC_BASE_URL;
}

function absolutePublicUrl(req: Request, value: string | undefined): string | undefined {
  if (!value) return undefined;
  if (/^https?:\/\//i.test(value)) return value;
  if (!value.startsWith('/')) return value;
  return `${resolvePublicBaseUrl(req)}${value}`;
}

function appendWalletMetadataVersion(value: string | undefined): string | undefined {
  if (!value) return undefined;
  const version = (process.env.WALLET_METADATA_IMAGE_VERSION || DEFAULT_WALLET_METADATA_VERSION).trim();
  if (!version) return value;
  const separator = value.includes('?') ? '&' : '?';
  return `${value}${separator}v=${encodeURIComponent(version)}`;
}

function localLogoPathForSymbol(symbol: string, originalLogoUri: string): string {
  if (originalLogoUri.includes('/token-lists/logos/gru/')) {
    const fileName = originalLogoUri.split('/').pop()?.replace(/\.svg$/i, '');
    if (fileName) return `/api/v1/report/logo/${fileName}`;
  }
  if (originalLogoUri.includes('/blockchains/bitcoin/info/logo.png')) return '/api/v1/report/logo/cWBTC';
  if (originalLogoUri.includes('/ipfs/')) {
    const cid = originalLogoUri.split('/').pop();
    if (cid) return `/api/v1/report/logo/ipfs-${cid}`;
  }
  if (symbol === 'cWUSDC') return '/api/v1/report/logo/cUSDC';
  return originalLogoUri;
}

function resolveWalletWatchAssetSymbol(spec: { symbol: string; familySymbol?: string; deploymentVersion?: string }): string {
  // MetaMask validates wallet_watchAsset.symbol against ERC-20 symbol().
  // Staged V2 Chain 138 deployments currently keep the family symbol on-chain
  // (for example cUSDC), while the catalog symbol distinguishes the row
  // (for example cUSDC_V2). Keep the catalog identity in metadata and send the
  // contract-facing symbol in options.symbol so EIP-747 succeeds.
  if (spec.deploymentVersion && spec.familySymbol) return spec.familySymbol;
  return spec.symbol;
}

function usesGenericEthDiamondIcons(iconUrls: unknown): boolean {
  if (!Array.isArray(iconUrls) || iconUrls.length === 0) return true;
  return iconUrls.every(
    (url) => typeof url === 'string' && url.includes('eth-diamond-black.png'),
  );
}

function enrichNetworksWithBuiltInMetadata(payload: NetworksPayload): NetworksPayload {
  const builtInByChain = new Map(getNetworks().map((entry) => [entry.chainIdDecimal, entry]));
  const networks = (payload.networks as NetworkEntry[]).map((network) => {
    const builtIn = builtInByChain.get(Number(network.chainIdDecimal));
    if (!builtIn) return network;
    const oracles =
      Array.isArray(network.oracles) && network.oracles.length > 0 ? network.oracles : builtIn.oracles;
    const iconUrls =
      usesGenericEthDiamondIcons(network.iconUrls) && builtIn.iconUrls?.length
        ? builtIn.iconUrls
        : network.iconUrls;
    return { ...network, oracles, iconUrls };
  });
  return { ...payload, networks };
}

type RuntimeNetworksPayload = {
  version?: string | { major?: number; minor?: number; patch?: number };
  networks?: unknown[];
  chains?: unknown[];
};

type NetworksPayload = {
  source: 'remote-url' | 'runtime-file' | 'built-in';
  version: string;
  networks: unknown[];
  lastModified?: string;
};

function uniquePaths(paths: Array<string | undefined | null>): string[] {
  const seen = new Set<string>();
  const out: string[] = [];

  for (const candidate of paths) {
    if (typeof candidate !== 'string') continue;
    const trimmed = candidate.trim();
    if (!trimmed || seen.has(trimmed)) continue;
    seen.add(trimmed);
    out.push(trimmed);
  }

  return out;
}

function resolveRuntimeNetworksPath(): string | null {
  const candidates = uniquePaths([
    process.env.NETWORKS_JSON_PATH,
    process.env.CONFIG_NETWORKS_JSON_PATH,
    '/opt/explorer/config/metamask/DUAL_CHAIN_NETWORKS.json',
    path.join(os.homedir(), 'projects/explorer-monorepo/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json'),
    path.join(os.homedir(), 'projects/explorer-monorepo/backend/config/metamask/DUAL_CHAIN_NETWORKS.json'),
    path.join(os.homedir(), 'projects/proxmox/docs/04-configuration/metamask/DUAL_CHAIN_NETWORKS.json'),
  ]);

  for (const candidate of candidates) {
    if (fs.existsSync(candidate)) return candidate;
  }
  return null;
}

function normalizeVersion(value: RuntimeNetworksPayload['version']): string {
  if (typeof value === 'string' && value.trim() !== '') return value.trim();
  if (value && typeof value === 'object') {
    const major = 'major' in value ? Number(value.major ?? 0) : 0;
    const minor = 'minor' in value ? Number(value.minor ?? 0) : 0;
    const patch = 'patch' in value ? Number(value.patch ?? 0) : 0;
    return `${major}.${minor}.${patch}`;
  }
  return API_VERSION;
}

function extractNetworks(payload: RuntimeNetworksPayload | { version?: string; networks?: unknown[] }): unknown[] {
  if (Array.isArray(payload.networks)) return payload.networks;
  if ('chains' in payload && Array.isArray(payload.chains)) return payload.chains;
  return [];
}

async function loadRemoteNetworksPayload(): Promise<NetworksPayload | null> {
  const networksJsonUrl = process.env.NETWORKS_JSON_URL?.trim();
  if (!networksJsonUrl) return null;

  try {
    const data = (await fetchRemoteJson(networksJsonUrl)) as RuntimeNetworksPayload;
    return {
      source: 'remote-url',
      version: normalizeVersion(data.version),
      networks: extractNetworks(data),
    };
  } catch (error) {
    logger.error('NETWORKS_JSON_URL fetch failed, trying runtime file/built-in networks:', error);
    return null;
  }
}

function loadRuntimeNetworksPayload(): NetworksPayload | null {
  const filePath = resolveRuntimeNetworksPath();
  if (!filePath) return null;

  try {
    const raw = fs.readFileSync(filePath, 'utf8');
    const stat = fs.statSync(filePath);
    const parsed = JSON.parse(raw) as RuntimeNetworksPayload;

    return {
      source: 'runtime-file',
      version: normalizeVersion(parsed.version),
      networks: extractNetworks(parsed),
      lastModified: stat.mtime.toISOString(),
    };
  } catch (error) {
    logger.error('NETWORKS_JSON_PATH read failed, using built-in networks:', error);
    return null;
  }
}

async function resolveNetworksPayload(): Promise<NetworksPayload> {
  const remotePayload = await loadRemoteNetworksPayload();
  if (remotePayload) return enrichNetworksWithBuiltInMetadata(remotePayload);

  const runtimePayload = loadRuntimeNetworksPayload();
  if (runtimePayload) return enrichNetworksWithBuiltInMetadata(runtimePayload);

  return {
    source: 'built-in',
    version: API_VERSION,
    networks: getNetworks(),
  };
}

async function sendNetworks(_req: Request, res: Response): Promise<void> {
  res.set('Cache-Control', 'public, max-age=0, must-revalidate');
  const payload = await resolveNetworksPayload();
  res.json(payload);
}

/**
 * GET /api/v1/networks
 * Full EIP-3085 chain params for wallet_addEthereumChain (Chain 138, 1, 651940).
 * If NETWORKS_JSON_URL is set (e.g. GitHub raw URL), fetches and returns that JSON; otherwise uses built-in networks.
 */
router.get(['/networks', '/config/networks', '/metamask/networks'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
  try {
    await sendNetworks(req, res);
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

/**
 * GET /api/v1/config/metamask
 * Wallet-facing aliases for Chain 138 add-chain params and watchAsset token entries.
 */
router.get(['/config/metamask', '/metamask'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
  try {
    res.set('Cache-Control', 'public, max-age=0, must-revalidate');
    const chainId = parseInt(String(req.query.chainId ?? '138'), 10) || 138;
    const payload = await resolveNetworksPayload();
    const networks = payload.networks as NetworkEntry[];
    const network = networks.find((entry) => Number(entry.chainIdDecimal) === chainId);
    if (!network) {
      res.status(404).json({ error: 'Chain not found', chainId });
      return;
    }

    const watchAssets = dedupeWatchAssetEntriesBySymbol(
      getCanonicalTokensByChain(chainId)
        .filter((spec) => isWalletWatchAssetEligible(spec, chainId))
        .map((spec) => {
          const address = spec.addresses[chainId];
          if (!address) return null;
          const originalLogoURI = getLogoUriForSpec(spec);
          return {
            type: 'ERC20' as const,
            options: {
              address,
              symbol: resolveWalletWatchAssetSymbol(spec),
              decimals: spec.decimals,
              image: appendWalletMetadataVersion(
                absolutePublicUrl(req, localLogoPathForSymbol(spec.symbol, originalLogoURI)),
              ),
            },
            metadata: {
              name: spec.name,
              catalogSymbol: spec.symbol,
              registryFamily: getTokenRegistryFamily(spec),
              familySymbol: spec.familySymbol,
              deploymentVersion: spec.deploymentVersion,
              deploymentStatus: spec.deploymentStatus,
            },
          };
        })
        .filter((entry): entry is NonNullable<typeof entry> => Boolean(entry)),
    );

    const publicBase = resolvePublicBaseUrl(req);

    res.json({
      source: payload.source,
      version: payload.version,
      chainId,
      addEthereumChain: toWalletAddEthereumChainParams(network),
      watchAssets,
      priceFeed: {
        iso4217: true,
        spotPricesV2: `${publicBase}/token-aggregation/api/v1/prices/metamask/v2/chains/${chainId}/spot-prices?tokenAddresses={addresses}&vsCurrency=usd`,
        submissionBundle: `${publicBase}/token-aggregation/api/v1/report/metamask-price-feed?chainId=${chainId}`,
        coingeckoReport: `${publicBase}/token-aggregation/api/v1/report/coingecko?chainId=${chainId}`,
        note: 'MetaMask native USD display requires chain 138 on price.api.cx.metamask.io or CoinGecko listing; these endpoints supply ISO-4217 valuation until then.',
      },
      caveats: [
        'MetaMask custom-token prices are controlled by MetaMask and its upstream asset/price providers; spotPricesV2 supplies ISO-4217 USD rates but MetaMask will not read them until chain 138 is registered on price.api.cx.metamask.io or tokens are listed on CoinGecko.',
        'MetaMask stores one custom token row per symbol on a network. Staged V2 deployments that share a family symbol with live V1 contracts are omitted here so balances do not disappear after import.',
        'After metadata changes, remove and re-add the custom network/token in MetaMask or use the companion UI to call wallet_addEthereumChain and wallet_watchAsset again.',
      ],
    });
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

/**
 * GET /api/v1/config
 * Oracles (and optional config) per chain. Query: chainId (optional).
 * If chainId provided, returns config for that chain only.
 */
router.get('/config', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
  try {
    res.set('Cache-Control', 'public, max-age=0, must-revalidate');
    const chainIdParam = req.query.chainId as string | undefined;
    const payload = await resolveNetworksPayload();
    const networks = payload.networks as Array<{ chainIdDecimal?: number; oracles?: unknown[] }>;
    if (chainIdParam) {
      const chainId = parseInt(chainIdParam, 10);
      const matchingNetwork = networks.find((network) => Number(network.chainIdDecimal) === chainId);
      const builtIn = getConfigByChain(chainId);
      const runtimeOracles = matchingNetwork?.oracles;
      const config = matchingNetwork
        ? {
            oracles:
              Array.isArray(runtimeOracles) && runtimeOracles.length > 0
                ? runtimeOracles
                : (builtIn?.oracles ?? []),
          }
        : builtIn;
      if (!config) {
        return res.status(404).json({ error: 'Chain not found', chainId });
      }
      return res.json({
        source: payload.source,
        version: payload.version,
        chainId,
        ...config,
      });
    }
    const chains = networks.map((n) => ({
      chainId: Number(n.chainIdDecimal),
      oracles: Array.isArray(n.oracles) ? n.oracles : [],
    }));
    res.json({
      source: payload.source,
      version: payload.version,
      chains,
    });
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

export default router;
