/**
 * useForexMarkets.js
 *
 * React hook — wraps ForexEngine to provide per-pair FX market state.
 *
 * Returned market object per pair:
 * {
 *   pair, price, bid, ask, mid,
 *   spread, spreadPips, pipSize, decimals,
 *   open, high, low, close, volumeLots,
 *   basePrice, change, changePips, percentChange, isUp, isDown,
 *   orderBook:  { bids: Level[], asks: Level[] },
 *   ticks:      Tick[],
 *   session:    { label, volMult, spreadMult, liquidityScore },
 *   isWeekend,  isRollover,
 *   ts,
 *   _prev,
 * }
 *
 * Drop-in parallel to useHybridMarkets — same options shape, FX-aware output.
 */

import { useEffect, useMemo, useRef, useState } from "react";
import RNG from "./RNG";
import ForexEngine from "./ForexEngine";

// ─────────────────────────────────────────────────────────────────────────────
// Defaults
// ─────────────────────────────────────────────────────────────────────────────

const DEFAULT_TICK_OPTS = { drift: true, book: true, trades: true };

const DEFAULT_ANCHOR = {
  enabled:        true,
  internalWeight: 0.05,   // FX: trust live feed even more than equities
  externalWeight: 0.95,
};

// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────

function toPairsKey(pairs) {
  return (Array.isArray(pairs) ? pairs : [])
    .filter(Boolean)
    .map((s) => String(s).trim().toUpperCase())
    .filter(Boolean)
    .sort()
    .join("|");
}

function safeRate(value, fallback = 0) {
  const n = Number(value);
  return Number.isFinite(n) && n > 0 ? n : fallback;
}

function priceToPips(priceOffset, pipSize) {
  return Math.abs(priceOffset) / Math.max(pipSize, 1e-8);
}

// ─────────────────────────────────────────────────────────────────────────────
// Hook
// ─────────────────────────────────────────────────────────────────────────────

/**
 * @param {object}   options
 *
 * @param {string[]} options.pairs
 *   Currency pairs to simulate, e.g. ["EURUSD", "GBPJPY"].
 *
 * @param {object}   options.ratesByPair
 *   External price feed: { [PAIR]: { price, fallback?, bid?, ask? } }
 *   Update this from your live WebSocket/REST feed; the engine anchors to it.
 *
 * @param {number}   options.tickMs          Tick cadence in ms (default 5000)
 * @param {number}   options.seed            RNG seed (default 42)
 * @param {object}   options.anchor          { enabled, internalWeight, externalWeight }
 * @param {object}   options.tickOptions     { drift, book, trades }
 * @param {boolean}  options.persistBaseline Keep baseline across pair-set changes
 * @param {"session"|"external"|"firstTick"} options.baselineMode
 * @param {boolean}  options.resetBaselineOnPairReadd
 */
