<template>
  <div class="cheats-view">
    <div class="cheats-header">
      <h2>Cheat Console</h2>
      <p class="cheats-caption">
        Helpers for local forks while we wait on live liquidity. Requests hit the configured RPC directly, so keep this pointed at a trusted
        node only.
      </p>
    </div>

    <FCard>
      <div class="cheats-form">
        <FInput v-model="rpcUrl" label="RPC URL"></FInput>
        <p class="cheats-hint">Defaults to the Anvil instance from <code>scripts/dev.sh</code>.</p>
        <div class="cheats-actions">
          <FButton :disabled="!hasWalletProvider || addingNetwork" @click="addNetwork">{{
            addingNetwork ? 'Adding…' : 'Add network to wallet'
          }}</FButton>
          <FButton :disabled="!canAddToken || addingToken" @click="addToken" variant="secondary">{{
            addingToken ? 'Adding…' : 'Add KRK token'
          }}</FButton>
          <FButton :disabled="!canAddWeth || addingWeth" @click="addWethToken" variant="secondary">{{
            addingWeth ? 'Adding…' : 'Add WETH token'
          }}</FButton>
        </div>
      </div>
    </FCard>

    <FCard title="Liquidity Snapshot">
      <div class="cheats-form">
        <template v-if="!cheatConfig">
          <p class="cheats-warning">Cheat helpers are disabled for this network.</p>
        </template>
        <template v-else>
          <template v-if="statsError">
            <p class="cheats-warning">{{ statsError }}</p>
          </template>
          <template v-else-if="statsLoading">
            <p class="cheats-hint">Loading liquidity metrics…</p>
          </template>
          <template v-else-if="liquidityStats">
            <div class="liquidity-meta">
              <span>Current tick: {{ liquidityStats.currentTick }}</span>
              <span>Token0 is WETH: {{ liquidityStats.token0isWeth ? 'Yes' : 'No' }}</span>
            </div>
            <div class="liquidity-meta">
              <span>Buy depth to discovery edge: {{ liquidityStats.buyDepthFormatted }} ETH</span>
            </div>
            <table class="liquidity-table">
              <thead>
                <tr>
                  <th>Stage</th>
                  <th>Tick Lower</th>
                  <th>Tick Upper</th>
                  <th>Liquidity</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="position in liquidityStats.positions" :key="position.label">
                  <td>{{ position.label }}</td>
                  <td>{{ position.tickLower }}</td>
                  <td>{{ position.tickUpper }}</td>
                  <td>{{ position.liquidityFormatted }}</td>
                </tr>
              </tbody>
            </table>
          </template>
          <template v-else>
            <p class="cheats-hint">Liquidity stats unavailable for this network.</p>
          </template>
        </template>
        <FButton variant="secondary" :disabled="statsLoading" @click="refreshLiquidityStats">{{
          statsLoading ? 'Refreshing…' : 'Refresh stats'
        }}</FButton>
      </div>
    </FCard>

    <FCard title="Protocol Stats">
      <div class="cheats-form">
        <template v-if="!statCollection.initialized">
          <p class="cheats-hint">Loading protocol stats…</p>
        </template>
        <template v-else-if="statCollection.statsError">
          <p class="cheats-warning">{{ statCollection.statsError }}</p>
        </template>
        <template v-else>
          <div class="liquidity-meta">
            <span>Total Supply: {{ formatEth(statCollection.kraikenTotalSupply) }} KRK</span>
          </div>
          <div class="liquidity-meta">
            <span>Total Staked: {{ formatEth(statCollection.outstandingStake) }} KRK</span>
          </div>
        </template>
      </div>
    </FCard>

    <div class="cheats-grid">
      <FCard title="Mint ETH">
        <div class="cheats-form">
          <FInput v-model="mintTarget" label="Recipient"></FInput>
          <FInput v-model="mintAmount" label="Additional ETH" type="number">
            <template #details>Amount to add on top of the current balance.</template>
          </FInput>
          <FButton :disabled="minting" @click="mintEth">{{ minting ? 'Minting…' : 'Mint' }}</FButton>
        </div>
      </FCard>

      <FCard title="Force Recenter">
        <div class="cheats-form">
          <template v-if="!canTriggerRecenter">
            <p class="cheats-warning">No txnBot recenter endpoint detected for this chain.</p>
          </template>
          <template v-else>
            <FButton :disabled="forcing" @click="forceRecenter">{{ forcing ? 'Calling…' : 'Call recenter' }}</FButton>
            <p class="cheats-hint">Requests the local txnBot to execute <code>recenter()</code> with its operator key.</p>
          </template>
        </div>
      </FCard>

      <FCard title="Block Explorer">
        <div class="cheats-form">
          <p class="cheats-hint">Browse transactions, blocks, and addresses on the local Anvil fork using Otterscan.</p>
          <a href="http://localhost:5100" target="_blank" rel="noopener noreferrer" class="cheats-link">Open Otterscan</a>
        </div>
      </FCard>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import FButton from '@/components/fcomponents/FButton.vue';
