// ZAP panel — fund a position with ONE token: a pool-side token, WETH/USDG,
// or ANY token (custom picker). Shared by POOLS (mint/add) and POSITIONS
// (increase): the parent picks the target, this panel solves the split,
// previews it, and walks the execution sequence. See lib/zap.ts for the math.
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'
import { useTranslation } from 'react-i18next'
import { useAccount, usePublicClient } from 'wagmi'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { formatUnits, parseUnits, type Address } from 'viem'
import { ADDR, CHAIN_ID, NATIVE } from '../config/addresses'
import { ENV } from '../config/env'
import { simulateClAdd, simulateV2Add, fmtApr } from '../lib/apr'
import { applySlippage } from '../lib/clmath'
import { fmtAmount, fmtUsd } from '../lib/format'
import { autoSlippage, retrySlippage, slippagePctToBps, slippageTone, SLIPPAGE_CHOICES, type AutoSlippage } from '../lib/swapGate'
import { SOLVER_AUTO_REFRESHES, solverQuoteAutoRefreshExhausted, solverQuoteCanAutoRefresh } from '../lib/solverRefresh'
import { autostake } from '../lib/autostake'
import {
  DEPOSIT_MINS_BPS,
  executeZap,
  planZap,
  stakeableTarget,
  zapRouteLabel,
  zapStages,
  zapVenueFeeBps,
  type ZapFailReason,
  type ZapPlan,
  type ZapTarget,
} from '../lib/zap'
import { useBalances } from '../hooks/useBalances'
import { usePools } from '../hooks/usePools'
import { useTokenList } from '../hooks/useTokenList'
import type { PoolStat } from '../lib/poolstats'
import type { TokenInfo } from '../types'
import { StakeAfterToggle } from './StakeAfterToggle'
import { TokenSelect } from './TokenSelect'
import { Btn, NumInput } from './ui'

const ETH_GAS_BUFFER = parseUnits('0.001', 18)
const ZAP_PLAN_REFRESH_MS = 30_000

const ETH_TOKEN: TokenInfo = { address: NATIVE as Address, symbol: 'ETH', decimals: 18, native: true }
const WETH_TOKEN: TokenInfo = { address: ADDR.WETH, symbol: 'WETH', decimals: 18 }
const USDG_TOKEN: TokenInfo = { address: ADDR.USDG, symbol: 'USDG', decimals: 6 }

