import type { MarketData } from '../adapters/base-adapter';
import type { TokenMarketData } from '../database/repositories/token-repo';
import { getChainConfig } from '../config/chains';
import { getCanonicalTokenByAddress } from '../config/canonical-tokens';
import {
  ethUsdOracleDriftBps,
  readChain138EthOracleDriftBps,
  resolveCanonicalPriceUsd,
  type CanonicalPriceResolution,
} from './canonical-price-oracle';

const CHAIN_138 = 138;

/**
 * When true (default), bridged GRU v2 **`cW*`** tokens (non–Chain-138) prefer DexScreener before CoinGecko/CMC.
 * Native Chain 138 assets use **`c*`** naming (e.g. cUSDT); those are not bridged `cW*` and this flag does not apply on 138.
 */
function readEdgeCwDexFirst(): boolean {
  const raw = process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST;
  if (raw === undefined || raw === '') return true;
  return raw === '1' || raw.toLowerCase() === 'true' || raw.toLowerCase() === 'yes';
}

/**
 * True for **bridged** GRU v2 transport on a public network: symbols **`cW*`** (e.g. cWUSDT, cWEURC, cWXAUC, cWETH).
 * On **Chain 138**, native GRU v2 uses **`c*`** (cUSDT, cUSDC, …), not `cW*` — this helper always returns false on 138
 * so valuation for home-chain assets follows the default layer order.
 */
export function isGruV2CwMeshEdgeToken(chainId: number, normalizedAddress: string): boolean {
  if (chainId === CHAIN_138) return false;
  const spec = getCanonicalTokenByAddress(chainId, normalizedAddress.toLowerCase());
  if (!spec) return false;
  return /^cW/i.test(spec.symbol.trim());
}

export type PriceSourceLayer =
  | 'native_oracle'
  | 'indexer_market'
  | 'external_coingecko'
  | 'external_coinmarketcap'
  | 'external_dexscreener'
  | 'canonical_env'
  | 'canonical_repo_fallback'
  | 'none';

export interface ExplorerLinks {
  chainId: number;
  explorerBaseUrl?: string;
  /** Primary explorer URL for this contract (Blockscout/Etherscan style). */
  addressUrl?: string;
  /** Alias for UIs that expect a “token page”; often same as address on explorers. */
  tokenUrl?: string;
}

export interface TokenPricing {
  /** Omitted when no layer produced a price. */
  priceUsd?: number;
  sourceLayer: PriceSourceLayer;
  /** 1 = highest precedence (indexer). */
  precedenceRank: number;
  /** True when the chosen layer is older than configured max age (indexer only today). */
  stale: boolean;
  maxAgeSeconds: number;
  /** ISO 8601 — effective “as of” for the displayed price. */
  asOf: string;
  ageSeconds?: number;
  indexerLastUpdated?: string;
  referenceSymbol?: string;
  canonicalResolutionSource?: 'env' | 'on-chain' | 'repo-fallback' | 'unresolved';
  /** Bridged `cW*` on an edge chain used DexScreener-priority ordering (native `c*` on 138 never sets this). */
  gruV2CwEdgeDexPriority?: boolean;
}

export interface ValuationInput {
  chainId: number;
  normalizedAddress: string;
  indexer: TokenMarketData | null;
  coingecko?: MarketData | null;
  cmc?: MarketData | null;
  dexscreener?: MarketData | null;
}

function readIndexerMaxAgeSeconds(): number {
  const raw = process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS;
  const n = raw ? parseInt(raw, 10) : NaN;
  return Number.isFinite(n) && n > 0 ? n : 900;
}

function ageMsOf(date: Date): number {
  return Date.now() - date.getTime();
}

function iso(d: Date): string {
  return d.toISOString();
}

