const WebSocket = require("ws");

const mysql = require("mysql2");

const db = mysql.createConnection({
  host: "localhost",
  user: "eneriwle_realnexa",
  password: "c01-3985-2023Z",
  database: "eneriwle_realnexatrade",
});

// Copy-trading storage is deliberately lightweight and is shared with the
// PHP customer/admin controls. These statements are safe on an existing DB.
db.query(
  "CREATE TABLE IF NOT EXISTS copy_trading_settings (id TINYINT PRIMARY KEY, enabled TINYINT(1) NOT NULL DEFAULT 1)",
);
db.query(
  "INSERT IGNORE INTO copy_trading_settings (id, enabled) VALUES (1, 1)",
);
db.query(
  "CREATE TABLE IF NOT EXISTS copy_subscriptions (id INT AUTO_INCREMENT PRIMARY KEY, follower_id INT NOT NULL UNIQUE, leader_id INT NOT NULL, active TINYINT(1) NOT NULL DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX (leader_id))",
);
db.query(
  "ALTER TABLE users ADD COLUMN copy_trading_enabled TINYINT(1) NOT NULL DEFAULT 1",
  () => {},
);
db.query(
  "ALTER TABLE users ADD COLUMN copy_provider_enabled TINYINT(1) NOT NULL DEFAULT 0",
  () => {},
);
db.query(
  "ALTER TABLE users ADD COLUMN copy_provider_admin_approved TINYINT(1) NOT NULL DEFAULT 0",
  () => {},
);

const wss = new WebSocket.Server({ port: 3001 });

console.log("🚀 WebSocket server running on ws://localhost:3001");

// 🔐 STORE USERS + ACTIVE TRADES
const clients = new Map(); // ws → userId
const userTrades = new Map(); // userId → trade session
const userSockets = new Map(); // userId → Set of sockets
const userActivities = new Map(); // userId → sidebar activity feed

function sendToUser(userId, payload) {
  const sockets = userSockets.get(userId) || new Set();
  const message = JSON.stringify(payload);

  sockets.forEach((socket) => {
    if (socket.readyState === WebSocket.OPEN) socket.send(message);
  });
}

