import { AbiCoder, Contract, JsonRpcProvider, ZeroAddress, formatUnits } from 'ethers';
import { getCanonicalTokenByAddress, resolveCanonicalQuoteAddress } from '../config/canonical-tokens';
import { resolveChain138RpcUrl } from '../config/chain138-rpc';
import { getProviderCapabilities } from '../config/provider-capabilities';
import { resolveRoutingPolicy } from '../config/routing-policies';
import {
  getCommodityIntermediateAddresses,
  getDefaultIntermediateAddresses,
  getRoutingAddressForSymbol,
  getRoutingSymbolForAddress,
} from '../config/routing-assets';
import { PlannerMetricsRepository } from '../database/repositories/planner-metrics-repo';
import { RouteGraphBuilder } from './route-graph-builder';
import { quoteChain138DodoV3AmountOut } from './dodo-v3-pilot';
import {
  BridgeRouteCandidate,
  EncodedBridgeIntentPlan,
  EncodedPlannerRoutePlan,
  PlannerAlternative,
  PlannerDecision,
  PlannerLeg,
  PlannerProvider,
  PlannerRequest,
  PlannerResponse,
  RoutingPolicy,
  SwapGraphEdge,
} from './planner-v2-types';

const abiCoder = AbiCoder.defaultAbiCoder();
const ROUTER_V2_RECIPIENT_PLACEHOLDER = ZeroAddress;
const DEFAULT_INTENT_BRIDGE_COORDINATOR_V2 = normalizeAddress('0x7D0022B7e8360172fd9C0bB6778113b7Ea3674E7');
const PROVIDER_PRIORITY: PlannerProvider[] = ['dodo', 'dodo_v3', 'uniswap_v3', 'uniswap_v2', 'sushiswap', 'balancer', 'curve', 'one_inch', 'partner'];
const PROVIDER_ENUM: Partial<Record<PlannerProvider, number>> = {
  dodo: 0,
  uniswap_v3: 1,
  uniswap_v2: 2,
  sushiswap: 3,
  balancer: 4,
  curve: 5,
  one_inch: 6,
  partner: 7,
  dodo_v3: 8,
};
const PROVIDER_GAS_USD: Record<PlannerProvider, number> = {
  dodo: 0.22,
  dodo_v3: 0.3,
  uniswap_v3: 0.28,
  uniswap_v2: 0.24,
  sushiswap: 0.25,
  balancer: 0.34,
  curve: 0.29,
  one_inch: 0.48,
  partner: 0.55,
};
const uniswapQuoterAbi = [
  'function quoteExactInputSingle((address,address,uint256,uint24,uint160) params) view returns (uint256,uint160,uint32,uint256)',
  'function quoteExactInputSingle(address tokenIn,address tokenOut,uint24 fee,uint256 amountIn,uint160 sqrtPriceLimitX96) view returns (uint256)',
] as const;

interface RouteCandidate {
  decision: PlannerDecision;
  estimatedAmountOut: bigint;
  estimatedGasUsd: number;
  score: number;
  legs: PlannerLeg[];
  selectedRouteReason: string;
  rejectedAlternatives: string[];
  riskFlags: string[];
}

function normalizeAddress(value: string): string {
  return value.trim().toLowerCase();
}

let sharedProvider: JsonRpcProvider | null = null;
let sharedProviderUrl = '';

function getProvider(): JsonRpcProvider {
  const rpcUrl = resolveChain138RpcUrl();
  if (!sharedProvider || sharedProviderUrl !== rpcUrl) {
    sharedProvider = new JsonRpcProvider(rpcUrl);
    sharedProviderUrl = rpcUrl;
  }
  return sharedProvider;
}

function safeBigInt(value: string): bigint {
  return BigInt(value);
}

function quoteAmountOut(amountIn: bigint, reserveIn: bigint, reserveOut: bigint): bigint {
  if (amountIn <= 0n || reserveIn <= 0n || reserveOut <= 0n) {
    return 0n;
  }
  const amountInWithFee = amountIn * 997n;
  return (reserveOut * amountInWithFee) / (reserveIn * 1000n + amountInWithFee);
}

