import { Router, Request, Response } from 'express';
import { cacheMiddleware } from '../middleware/cache';
import {
  AggregatorRouteLeg,
  LiveAggregatorRoute,
  filterLiveAggregatorRoutes,
  loadAggregatorRouteMatrix,
} from '../../config/aggregator-route-matrix';
import {
  buildPartnerPayload,
  PartnerName,
} from '../../services/partner-payload-adapters';
import { dispatchPartnerPayload } from '../../services/partner-payload-dispatcher';
import { buildInternalExecutionPlan } from '../../services/internal-execution-plan';
import { BestExecutionPlanner } from '../../services/best-execution-planner';
import { routeFromPlannerLegs } from '../../services/aggregator-route-matrix-generator';

const router: Router = Router();
const planner = new BestExecutionPlanner();

interface PartnerPayloadRequestBody {
  partner?: string;
  amount?: string;
  fromChainId?: number;
  toChainId?: number;
  routeType?: string;
  tokenIn?: string;
  tokenOut?: string;
  takerAddress?: string;
  fromAddress?: string;
  toAddress?: string;
  recipient?: string;
  slippagePercent?: string;
  slippageBps?: string;
  includeUnsupported?: boolean;
  routeId?: string;
}

function normalizePartner(input: string | undefined): PartnerName | null {
  if (!input) return null;
  const value = input.trim().toLowerCase();
  if (value === '1inch') return '1inch';
  if (value === '0x' || value === 'zeroex') return '0x';
  if (value === 'lifi') return 'LiFi';
  return null;
}

function buildPayloads(args: {
  partner: PartnerName;
  amount: string;
  fromChainId?: number;
  toChainId?: number;
  routeType?: string;
  tokenIn?: string;
  tokenOut?: string;
  takerAddress?: string;
  fromAddress?: string;
  toAddress?: string;
  recipient?: string;
  slippagePercent?: string;
  slippageBps?: string;
  includeUnsupported?: boolean;
  routeId?: string;
}) {
  return buildPayloadsAsync(args);
}

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

function plannerLegsToAggregatorLegs(routeLegs: Array<{
  kind: string;
  provider: string;
  target?: string;
  poolAddress?: string;
  bridgeAddress?: string;
  tokenInAddress: string;
  tokenOutAddress: string;
}>): AggregatorRouteLeg[] {
  return routeLegs.map((leg) => ({
    kind: leg.kind,
    protocol: leg.provider,
    executor: leg.provider,
    executorAddress: leg.target,
    poolAddress: leg.poolAddress || leg.bridgeAddress,
    tokenInAddress: leg.tokenInAddress,
    tokenOutAddress: leg.tokenOutAddress,
  }));
}

async function resolvePlannerDerivedRoutes(args: {
  amount: string;
  fromChainId?: number;
  toChainId?: number;
  routeType?: string;
  tokenIn?: string;
  tokenOut?: string;
  recipient?: string;
  slippageBps?: string;
}): Promise<LiveAggregatorRoute[]> {
  if (!args.fromChainId || !args.tokenIn || !args.tokenOut) {
    return [];
  }

  const wantsBridge = Boolean(args.toChainId && args.toChainId !== args.fromChainId);
  if (args.routeType === 'swap' && wantsBridge) {
    return [];
  }

  const response = await planner.plan({
    sourceChainId: args.fromChainId,
    destinationChainId: wantsBridge ? args.toChainId : args.fromChainId,
    tokenIn: args.tokenIn,
    tokenOut: args.tokenOut,
    amountIn: args.amount,
    recipient: args.recipient,
    constraints: {
      allowBridge: wantsBridge || args.routeType === 'bridge' || args.routeType === 'swap-bridge-swap',
      maxSlippageBps: args.slippageBps ? Number(args.slippageBps) : undefined,
    },
  });

  if (response.decision === 'unresolved' || response.legs.length === 0) {
    return [];
  }

  const aggregatorLegs = plannerLegsToAggregatorLegs(response.legs);
  const bridgeLeg = response.legs.find((leg) => leg.kind === 'bridge');
  const routeType = bridgeLeg ? 'bridge' : 'swap';

  return [
    routeFromPlannerLegs({
      routeId: response.planId,
      fromChainId: response.sourceChainId,
      toChainId: response.destinationChainId,
      tokenInAddress: response.legs[0]?.tokenInAddress || args.tokenIn,
      tokenOutAddress: response.legs[response.legs.length - 1]?.tokenOutAddress || args.tokenOut,
      assetAddress: bridgeLeg?.tokenInAddress,
      assetSymbol: bridgeLeg?.tokenInSymbol,
      routeType,
      bridgeType: bridgeLeg?.bridgeType,
      bridgeAddress: bridgeLeg?.bridgeAddress,
      label: response.selectedRouteReason,
      legs: aggregatorLegs,
      notes: response.riskFlags,
    }),
  ];
}