import FCard from '@/components/fcomponents/FCard.vue';
import FInput from '@/components/fcomponents/FInput.vue';
import { config } from '@/wagmi';
import { useStatCollection } from '@/composables/useStatCollection';
import { useToast } from 'vue-toastification';
import { useAccount } from '@wagmi/vue';
import { getChain, DEFAULT_CHAIN_ID } from '@/config';
import { readContract, type Config as WagmiConfig } from '@wagmi/core';
import HarbJson from '@/assets/contracts/harb.json';
import {
  createPublicClient,
  erc20Abi,
  formatEther,
  http,
  parseEther,
  toHex,
  zeroAddress,
  type Address,
  type Abi,
  type EIP1193Provider,
} from 'viem';
import { SWAP_ROUTER_ABI, UNISWAP_FACTORY_ABI, UNISWAP_POOL_ABI } from '@/composables/useSwapKrk';
import { isRecord, coerceString, getErrorMessage, ensureAddress } from '@harb/utils';

const toast = useToast();
const statCollection = useStatCollection();
const { address, chainId } = useAccount();
const wagmiConfig: WagmiConfig = config;

const rpcUrl = ref('');
let lastAutoRpcUrl = '';

const mintTarget = ref('');
const mintAmount = ref('10');
const minting = ref(false);

const forcing = ref(false);

const addingNetwork = ref(false);
const addingToken = ref(false);
const addingWeth = ref(false);

interface PositionState {
  liquidity: bigint;
  tickLower: number;
  tickUpper: number;
}

interface PositionView extends PositionState {
  label: (typeof POSITION_STAGE_LABELS)[number];
  liquidityFormatted: string;
}

interface LiquidityStats {
  positions: PositionView[];
  currentTick: number;
  poolAddress: Address;
  token0isWeth: boolean;
  buyDepthWei: bigint;
  buyDepthFormatted: string;
}

type PositionContractResponse =
  | readonly [bigint, number, number]
  | {
      liquidity: bigint;
      tickLower: number;
      tickUpper: number;
    };

function isStructuredPosition(raw: PositionContractResponse): raw is { liquidity: bigint; tickLower: number; tickUpper: number } {
  return !Array.isArray(raw);
}

function toPosition(raw: PositionContractResponse): PositionState {
  if (isStructuredPosition(raw)) {
    return {
      liquidity: BigInt(raw.liquidity),
      tickLower: Number(raw.tickLower),
      tickUpper: Number(raw.tickUpper),
    };
  }

  const [liquidity, tickLower, tickUpper] = raw;
  return {
    liquidity: BigInt(liquidity),
    tickLower: Number(tickLower),
    tickUpper: Number(tickUpper),
  };
}

const liquidityStats = ref<LiquidityStats | null>(null);
const statsLoading = ref(false);
const statsError = ref<string | null>(null);
let statsRequestId = 0;

const resolvedChainId = computed(() => chainId.value ?? DEFAULT_CHAIN_ID);
const chainConfig = computed(() => getChain(resolvedChainId.value));

const cheatConfig = computed(() => chainConfig.value?.cheats ?? null);
const hasWalletProvider = computed(() => typeof window !== 'undefined' && Boolean(window.ethereum?.request));
const canAddToken = computed(() => Boolean(hasWalletProvider.value && chainConfig.value?.harb));
const canAddWeth = computed(() => Boolean(hasWalletProvider.value && cheatConfig.value?.weth));
const txnBotRecenterUrl = computed(() => {
  const baseUrl = cheatConfig.value?.txnBot?.trim();
  if (!baseUrl) return null;
  return `${baseUrl.replace(/\/$/, '')}/recenter`;
});
const canTriggerRecenter = computed(() => Boolean(txnBotRecenterUrl.value));