function sortEdges(edges: SwapGraphEdge[]): SwapGraphEdge[] {
  return edges.slice().sort((a, b) => {
    const providerDiff = PROVIDER_PRIORITY.indexOf(a.provider) - PROVIDER_PRIORITY.indexOf(b.provider);
    if (providerDiff !== 0) return providerDiff;
    const liquidityDiff = (b.totalLiquidityUsd || 0) - (a.totalLiquidityUsd || 0);
    if (liquidityDiff !== 0) return liquidityDiff;
    return a.tokenOutAddress.localeCompare(b.tokenOutAddress);
  });
}

function decimalsForAddress(chainId: number, address: string): number {
  const spec = getCanonicalTokenByAddress(chainId, address);
  if (spec?.decimals) return Number(spec.decimals);
  const symbol = getRoutingSymbolForAddress(chainId, address);
  if (symbol === 'WETH' || symbol === 'WETH10') return 18;
  return 6;
}

function normalizedOutput(chainId: number, address: string, amount: bigint): number {
  const decimals = decimalsForAddress(chainId, address);
  return Number(formatUnits(amount, decimals));
}

function providerDataHexForEdge(edge: SwapGraphEdge): string {
  if (edge.providerDataHex) {
    return edge.providerDataHex;
  }
  if (edge.provider === 'dodo' && edge.poolAddress) {
    return abiCoder.encode(['address'], [edge.poolAddress]);
  }
  return '0x';
}

function providerDataHexForLeg(leg: PlannerLeg): string | undefined {
  if (leg.providerDataHex && leg.providerDataHex !== '0x') {
    return leg.providerDataHex;
  }
  if (leg.provider === 'dodo' && leg.poolAddress) {
    return abiCoder.encode(['address'], [leg.poolAddress]);
  }
  return undefined;
}

function buildMinAmountOut(amountOut: bigint, maxSlippageBps: number): bigint {
  return (amountOut * BigInt(10_000 - maxSlippageBps)) / 10_000n;
}

function computeConfidence(legs: PlannerLeg[]): number {
  const stalePenalty = legs.some((leg) => (leg.freshnessSeconds || 0) > 1800) ? 0.15 : 0;
  const hopPenalty = Math.max(0, legs.length - 1) * 0.07;
  return Math.max(0.1, Math.min(0.99, 0.92 - stalePenalty - hopPenalty));
}

function computeRiskFlags(legs: PlannerLeg[], policy: RoutingPolicy): string[] {
  const flags = new Set<string>();
  if (legs.length > 1) {
    flags.add('multi-hop');
  }
  if (legs.some((leg) => leg.kind === 'bridge')) {
    flags.add('cross-chain');
  }
  if (legs.some((leg) => (leg.freshnessSeconds || 0) > 1800)) {
    flags.add('stale-liquidity');
  }
  if (!policy.allowCommodityIntermediates && legs.some((leg) => leg.tokenInSymbol === 'cXAUC' || leg.tokenOutSymbol === 'cXAUC')) {
    flags.add('commodity-path-blocked');
  }
  if (legs.some((leg) => leg.provider === 'dodo_v3')) {
    flags.add('pilot-venue');
    if (!legs.every((leg) => leg.provider !== 'dodo_v3' || Boolean(providerDataHexForLeg(leg)))) {
      flags.add('manual-execution-only');
    }
  }
  return Array.from(flags);
}

async function quoteUniswapV3AmountOut(edge: SwapGraphEdge, amountIn: bigint): Promise<bigint> {
  const providerData = (edge.providerData || {}) as { quoter?: string; fee?: number };
  const quoter = normalizeAddress(String(providerData.quoter || ''));
  const fee = Number(providerData.fee || 3000);
  if (!quoter) {
    return 0n;
  }

  const contract = new Contract(quoter, uniswapQuoterAbi, getProvider());
  try {
    const result = await contract.quoteExactInputSingle([
      edge.tokenInAddress,
      edge.tokenOutAddress,
      amountIn,
      fee,
      0,
    ]);
    return BigInt(String(Array.isArray(result) ? result[0] : result));
  } catch {
    try {
      const result = await contract['quoteExactInputSingle(address,address,uint24,uint256,uint160)'](
        edge.tokenInAddress,
        edge.tokenOutAddress,
        fee,
        amountIn,
        0
      );
      return BigInt(String(result));
    } catch {
      return 0n;
    }
  }
}