async function buildPayloadsAsync(args: {
  partner: PartnerName;
  amount: string;
  fromChainId?: number;
  toChainId?: number;
  routeType?: string;
  tokenIn?: string;
  tokenOut?: string;
  takerAddress?: string;
  fromAddress?: string;
  toAddress?: string;
  recipient?: string;
  slippagePercent?: string;
  slippageBps?: string;
  includeUnsupported?: boolean;
  routeId?: string;
}) {
  const matrix = loadAggregatorRouteMatrix();
  if (!matrix && !args.fromChainId) {
    return {
      error: {
        status: 503,
        body: {
          error: 'Aggregator route matrix not available',
        },
      },
    };
  }

  const plannerRoutes = await resolvePlannerDerivedRoutes(args);
  const matrixRoutes = matrix
    ? filterLiveAggregatorRoutes(
        [...matrix.liveSwapRoutes, ...matrix.liveBridgeRoutes],
        {
          fromChainId: args.fromChainId,
          toChainId: args.toChainId,
          routeType: args.routeType,
          tokenIn: args.tokenIn,
          tokenOut: args.tokenOut,
        }
      )
    : [];
  const liveRoutes = dedupeRoutes([...plannerRoutes, ...matrixRoutes]).filter((route) =>
    args.routeId ? route.routeId === args.routeId : true
  );

  const payloads = liveRoutes.map((route) =>
    buildPartnerPayload(args.partner, route, {
      amount: args.amount,
      takerAddress: args.takerAddress,
      fromAddress: args.fromAddress,
      toAddress: args.toAddress,
      recipient: args.recipient,
      slippagePercent: args.slippagePercent,
      slippageBps: args.slippageBps,
    })
  );

  const filteredPayloads = args.includeUnsupported ? payloads : payloads.filter((payload) => payload.supported);

  return {
    result: {
      generatedAt: new Date().toISOString(),
      format: 'partner-payload-templates-v1',
      partner: args.partner,
      amount: args.amount,
      routeSource: plannerRoutes.length > 0 ? 'planner-v2-preferred' : 'matrix-fallback',
      count: filteredPayloads.length,
      supportedCount: payloads.filter((payload) => payload.supported).length,
      payloads: filteredPayloads,
    },
  };
}

/**
 * GET /api/v1/routes/partner-payloads
 * Returns partner-specific request payload templates generated from live ingestion routes.
 * By default returns only supported payloads; pass includeUnsupported=true to inspect all templates.
 */
router.get('/routes/partner-payloads', cacheMiddleware(30 * 1000), async (req: Request, res: Response) => {
  const partner = normalizePartner(req.query.partner ? String(req.query.partner) : undefined);
  if (!partner) {
    return res.status(400).json({
      error: 'partner is required and must be one of: 1inch, 0x, LiFi',
      example: '/api/v1/routes/partner-payloads?partner=LiFi&amount=1000000',
    });
  }

  const amount = req.query.amount ? String(req.query.amount) : '';
  if (!amount) {
    return res.status(400).json({
      error: 'amount is required',
      example: '/api/v1/routes/partner-payloads?partner=0x&amount=1000000',
    });
  }

  const response = await buildPayloads({
    partner,
    amount,
    fromChainId: req.query.fromChainId ? parseInt(String(req.query.fromChainId), 10) : undefined,
    toChainId: req.query.toChainId ? parseInt(String(req.query.toChainId), 10) : undefined,
    routeType: req.query.routeType ? String(req.query.routeType) : undefined,
    tokenIn: req.query.tokenIn ? String(req.query.tokenIn) : undefined,
    tokenOut: req.query.tokenOut ? String(req.query.tokenOut) : undefined,
    takerAddress: req.query.takerAddress ? String(req.query.takerAddress) : undefined,
    fromAddress: req.query.fromAddress ? String(req.query.fromAddress) : undefined,
    toAddress: req.query.toAddress ? String(req.query.toAddress) : undefined,
    recipient: req.query.recipient ? String(req.query.recipient) : undefined,
    slippagePercent: req.query.slippagePercent ? String(req.query.slippagePercent) : undefined,
    slippageBps: req.query.slippageBps ? String(req.query.slippageBps) : undefined,
    includeUnsupported: String(req.query.includeUnsupported ?? 'false').toLowerCase() === 'true',
    routeId: req.query.routeId ? String(req.query.routeId) : undefined,
  });

  if (response.error) {
    return res.status(response.error.status).json(response.error.body);
  }

  return res.json(response.result);
});

/**
 * POST /api/v1/routes/partner-payloads/resolve
 * Accepts JSON body and returns only supported partner payloads by default.
 */