function broadcastChat(payload) {
  const message = JSON.stringify({ type: "chatMessage", ...payload });
  wss.clients.forEach((client) => {
    if (client.isChat && client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

function balanceColumn(accountType) {
  return accountType === "demo" ? "demo_balance" : "balance";
}

function publishBalances(userId) {
  db.query(
    "SELECT balance, demo_balance FROM users WHERE id = ? LIMIT 1",
    [userId],
    (err, results) => {
      if (err || !results[0]) return;
      sendToUser(userId, {
        type: "balances",
        real: Number(results[0].balance),
        demo: Number(results[0].demo_balance),
      });
    },
  );
}

function getActivity(userId) {
  if (!userActivities.has(userId)) {
    userActivities.set(userId, {
      open: null,
      closed: [],
      transactions: [],
      wins: 0,
      losses: 0,
      sessionPL: 0,
    });
  }
  return userActivities.get(userId);
}

function publishActivity(userId) {
  const activity = getActivity(userId);
  sendToUser(userId, {
    type: "tradeActivity",
    open: activity.open,
    closed: activity.closed,
    transactions: activity.transactions,
    summary: {
      totalTrades: activity.wins + activity.losses,
      wins: activity.wins,
      losses: activity.losses,
      sessionPL: activity.sessionPL,
    },
  });
}

// A copied position must not depend on a follower keeping a browser open.
// This lightweight server-side socket joins the same tick/settlement path as
// a browser connection, but never sends network data. It is intentionally
// inserted before any real browser sockets, making it the single settlement
// owner for that copied account and preventing duplicate rounds.
function attachServerTradeSocket(userId, symbol) {
  const socket = {
    isServerTradeSocket: true,
    isChat: false,
    symbol,
    readyState: WebSocket.OPEN,
    send: () => {},
  };

  clients.set(socket, userId);
  wss.clients.add(socket);
  if (!userSockets.has(userId)) userSockets.set(userId, new Set());
  userSockets.get(userId).add(socket);
  return socket;
}

function detachServerTradeSocket(userId, trade) {
  const socket = trade?.serverTradeSocket;
  if (!socket) return;

  wss.clients.delete(socket);
  clients.delete(socket);
  const sockets = userSockets.get(userId);
  if (sockets) {
    sockets.delete(socket);
    if (sockets.size === 0) userSockets.delete(userId);
  }
  trade.serverTradeSocket = null;
}

// Copied accounts follow the source session's lifecycle. A follower can still
// stop earlier when its own balance cannot fund the next stake, but it must
// never continue placing rounds after the source session has ended.
function stopCopiedFollowers(leaderId, reason) {
  userTrades.forEach((candidate, followerId) => {
    if (candidate?.copyLeaderId !== leaderId || candidate.stopped) return;
    stopUserTrade(followerId, candidate, `leader_${reason}`);
  });
}

function stopUserTrade(userId, trade, reason) {
  if (!trade || trade.stopped) return;

  trade.stopped = true;
  trade.forceStop = true;
  detachServerTradeSocket(userId, trade);
  if (userTrades.get(userId) === trade) userTrades.delete(userId);

  // The source account is authoritative. This covers manual stops, targets,
  // insufficient balance, and any other terminal reason in one place.
  if (!trade.copyLeaderId) stopCopiedFollowers(userId, reason);

  const activity = getActivity(userId);
  activity.open = null;
  activity.sessionPL = trade.profit - trade.loss;
  publishActivity(userId);

  // Persist completed sessions for history, the admin portal, and the
  // copy-trading marketplace performance cards.
  if (trade.round > 0) {
    db.query(
      "INSERT INTO trade_sessions (user_id, profit, loss, net_pl) VALUES (?, ?, ?, ?)",
      [userId, trade.profit, trade.loss, trade.profit - trade.loss],
    );
  }

  sendToUser(userId, {
    type: "tradeStopped",
    reason,
    profit: trade.profit,
    loss: trade.loss,
    netPL: trade.profit - trade.loss,
  });
}

// Start an independent, server-owned mirror for every eligible follower. The
// follower receives the same contract parameters, but settlement and balance
// accounting remain isolated in their own account/session.
function startCopiedTrades(leaderId, data, symbol) {
  const baseStake = Number(data.baseStake ?? data.stake);
  if (!Number.isFinite(baseStake) || baseStake <= 0) return;

  db.query(
    "SELECT enabled FROM copy_trading_settings WHERE id = 1 LIMIT 1",
    (settingErr, settings) => {
      if (settingErr || !settings[0] || !settings[0].enabled) return;
      db.query(
        "SELECT s.follower_id FROM copy_subscriptions s JOIN users u ON u.id=s.follower_id JOIN users leader ON leader.id=s.leader_id WHERE s.leader_id=? AND s.active=1 AND u.balance>=? AND u.copy_trading_enabled=1 AND leader.copy_trading_enabled=1 AND leader.copy_provider_enabled=1",
        [leaderId, baseStake],
        (err, followers) => {
          if (err || !followers) return;
          const sourceTrade = userTrades.get(leaderId);
          // A follower must join the same first round as the source. If the
          // source has already moved on, do not create a late, out-of-sync copy.
          if (!sourceTrade || sourceTrade.stopped || sourceTrade.round !== 1)
            return;
          followers.forEach(({ follower_id }) => {
            const followerId = Number(follower_id);
            if (userTrades.has(followerId)) return;
            const session = {
              sessionId: `${followerId}-copy-${Date.now()}`,
              symbol: symbol || "R_10",
              accountType: "real",
              tradeType: data.tradeType,
              baseStake,
              stake: baseStake,
              manual: Boolean(data.manual || data.tradeMode === "manual"),
              lossMultiple: Math.max(1, Number(data.lossMultiple) || 1),
              digit: data.digit,
              barrier: data.barrier,
              entryDigit: null,
              tickCount: 0,
              round: 0,
              pendingRound: false,
              resolving: false,
              profit: 0,
              loss: 0,
              targetProfit: Math.max(1, Number(data.targetProfit) || 100),
              targetLoss: Math.max(1, Number(data.targetLoss) || 100),
              stopped: false,
              forceStop: false,
              copyLeaderId: leaderId,
            };
            initMarketState(session.symbol);
            initMarketHistory(session.symbol);
            // Server-owned settlement keeps this copied trade running after the
            // follower logs out. Real browser sockets, when present, still get
            // the market/history updates normally.
            session.serverTradeSocket = attachServerTradeSocket(
              followerId,
              session.symbol,
            );
            const followerSockets = userSockets.get(followerId) || new Set();
            followerSockets.forEach((socket) => {
              if (socket.isServerTradeSocket) return;
              socket.symbol = session.symbol;
              if (socket.readyState === WebSocket.OPEN) {
                socket.send(
                  JSON.stringify({
                    type: "history",
                    data: marketHistory[session.symbol],
                  }),
                );
              }
            });
            userTrades.set(followerId, session);
            userActivities.set(followerId, {
              open: null,
              closed: [],
              transactions: [],
              wins: 0,
              losses: 0,
              sessionPL: 0,
            });
            beginRound(followerId, session);
          });
        },
      );
    },
  );
}

function beginRound(userId, trade) {
  if (!trade || trade.stopped || trade.pendingRound || trade.resolving) return;

  // A copied trade has no independent lifetime. Before any new deduction,
  // confirm that the leading session is still live.
  if (trade.copyLeaderId) {
    const sourceTrade = userTrades.get(trade.copyLeaderId);
    if (!sourceTrade || sourceTrade.stopped) {
      stopUserTrade(userId, trade, "leader_stopped");
      return;
    }
  }

  const stake = Number(trade.stake);
  if (!Number.isFinite(stake) || stake <= 0) {
    stopUserTrade(userId, trade, "balance");
    return;
  }

  trade.pendingRound = true;
  const accountColumn = balanceColumn(trade.accountType);

  db.query(
    `UPDATE users SET ${accountColumn} = ${accountColumn} - ? WHERE id = ? AND ${accountColumn} >= ?`,
    [stake, userId, stake],
    (err, result) => {
      trade.pendingRound = false;
      if (err) {
        console.log(err);
        stopUserTrade(userId, trade, "balance");
        return;
      }

      if (trade.stopped || userTrades.get(userId) !== trade) return;

      if (result.affectedRows === 0) {
        stopUserTrade(userId, trade, "balance");
        return;
      }

      trade.tickCount = 0;
      trade.entryDigit = null;
      trade.resolving = false;
      trade.round += 1;

      const activity = getActivity(userId);
      const now = Date.now();
      activity.open = {
        id: `${trade.sessionId}-${trade.round}`,
        symbol: trade.symbol,
        tradeType: trade.tradeType,
        stake,
        potentialPayout: Number((stake * 1.95).toFixed(2)),
        tickCount: 0,
        openedAt: now,
      };
      activity.transactions.unshift({
        id: `${trade.sessionId}-${trade.round}-stake`,
        kind: "stake",
        symbol: trade.symbol,
        tradeType: trade.tradeType,
        amount: -stake,
        timestamp: now,
      });
      activity.transactions = activity.transactions.slice(0, 100);
      publishActivity(userId);

      // Only begin follower sessions after the source account's first stake
      // has been accepted. This prevents a rejected source trade from ever
      // creating live copied positions.
      if (!trade.copyLeaderId && trade.round === 1) {
        startCopiedTrades(userId, trade, trade.symbol);
      }

      db.query(
        `SELECT ${accountColumn} AS balance FROM users WHERE id = ? LIMIT 1`,
        [userId],
        (balanceErr, results) => {
          if (balanceErr || !results[0] || trade.stopped) return;

          // Keep the reserved stake server-side. The UI updates its balance
          // only after a completed tradeResult, alongside session P/L.
          sendToUser(userId, { type: "tradeStarted", trade });
        },
      );
    },
  );
}

// --- MARKET ENGINE CONFIG ---

// volatility profiles (VERY IMPORTANT)
const markets = {
  // 1s markets
  R_10: { vol: 0.8, drift: 0.01, reversion: 0.02 },
  R_15: { vol: 1.2, drift: 0.015, reversion: 0.02 }, // ✅ added
  R_25: { vol: 1.5, drift: 0.015, reversion: 0.018 },
  R_30: { vol: 2, drift: 0.02, reversion: 0.017 }, // ✅ added
  R_50: { vol: 3, drift: 0.02, reversion: 0.015 },
  R_75: { vol: 5, drift: 0.025, reversion: 0.012 },
  R_100: { vol: 7.5, drift: 0.03, reversion: 0.01 },

  // normal markets
  R_10_2: { vol: 0.8, drift: 0.005, reversion: 0.02 },
  R_25_2: { vol: 1.5, drift: 0.01, reversion: 0.018 },
  R_50_2: { vol: 3, drift: 0.015, reversion: 0.015 },
  R_100_2: { vol: 7.5, drift: 0.02, reversion: 0.012 },
};

const mean = 9600;

// --- DIGIT TRACKING ---
// ================= DIGIT BIAS ENGINE =================

let digitWeights = Array(10).fill(1);

// hot/cold control
let hotDigits = [];
let coldDigits = [];

// rotate every 15 seconds
setInterval(() => {
  hotDigits = [];
  coldDigits = [];

  // pick 2 hot digits
  while (hotDigits.length < 2) {
    const d = Math.floor(Math.random() * 10);
    if (!hotDigits.includes(d)) hotDigits.push(d);
  }

  // pick 2 cold digits
  while (coldDigits.length < 2) {
    const d = Math.floor(Math.random() * 10);
    if (!hotDigits.includes(d) && !coldDigits.includes(d)) {
      coldDigits.push(d);
    }
  }

  // reset weights
  digitWeights = Array(10).fill(1);

  // apply bias
  hotDigits.forEach((d) => (digitWeights[d] = 1.8)); // 🔥 hotter
  coldDigits.forEach((d) => (digitWeights[d] = 0.6)); // ❄ colder

  console.log("🔥 Hot:", hotDigits, "❄ Cold:", coldDigits);
}, 15000);

let recentDigits = [];

function adjustWeightsFromHistory() {
  const counts = Array(10).fill(0);

  recentDigits.forEach((d) => counts[d]++);

  const total = recentDigits.length || 1;

  for (let i = 0; i < 10; i++) {
    const ratio = counts[i] / total;

    // prevent clustering (digits having same ratio)
    const neighborDiff = Math.abs(
      (counts[i] || 0) - (counts[(i + 1) % 10] || 0),
    );

    if (neighborDiff < 3) {
      digitWeights[i] *= 1.15; // push away
    }

    // normal balancing
    if (ratio > 0.13) digitWeights[i] *= 0.7;
    if (ratio < 0.07) digitWeights[i] *= 1.3;
  }
}

function getBiasedDigit() {
  adjustWeightsFromHistory();

  const totalWeight = digitWeights.reduce((a, b) => a + b, 0);
  let rand = Math.random() * totalWeight;

  for (let i = 0; i < 10; i++) {
    rand -= digitWeights[i];
    if (rand <= 0) {
      recentDigits.push(i);

      if (recentDigits.length > 200) {
        recentDigits.shift();
      }

      return i;
    }
  }

  return 0;
}

// --- ODDS ENGINE ---
function calculateOdds() {
  // slightly house-biased odds like Deriv
  return {
    even: 95.22,
    odd: 95.22,
  };
}

// --- BROADCAST FUNCTION ---
function broadcast(data) {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify(data));
    }
  });
}
const marketState = {};