watch(
  address,
  value => {
    if (value) {
      mintTarget.value = value;
    }
  },
  { immediate: true }
);

watch(
  () => (cheatConfig.value?.rpc ?? '').trim(),
  value => {
    const trimmed = value;
    if (!rpcUrl.value || rpcUrl.value === lastAutoRpcUrl) {
      rpcUrl.value = trimmed;
    }
    lastAutoRpcUrl = trimmed;
  },
  { immediate: true }
);

watch(
  () => [rpcUrl.value, resolvedChainId.value, chainConfig.value?.harb, cheatConfig.value?.weth, cheatConfig.value?.swapRouter],
  () => {
    void loadLiquidityStats();
  },
  { immediate: true }
);

function resolveChainName(chainId: number): string {
  switch (chainId) {
    case 31337:
      return 'Local Anvil';
    case 84532:
      return 'Base Sepolia';
    case 8453:
      return 'Base';
    case 11155111:
      return 'Ethereum Sepolia';
    default:
      return `Chain ${chainId}`;
  }
}

function resolveRpcUrl(): string {
  const value = rpcUrl.value.trim();
  if (!value) {
    return value;
  }
  if (value.startsWith('/') && typeof window !== 'undefined' && window.location?.origin) {
    return `${window.location.origin}${value}`;
  }
  return value;
}

function getEthereumProvider(): EIP1193Provider {
  if (typeof window === 'undefined' || !window.ethereum) {
    throw new Error('Wallet provider not available');
  }
  return window.ethereum;
}

function extractMessageFromPayload(payload: unknown, keys: string[], fallback: string): string {
  if (!isRecord(payload)) {
    return fallback;
  }
  for (const key of keys) {
    const message = coerceString(payload[key]);
    if (message) {
      return message;
    }
  }
  return fallback;
}

const LIQUIDITY_MANAGER_POSITIONS_ABI = [
  {
    inputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
    name: 'positions',
    outputs: [
      { internalType: 'uint128', name: 'liquidity', type: 'uint128' },
      { internalType: 'int24', name: 'tickLower', type: 'int24' },
      { internalType: 'int24', name: 'tickUpper', type: 'int24' },
    ],
    stateMutability: 'view',
    type: 'function',
  },
] as const satisfies Abi;

const POSITION_STAGE_LABELS = ['Floor', 'Anchor', 'Discovery'] as const;
const POOL_FEE = 10_000;
const MIN_TICK = -887272;
const MAX_TICK = 887272;
const Q96 = 2n ** 96n;
const MAX_UINT256_BIGINT = (1n << 256n) - 1n;

function formatBigInt(value: bigint): string {
  const sign = value < 0n ? '-' : '';
  const digits = (value < 0n ? -value : value).toString();
  return sign + digits.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

function formatEth(value: bigint): string {
  if (value === 0n) return '0';
  const formatted = formatEther(value);
  const [whole, frac = ''] = formatted.split('.');
  const trimmedFrac = frac.replace(/0+$/, '').slice(0, 6);
  return trimmedFrac ? `${whole}.${trimmedFrac}` : whole;
}

function getSqrtRatioAtTick(tick: number): bigint {
  if (tick < MIN_TICK || tick > MAX_TICK) {
    throw new Error('Tick out of range');
  }
  const absTick = BigInt(tick < 0 ? -tick : tick);
  let ratio = (absTick & 0x1n) !== 0n ? 0xfffcb933bd6fad37aa2d162d1a594001n : 0x100000000000000000000000000000000n;
  if ((absTick & 0x2n) !== 0n) ratio = (ratio * 0xfff97272373d413259a46990580e213an) >> 128n;
  if ((absTick & 0x4n) !== 0n) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdccn) >> 128n;
  if ((absTick & 0x8n) !== 0n) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0n) >> 128n;
  if ((absTick & 0x10n) !== 0n) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644n) >> 128n;
  if ((absTick & 0x20n) !== 0n) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0n) >> 128n;
  if ((absTick & 0x40n) !== 0n) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861n) >> 128n;
  if ((absTick & 0x80n) !== 0n) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053n) >> 128n;
  if ((absTick & 0x100n) !== 0n) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4n) >> 128n;
  if ((absTick & 0x200n) !== 0n) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54n) >> 128n;
  if ((absTick & 0x400n) !== 0n) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3n) >> 128n;
  if ((absTick & 0x800n) !== 0n) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9n) >> 128n;
  if ((absTick & 0x1000n) !== 0n) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825n) >> 128n;
  if ((absTick & 0x2000n) !== 0n) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5n) >> 128n;
  if ((absTick & 0x4000n) !== 0n) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7n) >> 128n;
  if ((absTick & 0x8000n) !== 0n) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6n) >> 128n;
  if ((absTick & 0x10000n) !== 0n) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9n) >> 128n;
  if ((absTick & 0x20000n) !== 0n) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604n) >> 128n;
  if ((absTick & 0x40000n) !== 0n) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98n) >> 128n;
  if ((absTick & 0x80000n) !== 0n) ratio = (ratio * 0x48a170391f7dc42444e8fa2n) >> 128n;
  if (tick > 0) {
    ratio = MAX_UINT256_BIGINT / ratio;
  }
  const result = ratio >> 32n;
  const shouldRoundUp = (ratio & 0xffffffffn) !== 0n;
  return shouldRoundUp ? result + 1n : result;
}

