import { ApiPromise } from '@pezkuwi/api';
import { KNOWN_TOKENS, PoolInfo, SwapQuote, UserLiquidityPosition, NATIVE_TOKEN_ID } from '../types/dex';

// Re-export NATIVE_TOKEN_ID for convenience
export { NATIVE_TOKEN_ID };

// LP tokens typically use 12 decimals on Asset Hub
const LP_TOKEN_DECIMALS = 12;

/**
 * Helper to convert asset ID to XCM Location format for assetConversion pallet
 * @param id - Asset ID (-1 for native token, positive for assets)
 */
export const formatAssetLocation = (id: number) => {
  if (id === NATIVE_TOKEN_ID) {
    // Native token from relay chain
    return { parents: 1, interior: 'Here' };
  }
  // Asset on Asset Hub - XCM location format with PalletInstance 50 (assets pallet)
  return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } };
};

/**
 * Format balance with proper decimals
 * @param balance - Raw balance string
 * @param decimals - Token decimals
 * @param precision - Display precision (default 4)
 */
export const formatTokenBalance = (
  balance: string | number | bigint,
  decimals: number,
  precision: number = 4
): string => {
  const balanceBigInt = BigInt(balance);
  const divisor = BigInt(10 ** decimals);
  const integerPart = balanceBigInt / divisor;
  const fractionalPart = balanceBigInt % divisor;

  const fractionalStr = fractionalPart.toString().padStart(decimals, '0');
  const displayFractional = fractionalStr.slice(0, precision);

  return `${integerPart}.${displayFractional}`;
};

/**
 * Parse user input to raw balance
 * @param input - User input string (e.g., "10.5")
 * @param decimals - Token decimals
 */
export const parseTokenInput = (input: string, decimals: number): string => {
  if (!input || input === '' || input === '.') return '0';

  // Remove non-numeric chars except decimal point
  const cleaned = input.replace(/[^\d.]/g, '');
  const [integer, fractional] = cleaned.split('.');

  const integerPart = BigInt(integer || '0') * BigInt(10 ** decimals);

  if (!fractional) {
    return integerPart.toString();
  }

  // Pad or truncate fractional part
  const fractionalPadded = fractional.padEnd(decimals, '0').slice(0, decimals);
  const fractionalPart = BigInt(fractionalPadded);

  return (integerPart + fractionalPart).toString();
};

/**
 * Calculate price impact for a swap
 * @param reserveIn - Reserve of input token
 * @param reserveOut - Reserve of output token
 * @param amountIn - Amount being swapped in
 */
export const calculatePriceImpact = (
  reserveIn: string,
  reserveOut: string,
  amountIn: string
): string => {
  const reserveInBig = BigInt(reserveIn);
  const reserveOutBig = BigInt(reserveOut);
  const amountInBig = BigInt(amountIn);

  if (reserveInBig === BigInt(0) || reserveOutBig === BigInt(0)) {
    return '0';
  }

  // Price before = reserveOut / reserveIn
  // Amount out with constant product: (amountIn * reserveOut) / (reserveIn + amountIn)
  const amountOut =
    (amountInBig * reserveOutBig) / (reserveInBig + amountInBig);

  // Price after = (reserveOut - amountOut) / (reserveIn + amountIn)
  const priceBefore = (reserveOutBig * BigInt(10000)) / reserveInBig;
  const priceAfter =
    ((reserveOutBig - amountOut) * BigInt(10000)) /
    (reserveInBig + amountInBig);

  // Impact = |priceAfter - priceBefore| / priceBefore * 100
  const impact = ((priceBefore - priceAfter) * BigInt(100)) / priceBefore;

  return (Number(impact) / 100).toFixed(2);
};

/**
 * Calculate output amount for a swap (constant product formula)
 * @param amountIn - Input amount
 * @param reserveIn - Reserve of input token
 * @param reserveOut - Reserve of output token
 * @param feeRate - Fee rate (e.g., 30 for 0.3%)
 */