function initMarketState(symbol) {
  if (marketState[symbol]) return;

  marketState[symbol] = {
    price: mean,
    volatilityState: 1,
  };
}

const marketHistory = {};
const MAX_HISTORY = 500;

function initMarketHistory(symbol) {
  if (marketHistory[symbol]) return;

  const history = [];
  let tempPrice = mean;
  const m = markets[symbol];

  for (let i = 0; i < 200; i++) {
    const rand = (Math.random() - 0.5) * m.vol;
    const reversion = (mean - tempPrice) * m.reversion;

    tempPrice += m.drift + rand + reversion;

    history.push({
      price: parseFloat(tempPrice.toFixed(2)),
      time: Date.now() - (200 - i) * 1000,
    });
  }

  marketHistory[symbol] = history;
}

// --- TICK LOOP (1 second) ---
setInterval(() => {
  Object.keys(markets).forEach((symbol) => {
    const m = markets[symbol];
    if (!m) return;

    // ✅ INIT GLOBAL STATE
    initMarketState(symbol);

    const state = marketState[symbol];

    // 🔥 volatility clustering (GLOBAL)
    state.volatilityState += (Math.random() - 0.5) * 0.2;
    state.volatilityState = Math.max(0.5, Math.min(2.5, state.volatilityState));

    const rand = (Math.random() - 0.5) * m.vol * state.volatilityState;

    let spike = 0;
    if (Math.random() < 0.03 * (m.vol / 2)) {
      spike = (Math.random() - 0.5) * m.vol * 5;
    }

    const reversion = (mean - state.price) * m.reversion;

    // ✅ GLOBAL PRICE UPDATE
    state.price += m.drift + rand + reversion + spike;

    const newPrice = parseFloat(state.price.toFixed(2));
    // Every client sees the same projected last digit for this live price.
    // The trade engine settles contracts against this exact tick digit.
    const digit = Math.abs(Math.round(newPrice * 100)) % 10;

    // ✅ STORE GLOBAL HISTORY
    if (!marketHistory[symbol]) initMarketHistory(symbol);

    marketHistory[symbol].push({
      price: newPrice,
      time: Date.now(),
    });

    if (marketHistory[symbol].length > MAX_HISTORY) {
      marketHistory[symbol].shift();
    }

    // ✅ SEND TO ONLY USERS USING THIS SYMBOL
    wss.clients.forEach((client) => {
      if (client.readyState !== WebSocket.OPEN) return;
      if (client.isChat) return;

      if ((client.symbol || "R_10") !== symbol) return;

      client.send(
        JSON.stringify({
          type: "tick",
          price: newPrice,
          digit: digit,
          time: Date.now(),
        }),
      );

      // 🔥 TRADE RESOLUTION (SAFE ADD)

      const userId = clients.get(client);
      if (!userId) return;

      const sockets = userSockets.get(userId);
      if (!sockets) return;

      // 🔥 ONLY ONE EXECUTION PER USER PER TICK
      const mainSocket = sockets.values().next().value;
      if (client !== mainSocket) return;

      const trade = userTrades.get(userId);
      if (!trade || trade.stopped) return;

      // 🔥 HARD STOP CHECK (VERY IMPORTANT)
      if (trade.forceStop) return;
      if (!trade || trade.stopped) return;

      if (trade.copyLeaderId) {
        // Copy the leader's phase and entry digit from this exact market tick.
        // This prevents query/socket timing from giving the follower a
        // different three-tick outcome or Session P/L.
        const sourceTrade = userTrades.get(trade.copyLeaderId);
        if (!sourceTrade || sourceTrade.stopped) {
          stopUserTrade(userId, trade, "leader_stopped");
          return;
        }
        trade.tickCount = sourceTrade.tickCount;
        trade.entryDigit = sourceTrade.entryDigit;
        trade.stake = sourceTrade.stake;
      } else {
        if (!trade.tickCount) trade.tickCount = 0;
        trade.tickCount++;
      }
      const activity = getActivity(userId);
      if (activity.open) {
        activity.open.tickCount = trade.tickCount;
        activity.open.lastDigit = digit;
        publishActivity(userId);
      }

      // Auto contracts retain their three-tick lifecycle. Manual contracts
      // resolve on the very next live tick, giving each button click exactly
      // one contract and one settlement.
      if (trade.tickCount === 1 && !trade.manual) {
        trade.entryDigit = digit;
        return;
      }

      // 🟡 TICK 2 → WAIT (auto contracts only)
      if (trade.tickCount === 2 && !trade.manual) {
        return;
      }

      // 🟢 Resolve auto on tick 3 and manual on tick 1.
      if (trade.tickCount === (trade.manual ? 1 : 3)) {
        if (trade.resolving) return;
        trade.resolving = true;
        let win = false;

        if (trade.tradeType === "even") win = digit % 2 === 0;
        if (trade.tradeType === "odd") win = digit % 2 !== 0;

        if (trade.tradeType === "match") win = digit === trade.digit;
        if (trade.tradeType === "differs") win = digit !== trade.digit;

        if (trade.tradeType === "over") win = digit > trade.barrier;
        if (trade.tradeType === "under") win = digit < trade.barrier;

        const roundStake = Number(trade.stake);
        const payout = roundStake * 1.95;
        let profit = 0;
        let loss = 0;

        if (win) {
          profit = payout - roundStake;
          trade.profit += profit;
        } else {
          loss = roundStake;
          trade.loss += loss;
        }

        const netPL = trade.profit - trade.loss;

        let updateAmount = win ? payout : 0;
        const accountColumn = balanceColumn(trade.accountType);

        db.query(
          `UPDATE users SET ${accountColumn} = ${accountColumn} + ? WHERE id = ?`,
          [updateAmount, userId],
          (err) => {
            if (err) {
              console.log(err);
              stopUserTrade(userId, trade, "balance");
              return;
            }

            if (trade.stopped || userTrades.get(userId) !== trade) return;

            db.query(
              `SELECT ${accountColumn} AS balance FROM users WHERE id = ? LIMIT 1`,
              [userId],

              (err, results) => {
                if (err || !results[0]) {
                  stopUserTrade(userId, trade, "balance");
                  return;
                }
                if (trade.stopped || userTrades.get(userId) !== trade) return;

                const currentBalance = Number(results[0].balance);
                const roundPL = win ? profit : -roundStake;
                const activity = getActivity(userId);
                const completedAt = Date.now();
                activity.open = null;
                activity.closed.unshift({
                  id: `${trade.sessionId}-${trade.round}-closed`,
                  symbol: trade.symbol,
                  tradeType: trade.tradeType,
                  result: win ? "win" : "loss",
                  profitLoss: roundPL,
                  stake: roundStake,
                  contractValue: win ? payout : 0,
                  potentialPayout: payout,
                  tickCount: trade.tickCount,
                  lastDigit: digit,
                  closedAt: completedAt,
                });
                activity.closed = activity.closed.slice(0, 100);
                activity.transactions.unshift({
                  id: `${trade.sessionId}-${trade.round}-result`,
                  kind: "closed",
                  symbol: trade.symbol,
                  tradeType: trade.tradeType,
                  amount: roundPL,
                  result: win ? "win" : "loss",
                  timestamp: completedAt,
                });
                activity.transactions = activity.transactions.slice(0, 100);
                if (win) activity.wins += 1;
                else activity.losses += 1;
                activity.sessionPL = netPL;
                publishActivity(userId);
                // Send both balances after every settlement.  The UI must not
                // infer an account from a generic balance event: a demo trade
                // only changes demo_balance and a real trade only changes balance.
                publishBalances(userId);

                if (trade.manual) {
                  sendToUser(userId, {
                    type: "tradeResult",
                    result: win ? "win" : "loss",
                    amount: win ? profit : loss,
                    balance: currentBalance,
                    netPL,
                    totalProfit: trade.profit,
                    totalLoss: trade.loss,
                    nextStake: trade.baseStake,
                    round: trade.round,
                    settlementDigit: digit,
                    accountType: trade.accountType,
                  });
                  stopUserTrade(userId, trade, "manual_complete");
                  return;
                }

                if (netPL >= trade.targetProfit || netPL <= -trade.targetLoss) {
                  sendToUser(userId, {
                    type: "tradeResult",
                    result: win ? "win" : "loss",
                    amount: win ? profit : loss,
                    balance: currentBalance,
                    netPL,
                    totalProfit: trade.profit,
                    totalLoss: trade.loss,
                    nextStake: trade.stake,
                    round: trade.round,
                    settlementDigit: digit,
                    accountType: trade.accountType,
                  });
                  stopUserTrade(
                    userId,
                    trade,
                    netPL >= trade.targetProfit ? "profit" : "loss",
                  );
                  return;
                }

                trade.stake = win
                  ? trade.baseStake
                  : Number((roundStake * trade.lossMultiple).toFixed(2));

                sendToUser(userId, {
                  type: "tradeResult",
                  result: win ? "win" : "loss",
                  amount: win ? profit : loss,
                  balance: currentBalance,
                  netPL,
                  totalProfit: trade.profit,
                  totalLoss: trade.loss,
                  nextStake: trade.stake,
                  round: trade.round,
                  settlementDigit: digit,
                  accountType: trade.accountType,
                });

                trade.resolving = false;
                if (currentBalance < trade.stake) {
                  stopUserTrade(userId, trade, "balance");
                  return;
                }

                beginRound(userId, trade);
              },
            );
          },
        );
      }

      client.send(
        JSON.stringify({
          type: "odds",
          ...calculateOdds(),
        }),
      );
    });
  });
}, 1000);

