import fs from 'fs';
import path from 'path';
import {
  AggregatorFamily,
  AggregatorRouteMatrix,
  AggregatorRouteLeg,
  LiveAggregatorRoute,
  NonLiveAggregatorRoute,
} from '../config/aggregator-route-matrix';
import { getProviderCapabilities } from '../config/provider-capabilities';
import { resolveChain138RpcUrl } from '../config/chain138-rpc';
import { getChain138RoutingAssets, getRoutingSymbolForAddress } from '../config/routing-assets';
import { RouteGraphBuilder } from './route-graph-builder';
import { PlannerProvider, SwapGraphEdge } from './planner-v2-types';

const PARTNER_FAMILIES: AggregatorFamily[] = ['1inch', '0x', 'LiFi'];

function providerProtocol(provider: PlannerProvider): string {
  switch (provider) {
    case 'dodo':
      return 'dodo_pmm';
    case 'dodo_v3':
      return 'dodo_v3';
    case 'uniswap_v3':
      return 'uniswap_v3';
    case 'uniswap_v2':
      return 'uniswap_v2';
    case 'sushiswap':
      return 'sushiswap';
    case 'balancer':
      return 'balancer';
    case 'curve':
      return 'curve';
    case 'one_inch':
      return 'one_inch';
    case 'partner':
      return 'partner';
  }
}

function providerLabel(provider: PlannerProvider): string {
  switch (provider) {
    case 'dodo':
      return 'DODO PMM';
    case 'dodo_v3':
      return 'DODO V3 / D3MM';
    case 'uniswap_v3':
      return 'Uniswap V3';
    case 'uniswap_v2':
      return 'Uniswap V2';
    case 'sushiswap':
      return 'SushiSwap';
    case 'balancer':
      return 'Balancer';
    case 'curve':
      return 'Curve';
    case 'one_inch':
      return '1inch';
    case 'partner':
      return 'Partner';
  }
}

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

function makeRouteId(prefix: string, parts: Array<string | number | undefined>): string {
  return [prefix, ...parts]
    .filter((part): part is string | number => part !== undefined && part !== null && String(part).trim().length > 0)
    .map((part) => String(part).trim().toLowerCase().replace(/[^a-z0-9]+/g, '-'))
    .join('-');
}

function dedupeRoutes<T extends { routeId: string }>(routes: T[]): T[] {
  const byId = new Map<string, T>();
  for (const route of routes) {
    byId.set(route.routeId, route);
  }
  return Array.from(byId.values());
}

function toSwapLeg(edge: SwapGraphEdge): AggregatorRouteLeg {
  return {
    kind: 'swap',
    protocol: providerProtocol(edge.provider),
    executor: providerLabel(edge.provider),
    executorAddress: normalizeAddress(edge.target),
    poolAddress: normalizeAddress(edge.poolAddress),
    tokenInAddress: normalizeAddress(edge.tokenInAddress),
    tokenOutAddress: normalizeAddress(edge.tokenOutAddress),
    reserves: {
      reserveIn: edge.reserveIn,
      reserveOut: edge.reserveOut,
    },
  };
}

function buildSwapRoute(homeChainId: number, edge: SwapGraphEdge): LiveAggregatorRoute {
  return {
    routeId: makeRouteId(`chain-${homeChainId}-swap`, [
      edge.provider,
      edge.tokenInSymbol || edge.tokenInAddress,
      edge.tokenOutSymbol || edge.tokenOutAddress,
      edge.poolAddress?.slice(0, 10),
    ]),
    status: 'live',
    aggregatorFamilies: PARTNER_FAMILIES,
    fromChainId: homeChainId,
    toChainId: homeChainId,
    tokenInSymbol: edge.tokenInSymbol,
    tokenInAddress: normalizeAddress(edge.tokenInAddress),
    tokenOutSymbol: edge.tokenOutSymbol,
    tokenOutAddress: normalizeAddress(edge.tokenOutAddress),
    routeType: 'swap',
    hopCount: 1,
    label: `${providerLabel(edge.provider)} ${edge.tokenInSymbol || edge.tokenInAddress} -> ${edge.tokenOutSymbol || edge.tokenOutAddress}`,
    intermediateSymbols: [],
    legs: [toSwapLeg(edge)],
    tags: ['planner-v2-generated', edge.provider],
    notes: [
      'Generated from live planner route graph.',
      ...(edge.notes || []),
    ],
  };
}

function buildBridgeRoute(args: {
  fromChainId: number;
  toChainId: number;
  assetSymbol: string;
  assetAddress?: string;
  bridgeType: string;
  bridgeAddress: string;
  label: string;
  notes?: string[];
}): LiveAggregatorRoute {
  return {
    routeId: makeRouteId('bridge', [
      args.fromChainId,
      args.toChainId,
      args.assetSymbol,
      args.label,
    ]),
    status: 'live',
    aggregatorFamilies: PARTNER_FAMILIES,
    fromChainId: args.fromChainId,
    toChainId: args.toChainId,
    assetSymbol: args.assetSymbol,
    assetAddress: normalizeAddress(args.assetAddress),
    routeType: 'bridge',
    bridgeType: args.bridgeType,
    bridgeAddress: normalizeAddress(args.bridgeAddress),
    label: `${args.label} ${args.assetSymbol} ${args.fromChainId} -> ${args.toChainId}`,
    tags: ['planner-v2-generated', 'bridge'],
    notes: [
      'Generated from bridge registry and planner visibility.',
      ...(args.notes || []),
    ],
  };
}