export const getAmountOut = (
  amountIn: string,
  reserveIn: string,
  reserveOut: string,
  feeRate: number = 30
): string => {
  const amountInBig = BigInt(amountIn);
  const reserveInBig = BigInt(reserveIn);
  const reserveOutBig = BigInt(reserveOut);

  if (
    amountInBig === BigInt(0) ||
    reserveInBig === BigInt(0) ||
    reserveOutBig === BigInt(0)
  ) {
    return '0';
  }

  // amountInWithFee = amountIn * (10000 - feeRate) / 10000
  const amountInWithFee = (amountInBig * BigInt(10000 - feeRate)) / BigInt(10000);

  // amountOut = (amountInWithFee * reserveOut) / (reserveIn + amountInWithFee)
  const numerator = amountInWithFee * reserveOutBig;
  const denominator = reserveInBig + amountInWithFee;

  return (numerator / denominator).toString();
};

/**
 * Calculate required amount1 for given amount2 (maintaining ratio)
 * @param amount2 - Amount of token 2
 * @param reserve1 - Reserve of token 1
 * @param reserve2 - Reserve of token 2
 */
export const quote = (
  amount2: string,
  reserve1: string,
  reserve2: string
): string => {
  const amount2Big = BigInt(amount2);
  const reserve1Big = BigInt(reserve1);
  const reserve2Big = BigInt(reserve2);

  if (reserve2Big === BigInt(0)) return '0';

  return ((amount2Big * reserve1Big) / reserve2Big).toString();
};

/**
 * Parse XCM Location to extract asset ID
 * @param location - XCM Location object
 * @returns asset ID (-1 for native, positive for assets)
 */