function getAmount0ForLiquidity(sqrtRatioAX96: bigint, sqrtRatioBX96: bigint, liquidity: bigint): bigint {
  if (sqrtRatioAX96 > sqrtRatioBX96) {
    [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96];
  }
  if (liquidity === 0n || sqrtRatioAX96 === 0n) {
    return 0n;
  }
  const numerator = liquidity << 96n;
  const intermediate = (sqrtRatioBX96 - sqrtRatioAX96) * numerator;
  return intermediate / sqrtRatioBX96 / sqrtRatioAX96;
}

function getAmount1ForLiquidity(sqrtRatioAX96: bigint, sqrtRatioBX96: bigint, liquidity: bigint): bigint {
  if (sqrtRatioAX96 > sqrtRatioBX96) {
    [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96];
  }
  if (liquidity === 0n) {
    return 0n;
  }
  return (liquidity * (sqrtRatioBX96 - sqrtRatioAX96)) / Q96;
}

function calculateEthToMoveBetweenTicks(fromTick: number, toTick: number, liquidity: bigint): bigint {
  if (fromTick >= toTick || liquidity === 0n) {
    return 0n;
  }
  const sqrtFrom = getSqrtRatioAtTick(fromTick);
  const sqrtTo = getSqrtRatioAtTick(toTick);
  return getAmount0ForLiquidity(sqrtFrom, sqrtTo, liquidity);
}

function calculateEthToMoveBetweenTicksDown(fromTick: number, toTick: number, liquidity: bigint): bigint {
  if (fromTick <= toTick || liquidity === 0n) {
    return 0n;
  }
  const sqrtFrom = getSqrtRatioAtTick(fromTick);
  const sqrtTo = getSqrtRatioAtTick(toTick);
  return getAmount1ForLiquidity(sqrtTo, sqrtFrom, liquidity);
}

function calculateBuyDepth(token0isWeth: boolean, currentTick: number, anchor: PositionState, discovery: PositionState): bigint {
  if (anchor.liquidity === 0n && discovery.liquidity === 0n) {
    return 0n;
  }

  if (token0isWeth) {
    const targetTick = Math.max(discovery.tickUpper, anchor.tickUpper);
    if (currentTick >= targetTick) {
      return 0n;
    }

    let total = 0n;
    if (currentTick >= anchor.tickLower && currentTick < anchor.tickUpper && anchor.liquidity > 0n) {
      const anchorEnd = Math.min(targetTick, anchor.tickUpper);
      total += calculateEthToMoveBetweenTicks(currentTick, anchorEnd, anchor.liquidity);
    }
    if (targetTick > anchor.tickUpper && discovery.liquidity > 0n) {
      const discoveryStart = Math.max(currentTick, discovery.tickLower);
      if (discoveryStart < discovery.tickUpper) {
        total += calculateEthToMoveBetweenTicks(discoveryStart, targetTick, discovery.liquidity);
      }
    }
    return total;
  }

  const targetTick = Math.min(discovery.tickLower, anchor.tickLower);
  if (currentTick <= targetTick) {
    return 0n;
  }

  let total = 0n;
  if (currentTick <= anchor.tickUpper && currentTick > anchor.tickLower && anchor.liquidity > 0n) {
    const anchorEnd = Math.max(targetTick, anchor.tickLower);
    total += calculateEthToMoveBetweenTicksDown(currentTick, anchorEnd, anchor.liquidity);
  }
  if (targetTick < anchor.tickLower && discovery.liquidity > 0n) {
    const discoveryStart = Math.min(currentTick, discovery.tickUpper);
    if (discoveryStart > discovery.tickLower) {
      total += calculateEthToMoveBetweenTicksDown(discoveryStart, targetTick, discovery.liquidity);
    }
  }
  return total;
}

