'use strict';

/**
 * Dashboard state — the single in-memory object the bot writes to and the
 * local dashboard reads from.
 *
 * Everything the Status tab shows comes from here. Nothing here is persisted
 * to disk (FE-C5), nothing here is sent anywhere except to 127.0.0.1 and, if
 * the operator opts in, the relay.
 *
 * HARD RULE: the private key never enters this object. Not masked, not
 * truncated, not hashed. Only the PUBLIC address is exposed (FE-S6).
 *
 * FE-N3: the log buffer is bounded so a multi-day session does not grow
 * without limit.
 */

const MAX_LOG_ENTRIES = 500;

function createState() {
  const state = {
    version: require('../../package.json').version,
    startedAt: Date.now(),
    lastUpdated: Date.now(),

    bot: {
      // running | paused | stopped | error
      status: 'starting',
      dryRun: true,
      error: null,
    },

    license: {
      configured: false,
      enforced: false,
      reason: null,
      // Last 4 characters only, for the "Licensed" badge (FE-S2 / FE-C8).
      keyMasked: null,
    },

    wallet: {
      address: null,          // PUBLIC address only
      solBalance: null,
      balanceUpdatedAt: null,
    },

    pairs: [],
    pairWarnings: [],         // surfaced as visible rows, not buried (FE-S7)

    routing: {
      jitoEnabled: false,
      tipLamports: null,
      tipSol: null,
    },

    config: {},               // read-only threshold summary (FE-S9)

    /**
     * P&L. FE-S5: never present an estimate as realised, and always label the
     * period explicitly.
     *
     * realizedSol counts ONLY confirmed, actually-submitted round trips. In
     * dry run nothing is submitted, so it stays zero and `dryRun` says why —
     * showing a simulated profit figure here would be exactly the fabricated
     * number FE-P6 forbids.
     */
    pnl: {
      periodLabel: 'This session',
      realizedSol: 0,
      completedRoundTrips: 0,
      dryRun: true,
    },

    skips: {
      total: 0,
      byReason: {},           // grouped so a long run does not drown the log
    },

    log: [],
  };

  function touch() {
    state.lastUpdated = Date.now();
  }

  return {
    /** Read-only snapshot for the HTTP layer. */
    snapshot() {
      return state;
    },

    setStatus(status, error) {
      state.bot.status = status;
      state.bot.error = error || null;
      touch();
    },

    setDryRun(dryRun) {
      state.bot.dryRun = Boolean(dryRun);
      state.pnl.dryRun = Boolean(dryRun);
      touch();
    },

    setLicense({ configured, enforced, reason, key }) {
      state.license.configured = Boolean(configured);
      state.license.enforced = Boolean(enforced);
      state.license.reason = reason || null;
      // Only ever the last 4. The full key is a secret (FE-C8).
      state.license.keyMasked = key ? String(key).slice(-4) : null;
      touch();
    },

    setWallet(address, solBalance) {
      state.wallet.address = address || null;
      if (solBalance != null) {
        state.wallet.solBalance = solBalance;
        state.wallet.balanceUpdatedAt = Date.now();
      }
      touch();
    },

    setPairs(pairs, warnings) {
      state.pairs = (pairs || []).map((p) => ({
        symbol: p.symbol,
        base: p.base,
        quote: p.quote,
      }));
      state.pairWarnings = (warnings || []).map((w) => ({
        entry: w.entry,
        reason: w.reason,
      }));
      touch();
    },

    setRouting({ jitoEnabled, tipLamports }) {
      state.routing.jitoEnabled = Boolean(jitoEnabled);
      state.routing.tipLamports = tipLamports != null ? tipLamports : null;
      state.routing.tipSol = tipLamports != null ? tipLamports / 1e9 : null;
      touch();
    },

    setConfig(config) {
      state.config = {
        minProfitThresholdPct: config.minProfitPct,
        maxTradeSizeSol: config.maxTradeSizeSol,
        pollIntervalMs: config.pollIntervalMs,
        slippageBps: config.slippageBps,
        maxPriceImpactPct: config.maxPriceImpactPct,
        maxProfitDegradationPct: config.maxProfitDegradationPct,
      };
      touch();
    },

    /**
     * A skip is normal operation, not an error (FE-S10). Recorded with its
     * specific reason and grouped by reason so the UI can show counts rather
     * than an unreadable wall of identical lines.
     */
    recordSkip(reason, message, pairSymbol) {
      state.skips.total += 1;
      state.skips.byReason[reason] = (state.skips.byReason[reason] || 0) + 1;
      this.log('skip', message, { reason, pair: pairSymbol });
    },

    /** Only confirmed, actually-submitted trades move realised P&L. */
    recordRoundTrip({ profitSol, submitted }) {
      if (!submitted) return;          // dry runs never touch realised P&L
      state.pnl.completedRoundTrips += 1;
      if (typeof profitSol === 'number' && Number.isFinite(profitSol)) {
        state.pnl.realizedSol += profitSol;
      }
      touch();
    },

    /** level: info | trade | skip | error */
    log(level, message, meta) {
      state.log.push({
        ts: Date.now(),
        level: level,
        message: String(message),
        meta: meta || null,
      });
      // Bounded buffer — drop oldest first (FE-N3).
      if (state.log.length > MAX_LOG_ENTRIES) {
        state.log.splice(0, state.log.length - MAX_LOG_ENTRIES);
      }
      touch();
    },

    touch,
  };
}

module.exports = { createState, MAX_LOG_ENTRIES };