// --- CONNECTION HANDLING ---
wss.on("connection", (ws, request) => {
  console.log("👤 Client connected");

  // ✅ SET FIRST (VERY IMPORTANT)
  ws.isChat = request.url === "/chat";
  ws.symbol = ws.isChat ? null : "R_10";

  // ✅ INIT GLOBAL STATE
  if (!ws.isChat) {
    initMarketState(ws.symbol);
    initMarketHistory(ws.symbol);
  }

  // ✅ SEND HISTORY
  if (!ws.isChat) {
    ws.send(
      JSON.stringify({
        type: "history",
        data: marketHistory[ws.symbol],
      }),
    );
  }
  8;
  ws.on("message", (msg) => {
    let data;
    try {
      data = JSON.parse(msg);
    } catch {
      return;
    }

    if (ws.isChat && data.type === "message") {
      const userId = Number(data.user_id);
      const sender = data.sender === "admin" ? "admin" : "user";
      const message =
        typeof data.message === "string" ? data.message.trim() : "";
      const file = typeof data.file === "string" ? data.file : null;

      if (!Number.isInteger(userId) || userId <= 0 || (!message && !file))
        return;

      db.query(
        "INSERT INTO chat_messages (user_id, sender, message, file) VALUES (?, ?, ?, ?)",
        [userId, sender, message, file],
        (err, result) => {
          if (err) return console.error("Chat message insert failed:", err);
          broadcastChat({
            id: result.insertId,
            user_id: userId,
            sender,
            message,
            file,
            created_at: new Date().toISOString(),
          });
        },
      );
      return;
    }

    // 🔐 AUTH USER
    if (data.type === "auth") {
      clients.set(ws, data.userId);

      if (!userSockets.has(data.userId)) {
        userSockets.set(data.userId, new Set());
      }

      userSockets.get(data.userId).add(ws);

      // 🔥 SEND ACTIVE TRADE IF EXISTS (IMPORTANT)
      const existingTrade = userTrades.get(data.userId);
      if (existingTrade) {
        ws.send(
          JSON.stringify({
            type: "syncTrade",
            trade: existingTrade,
          }),
        );
      }

      publishActivity(data.userId);
      publishBalances(data.userId);

      return;
    }
    // 🚀 START TRADE
    if (data.type === "startTrade") {
      const userId = clients.get(ws);
      if (!userId) return;

      const existingTrade = userTrades.get(userId);
      if (existingTrade && !existingTrade.stopped) {
        ws.send(
          JSON.stringify({
            type: "syncTrade",
            trade: existingTrade,
          }),
        );
        return;
      }

      const baseStake = Number(data.baseStake ?? data.stake);
      if (!Number.isFinite(baseStake) || baseStake <= 0) return;

      const session = {
        sessionId: `${userId}-${Date.now()}`,
        symbol: ws.symbol || "R_10",
        accountType: data.accountType === "demo" ? "demo" : "real",
        tradeType: data.tradeType,
        baseStake,
        stake: baseStake,
        manual: data.tradeMode === "manual",
        lossMultiple: Math.max(1, Number(data.lossMultiple) || 1),
        digit: data.digit,
        barrier: data.barrier,
        entryDigit: null,
        tickCount: 0,
        round: 0,
        pendingRound: false,
        resolving: false,
        profit: 0,
        loss: 0,
        targetProfit: Math.max(1, Number(data.targetProfit) || 100),
        targetLoss: Math.max(1, Number(data.targetLoss) || 100),
        stopped: false,
        forceStop: false,
      };

      userTrades.set(userId, session);
      userActivities.set(userId, {
        open: null,
        closed: [],
        transactions: [],
        wins: 0,
        losses: 0,
        sessionPL: 0,
      });
      beginRound(userId, session);

      return;
    }
    if (data.type === "resetDemoBalance") {
      const userId = clients.get(ws);
      if (!userId) return;

      const activeTrade = userTrades.get(userId);
      if (activeTrade?.accountType === "demo") return;

      db.query(
        "UPDATE users SET demo_balance = 10000.00 WHERE id = ?",
        [userId],
        (err) => {
          if (err) return console.log(err);
          publishBalances(userId);
        },
      );
      return;
    }
    // 🛑 STOP TRADE
    if (data.type === "stopTrade") {
      const userId = clients.get(ws);
      if (!userId) return;

      const trade = userTrades.get(userId);
      if (trade) stopUserTrade(userId, trade, "manual");
      else {
        sendToUser(userId, {
          type: "tradeStopped",
          reason: "manual",
          profit: 0,
          loss: 0,
          netPL: 0,
        });
      }

      return;
    }
    if (data.type === "ping") {
      ws.send(JSON.stringify({ type: "pong" }));
    }

    // The scanner receives a read-only snapshot for every available
    // volatility market. It never changes the client's selected stream.
    if (data.type === "scanMarkets") {
      Object.keys(markets).forEach((symbol) => initMarketHistory(symbol));
      ws.send(
        JSON.stringify({
          type: "marketScanHistory",
          data: marketHistory,
        }),
      );
      return;
    }

    // 🔥 HANDLE SYMBOL CHANGE
    if (data.type === "setSymbol") {
      const userId = clients.get(ws);
      // A contract must settle against the same market stream the user saw
      // when it was opened.
      if (userId && userTrades.has(userId)) return;

      if (markets[data.symbol]) {
        ws.symbol = data.symbol;

        // reset user state
        if (data.type === "setSymbol") {
          if (markets[data.symbol]) {
            ws.symbol = data.symbol;

            // ✅ INIT GLOBAL MARKET
            initMarketState(ws.symbol);
            initMarketHistory(ws.symbol);

            // ✅ SEND HISTORY
            ws.send(
              JSON.stringify({
                type: "history",
                data: marketHistory[ws.symbol],
              }),
            );

            console.log("📊 Switched to:", ws.symbol);
          }
        }

        // ✅ INIT GLOBAL HISTORY (if not exists)
        initMarketHistory(ws.symbol);

        // ✅ SEND SHARED HISTORY
        ws.send(
          JSON.stringify({
            type: "history",
            data: marketHistory[ws.symbol],
          }),
        );

        console.log("📊 Switched to:", ws.symbol);
      }
    }
  });

  ws.on("close", () => {
    const userId = clients.get(ws);

    if (userId && userSockets.has(userId)) {
      userSockets.get(userId).delete(ws);

      if (userSockets.get(userId).size === 0) {
        userSockets.delete(userId);
      }
    }

    clients.delete(ws);
    console.log("❌ Client disconnected");
  });
});