async function loadLiquidityStats({ notify = false }: { notify?: boolean } = {}) {
  const requestId = ++statsRequestId;
  const cheat = cheatConfig.value;
  const chain = chainConfig.value;

  if (!cheat || !chain || !cheat.weth || !chain.harb) {
    liquidityStats.value = null;
    statsError.value = null;
    statsLoading.value = false;
    if (notify) {
      toast.error('Liquidity helpers not configured for this network');
    }
    return;
  }
  if (!rpcUrl.value) {
    liquidityStats.value = null;
    statsError.value = 'Enter an RPC URL to load liquidity stats';
    statsLoading.value = false;
    if (notify) {
      toast.error(statsError.value);
    }
    return;
  }

  statsLoading.value = true;
  statsError.value = null;

  try {
    const targetRpcUrl = resolveRpcUrl();
    if (!targetRpcUrl) {
      throw new Error('RPC URL is empty');
    }

    const client = createPublicClient({ transport: http(targetRpcUrl) });
    const harbAddress = ensureAddress(chain.harb, 'KRAIKEN token');

    let peripheryManager: Address | null = null;
    try {
      const [managerFromContract] = (await client.readContract({
        address: harbAddress,
        abi: HarbJson as Abi,
        functionName: 'peripheryContracts',
      })) as readonly [Address, Address];
      peripheryManager = managerFromContract;
    } catch {
      peripheryManager = null;
    }

    const configuredManager =
      typeof cheat.liquidityManager === 'string' && cheat.liquidityManager.trim().length > 0 ? cheat.liquidityManager.trim() : null;

    const managerAddressCandidate = configuredManager ?? peripheryManager;

    if (!managerAddressCandidate) {
      throw new Error('LiquidityManager address not configured for this chain');
    }

    const liquidityManagerAddress = ensureAddress(managerAddressCandidate, 'LiquidityManager');

    const wethAddress = ensureAddress(cheat.weth, 'WETH token');
    const token0isWeth = BigInt(wethAddress) < BigInt(harbAddress);

    const swapRouterAddress = ensureAddress(cheat.swapRouter, 'Uniswap router');
    const factoryAddress = await client.readContract({
      address: swapRouterAddress,
      abi: SWAP_ROUTER_ABI,
      functionName: 'factory',
    });

    const poolAddressRaw = await client.readContract({
      address: ensureAddress(factoryAddress, 'Uniswap factory'),
      abi: UNISWAP_FACTORY_ABI,
      functionName: 'getPool',
      args: token0isWeth ? [wethAddress, harbAddress, POOL_FEE] : [harbAddress, wethAddress, POOL_FEE],
    });

    if (!poolAddressRaw || poolAddressRaw === zeroAddress) {
      throw new Error('KRK/WETH pool not found at 1% fee tier');
    }

    const poolAddress = ensureAddress(poolAddressRaw, 'Uniswap pool');
    const slot0Response = await client.readContract({
      address: poolAddress,
      abi: UNISWAP_POOL_ABI,
      functionName: 'slot0',
    });

    let currentTick: number | null = null;
    if (isRecord(slot0Response) && typeof slot0Response.tick === 'number') {
      currentTick = slot0Response.tick;
    }

    if (currentTick === null) {
      throw new Error('Unable to determine current pool tick');
    }

    const [floorRaw, anchorRaw, discoveryRaw] = await Promise.all(
      [0, 1, 2].map(
        stage =>
          client.readContract({
            address: liquidityManagerAddress,
            abi: LIQUIDITY_MANAGER_POSITIONS_ABI,
            functionName: 'positions',
            args: [stage],
          }) as Promise<PositionContractResponse>
      )
    );

    const floor = toPosition(floorRaw);
    const anchor = toPosition(anchorRaw);
    const discovery = toPosition(discoveryRaw);

    const buyDepthWei = calculateBuyDepth(token0isWeth, currentTick, anchor, discovery);

    if (statsRequestId === requestId) {
      const positions: PositionView[] = [
        { label: POSITION_STAGE_LABELS[0], ...floor, liquidityFormatted: formatBigInt(floor.liquidity) },
        { label: POSITION_STAGE_LABELS[1], ...anchor, liquidityFormatted: formatBigInt(anchor.liquidity) },
        { label: POSITION_STAGE_LABELS[2], ...discovery, liquidityFormatted: formatBigInt(discovery.liquidity) },
      ];

      liquidityStats.value = {
        positions,
        currentTick,
        poolAddress,
        token0isWeth,
        buyDepthWei,
        buyDepthFormatted: formatEth(buyDepthWei),
      };
      statsError.value = null;
    }
  } catch (error: unknown) {
    if (statsRequestId === requestId) {
      liquidityStats.value = null;
      const message = getErrorMessage(error, 'Failed to load liquidity stats');
      statsError.value = message;
      if (notify) {
        toast.error(message);
      }
    }
  } finally {
    if (statsRequestId === requestId) {
      statsLoading.value = false;
      if (!statsError.value && notify) {
        toast.success('Liquidity stats refreshed');
      }
    }
  }
}