const parseAssetLocation = (location: unknown): number => {
  try {
    const loc = location as { parents?: number; interior?: unknown };

    // Native token: { parents: 1, interior: { here: null } } or { parents: 1, interior: 'Here' }
    if (loc.parents === 1) {
      const interior = loc.interior as { here?: null } | string;
      if (interior === 'Here' || (typeof interior === 'object' && 'here' in interior)) {
        return NATIVE_TOKEN_ID;
      }
    }

    // Asset on Asset Hub: { parents: 0, interior: { x2: [{ palletInstance: 50 }, { generalIndex: id }] } }
    // Note: Keys might be lowercase (x2, generalIndex) when coming from chain
    const interior = loc.interior as {
      X2?: Array<{ GeneralIndex?: number; generalIndex?: number }>;
      x2?: Array<{ GeneralIndex?: number; generalIndex?: number }>;
    };

    const x2 = interior?.X2 || interior?.x2;
    if (x2?.[1]) {
      const generalIndex = x2[1].GeneralIndex ?? x2[1].generalIndex;
      if (generalIndex !== undefined) {
        return generalIndex;
      }
    }

    // Try to parse as JSON and extract
    const locJson = JSON.stringify(location);
    const match = locJson.match(/generalIndex['":\s]+(\d+)/i);
    if (match) {
      return parseInt(match[1], 10);
    }

    return 0; // Default fallback
  } catch {
    return 0;
  }
};

/**
 * Fetch all existing pools from chain
 * @param api - Polkadot API instance
 */
export const fetchPools = async (api: ApiPromise): Promise<PoolInfo[]> => {
  try {
    const pools: PoolInfo[] = [];

    // Query all pool accounts
    const poolKeys = await api.query.assetConversion.pools.keys();

    for (const key of poolKeys) {
      // Extract asset locations from storage key
      // The key args contain a tuple of XCM Locations: [[loc1, loc2]]
      const poolPair = key.args[0] as unknown as Array<{ toJSON: () => unknown }>;
      const asset1Location = poolPair[0];
      const asset2Location = poolPair[1];

      // Parse XCM Locations to get asset IDs
      const asset1 = parseAssetLocation(asset1Location.toJSON());
      const asset2 = parseAssetLocation(asset2Location.toJSON());

      // Get pool info (contains lpToken ID)
      const poolInfo = await api.query.assetConversion.pools([
        formatAssetLocation(asset1),
        formatAssetLocation(asset2)
      ]);

      if ((poolInfo as any).isNone) continue;

      const poolData = (poolInfo as any).unwrap().toJSON();
      const lpTokenId = poolData.lpToken;

      // Get LP token supply from poolAssets pallet
      let lpTokenSupply = '0';
      try {
        if (api.query.poolAssets?.asset) {
          const lpAssetDetails = await api.query.poolAssets.asset(lpTokenId);
          if ((lpAssetDetails as any).isSome) {
            lpTokenSupply = ((lpAssetDetails as any).unwrap() as any).supply.toString();
          }
        }
      } catch (error) {
        console.warn('Could not query LP token supply:', error);
      }

      // Get reserves using runtime API (quotePriceExactTokensForTokens)
      let reserve1 = '0';
      let reserve2 = '0';

      try {
        // Get token decimals first
        const token1 = KNOWN_TOKENS[asset1] || { decimals: 12 };
        const token2 = KNOWN_TOKENS[asset2] || { decimals: 12 };

        // Query price to verify pool has liquidity and estimate reserves
        const oneUnit = BigInt(Math.pow(10, token1.decimals));
        const quote = await (api.call as any).assetConversionApi.quotePriceExactTokensForTokens(
          formatAssetLocation(asset1),
          formatAssetLocation(asset2),
          oneUnit.toString(),
          true
        );

        if (quote && !(quote as any).isNone) {
          // Pool has liquidity - estimate reserves from LP supply
          if (lpTokenSupply !== '0') {
            const lpSupply = BigInt(lpTokenSupply);
            const price = Number((quote as any).unwrap().toString()) / Math.pow(10, token2.decimals);

            if (price > 0) {
              // LP supply ≈ sqrt(reserve1 * reserve2)
              // With price = reserve2/reserve1, solve for reserves
              const sqrtPrice = Math.sqrt(price);
              const r1 = Number(lpSupply) / sqrtPrice;
              const r2 = Number(lpSupply) * sqrtPrice;
              reserve1 = BigInt(Math.floor(r1)).toString();
              reserve2 = BigInt(Math.floor(r2)).toString();
            }
          }
        }
      } catch (error) {
        console.warn('Could not fetch reserves via runtime API:', error);
        // Fallback: calculate from LP supply using geometric mean
        if (lpTokenSupply !== '0') {
          reserve1 = lpTokenSupply;
          reserve2 = lpTokenSupply;
        }
      }

      // Get token info
      const token1 = KNOWN_TOKENS[asset1] || {
        id: asset1,
        symbol: `Asset ${asset1}`,
        name: `Unknown Asset ${asset1}`,
        decimals: 12,
      };
      const token2 = KNOWN_TOKENS[asset2] || {
        id: asset2,
        symbol: `Asset ${asset2}`,
        name: `Unknown Asset ${asset2}`,
        decimals: 12,
      };

      pools.push({
        id: `${asset1}-${asset2}`,
        asset1,
        asset2,
        asset1Symbol: token1.symbol,
        asset2Symbol: token2.symbol,
        asset1Decimals: token1.decimals,
        asset2Decimals: token2.decimals,
        reserve1,
        reserve2,
        lpTokenSupply,
        feeRate: '0.3', // Default 0.3%
      });
    }

    return pools;
  } catch (error) {
    console.error('Failed to fetch pools:', error);
    return [];
  }
};

/**
 * Validate amounts are greater than zero
 */
export const validateAmount = (amount: string): boolean => {
  try {
    const amountBig = BigInt(amount);
    return amountBig > BigInt(0);
  } catch {
    return false;
  }
};

/**
 * Calculate minimum amount with slippage tolerance
 * @param amount - Expected amount
 * @param slippage - Slippage percentage (e.g., 1 for 1%)
 */
export const calculateMinAmount = (amount: string, slippage: number): string => {
  const amountBig = BigInt(amount);
  const slippageFactor = BigInt(10000 - slippage * 100);
  return ((amountBig * slippageFactor) / BigInt(10000)).toString();
};

/**
 * Get token symbol safely
 */
export const getTokenSymbol = (assetId: number): string => {
  return KNOWN_TOKENS[assetId]?.symbol || `Asset ${assetId}`;
};

/**
 * Get token decimals safely
 */
export const getTokenDecimals = (assetId: number): number => {
  return KNOWN_TOKENS[assetId]?.decimals || 12;
};

/**
 * Calculate TVL (Total Value Locked) for a pool
 * @param reserve1 - Reserve of first token
 * @param reserve2 - Reserve of second token
 * @param decimals1 - Decimals of first token
 * @param decimals2 - Decimals of second token
 * @param price1USD - Price of first token in USD (optional)
 * @param price2USD - Price of second token in USD (optional)
 * @returns TVL in USD as string, or reserves sum if prices not available
 */
export const calculatePoolTVL = (
  reserve1: string,
  reserve2: string,
  decimals1: number = 12,
  decimals2: number = 12,
  price1USD?: number,
  price2USD?: number
): string => {
  try {
    const r1 = BigInt(reserve1);
    const r2 = BigInt(reserve2);

    if (price1USD && price2USD) {
      // Convert reserves to human-readable amounts
      const amount1 = Number(r1) / Math.pow(10, decimals1);
      const amount2 = Number(r2) / Math.pow(10, decimals2);

      // Calculate USD value
      const value1 = amount1 * price1USD;
      const value2 = amount2 * price2USD;
      const totalTVL = value1 + value2;

      return totalTVL.toFixed(2);
    }

    // Fallback: return sum of reserves (not USD value)
    // This is useful for display even without price data
    const total = r1 + r2;
    return formatTokenBalance(total.toString(), decimals1, 2);
  } catch (error) {
    console.error('Error calculating TVL:', error);
    return '0';
  }
};

/**
 * Calculate APR (Annual Percentage Rate) for a pool
 * @param feesEarned24h - Fees earned in last 24 hours (in smallest unit)
 * @param totalLiquidity - Total liquidity in pool (in smallest unit)
 * @param decimals - Token decimals
 * @returns APR as percentage string
 */
export const calculatePoolAPR = (
  feesEarned24h: string,
  totalLiquidity: string,
  decimals: number = 12
): string => {
  try {
    const fees24h = BigInt(feesEarned24h);
    const liquidity = BigInt(totalLiquidity);

    if (liquidity === BigInt(0)) {
      return '0.00';
    }

    // Daily rate = fees24h / totalLiquidity
    // APR = daily rate * 365 * 100 (for percentage)
    const dailyRate = (fees24h * BigInt(100000)) / liquidity; // Multiply by 100000 for precision
    const apr = (dailyRate * BigInt(365)) / BigInt(1000); // Divide by 1000 to get percentage

    return (Number(apr) / 100).toFixed(2);
  } catch (error) {
    console.error('Error calculating APR:', error);
    return '0.00';
  }
};

/**
 * Find best swap route using multi-hop
 * @param api - Polkadot API instance
 * @param assetIn - Input asset ID
 * @param assetOut - Output asset ID
 * @param amountIn - Amount to swap in
 * @returns Best swap route with quote
 */
export const findBestSwapRoute = async (
  api: ApiPromise,
  assetIn: number,
  assetOut: number,
  amountIn: string
): Promise<SwapQuote> => {
  try {
    // Get all available pools
    const pools = await fetchPools(api);

    // Direct swap path
    const directPool = pools.find(
      (p) =>
        (p.asset1 === assetIn && p.asset2 === assetOut) ||
        (p.asset1 === assetOut && p.asset2 === assetIn)
    );

    let bestQuote: SwapQuote = {
      amountIn,
      amountOut: '0',
      path: [assetIn, assetOut],
      priceImpact: '0',
      minimumReceived: '0',
      route: `${getTokenSymbol(assetIn)} → ${getTokenSymbol(assetOut)}`,
    };

    // Try direct swap
    if (directPool) {
      const isForward = directPool.asset1 === assetIn;
      const reserveIn = isForward ? directPool.reserve1 : directPool.reserve2;
      const reserveOut = isForward ? directPool.reserve2 : directPool.reserve1;

      const amountOut = getAmountOut(amountIn, reserveIn, reserveOut);
      const priceImpact = calculatePriceImpact(reserveIn, reserveOut, amountIn);
      const minimumReceived = calculateMinAmount(amountOut, 1); // 1% slippage

      bestQuote = {
        amountIn,
        amountOut,
        path: [assetIn, assetOut],
        priceImpact,
        minimumReceived,
        route: `${getTokenSymbol(assetIn)} → ${getTokenSymbol(assetOut)}`,
      };
    }

    // Try multi-hop routes (through intermediate tokens)
    // Common intermediate tokens: wHEZ (0), PEZ (1), wUSDT (2)
    const intermediateTokens = [0, 1, 2].filter(
      (id) => id !== assetIn && id !== assetOut
    );

    for (const intermediate of intermediateTokens) {
      try {
        // Find first hop pool
        const pool1 = pools.find(
          (p) =>
            (p.asset1 === assetIn && p.asset2 === intermediate) ||
            (p.asset1 === intermediate && p.asset2 === assetIn)
        );

        // Find second hop pool
        const pool2 = pools.find(
          (p) =>
            (p.asset1 === intermediate && p.asset2 === assetOut) ||
            (p.asset1 === assetOut && p.asset2 === intermediate)
        );

        if (!pool1 || !pool2) continue;

        // Calculate first hop
        const isForward1 = pool1.asset1 === assetIn;
        const reserveIn1 = isForward1 ? pool1.reserve1 : pool1.reserve2;
        const reserveOut1 = isForward1 ? pool1.reserve2 : pool1.reserve1;
        const amountIntermediate = getAmountOut(amountIn, reserveIn1, reserveOut1);

        // Calculate second hop
        const isForward2 = pool2.asset1 === intermediate;
        const reserveIn2 = isForward2 ? pool2.reserve1 : pool2.reserve2;
        const reserveOut2 = isForward2 ? pool2.reserve2 : pool2.reserve1;
        const amountOut = getAmountOut(amountIntermediate, reserveIn2, reserveOut2);

        // Calculate combined price impact
        const impact1 = calculatePriceImpact(reserveIn1, reserveOut1, amountIn);
        const impact2 = calculatePriceImpact(
          reserveIn2,
          reserveOut2,
          amountIntermediate
        );
        const totalImpact = (
          parseFloat(impact1) + parseFloat(impact2)
        ).toFixed(2);

        // If this route gives better output, use it
        if (BigInt(amountOut) > BigInt(bestQuote.amountOut)) {
          const minimumReceived = calculateMinAmount(amountOut, 1);
          bestQuote = {
            amountIn,
            amountOut,
            path: [assetIn, intermediate, assetOut],
            priceImpact: totalImpact,
            minimumReceived,
            route: `${getTokenSymbol(assetIn)} → ${getTokenSymbol(intermediate)} → ${getTokenSymbol(assetOut)}`,
          };
        }
      } catch (error) {
        console.warn(`Error calculating route through ${intermediate}:`, error);
        continue;
      }
    }

    return bestQuote;
  } catch (error) {
    console.error('Error finding best swap route:', error);
    return {
      amountIn,
      amountOut: '0',
      path: [assetIn, assetOut],
      priceImpact: '0',
      minimumReceived: '0',
      route: 'Error',
    };
  }
};

/**
 * Fetch user's LP token positions across all pools
 * @param api - Polkadot API instance
 * @param userAddress - User's wallet address
 */
export const fetchUserLPPositions = async (
  api: ApiPromise,
  userAddress: string
): Promise<UserLiquidityPosition[]> => {
  try {
    const positions: UserLiquidityPosition[] = [];

    // Query all pool accounts
    const poolKeys = await api.query.assetConversion.pools.keys();

    for (const key of poolKeys) {
      try {
        // Extract asset locations from storage key
        const [asset1Location, asset2Location] = key.args;
        const asset1 = parseAssetLocation(asset1Location.toJSON());
        const asset2 = parseAssetLocation(asset2Location.toJSON());

        // Get pool info to get LP token ID
        const poolInfo = await api.query.assetConversion.pools([
          formatAssetLocation(asset1),
          formatAssetLocation(asset2)
        ]);

        if ((poolInfo as any).isNone) continue;

        const poolData = (poolInfo as any).unwrap().toJSON();
        const lpTokenId = poolData.lpToken;

        // Get user's LP token balance from poolAssets pallet
        let lpTokenBalance = '0';
        if (api.query.poolAssets?.account) {
          const lpAccount = await api.query.poolAssets.account(lpTokenId, userAddress);
          if ((lpAccount as any).isSome) {
            lpTokenBalance = ((lpAccount as any).unwrap() as any).balance.toString();
          }
        }

        // Skip if user has no LP tokens for this pool
        if (lpTokenBalance === '0' || BigInt(lpTokenBalance) === BigInt(0)) {
          continue;
        }

        // Get total LP supply
        let lpSupply = BigInt(0);
        if (api.query.poolAssets?.asset) {
          const lpAssetDetails = await api.query.poolAssets.asset(lpTokenId);
          if ((lpAssetDetails as any).isSome) {
            lpSupply = BigInt(((lpAssetDetails as any).unwrap() as any).supply.toString());
          }
        }

        if (lpSupply === BigInt(0)) {
          continue; // Avoid division by zero
        }

        const userLPBig = BigInt(lpTokenBalance);

        // Share percentage: (userLP / totalLP) * 100
        const sharePercentage = (userLPBig * BigInt(10000)) / lpSupply;
        const shareOfPool = (Number(sharePercentage) / 100).toFixed(2);

        // Estimate reserves and calculate user's share
        const token1 = KNOWN_TOKENS[asset1] || { decimals: 12, symbol: `Asset ${asset1}` };
        const token2 = KNOWN_TOKENS[asset2] || { decimals: 12, symbol: `Asset ${asset2}` };

        // Try to get price ratio for reserve estimation
        let asset1Amount = '0';
        let asset2Amount = '0';

        try {
          const oneUnit = BigInt(Math.pow(10, token1.decimals));
          const quote = await (api.call as any).assetConversionApi.quotePriceExactTokensForTokens(
            formatAssetLocation(asset1),
            formatAssetLocation(asset2),
            oneUnit.toString(),
            true
          );

          if (quote && !(quote as any).isNone) {
            const price = Number((quote as any).unwrap().toString()) / Math.pow(10, token2.decimals);

            if (price > 0) {
              // Estimate total reserves from LP supply
              const sqrtPrice = Math.sqrt(price);
              const totalReserve1 = Number(lpSupply) / sqrtPrice;
              const totalReserve2 = Number(lpSupply) * sqrtPrice;

              // User's share of reserves
              const userShare = Number(userLPBig) / Number(lpSupply);
              asset1Amount = BigInt(Math.floor(totalReserve1 * userShare)).toString();
              asset2Amount = BigInt(Math.floor(totalReserve2 * userShare)).toString();
            }
          }
        } catch (error) {
          console.warn('Could not estimate user position amounts:', error);
          // Fallback: use LP balance as approximation
          asset1Amount = ((userLPBig * BigInt(50)) / BigInt(100)).toString();
          asset2Amount = ((userLPBig * BigInt(50)) / BigInt(100)).toString();
        }

        positions.push({
          poolId: `${asset1}-${asset2}`,
          asset1,
          asset2,
          lpTokenBalance,
          shareOfPool,
          asset1Amount,
          asset2Amount,
          valueUSD: undefined,
          feesEarned: undefined,
        });
      } catch (error) {
        console.warn(`Error fetching LP position:`, error);
      }
    }

    return positions;
  } catch (error) {
    console.error('Failed to fetch user LP positions:', error);
    return [];
  }
};