export class BestExecutionPlanner {
  private graphBuilder: RouteGraphBuilder;
  private plannerRepo: PlannerMetricsRepository;

  constructor(
    graphBuilder = new RouteGraphBuilder(),
    plannerRepo = new PlannerMetricsRepository()
  ) {
    this.graphBuilder = graphBuilder;
    this.plannerRepo = plannerRepo;
  }

  async plan(request: PlannerRequest): Promise<PlannerResponse> {
    const normalizedRequest = {
      ...request,
      tokenIn: normalizeAddress(request.tokenIn),
      tokenOut: normalizeAddress(request.tokenOut),
      destinationChainId: request.destinationChainId || request.sourceChainId,
    };
    const policy = resolveRoutingPolicy(normalizedRequest.sourceChainId, normalizedRequest.constraints || {});
    const requestHash = this.requestHash(normalizedRequest, policy);
    const cached = await this.plannerRepo.getCachedPlan(requestHash);
    if (cached) {
      return cached;
    }

    const capabilities = getProviderCapabilities(normalizedRequest.sourceChainId);
    await this.plannerRepo.recordProviderSnapshots(normalizedRequest.sourceChainId, capabilities);

    const response =
      normalizedRequest.destinationChainId === normalizedRequest.sourceChainId
        ? await this.planOneChain(normalizedRequest, policy)
        : await this.planCrossChain(normalizedRequest, policy);

    await this.plannerRepo.cachePlan(requestHash, response);
    await this.plannerRepo.recordPlannedRouteMetrics(response);
    return response;
  }

  getCapabilities(chainId: number) {
    return getProviderCapabilities(chainId);
  }

  private requestHash(request: PlannerRequest, policy: RoutingPolicy): string {
    return JSON.stringify({
      request,
      policy,
    });
  }

  private async quoteEdgeAmountOut(
    edge: SwapGraphEdge,
    amountIn: bigint,
    quoteCache: Map<string, bigint>
  ): Promise<bigint> {
    if (edge.provider === 'uniswap_v3') {
      const cacheKey = [edge.provider, edge.target, edge.tokenInAddress, edge.tokenOutAddress, amountIn.toString()].join(':');
      const cached = quoteCache.get(cacheKey);
      if (cached !== undefined) {
        return cached;
      }

      const quoted = await quoteUniswapV3AmountOut(edge, amountIn);
      if (quoted > 0n) {
        quoteCache.set(cacheKey, quoted);
        return quoted;
      }
      const fallback = quoteAmountOut(amountIn, safeBigInt(edge.reserveIn), safeBigInt(edge.reserveOut));
      quoteCache.set(cacheKey, fallback);
      return fallback;
    }

    if (edge.provider !== 'dodo_v3') {
      return quoteAmountOut(amountIn, safeBigInt(edge.reserveIn), safeBigInt(edge.reserveOut));
    }

    const cacheKey = [edge.provider, edge.poolAddress, edge.tokenInAddress, edge.tokenOutAddress, amountIn.toString()].join(':');
    const cached = quoteCache.get(cacheKey);
    if (cached !== undefined) {
      return cached;
    }

    try {
      const quoted = await quoteChain138DodoV3AmountOut({
        tokenInAddress: edge.tokenInAddress,
        tokenOutAddress: edge.tokenOutAddress,
        amountIn,
      });
      quoteCache.set(cacheKey, quoted);
      return quoted;
    } catch {
      quoteCache.set(cacheKey, 0n);
      return 0n;
    }
  }