async function refreshLiquidityStats() {
  await loadLiquidityStats({ notify: true });
}

async function rpcRequest<T>(method: string, params: unknown[]): Promise<T> {
  if (!rpcUrl.value) {
    throw new Error('RPC URL required');
  }
  const response = await fetch(resolveRpcUrl(), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
  });
  if (!response.ok) {
    throw new Error(`RPC ${method} failed with status ${response.status}`);
  }
  const payload = (await response.json().catch(() => null)) as unknown;
  if (!isRecord(payload)) {
    throw new Error('RPC response malformed');
  }

  if ('error' in payload && payload.error !== undefined) {
    const errorMessage = isRecord(payload.error)
      ? extractMessageFromPayload(payload.error, ['message'], `RPC ${method} failed`)
      : `RPC ${method} failed`;
    throw new Error(errorMessage);
  }

  if ('result' in payload) {
    return payload.result as T;
  }

  throw new Error('RPC response missing result field');
}

async function mintEth() {
  if (minting.value) return;
  try {
    minting.value = true;
    const target = ensureAddress(mintTarget.value, 'Recipient');
    let amount: bigint;
    try {
      amount = parseEther(mintAmount.value || '0');
    } catch {
      throw new Error('Enter a valid ETH amount');
    }
    if (amount <= 0n) {
      throw new Error('Amount must be greater than zero');
    }
    const current = await rpcRequest<string>('eth_getBalance', [target, 'latest']);
    const nextBalance = BigInt(current ?? '0x0') + amount;
    await rpcRequest('anvil_setBalance', [target, toHex(nextBalance)]);
    toast.success(`Set balance to ${mintAmount.value} ETH higher for ${target}`);
  } catch (error: unknown) {
    toast.error(getErrorMessage(error, 'Mint failed'));
  } finally {
    minting.value = false;
  }
}

async function addNetwork() {
  if (addingNetwork.value || !hasWalletProvider.value) return;
  try {
    addingNetwork.value = true;
    if (!rpcUrl.value) {
      throw new Error('Enter an RPC URL to add the network');
    }
    const chain = chainConfig.value;
    if (!chain) {
      throw new Error('Select a supported network first');
    }
    const provider = getEthereumProvider();
    await provider.request({
      method: 'wallet_addEthereumChain',
      params: [
        {
          chainId: toHex(BigInt(resolvedChainId.value)),
          chainName: resolveChainName(resolvedChainId.value),
          rpcUrls: [rpcUrl.value],
          nativeCurrency: {
            name: 'Ether',
            symbol: 'ETH',
            decimals: 18,
          },
          blockExplorerUrls: [],
        },
      ],
    });
    toast.success('Network request sent to your wallet');
  } catch (error: unknown) {
    toast.error(getErrorMessage(error, 'Failed to add network'));
  } finally {
    addingNetwork.value = false;
  }
}