/** Indexer rows seeded from the static FX snapshot should not beat live reference feeds. */
function indexerMatchesRepoFallbackSnapshot(
  indexer: TokenMarketData,
  canonical: CanonicalPriceResolution
): boolean {
  if (canonical.source !== 'repo-fallback' || canonical.priceUsd === undefined) {
    return false;
  }
  if (indexer.priceUsd === undefined || indexer.priceUsd === null) {
    return false;
  }
  return Math.abs(indexer.priceUsd - canonical.priceUsd) < 1e-6;
}

/**
 * Layered USD valuation:
 * - Default: indexer (staleness-aware) → CoinGecko → CMC → DexScreener → canonical env/repo.
 * - **Bridged GRU v2 `cW*` on edge networks** (not Chain 138; native **`c*`** lives on 138 only): indexer →
 *   **DexScreener → CoinGecko → CMC** → canonical, so that chain’s DEX/aggregated prices lead before generic repo FX.
 * If the indexer price exists but is stale, we fall through to fresher layers before accepting the stale indexer.
 */
export function resolveUsdValuation(input: ValuationInput): TokenPricing {
  const maxAgeSeconds = readIndexerMaxAgeSeconds();
  const { chainId, normalizedAddress, indexer } = input;

  const edgeCwDexPriority = readEdgeCwDexFirst() && isGruV2CwMeshEdgeToken(chainId, normalizedAddress);

  const canonical = resolveCanonicalPriceUsd(chainId, normalizedAddress);
  const referenceSymbol = canonical.referenceSymbol;
  const canonicalResolutionSource = canonical.source === 'unresolved' ? 'unresolved' : canonical.source;

  type Candidate = {
    layer: PriceSourceLayer;
    rank: number;
    priceUsd: number;
    stale: boolean;
    asOf: Date;
  };

  const out: Candidate[] = [];

  const onChainEthUsd =
    chainId === CHAIN_138 &&
    referenceSymbol === 'ETH' &&
    canonical.source === 'on-chain' &&
    canonical.priceUsd !== undefined
      ? canonical.priceUsd
      : undefined;

  if (onChainEthUsd !== undefined) {
    out.push({
      layer: 'native_oracle',
      rank: 0,
      priceUsd: onChainEthUsd,
      stale: false,
      asOf: new Date(),
    });
  }

  if (indexer?.priceUsd !== undefined && indexer.priceUsd !== null) {
    const lu = indexer.lastUpdated instanceof Date ? indexer.lastUpdated : new Date(indexer.lastUpdated);
    let stale =
      ageMsOf(lu) > maxAgeSeconds * 1000 || indexerMatchesRepoFallbackSnapshot(indexer, canonical);
    if (
      onChainEthUsd !== undefined &&
      ethUsdOracleDriftBps(indexer.priceUsd, onChainEthUsd) > readChain138EthOracleDriftBps()
    ) {
      stale = true;
    }
    out.push({
      layer: 'indexer_market',
      rank: 1,
      priceUsd: indexer.priceUsd,
      stale,
      asOf: lu,
    });
  }

  const pushExternal = (layer: PriceSourceLayer, rank: number, md: MarketData | null | undefined): void => {
    if (!md || md.priceUsd === undefined || md.priceUsd === null) return;
    const asOf = md.lastUpdated instanceof Date ? md.lastUpdated : new Date();
    out.push({
      layer,
      rank,
      priceUsd: md.priceUsd,
      stale: false,
      asOf,
    });
  };

  if (edgeCwDexPriority) {
    pushExternal('external_dexscreener', 2, input.dexscreener);
    pushExternal('external_coingecko', 3, input.coingecko);
    pushExternal('external_coinmarketcap', 4, input.cmc);
  } else {
    pushExternal('external_coingecko', 2, input.coingecko);
    pushExternal('external_coinmarketcap', 3, input.cmc);
    pushExternal('external_dexscreener', 4, input.dexscreener);
  }

  if (canonical.priceUsd !== undefined && canonical.source === 'env') {
    out.push({
      layer: 'canonical_env',
      rank: 5,
      priceUsd: canonical.priceUsd,
      stale: false,
      asOf: new Date(),
    });
  }

  if (canonical.priceUsd !== undefined && canonical.source === 'repo-fallback') {
    out.push({
      layer: 'canonical_repo_fallback',
      rank: 6,
      priceUsd: canonical.priceUsd,
      stale: false,
      asOf: new Date(),
    });
  }

  const byRank = [...out].sort((a, b) => a.rank - b.rank);
  const fresh = byRank.find((c) => !c.stale);
  const winner = fresh || byRank[0];

  if (!winner) {
    return {
      sourceLayer: 'none',
      precedenceRank: 99,
      stale: true,
      maxAgeSeconds,
      asOf: iso(new Date()),
      referenceSymbol,
      canonicalResolutionSource,
      gruV2CwEdgeDexPriority: edgeCwDexPriority,
    };
  }

  const ageSeconds = Math.max(0, Math.floor(ageMsOf(winner.asOf) / 1000));

  return {
    priceUsd: winner.priceUsd,
    sourceLayer: winner.layer,
    precedenceRank: winner.rank,
    stale: winner.stale,
    maxAgeSeconds,
    asOf: iso(winner.asOf),
    ageSeconds,
    indexerLastUpdated: indexer?.lastUpdated
      ? indexer.lastUpdated instanceof Date
        ? iso(indexer.lastUpdated)
        : String(indexer.lastUpdated)
      : undefined,
    referenceSymbol,
    canonicalResolutionSource,
    gruV2CwEdgeDexPriority: edgeCwDexPriority,
  };
}