  private async planOneChain(request: PlannerRequest, policy: RoutingPolicy): Promise<PlannerResponse> {
    const sourceResolution = resolveCanonicalQuoteAddress(request.sourceChainId, request.tokenIn);
    const targetResolution = resolveCanonicalQuoteAddress(request.sourceChainId, request.tokenOut);
    const amountIn = safeBigInt(request.amountIn);
    const maxSlippageBps = request.constraints?.maxSlippageBps || 100;

    const edges = await this.graphBuilder.buildSwapEdges(request.sourceChainId);
    const allowedProviders = new Set(policy.allowedProviders);
    const allowedTokens = new Set<string>([
      sourceResolution.lookupAddress,
      targetResolution.lookupAddress,
      ...policy.defaultIntermediateAddresses,
      ...(policy.allowCommodityIntermediates ? getCommodityIntermediateAddresses(request.sourceChainId) : []),
    ]);
    const eligibleEdges = sortEdges(
      edges.filter((edge) => {
        if (!allowedProviders.has(edge.provider)) return false;
        if (!edge.target || !providerDataHexForEdge(edge)) return false;
        if (edge.tokenOutSymbol === 'cXAUC' || edge.tokenInSymbol === 'cXAUC') {
          return policy.allowCommodityIntermediates;
        }
        return allowedTokens.has(edge.tokenOutAddress) || edge.tokenOutAddress === targetResolution.lookupAddress;
      })
    );

    const byTokenIn = new Map<string, SwapGraphEdge[]>();
    for (const edge of eligibleEdges) {
      const key = edge.tokenInAddress;
      const list = byTokenIn.get(key) || [];
      list.push(edge);
      byTokenIn.set(key, list);
    }

    const candidates: RouteCandidate[] = [];
    const visited = new Set<string>([sourceResolution.lookupAddress]);
    const quoteCache = new Map<string, bigint>();
    const dfs = async (
      currentToken: string,
      currentAmount: bigint,
      path: PlannerLeg[],
      depth: number
    ): Promise<void> => {
      if (currentToken === targetResolution.lookupAddress && path.length > 0) {
        candidates.push(this.toCandidate(path, currentAmount, request.sourceChainId, targetResolution.lookupAddress, policy));
        return;
      }

      if (depth >= policy.maxLegs) {
        return;
      }

      for (const edge of byTokenIn.get(currentToken) || []) {
        if (visited.has(edge.tokenOutAddress)) continue;
        const quotedAmount = await this.quoteEdgeAmountOut(edge, currentAmount, quoteCache);
        if (quotedAmount <= 0n) continue;

        const leg: PlannerLeg = {
          kind: 'swap',
          provider: edge.provider,
          sourceChainId: request.sourceChainId,
          destinationChainId: request.sourceChainId,
          tokenInAddress: edge.tokenInAddress,
          tokenOutAddress: edge.tokenOutAddress,
          tokenInSymbol: edge.tokenInSymbol,
          tokenOutSymbol: edge.tokenOutSymbol,
          estimatedAmountIn: currentAmount.toString(),
          estimatedAmountOut: quotedAmount.toString(),
          minAmountOut: buildMinAmountOut(quotedAmount, maxSlippageBps).toString(),
          target: edge.target,
          poolAddress: edge.poolAddress,
          providerData: edge.providerData,
          providerDataHex: providerDataHexForEdge(edge),
          gasEstimate: Math.round((PROVIDER_GAS_USD[edge.provider] / 2500) * 1_000_000),
          freshnessSeconds: edge.freshnessSeconds,
          totalLiquidityUsd: edge.totalLiquidityUsd,
          notes: edge.notes,
        };

        visited.add(edge.tokenOutAddress);
        await dfs(edge.tokenOutAddress, quotedAmount, [...path, leg], depth + 1);
        visited.delete(edge.tokenOutAddress);
      }
    };

    await dfs(sourceResolution.lookupAddress, amountIn, [], 0);
    const sortedCandidates = candidates.sort((a, b) => b.score - a.score);
    const best = sortedCandidates[0];

    if (!best) {
      return this.emptyResponse(request, 'unresolved', 'No eligible one-chain route found with the current policy and provider capabilities.');
    }

    const routePlan = this.buildEncodedRoutePlan(
      request,
      best.legs,
      sourceResolution.lookupAddress,
      targetResolution.lookupAddress
    );
    return this.toResponse(
      request,
      best,
      sortedCandidates.slice(1, 4),
      'One-chain best execution selected from live pool graph.',
      routePlan
    );
  }