function resolveDefaultOutputPath(): string {
  return path.resolve(process.cwd(), '../../../config/aggregator-route-matrix.json');
}

export class AggregatorRouteMatrixGenerator {
  private graphBuilder: RouteGraphBuilder;

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

  async generate(homeChainId: number = 138): Promise<AggregatorRouteMatrix> {
    const edges = await this.graphBuilder.buildSwapEdges(homeChainId);
    const liveSwapRoutes = dedupeRoutes(edges.map((edge) => buildSwapRoute(homeChainId, edge)));

    const routingAssets = Object.values(getChain138RoutingAssets());
    const bridgeTargets = [1, 651940];
    const liveBridgeRoutes = dedupeRoutes(
      bridgeTargets.flatMap((destinationChainId) =>
        this.graphBuilder
          .buildBridgeCandidates(
            homeChainId,
            destinationChainId,
            routingAssets.map((asset) => asset.symbol)
          )
          .map((candidate) =>
            buildBridgeRoute({
              fromChainId: candidate.fromChainId,
              toChainId: candidate.toChainId,
              assetSymbol: candidate.assetSymbol,
              assetAddress: candidate.sourceTokenAddress,
              bridgeType: candidate.bridgeType,
              bridgeAddress: candidate.bridgeAddress,
              label: candidate.bridgeLabel,
              notes: candidate.notes,
            })
          )
      )
    );

    const blockedOrPlannedRoutes: NonLiveAggregatorRoute[] = getProviderCapabilities(homeChainId)
      .flatMap((record) =>
        record.pairs
          .filter((pair): pair is typeof pair & { status: 'planned' | 'blocked' } => pair.status !== 'live')
          .map((pair) => ({
            routeId: makeRouteId('chain-138-capability', [
              record.provider,
              pair.status,
              pair.tokenInSymbol,
              pair.tokenOutSymbol,
            ]),
            status: pair.status,
            fromChainId: pair.chainId,
            toChainId: pair.chainId,
            routeType: pair.legType === 'bridge' ? 'bridge' : 'swap',
            reason: pair.reason || pair.notes?.join(' ') || `${record.provider} ${pair.status}`,
            tokenInSymbols: [pair.tokenInSymbol, pair.tokenOutSymbol],
          }))
      );

    return {
      $schema: 'https://json-schema.org/draft/2020-12/schema',
      description: 'Planner-v2-generated aggregator route visibility matrix for Chain 138 and approved bridge lanes.',
      version: '2.0.0',
      updated: new Date().toISOString(),
      homeChainId,
      metadata: {
        generatedFrom: [
          'services/token-aggregation/src/services/route-graph-builder.ts',
          'services/token-aggregation/src/config/provider-capabilities.ts',
          'services/token-aggregation/src/config/cross-chain-bridges.ts',
        ],
        verification: {
          verifiedAt: new Date().toISOString(),
          verifiedBy: 'services/token-aggregation planner-v2 generator',
          rpc: resolveChain138RpcUrl(),
        },
        adapterNotes: [
          'This file is generated from planner-v2 graph and provider capability truth.',
          'Partner payload generation should prefer planner-v2 outputs over this visibility artifact when route inputs are available.',
          'Only live routes should be considered executable candidates.',
        ],
      },
      chains: {
        '138': { name: 'Chain 138' },
        '1': { name: 'Ethereum Mainnet' },
        '651940': { name: 'ALL Mainnet' },
      },
      tokens: Object.fromEntries(
        routingAssets.map((asset) => [asset.symbol, { address: asset.address, decimals: asset.decimals, kind: asset.kind }])
      ),
      liveSwapRoutes,
      liveBridgeRoutes,
      blockedOrPlannedRoutes: dedupeRoutes(blockedOrPlannedRoutes),
    };
  }

  async writeToFile(outputPath: string = resolveDefaultOutputPath(), homeChainId: number = 138): Promise<string> {
    const matrix = await this.generate(homeChainId);
    fs.writeFileSync(outputPath, `${JSON.stringify(matrix, null, 2)}\n`, 'utf8');
    return outputPath;
  }
}

export function routeFromPlannerLegs(args: {
  routeId: string;
  fromChainId: number;
  toChainId: number;
  tokenInAddress?: string;
  tokenOutAddress?: string;
  assetAddress?: string;
  assetSymbol?: string;
  routeType: 'swap' | 'bridge';
  bridgeType?: string;
  bridgeAddress?: string;
  label: string;
  legs: AggregatorRouteLeg[];
  notes?: string[];
}): LiveAggregatorRoute {
  return {
    routeId: args.routeId,
    status: 'live',
    aggregatorFamilies: PARTNER_FAMILIES,
    fromChainId: args.fromChainId,
    toChainId: args.toChainId,
    tokenInAddress: normalizeAddress(args.tokenInAddress),
    tokenOutAddress: normalizeAddress(args.tokenOutAddress),
    tokenInSymbol: args.tokenInAddress ? getRoutingSymbolForAddress(args.fromChainId, args.tokenInAddress) : undefined,
    tokenOutSymbol: args.tokenOutAddress ? getRoutingSymbolForAddress(args.toChainId, args.tokenOutAddress) : undefined,
    assetAddress: normalizeAddress(args.assetAddress),
    assetSymbol: args.assetSymbol,
    routeType: args.routeType,
    bridgeType: args.bridgeType,
    bridgeAddress: normalizeAddress(args.bridgeAddress),
    label: args.label,
    hopCount: args.routeType === 'swap' ? args.legs.length : undefined,
    legs: args.legs,
    tags: ['planner-v2-generated'],
    notes: args.notes || [],
  };
}