export function ZapPanel(props: {
  target: ZapTarget
  t0: TokenInfo
  t1: TokenInfo
  stat?: PoolStat
  upUsd?: number
  wethUsd?: number | null
}) {
  const { target, t0, t1 } = props
  const { t } = useTranslation()
  const pool = target.pool
  const { address: user } = useAccount()
  const client = usePublicClient({ chainId: CHAIN_ID })
  const pools = usePools()
  const tokenList = useTokenList()
  const autoStake = useSyncExternalStore(autostake.subscribe, autostake.get)
  const stakeable = stakeableTarget(target)

  // funding candidates: the pair itself, ETH, and the two majors (deduped —
  // a WETH/USDG pool doesn't repeat them); anything else via the picker
  const candidates = useMemo(() => {
    const seen = new Set<string>()
    return [t0, t1, ETH_TOKEN, WETH_TOKEN, USDG_TOKEN].filter((tok) => {
      const k = tok.address.toLowerCase()
      if (seen.has(k)) return false
      seen.add(k)
      return true
    })
  }, [t0, t1])
  const [selTok, setSelTok] = useState<TokenInfo | null>(null)
  const tIn = selTok ?? t0
  const tokenInAddr: Address = tIn.address
  const customSel = !candidates.some((c) => c.address.toLowerCase() === tokenInAddr.toLowerCase())

  const [amtStr, setAmtStr] = useState('')
  const [amount, setAmount] = useState(0n)
  // slippage: null = AUTO (quote-derived); a bps number = a preset chip or the
  // typed custom value (customSlip holds the raw text so the input can own focus)
  const [manualSlip, setManualSlip] = useState<number | null>(null)
  const [customSlip, setCustomSlip] = useState('')
  const [slipOpen, setSlipOpen] = useState(false)
  // floor under AUTO after a slippage halt: never re-offer the number that
  // just failed. Cleared when the trade context changes or a run succeeds.
  const [slipFloor, setSlipFloor] = useState<number | null>(null)
  const [running, setRunning] = useState(false)
  const [runAt, setRunAt] = useState<{ i: number; failed: boolean; reason?: ZapFailReason; slip?: number } | null>(
    null,
  )
  const [runPlan, setRunPlan] = useState<ZapPlan | null>(null)
  const [done, setDone] = useState(false)

  useEffect(() => {
    const h = setTimeout(() => {
      try {
        setAmount(parseUnits(amtStr === '' ? '0' : amtStr, tIn.decimals))
      } catch {
        setAmount(0n)
      }
    }, 350)
    return () => clearTimeout(h)
  }, [amtStr, tIn.decimals])

  // a new pool/token/amount is a new trade — yesterday's failure floor doesn't apply
  useEffect(() => {
    setSlipFloor(null)
  }, [pool.address, tokenInAddr, amount])

  const balAddrs = useMemo(() => {
    const s = new Map<string, Address>()
    for (const c of candidates) s.set(c.address.toLowerCase(), c.address)
    s.set(tokenInAddr.toLowerCase(), tokenInAddr)
    return [...s.values()]
  }, [candidates, tokenInAddr])
  const bal = useBalances(user, balAddrs)
  const balIn = bal.data?.[tokenInAddr.toLowerCase()]
  let spendable = balIn
  if (balIn !== undefined && tIn.native) spendable = balIn > ETH_GAS_BUFFER ? balIn - ETH_GAS_BUFFER : 0n
  const insufficient = spendable !== undefined && amount > spendable

  // ticks key parts (cl targets re-plan when the parent's range changes)
  const lo = target.kind === 'v2' ? 0 : target.tickLower
  const hi = target.kind === 'v2' ? 0 : target.tickUpper
  const up33State = pools.error ? 'up33-failed' : pools.data ? 'up33-ready' : 'up33-pending'

  const queryClient = useQueryClient()
  const zapPlanKey = ['zapPlan', pool.address, lo, hi, tokenInAddr, amount.toString(), up33State] as const
  const plan = useQuery({
    queryKey: zapPlanKey,
    // same cap as the swap's solver quote: auto-refresh a few times, then pause
    // — execution re-quotes the swap leg fresh regardless, and the manual
    // refresh below revives a paused (or failed-out) preview
    enabled: (query) =>
      !!client && !pools.isPending && amount > 0n && !running && solverQuoteCanAutoRefresh(query.state),
    refetchInterval: ZAP_PLAN_REFRESH_MS,
    staleTime: 15_000,
    retry: false,
    queryFn: () =>
      planZap({
        client: client!,
        pools: pools.error ? null : (pools.data?.pools ?? null),
        target,
        tokenIn: tokenInAddr,
        amountIn: amount,
      }),
  })
  const p = running ? runPlan : (plan.data ?? null)
  // once the auto-refresh cap is hit the preview stops updating — surface a
  // manual refresh. Gate on amount, not on data: four planning FAILURES also
  // exhaust the budget, and that is exactly when a retry control is needed.
  const planRecord = queryClient.getQueryCache().find({ queryKey: zapPlanKey, exact: true })
  const planPaused = amount > 0n && !running && !!planRecord && solverQuoteAutoRefreshExhausted(planRecord.state)
  const autoBase = p?.impactBps == null ? null : autoSlippage(p.impactBps, zapVenueFeeBps(p))
  // post-halt floor wins over the quote-derived number (and stands in for it
  // when the impact probe is unavailable — a failed run IS a measurement)
  const flooredBps = slipFloor === null ? null : Math.max(autoBase?.bps ?? 0, slipFloor)
  const automaticSlippage: AutoSlippage | null =
    flooredBps === null ? autoBase : { bps: flooredBps, tone: slippageTone(flooredBps) }
  const effectiveSlip = manualSlip ?? automaticSlippage?.bps
  const customInvalid = customSlip !== '' && slippagePctToBps(customSlip) === null
  const slippageRequired = !!p && p.legs.length > 0 && effectiveSlip === undefined
  const stages = useMemo(
    () => (p ? zapStages(p, target, tIn, t0, t1, autoStake) : []),
    [p, target, tIn, t0, t1, autoStake],
  )

  // projected APRs on the planned deposit (mint/add previews only — an
  // increase's projection is the parent card's business)
  const sim = useMemo(() => {
    if (!p || target.kind === 'cl-increase') return null
    const a0h = Number(formatUnits(p.dep0, t0.decimals))
    const a1h = Number(formatUnits(p.dep1, t1.decimals))
    if (target.kind === 'cl-mint')
      return simulateClAdd({
        pool: target.pool,
        tickLower: target.tickLower,
        tickUpper: target.tickUpper,
        liquidity: p.liquidity,
        amount0h: a0h,
        amount1h: a1h,
        dec0: t0.decimals,
        dec1: t1.decimals,
        stat: props.stat,
        upUsd: props.upUsd,
        wethUsd: props.wethUsd,
      })
    return simulateV2Add({
      pool: target.pool,
      amount0h: a0h,
      amount1h: a1h,
      dec0: t0.decimals,
      dec1: t1.decimals,
      stat: props.stat,
      upUsd: props.upUsd,
    })
  }, [p, target, t0, t1, props.stat, props.upUsd, props.wethUsd])

  // impact-vs-band honesty: a swap that eats a big slice of the band's width
  // will land the deposit off-ratio (dust) or out of band entirely
  const bandWarn = useMemo(() => {
    if (!p || p.impactBps === null || target.kind === 'v2') return null
    const halfPct = (Math.pow(1.0001, (target.tickUpper - target.tickLower) / 2) - 1) * 100
    const impactPct = p.impactBps / 100
    if (impactPct > Math.max(halfPct / 3, 2))
      return t('zap.bandWarn', { impact: impactPct.toFixed(2), band: halfPct.toFixed(1) })
    return null
  }, [p, target, t])

  const dustNote = (side: 0 | 1): string | null => {
    if (!p) return null
    const dust = side === 0 ? p.dust0 : p.dust1
    const dep = side === 0 ? p.dep0 : p.dep1
    if (dust === 0n || dep === 0n) return null
    if (dust * 200n < dep) return null // <0.5% — noise
    const token = side === 0 ? t0 : t1
    return `${fmtAmount(dust, token.decimals)} ${token.symbol}`
  }
  const dusts = [dustNote(0), dustNote(1)].filter(Boolean)

  const run = async () => {
    const frozen = plan.data
    if (!user || !frozen || amount === 0n || running) return
    const executionSlippage = frozen.legs.length === 0 ? 0 : effectiveSlip
    if (executionSlippage === undefined) return
    setRunPlan(frozen)
    setRunning(true)
    setDone(false)
    setRunAt({ i: 0, failed: false })
    try {
      const res = await executeZap({
        plan: frozen,
        target,
        user,
        slipBps: executionSlippage,
        stake: autoStake,
        tIn,
        t0,
        t1,
        onStage: (i) => setRunAt({ i, failed: false }),
      })
      if (res.ok) {
        setRunAt(null)
        setRunPlan(null)
        setDone(true)
        setSlipFloor(null)
        setAmtStr('')
      } else {
        const reason = res.reason ?? 'other'
        setRunAt({ i: res.failedAt ?? 0, failed: true, reason, slip: executionSlippage })
        // AUTO must not re-offer the tolerance that just failed — only ever raise
        if (reason === 'slippage') setSlipFloor((prev) => Math.max(prev ?? 0, retrySlippage(executionSlippage)))
        // and the retry must re-plan from a live quote, not the ≤30s cached one
        void plan.refetch()
      }
    } finally {
      setRunning(false)
    }
  }

  const failed = runAt?.failed ?? false
  let runLabel = t('zap.run')
  if (!user) runLabel = t('common.connectWallet')
  else if (slippageRequired) runLabel = t('zap.chooseSlippage')
  else if (stages.length > 0) runLabel = t('zap.runSteps', { n: stages.length })

  // slippage: one summary line; the full editor unfolds on demand (or when a
  // choice is REQUIRED — no impact probe / invalid custom value)
  const slipForced = slippageRequired || customInvalid
  const showSlipEditor = slipOpen || slipForced
  const slipSummaryTone = customInvalid
    ? 'red'
    : effectiveSlip !== undefined
      ? slippageTone(effectiveSlip)
      : 'dim'
  const slipSummary = customInvalid
    ? t('common.slipInvalid')
    : effectiveSlip !== undefined
      ? `${manualSlip === null && customSlip === '' ? 'AUTO · ' : ''}${effectiveSlip / 100}%`
      : t('zap.chooseSlippage')

  const totalSwapIn = p ? p.legs.reduce((s, l) => s + l.swapIn, 0n) : 0n
  const legRows = p
    ? p.legs.map((leg, i) => {
        const tOut = leg.buyIs0 ? t0 : t1
        return (
          <div className="spec-row" key={i}>
            <span className="sk">{p.legs.length > 1 ? t('zap.swapN', { n: i + 1 }) : t('zap.swapRow')}</span>
            <span className="sv">
              {fmtAmount(leg.swapIn, tIn.decimals)} {tIn.symbol} → ≈ {fmtAmount(leg.estOut, tOut.decimals)}{' '}
              {tOut.symbol}
            </span>
            <span className="sd">
              {effectiveSlip === undefined
                ? t('zap.chooseSlippage')
                : t('zap.swapMin', {
                    amt: fmtAmount(applySlippage(leg.estOut, effectiveSlip), tOut.decimals),
                    slip: effectiveSlip / 100,
                  })}
              {leg.impactBps === null ? (
                <span className="dim"> · {t('zap.impactOff')}</span>
              ) : (
                <span className={slippageTone(leg.impactBps)}>
                  {' '}
                  · {t('zap.impact', { pct: (leg.impactBps / 100).toFixed(2) })}
                </span>
              )}
              {/* zap still charges while market swaps are free — keep it bright */}
              <span className="fg"> · {t('zap.terminalFee', { pct: (ENV.zapFeeBps / 100).toFixed(2) })}</span>
              {' · '}
              {t('zap.via', { route: zapRouteLabel(leg.via) })}
            </span>
          </div>
        )
      })
    : null

  return (
    <div className="zap">
      <div className="form-row">
        <span className="lbl">{t('zap.zapIn')}</span>
        {candidates.map((c) => (
          <button
            key={c.address}
            className={`chip ${tokenInAddr.toLowerCase() === c.address.toLowerCase() ? 'on' : ''}`}
            onClick={() => setSelTok(c)}
            disabled={running}
            title={c.native ? t('zap.ethTip') : undefined}
          >
            {c.symbol}
          </button>
        ))}
        {customSel && <button className="chip on">{tIn.symbol}</button>}
        <TokenSelect list={tokenList.tokens} value={tIn} onChange={setSelTok} label={t('zap.pickOther')} />
      </div>
      <div className="form-row">
        <NumInput value={amtStr} onChange={setAmtStr} disabled={running} width={220} />
        {spendable !== undefined && (
          <>
            <button className="chip" disabled={running} onClick={() => setAmtStr(formatUnits(spendable, tIn.decimals))}>
              {t('common.max')}
            </button>
            <span className="dim mono-sm">
              {t('common.bal')} {fmtAmount(spendable, tIn.decimals)} {tIn.symbol}
            </span>
          </>
        )}
        {insufficient && <span className="red mono-sm">{t('common.exceedsBalance')}</span>}
      </div>
      <div className="form-row">
        <span className="lbl">{t('zap.slip')}</span>
        {!showSlipEditor ? (
          <button
            className={`chip ${slipSummaryTone}`}
            onClick={() => setSlipOpen(true)}
            disabled={running}
            title={t('zap.slipHint')}
          >
            {slipSummary} ▾
          </button>
        ) : (
          <>
            <button
              className={`chip ${manualSlip === null && customSlip === '' ? 'on' : ''}`}
              onClick={() => {
                setManualSlip(null)
                setCustomSlip('')
              }}
              disabled={running}
            >
              AUTO {automaticSlippage ? `${automaticSlippage.bps / 100}%` : '—'}
            </button>
            {SLIPPAGE_CHOICES.map((b) => (
              <button
                key={b}
                className={`chip ${manualSlip === b && customSlip === '' ? 'on' : ''}`}
                onClick={() => {
                  setManualSlip(b)
                  setCustomSlip('')
                }}
                disabled={running}
              >
                {b / 100}%
              </button>
            ))}
            <NumInput
              value={customSlip}
              onChange={(v) => {
                setCustomSlip(v)
                setManualSlip(slippagePctToBps(v))
              }}
              disabled={running}
              placeholder={t('common.slipCustom')}
              width={96}
              invalid={customInvalid}
            />
            <span className="dim mono-sm">%</span>
            {customInvalid && <span className="red mono-sm">{t('common.slipInvalid')}</span>}
            {effectiveSlip !== undefined && (
              <span className={`${slippageTone(effectiveSlip)} mono-sm`}>→ {effectiveSlip / 100}%</span>
            )}
            {!slipForced && (
              <button className="chip" onClick={() => setSlipOpen(false)} disabled={running} title={t('zap.slipHint')}>
                ▴
              </button>
            )}
          </>
        )}
      </div>

      {amount > 0n && plan.isLoading && (
        <div className="dim mono-sm">
          {t('zap.solving')}
          <span className="spin">▮</span>
        </div>
      )}
      {amount > 0n && plan.isError && !running && (
        <div className="red mono-sm">{t('zap.cantPlan', { err: (plan.error as Error).message })}</div>
      )}

      {p && (
        <div className="spec">
          <div className="spec-hd">{t('zap.planTitle')}</div>
          <div className="spec-row">
            <span className="sk">{t('zap.split')}</span>
            <span className="sv">
              {p.inIs0 !== null
                ? p.legs.length === 0
                  ? t('zap.keepAll', { amt: fmtAmount(p.keep, tIn.decimals), sym: tIn.symbol })
                  : t('zap.keepSwap', {
                      keep: fmtAmount(p.keep, tIn.decimals),
                      swap: fmtAmount(totalSwapIn, tIn.decimals),
                      sym: tIn.symbol,
                    })
                : p.legs.length === 2
                  ? t('zap.outsideSplit', {
                      a0: fmtAmount(p.legs.find((l) => l.buyIs0)?.swapIn ?? 0n, tIn.decimals),
                      a1: fmtAmount(p.legs.find((l) => !l.buyIs0)?.swapIn ?? 0n, tIn.decimals),
                      sym: tIn.symbol,
                      sym0: t0.symbol,
                      sym1: t1.symbol,
                    })
                  : t('zap.outsideOne', {
                      amt: fmtAmount(totalSwapIn, tIn.decimals),
                      sym: tIn.symbol,
                      outSym: p.legs[0]?.buyIs0 ? t0.symbol : t1.symbol,
                    })}
            </span>
            <span className="sd">
              {p.inIs0 !== null
                ? p.legs.length === 0
                  ? t('zap.splitSdSingle')
                  : t('zap.splitSd')
                : t('zap.outsideSd')}
            </span>
          </div>
          {legRows}
          <div className="spec-row">
            <span className="sk">{t('zap.depositRow')}</span>
            <span className="sv">
              ≈ {fmtAmount(p.dep0, t0.decimals)} {t0.symbol} + {fmtAmount(p.dep1, t1.decimals)} {t1.symbol}
            </span>
            <span className="sd">
              {dusts.length > 0 ? t('zap.dustSd', { dust: dusts.join(' + ') }) : t('zap.actualSd')}
            </span>
          </div>
          {sim && (
            <div className="spec-row">
              <span className="sk">{t('add.projected')}</span>
              {sim.inRange ? (
                <>
                  <span className="sv">
                    {t('add.projDep', { usd: fmtUsd(sim.depositUsd) })}
                    {Number.isFinite(sim.feeApr) && (
                      <>
                        {' '}
                        · {t('add.projFeeApr')}
                        {pool.protocol === 'up33' ? <span className="dim"> {t('add.projIfUnstaked')}</span> : ''} ≈{' '}
                        {fmtApr(sim.feeApr)}
                      </>
                    )}
                    {Number.isFinite(sim.emitApr) && (
                      <>
                        {' '}
                        · {t('add.projEmitApr')}
                        <span className="dim"> {t('add.projIfStaked')}</span> ≈{' '}
                        <span className="green">{fmtApr(sim.emitApr)}</span>
                      </>
                    )}
                  </span>
                  <span className="sd">
                    {t('add.projShare', { pct: sim.sharePct < 0.01 ? '<0.01' : sim.sharePct.toFixed(2) })}
                  </span>
                </>
              ) : (
                <span className="sv red">{t('add.projOut')}</span>
              )}
            </div>
          )}
          {bandWarn && (
            <div className="spec-row">
              <span className="sk">!!</span>
              <span className="sv amber">{bandWarn}</span>
            </div>
          )}
        </div>
      )}

      {planPaused && (
        <div className="form-row">
          <button
            className="chip amber"
            onClick={() => void plan.refetch()}
            title={t('zap.refreshTip', { s: ZAP_PLAN_REFRESH_MS / 1000, n: SOLVER_AUTO_REFRESHES })}
          >
            {plan.isFetching ? <span className="spin">▮</span> : `↻ ${t('zap.refreshPlan')}`}
          </button>
        </div>
      )}

      {stages.length > 0 && (
        <div className="zap-steps mono-sm">
          {stages.map((s, i) => {
            let state: 'todo' | 'done' | 'fail' | 'run' = 'todo'
            if (runAt && i < runAt.i) state = 'done'
            else if (runAt && i === runAt.i && failed) state = 'fail'
            else if (runAt && i === runAt.i && running) state = 'run'
            let mark = '·'
            if (state === 'done') mark = '✓'
            else if (state === 'run') mark = '▮'
            else if (state === 'fail') mark = '✗'
            return (
              <div key={i} className={`zstep ${state}`}>
                <span className="zn">{i + 1}</span>
                <span className="zm">{mark}</span>
                <span>{s.label}</span>
              </div>
            )
          })}
        </div>
      )}

      {failed && (
        <div className="red mono-sm">
          {runAt?.reason === 'slippage'
            ? t('zap.haltedSwap', {
                n: (runAt.i ?? 0) + 1,
                slip: (runAt.slip ?? 0) / 100,
                next: (automaticSlippage?.bps ?? retrySlippage(runAt.slip ?? 0)) / 100,
              })
            : runAt?.reason === 'deposit-moved'
              ? t('zap.haltedDeposit', { n: (runAt.i ?? 0) + 1, band: DEPOSIT_MINS_BPS / 100 })
              : t('zap.halted', { n: (runAt?.i ?? 0) + 1 })}
        </div>
      )}
      {done && <div className="green mono-sm">{t('zap.done')}</div>}

      <div className="form-row">
        <Btn
          busy={running}
          onClick={run}
          disabled={!user || amount === 0n || !plan.data || insufficient || slippageRequired || running}
        >
          {runLabel}
        </Btn>
        {stakeable && <StakeAfterToggle disabled={running} />}
        <span className="dim mono-sm">{t('zap.runHint')}</span>
      </div>
    </div>
  )
}