  private async planCrossChain(request: PlannerRequest, policy: RoutingPolicy): Promise<PlannerResponse> {
    const sourceResolution = resolveCanonicalQuoteAddress(request.sourceChainId, request.tokenIn);
    const destinationResolution = resolveCanonicalQuoteAddress(request.destinationChainId || request.sourceChainId, request.tokenOut);
    const amountIn = safeBigInt(request.amountIn);

    const candidateBridgeSymbols = Array.from(
      new Set(
        policy.defaultIntermediateAddresses
          .map((address) => getRoutingSymbolForAddress(request.sourceChainId, address))
          .filter((value): value is string => Boolean(value))
      )
    );
    const bridgeCandidates = this.graphBuilder.buildBridgeCandidates(
      request.sourceChainId,
      request.destinationChainId || request.sourceChainId,
      candidateBridgeSymbols
    ).filter((candidate) => policy.allowedBridgeLabels.includes(candidate.bridgeLabel));

    const combined: RouteCandidate[] = [];
    for (const bridgeCandidate of bridgeCandidates) {
      const sourceCandidate = await this.planSegment(
        request.sourceChainId,
        sourceResolution.lookupAddress,
        bridgeCandidate.sourceTokenAddress,
        amountIn,
        policy,
        Math.min(policy.maxLegs, 2)
      );
      if (!sourceCandidate) continue;

      const bridgeFeeBps = bridgeCandidate.bridgeType === 'CCIP' ? 20 : 25;
      const bridgedAmount = buildMinAmountOut(sourceCandidate.estimatedAmountOut, bridgeFeeBps);

      const destinationCandidate = await this.planSegment(
        request.destinationChainId || request.sourceChainId,
        bridgeCandidate.destinationTokenAddress,
        destinationResolution.lookupAddress,
        bridgedAmount,
        policy,
        Math.min(policy.maxLegs, 2)
      );

      const bridgeLeg: PlannerLeg = {
        kind: 'bridge',
        provider: 'partner',
        sourceChainId: request.sourceChainId,
        destinationChainId: request.destinationChainId || request.sourceChainId,
        tokenInAddress: bridgeCandidate.sourceTokenAddress,
        tokenOutAddress: bridgeCandidate.destinationTokenAddress,
        tokenInSymbol: bridgeCandidate.assetSymbol,
        tokenOutSymbol: bridgeCandidate.assetSymbol,
        estimatedAmountIn: sourceCandidate.estimatedAmountOut.toString(),
        estimatedAmountOut: bridgedAmount.toString(),
        minAmountOut: bridgedAmount.toString(),
        bridgeType: bridgeCandidate.bridgeType,
        bridgeAddress: bridgeCandidate.bridgeAddress,
        gasEstimate: 250000,
        notes: bridgeCandidate.notes,
      };

      const destinationLegs = destinationCandidate?.legs || [];
      const estimatedAmountOut = destinationCandidate?.estimatedAmountOut || bridgedAmount;
      const legs = [...sourceCandidate.legs, bridgeLeg, ...destinationLegs];
      combined.push({
        decision: sourceCandidate.legs.length > 0 || destinationLegs.length > 0 ? 'swap-bridge-swap' : 'bridge-only',
        estimatedAmountOut,
        estimatedGasUsd: sourceCandidate.estimatedGasUsd + 0.45 + (destinationCandidate?.estimatedGasUsd || 0),
        score: sourceCandidate.score + (destinationCandidate?.score || normalizedOutput(request.destinationChainId || request.sourceChainId, bridgeCandidate.destinationTokenAddress, bridgedAmount)) - 0.45,
        legs,
        selectedRouteReason: `Bridge asset ${bridgeCandidate.assetSymbol} selected via ${bridgeCandidate.bridgeLabel}.`,
        rejectedAlternatives: [],
        riskFlags: ['cross-chain'],
      });
    }

    const sortedCandidates = combined.sort((a, b) => b.score - a.score);
    const best = sortedCandidates[0];
    if (!best) {
      return this.emptyResponse(request, 'unresolved', 'No eligible cross-chain route found with the current policy and bridge registry.');
    }

    const sourceSwapLegs = best.legs.filter(
      (leg) => leg.kind === 'swap' && leg.sourceChainId === request.sourceChainId
    );
    const destinationSwapLegs = best.legs.filter(
      (leg) => leg.kind === 'swap' && leg.sourceChainId === (request.destinationChainId || request.sourceChainId)
    );
    const bridgeLeg = best.legs.find((leg) => leg.kind === 'bridge');
    const intentCoordinator = normalizeAddress(
      process.env.INTENT_BRIDGE_COORDINATOR_V2_ADDRESS ||
      (request.sourceChainId === 138 ? DEFAULT_INTENT_BRIDGE_COORDINATOR_V2 : '')
    );

    const sourceRoutePlan = sourceSwapLegs.length > 0
      ? this.buildEncodedRoutePlan(
          request,
          sourceSwapLegs,
          sourceResolution.lookupAddress,
          bridgeLeg?.tokenInAddress || sourceResolution.lookupAddress,
          intentCoordinator || ROUTER_V2_RECIPIENT_PLACEHOLDER
        )
      : undefined;
    const destinationRoutePlan = destinationSwapLegs.length > 0
      ? this.buildEncodedRoutePlan(
          {
            ...request,
            sourceChainId: request.destinationChainId || request.sourceChainId,
            recipient: request.recipient,
          },
          destinationSwapLegs,
          bridgeLeg?.tokenOutAddress || destinationResolution.lookupAddress,
          destinationResolution.lookupAddress
        ) || this.emptyEncodedRoutePlan(
          request.destinationChainId || request.sourceChainId,
          bridgeLeg?.tokenOutAddress || destinationResolution.lookupAddress,
          destinationResolution.lookupAddress,
          destinationSwapLegs[destinationSwapLegs.length - 1]?.estimatedAmountOut || bridgeLeg?.estimatedAmountOut || request.amountIn,
          request.recipient || ROUTER_V2_RECIPIENT_PLACEHOLDER
        )
      : this.emptyEncodedRoutePlan(
          request.destinationChainId || request.sourceChainId,
          bridgeLeg?.tokenOutAddress || destinationResolution.lookupAddress,
          destinationResolution.lookupAddress,
          bridgeLeg?.estimatedAmountOut || request.amountIn,
          request.recipient || ROUTER_V2_RECIPIENT_PLACEHOLDER
        );

    const bridgeIntentPlan: EncodedBridgeIntentPlan | undefined =
      bridgeLeg && sourceRoutePlan && intentCoordinator
        ? {
            sourcePlan: {
              ...sourceRoutePlan,
              recipient: intentCoordinator,
            },
            bridgeType: bridgeLeg.bridgeType || 'CCIP',
            bridgeData: abiCoder.encode(
              ['address', 'string'],
              [bridgeLeg.bridgeAddress || ZeroAddress, bridgeLeg.bridgeType || 'CCIP']
            ),
            destinationPlan: destinationRoutePlan,
            recipient: normalizeAddress(request.recipient || ROUTER_V2_RECIPIENT_PLACEHOLDER),
            deadline: String(Math.floor(Date.now() / 1000) + 300),
          }
        : undefined;

    return this.toResponse(
      request,
      best,
      sortedCandidates.slice(1, 4),
      'Cross-chain best execution selected from source swap, bridge registry, and destination swap candidates.',
      undefined,
      bridgeIntentPlan
    );
  }