export function useForexMarkets({
  pairs                    = [],
  ratesByPair              = {},
  tickMs                   = 5_000,
  seed                     = 42,
  anchor                   = DEFAULT_ANCHOR,
  tickOptions              = DEFAULT_TICK_OPTS,
  persistBaseline          = true,
  baselineMode             = "session",
  resetBaselineOnPairReadd = false,
} = {}) {

  // ── Refs ─────────────────────────────────────────────────────────────────

  const engineRef   = useRef(null);
  const intervalRef = useRef(null);
  const ratesRef    = useRef({});
  const baselineRef = useRef({});
  const rngRef      = useRef(null);

  // Keep ratesRef in sync — assign directly on every render so the interval
  // closure always reads fresh data without being in the dep array.
  ratesRef.current = ratesByPair ?? {};

  // ── Derived pair list ─────────────────────────────────────────────────────

  const pairsKey = useMemo(() => toPairsKey(pairs), [pairs]);

  const cleanedPairs = useMemo(
    () => (pairsKey ? pairsKey.split("|") : []),
    [pairsKey],
  );

  // Stable key: only changes when actual prices change, not on every render.
  // This lets the effect reinit the engine when real rates first arrive
  // without causing infinite loops from object reference churn.
  const ratesPriceKey = useMemo(() => {
    return cleanedPairs
      .map((p) => `${p}:${ratesByPair?.[p]?.price ?? ""}`)
      .join("|");
  }, [cleanedPairs, ratesByPair]);

  // ── State ─────────────────────────────────────────────────────────────────

  const [markets, setMarkets] = useState({});

  // ── Internal helpers ─────────────────────────────────────────────────────

  const getExternalPrice = (pair) => safeRate(ratesRef.current?.[pair]?.price);
  const getFallbackPrice = (pair) => safeRate(ratesRef.current?.[pair]?.fallback, 1.0);

  const ensureBaseline = (pair, candidateMid) => {
    if (!persistBaseline) return safeRate(candidateMid, 1);

    const existing = safeRate(baselineRef.current[pair]);
    if (existing > 0) return existing;

    let base = 0;
    if (baselineMode === "external") {
      const ext = getExternalPrice(pair);
      base = ext > 0 ? ext : safeRate(candidateMid, 1);
    } else {
      base = safeRate(candidateMid, 1);
    }

    if (base > 0) baselineRef.current[pair] = base;
    return base;
  };

  /**
   * Build the forex market snapshot consumers receive.
   * Forex-specific additions: changePips, spreadPips, pipSize, decimals,
   * isWeekend, isRollover, volumeLots, ticks (not "trades").
   */
  const snapshotPair = (eng, pair, prevState) => {
    const asset   = eng.assets[pair]       ?? {};
    const quote   = eng.quotes[pair]       ?? {};
    const book    = eng.orderBooks[pair]   ?? { bids: [], asks: [] };
    const ticks   = eng.recentTrades[pair] ?? [];
    const session = eng.sessions[pair]     ?? {};

    const price     = safeRate(quote.mid ?? asset.price, 1);
    const basePrice = ensureBaseline(pair, price);

    const pipSize   = quote.pipSize ?? 0.0001;
    const decimals  = quote.decimals ?? 5;

    const change        = price - basePrice;
    const changePips    = priceToPips(change, pipSize);
    const percentChange = basePrice > 0 ? (change / basePrice) * 100 : 0;

    const isWeekend  = session.label === "weekend-closed";
    const isRollover = session.label === "rollover";

    return {
      // Identity
      pair,
      session,
      isWeekend,
      isRollover,

      // Price
      price,
      basePrice,
      change,
      changePips:     parseFloat(changePips.toFixed(1)),
      percentChange,
      isUp:           change > 0,
      isDown:         change < 0,

      // OHLC (no volume in units — use lots)
      open:       safeRate(asset.open,  price),
      high:       safeRate(asset.high,  price),
      low:        safeRate(asset.low,   price),
      close:      safeRate(asset.close, price),
      volumeLots: asset.volumeLots ?? 0,

      // Quote
      bid:        safeRate(quote.bid),
      ask:        safeRate(quote.ask),
      mid:        safeRate(quote.mid, price),
      spread:     quote.spread     ?? 0,
      spreadPips: quote.spreadPips ?? 0,
      pipSize,
      decimals,

      // Depth & tape
      orderBook: book,
      ticks,

      // Meta
      ts:    Date.now(),
      _prev: prevState ?? null,
    };
  };

  // ── Main effect ───────────────────────────────────────────────────────────

  useEffect(() => {
    const clearTick = () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
    };

    if (!cleanedPairs.length) {
      clearTick();
      setMarkets({});
      if (!persistBaseline) baselineRef.current = {};
      return () => clearTick();
    }

    // Prune stale baselines when pairs are removed (if configured)
    if (persistBaseline && resetBaselineOnPairReadd) {
      const currentSet = new Set(cleanedPairs);
      for (const pair of Object.keys(baselineRef.current)) {
        if (!currentSet.has(pair)) delete baselineRef.current[pair];
      }
    }

    // ── Build engine ──────────────────────────────────────────────────────
    rngRef.current    = new RNG(seed);
    engineRef.current = new ForexEngine(cleanedPairs, seed, {});
    const eng         = engineRef.current;

    // Seed initial prices from ratesByPair directly — ratesRef.current is
    // assigned above synchronously so both work, but being explicit here
    // makes the data-flow obvious and avoids any async timing edge cases.
    for (const pair of cleanedPairs) {
      const rate     = ratesByPair?.[pair];
      const ext      = safeRate(rate?.price);
      const fallback = safeRate(rate?.fallback, 1.0);
      eng.updatePrice(pair, ext > 0 ? ext : fallback);
    }

    // ── First tick ────────────────────────────────────────────────────────
    const initial = {};
    for (const pair of cleanedPairs) {
      eng.tick(pair, tickOptions);
      initial[pair] = snapshotPair(eng, pair, null);
    }
    setMarkets(initial);

    // ── Interval ──────────────────────────────────────────────────────────
    clearTick();
    intervalRef.current = setInterval(() => {
      const eng = engineRef.current;
      if (!eng) return;

      const rates = ratesRef.current ?? {};

      setMarkets((prev) => {
        const next = { ...prev };

        for (const pair of cleanedPairs) {

          // External anchor: blend sim price toward live feed
          if (anchor?.enabled) {
            const externalRaw = safeRate(rates[pair]?.price);
            if (externalRaw > 0) {
              const internalRaw = safeRate(eng.assets[pair]?.price, externalRaw);
              const iw          = Number(anchor.internalWeight ?? 0.05);
              const ew          = Number(anchor.externalWeight ?? 0.95);
              const blended     = internalRaw * iw + externalRaw * ew;
              eng.updatePrice(pair, blended);

              // Set baseline from first external price if mode === "external"
              if (persistBaseline && baselineMode === "external") {
                if (!(safeRate(baselineRef.current[pair]) > 0)) {
                  baselineRef.current[pair] = externalRaw;
                }
              }
            }
          }

          eng.tick(pair, tickOptions);
          next[pair] = snapshotPair(eng, pair, prev[pair]);
        }

        // Remove stale pairs
        for (const existing of Object.keys(next)) {
          if (!cleanedPairs.includes(existing)) delete next[existing];
        }

        if (persistBaseline && resetBaselineOnPairReadd) {
          const currentSet = new Set(cleanedPairs);
          for (const pair of Object.keys(baselineRef.current)) {
            if (!currentSet.has(pair)) delete baselineRef.current[pair];
          }
        }

        return next;
      });
    }, tickMs);

    return () => clearTick();
  }, [
    pairsKey,
    ratesPriceKey,        // reinit engine when real prices first arrive from API
    tickMs,
    seed,
    anchor?.enabled,
    anchor?.internalWeight,
    anchor?.externalWeight,
    persistBaseline,
    baselineMode,
    resetBaselineOnPairReadd,
  ]);

  // ── Public API ────────────────────────────────────────────────────────────

  /** Get snapshot for one pair. */
  const get = (pair) => markets[pair] ?? null;

  /**
   * Reset the pip-change baseline for one pair (or all).
   * Call this when the user wants to zero out the %/pip change display.
   */
  const resetBaseline = (pair) => {
    if (!pair) {
      baselineRef.current = {};
      setMarkets((prev) => {
        const next = { ...prev };
        for (const k of Object.keys(next)) {
          const p = safeRate(next[k]?.price);
          if (p > 0) {
            baselineRef.current[k] = p;
            next[k] = { ...next[k], basePrice: p, change: 0, changePips: 0, percentChange: 0 };
          }
        }
        return next;
      });
      return;
    }

    delete baselineRef.current[pair];
    setMarkets((prev) => {
      if (!prev[pair]) return prev;
      const p = safeRate(prev[pair]?.price);
      if (p <= 0) return prev;
      baselineRef.current[pair] = p;
      return {
        ...prev,
        [pair]: { ...prev[pair], basePrice: p, change: 0, changePips: 0, percentChange: 0 },
      };
    });
  };

  /**
   * Manually push a new fair value into the engine for a pair.
   * Engine mean-reverts toward it gradually — no teleport.
   */
  const setFairValue = (pair, price) => {
    engineRef.current?.updatePrice(pair, price);
  };

  return {
    markets,        // { [pair]: ForexMarketSnapshot }
    get,            // (pair) => ForexMarketSnapshot | null
    engineRef,      // ForexEngine instance ref (advanced)
    baselineRef,    // baseline price map ref (advanced)
    resetBaseline,  // (pair?) => void
    setFairValue,   // (pair, price) => void
  };
}