router.post('/routes/partner-payloads/resolve', cacheMiddleware(30 * 1000), async (req: Request, res: Response) => {
  const body = (req.body ?? {}) as PartnerPayloadRequestBody;
  const partner = normalizePartner(body.partner);

  if (!partner) {
    return res.status(400).json({
      error: 'partner is required and must be one of: 1inch, 0x, LiFi',
      example: {
        partner: '0x',
        amount: '1000000',
        fromChainId: 138,
        routeType: 'swap',
      },
    });
  }

  if (!body.amount || !String(body.amount).trim()) {
    return res.status(400).json({
      error: 'amount is required',
    });
  }

  const response = await buildPayloads({
    partner,
    amount: String(body.amount),
    fromChainId: typeof body.fromChainId === 'number' ? body.fromChainId : undefined,
    toChainId: typeof body.toChainId === 'number' ? body.toChainId : undefined,
    routeType: body.routeType,
    tokenIn: body.tokenIn,
    tokenOut: body.tokenOut,
    takerAddress: body.takerAddress,
    fromAddress: body.fromAddress,
    toAddress: body.toAddress,
    recipient: body.recipient,
    slippagePercent: body.slippagePercent,
    slippageBps: body.slippageBps,
    includeUnsupported: body.includeUnsupported === true,
    routeId: body.routeId,
  });

  if (response.error) {
    return res.status(response.error.status).json(response.error.body);
  }

  return res.json(response.result);
});

/**
 * POST /api/v1/routes/partner-payloads/dispatch
 * Resolves partner payloads and dispatches exactly one supported payload.
 */
router.post('/routes/partner-payloads/dispatch', async (req: Request, res: Response) => {
  const body = (req.body ?? {}) as PartnerPayloadRequestBody;
  const partner = normalizePartner(body.partner);

  if (!partner) {
    return res.status(400).json({
      error: 'partner is required and must be one of: 1inch, 0x, LiFi',
    });
  }

  if (!body.amount || !String(body.amount).trim()) {
    return res.status(400).json({
      error: 'amount is required',
    });
  }

  const response = await buildPayloads({
    partner,
    amount: String(body.amount),
    fromChainId: typeof body.fromChainId === 'number' ? body.fromChainId : undefined,
    toChainId: typeof body.toChainId === 'number' ? body.toChainId : undefined,
    routeType: body.routeType,
    tokenIn: body.tokenIn,
    tokenOut: body.tokenOut,
    takerAddress: body.takerAddress,
    fromAddress: body.fromAddress,
    toAddress: body.toAddress,
    recipient: body.recipient,
    slippagePercent: body.slippagePercent,
    slippageBps: body.slippageBps,
    includeUnsupported: true,
    routeId: body.routeId,
  });

  if (response.error) {
    return res.status(response.error.status).json(response.error.body);
  }

  const supportedPayloads = response.result.payloads.filter((payload) => payload.supported);
  const selectedPayload = body.routeId
    ? supportedPayloads.find((payload) => payload.routeId === body.routeId)
    : supportedPayloads.length === 1
      ? supportedPayloads[0]
      : undefined;

  if (!selectedPayload) {
    const fallback = buildInternalExecutionPlan({
      routeId: body.routeId,
      fromChainId: typeof body.fromChainId === 'number' ? body.fromChainId : undefined,
      toChainId: typeof body.toChainId === 'number' ? body.toChainId : undefined,
      tokenIn: body.tokenIn,
      tokenOut: body.tokenOut,
      amountIn: String(body.amount),
      slippageBps: body.slippageBps,
    });

    return res.status(400).json({
      error: body.routeId
        ? 'No supported payload found for the requested routeId'
        : 'Dispatch requires exactly one supported payload; refine filters or pass routeId',
      supportedRouteIds: supportedPayloads.map((payload) => payload.routeId),
      fallbackPlan: fallback.plan,
      fallbackError: fallback.error,
    });
  }

  const dispatchResult = await dispatchPartnerPayload(selectedPayload);
  return res.json({
    generatedAt: new Date().toISOString(),
    partner,
    routeId: selectedPayload.routeId,
    dispatch: dispatchResult,
  });
});

/**
 * POST /api/v1/routes/internal-execution-plan
 * Returns a Chain 138 DODO PMM fallback execution plan for one live internal route.
 */
router.post('/routes/internal-execution-plan', (req: Request, res: Response) => {
  const body = (req.body ?? {}) as PartnerPayloadRequestBody;
  if (!body.amount || !String(body.amount).trim()) {
    return res.status(400).json({
      error: 'amount is required',
    });
  }

  const result = buildInternalExecutionPlan({
    routeId: body.routeId,
    fromChainId: typeof body.fromChainId === 'number' ? body.fromChainId : undefined,
    toChainId: typeof body.toChainId === 'number' ? body.toChainId : undefined,
    tokenIn: body.tokenIn,
    tokenOut: body.tokenOut,
    amountIn: String(body.amount),
    slippageBps: body.slippageBps,
  });

  if (result.error || !result.plan) {
    return res.status(400).json({
      error: result.error || 'Unable to build internal execution plan',
      candidateRouteIds: result.candidateRouteIds,
    });
  }

  return res.json({
    generatedAt: new Date().toISOString(),
    format: 'internal-execution-plan-v1',
    plan: result.plan,
  });
});

export default router;