  private async planSegment(
    chainId: number,
    tokenIn: string,
    tokenOut: string,
    amountIn: bigint,
    policy: RoutingPolicy,
    maxLegs: number
  ): Promise<RouteCandidate | null> {
    if (tokenIn === tokenOut) {
      return {
        decision: 'direct-pool',
        estimatedAmountOut: amountIn,
        estimatedGasUsd: 0,
        score: normalizedOutput(chainId, tokenOut, amountIn),
        legs: [],
        selectedRouteReason: 'Token already matches bridge asset or destination asset.',
        rejectedAlternatives: [],
        riskFlags: [],
      };
    }

    const request: PlannerRequest = {
      sourceChainId: chainId,
      tokenIn,
      tokenOut,
      amountIn: amountIn.toString(),
      constraints: {
        complianceProfile: policy.profile,
        allowBridge: false,
        maxLegs,
        allowedProviders: policy.allowedProviders,
        allowedIntermediates: policy.defaultIntermediateAddresses,
        allowCommodityIntermediates: policy.allowCommodityIntermediates,
      },
    };
    const response = await this.planOneChain(request, {
      ...policy,
      maxLegs,
    });
    if (!response.estimatedAmountOut || response.legs.length === 0) {
      return null;
    }
    return {
      decision: response.decision,
      estimatedAmountOut: safeBigInt(response.estimatedAmountOut),
      estimatedGasUsd: response.estimatedGasUsd,
      score: normalizedOutput(chainId, tokenOut, safeBigInt(response.estimatedAmountOut)) - response.estimatedGasUsd,
      legs: response.legs,
      selectedRouteReason: response.selectedRouteReason,
      rejectedAlternatives: response.rejectedAlternatives,
      riskFlags: response.riskFlags,
    };
  }