/**
 * Chooses one external feed for persisting indexer `market_data` rows — same **coingecko / dexscreener / cmc** order as
 * {@link resolveUsdValuation} (bridged **`cW*`** on edge chains prefer DexScreener when enabled).
 */
export function pickExternalMarketDataForIndexer(
  chainId: number,
  normalizedAddress: string,
  sources: { coingecko: MarketData | null; cmc: MarketData | null; dexscreener: MarketData | null }
): MarketData | null {
  const edgeCw = readEdgeCwDexFirst() && isGruV2CwMeshEdgeToken(chainId, normalizedAddress);
  if (edgeCw) {
    return sources.dexscreener ?? sources.coingecko ?? sources.cmc ?? null;
  }
  return sources.coingecko ?? sources.dexscreener ?? sources.cmc ?? null;
}

/**
 * Merge DB/indexer market row with layered valuation (priceUsd + optional fields).
 */
export function mergeMarketWithValuation(
  chainId: number,
  normalizedAddress: string,
  marketData: TokenMarketData | null,
  pricing: TokenPricing
): TokenMarketData | null {
  const addr = normalizedAddress.toLowerCase();
  if (pricing.sourceLayer === 'none' && !marketData) {
    return null;
  }
  if (!marketData) {
    if (pricing.priceUsd === undefined) {
      return null;
    }
    return {
      chainId,
      tokenAddress: addr,
      priceUsd: pricing.priceUsd,
      volume24h: 0,
      volume7d: 0,
      volume30d: 0,
      liquidityUsd: 0,
      holdersCount: 0,
      transfers24h: 0,
      lastUpdated: new Date(pricing.asOf),
    };
  }
  return {
    ...marketData,
    priceUsd: pricing.priceUsd !== undefined ? pricing.priceUsd : marketData.priceUsd,
  };
}

export function buildExplorerLinks(chainId: number, address: string): ExplorerLinks {
  const cfg = getChainConfig(chainId);
  const base = cfg?.explorerUrl?.replace(/\/$/, '') || '';
  const lower = address.toLowerCase();
  if (!base) {
    return { chainId };
  }
  const url = `${base}/address/${lower}`;
  return {
    chainId,
    explorerBaseUrl: base,
    addressUrl: url,
    tokenUrl: url,
  };
}