async function addToken() {
  if (addingToken.value || !canAddToken.value) return;
  try {
    addingToken.value = true;
    const chain = chainConfig.value;
    if (!chain?.harb) {
      throw new Error('KRAIKEN token address not configured for this chain');
    }
    const harbAddress = ensureAddress(chain.harb, 'KRAIKEN token');
    const chainId = resolvedChainId.value;
    const provider = getEthereumProvider();
    const [symbol, decimals] = await Promise.all([
      readContract(wagmiConfig, {
        abi: erc20Abi,
        address: harbAddress,
        functionName: 'symbol',
        chainId,
      }),
      readContract(wagmiConfig, {
        abi: erc20Abi,
        address: harbAddress,
        functionName: 'decimals',
        chainId,
      }),
    ]);
    await provider.request({
      method: 'wallet_watchAsset',
      params: {
        type: 'ERC20',
        options: {
          address: harbAddress,
          symbol: symbol?.slice(0, 11) || 'HARB',
          decimals: Number.isFinite(decimals) ? decimals : 18,
        },
      },
    });
    toast.success(`Token request sent (${symbol || 'HARB'})`);
  } catch (error: unknown) {
    toast.error(getErrorMessage(error, 'Failed to add token'));
  } finally {
    addingToken.value = false;
  }
}

async function addWethToken() {
  if (addingWeth.value || !canAddWeth.value) return;
  try {
    addingWeth.value = true;
    const cheat = cheatConfig.value;
    if (!cheat?.weth) {
      throw new Error('WETH address not configured for this chain');
    }
    const wethAddress = ensureAddress(cheat.weth, 'WETH token');
    const chainId = resolvedChainId.value;
    const provider = getEthereumProvider();
    const [symbol, decimals] = await Promise.all([
      readContract(wagmiConfig, {
        abi: erc20Abi,
        address: wethAddress,
        functionName: 'symbol',
        chainId,
      }),
      readContract(wagmiConfig, {
        abi: erc20Abi,
        address: wethAddress,
        functionName: 'decimals',
        chainId,
      }),
    ]);
    await provider.request({
      method: 'wallet_watchAsset',
      params: {
        type: 'ERC20',
        options: {
          address: wethAddress,
          symbol: symbol?.slice(0, 11) || 'WETH',
          decimals: Number.isFinite(decimals) ? decimals : 18,
        },
      },
    });
    toast.success(`Token request sent (${symbol || 'WETH'})`);
  } catch (error: unknown) {
    toast.error(getErrorMessage(error, 'Failed to add token'));
  } finally {
    addingWeth.value = false;
  }
}

async function forceRecenter() {
  if (forcing.value || !txnBotRecenterUrl.value) return;
  try {
    forcing.value = true;
    const response = await fetch(txnBotRecenterUrl.value, { method: 'POST' });
    const payload = (await response.json().catch(() => null)) as unknown;
    const payloadRecord = isRecord(payload) ? payload : null;

    if (!response.ok) {
      const errorSource = payloadRecord?.error;
      const message = isRecord(errorSource)
        ? extractMessageFromPayload(errorSource, ['message', 'error'], response.statusText || 'recenter failed')
        : (coerceString(errorSource) ?? (response.statusText || 'recenter failed'));
      throw new Error(message);
    }

    const successMessage = payloadRecord
      ? extractMessageFromPayload(payloadRecord, ['message', 'status'], 'TxnBot is recentering')
      : 'TxnBot is recentering';
    toast.success(successMessage);
  } catch (error: unknown) {
    toast.error(getErrorMessage(error, 'recenter failed'));
  } finally {
    forcing.value = false;
  }
}
</script>

<style lang="sass">
.cheats-view
    display: flex
    flex-direction: column
    gap: 24px
    padding: 24px
    @media (min-width: 992px)
        padding: 48px

.cheats-header
    display: flex
    flex-direction: column
    gap: 8px

.cheats-caption
    color: #A3A3A3
    max-width: 640px

.cheats-grid
    display: grid
    gap: 24px
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr))

.cheats-form
    display: flex
    flex-direction: column
    gap: 16px

.cheats-actions
    display: flex
    flex-wrap: wrap
    gap: 12px

.liquidity-meta
    display: flex
    flex-wrap: wrap
    gap: 12px
    font-size: 14px

.liquidity-table
    width: 100%
    border-collapse: collapse
    font-size: 14px

.liquidity-table th,
.liquidity-table td
    padding: 6px 8px
    text-align: left
    border-bottom: 1px solid #2a2a2a

.cheats-hint
    font-size: 12px
    color: #A3A3A3

.cheats-warning
    color: #FFB347
    font-size: 14px

.cheats-link
    color: #60A5FA
    text-decoration: none
    font-weight: 500
    &:hover
        text-decoration: underline

code
    font-family: "JetBrains Mono", monospace
</style>