  private toCandidate(
    legs: PlannerLeg[],
    estimatedAmountOut: bigint,
    chainId: number,
    outputToken: string,
    policy: RoutingPolicy
  ): RouteCandidate {
    const estimatedGasUsd = legs.reduce((total, leg) => total + PROVIDER_GAS_USD[leg.provider], 0);
    const stalePenalty = legs.reduce((total, leg) => {
      const freshness = leg.freshnessSeconds || 0;
      return total + (freshness > 1800 ? 0.15 : freshness > 300 ? 0.05 : 0);
    }, 0);
    const hopPenalty = Math.max(0, legs.length - 1) * 0.03;
    const score = normalizedOutput(chainId, outputToken, estimatedAmountOut) - estimatedGasUsd - stalePenalty - hopPenalty;

    return {
      decision: legs.length === 1 ? 'direct-pool' : 'multi-hop',
      estimatedAmountOut,
      estimatedGasUsd,
      score,
      legs,
      selectedRouteReason: legs.length === 1
        ? legs[0].provider === 'dodo_v3'
          ? 'Selected live DODO v3 / D3MM pilot quote for the requested direct pair.'
          : `Selected deepest eligible ${legs[0].provider} pool for the requested pair.`
        : `Selected multi-hop path through ${legs.map((leg) => leg.tokenOutSymbol || leg.tokenOutAddress).join(' -> ')}.`,
      rejectedAlternatives: [],
      riskFlags: computeRiskFlags(legs, policy),
    };
  }

  private toResponse(
    request: PlannerRequest,
    best: RouteCandidate,
    alternatives: RouteCandidate[],
    selectedRouteReason: string,
    routePlan?: EncodedPlannerRoutePlan,
    bridgeIntentPlan?: EncodedBridgeIntentPlan
  ): PlannerResponse {
    const planId = Buffer.from(this.requestHash(request, resolveRoutingPolicy(request.sourceChainId, request.constraints || {}))).toString('base64url');
    const alternativePayloads: PlannerAlternative[] = alternatives.map((candidate, index) => ({
      routeId: `${planId}-alt-${index + 1}`,
      decision: candidate.decision,
      estimatedAmountOut: candidate.estimatedAmountOut.toString(),
      estimatedGasUsd: Number(candidate.estimatedGasUsd.toFixed(4)),
      providerPath: candidate.legs.map((leg) => leg.provider),
      legCount: candidate.legs.length,
      score: Number(candidate.score.toFixed(6)),
      notes: [candidate.selectedRouteReason, ...candidate.riskFlags],
    }));
    const maxFreshness = best.legs.reduce<number | null>((current, leg) => {
      if (leg.freshnessSeconds === null || leg.freshnessSeconds === undefined) return current;
      if (current === null) return leg.freshnessSeconds;
      return Math.max(current, leg.freshnessSeconds);
    }, null);

    return {
      planId,
      generatedAt: new Date().toISOString(),
      decision: best.decision,
      sourceChainId: request.sourceChainId,
      destinationChainId: request.destinationChainId || request.sourceChainId,
      tokenIn: request.tokenIn,
      tokenOut: request.tokenOut,
      estimatedAmountOut: best.estimatedAmountOut.toString(),
      minAmountOut: best.legs.length > 0 ? best.legs[best.legs.length - 1].minAmountOut : request.amountIn,
      estimatedGasUsd: Number(best.estimatedGasUsd.toFixed(4)),
      legs: best.legs,
      alternatives: alternativePayloads,
      confidenceScore: Number(computeConfidence(best.legs).toFixed(4)),
      riskFlags: best.riskFlags,
      selectedRouteReason,
      rejectedAlternatives: best.rejectedAlternatives,
      staleness: {
        maxFreshnessSeconds: maxFreshness,
        hasStaleLeg: best.legs.some((leg) => (leg.freshnessSeconds || 0) > 300),
      },
      routePlan,
      bridgeIntentPlan,
    };
  }

  private emptyResponse(
    request: PlannerRequest,
    decision: PlannerDecision,
    reason: string
  ): PlannerResponse {
    const planId = Buffer.from(this.requestHash(request, resolveRoutingPolicy(request.sourceChainId, request.constraints || {}))).toString('base64url');
    return {
      planId,
      generatedAt: new Date().toISOString(),
      decision,
      sourceChainId: request.sourceChainId,
      destinationChainId: request.destinationChainId || request.sourceChainId,
      tokenIn: request.tokenIn,
      tokenOut: request.tokenOut,
      estimatedAmountOut: null,
      minAmountOut: null,
      estimatedGasUsd: 0,
      legs: [],
      alternatives: [],
      confidenceScore: 0,
      riskFlags: ['no-route'],
      selectedRouteReason: reason,
      rejectedAlternatives: [],
      staleness: {
        maxFreshnessSeconds: null,
        hasStaleLeg: false,
      },
    };
  }

  private buildEncodedRoutePlan(
    request: PlannerRequest,
    legs: PlannerLeg[],
    inputToken: string,
    outputToken: string,
    recipientOverride?: string
  ): EncodedPlannerRoutePlan | undefined {
    if (!legs.every((leg) => PROVIDER_ENUM[leg.provider] !== undefined && Boolean(providerDataHexForLeg(leg)))) {
      return undefined;
    }

    const recipient = normalizeAddress(recipientOverride || request.recipient || ROUTER_V2_RECIPIENT_PLACEHOLDER);
    const deadline = String(Math.floor(Date.now() / 1000) + 300);
    const amountIn = legs[0]?.estimatedAmountIn || request.amountIn;
    const minAmountOut = legs[legs.length - 1]?.minAmountOut || request.amountIn;

    return {
      chainId: request.sourceChainId,
      inputToken,
      outputToken,
      amountIn,
      minAmountOut,
      recipient,
      deadline,
      legs: legs.map((leg, index) => ({
        provider: PROVIDER_ENUM[leg.provider] as number,
        tokenIn: leg.tokenInAddress,
        tokenOut: leg.tokenOutAddress,
        amountSource: index === 0 ? 0 : 1,
        minAmountOut: leg.minAmountOut,
        target: leg.target || ZeroAddress,
        providerData: providerDataHexForLeg(leg) as string,
      })),
    };
  }

  private emptyEncodedRoutePlan(
    chainId: number,
    inputToken: string,
    outputToken: string,
    amountIn: string,
    recipient: string
  ): EncodedPlannerRoutePlan {
    return {
      chainId,
      inputToken,
      outputToken,
      amountIn,
      minAmountOut: amountIn,
      recipient: normalizeAddress(recipient),
      deadline: String(Math.floor(Date.now() / 1000) + 300),
      legs: [],
    };
  }
}
