// index.js
import fs from 'fs';
import express from 'express';
import path from 'path';
import http from 'http';
import { Server } from 'socket.io';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { v4 as uuid } from 'uuid';
import cookieParser from 'cookie-parser';
import * as crypto from 'crypto';
import { openDb } from './db.js';  // your SQLite helper

// Use an env var in production
const JWT_SECRET = process.env.JWT_SECRET || 'a-very-strong-secret';
const COOKIE_OPTIONS = {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',  // only over HTTPS in prod
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000,  // 7 days
  path: '/'
};

const CLEAR_COOKIE_OPTIONS = {
  httpOnly:  COOKIE_OPTIONS.httpOnly,
  secure:    COOKIE_OPTIONS.secure,
  sameSite:  COOKIE_OPTIONS.sameSite,
  path:      COOKIE_OPTIONS.path,
  // → deliberately leave out maxAge
};
const app    = express();
const server = http.createServer(app);
const io     = new Server(server, { cors:{ origin:"*" } });

app.use(express.json());
app.use(cookieParser());
app.use(express.static('../public'));

// ─── AUTH MIDDLEWARE ───────────────────────────────────────────────────────────
async function authMiddleware(req, res, next) {
  const token = req.cookies.token;
  if (!token) return res.status(401).json({ error: 'Unauthenticated' });
  try {
    const payload = jwt.verify(token, JWT_SECRET);
    req.user = payload; // { id, username }
    next();
  } catch {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// ─── “WHO AM I?” ENDPOINT ──────────────────────────────────────────────────────
app.get('/api/me', authMiddleware, async (req, res) => {
  // You only store id/username in token; level/avatarUrl aren’t in DB yet
  res.json({
    id:       req.user.id,
    username: req.user.username,
    level:    1,
    avatarUrl: null
  });
});

// ─── REGISTER ─────────────────────────────────────────────────────────────────
app.post('/api/register', async (req, res) => {
  const { username, password } = req.body;
  if (!username || !password) {
    return res.status(400).json({ error: 'Missing fields' });
  }
  const db     = await openDb();
  const hashed = await bcrypt.hash(password, 10);
  try {
    await db.run(
      'INSERT INTO users (username, password) VALUES (?, ?)',
      [username, hashed]
    );
    return res.json({ success: true });
  } catch(err) {
    if (err.code === 'SQLITE_CONSTRAINT') {
      return res.status(409).json({ error: 'Username taken' });
    }
    console.error(err);
    return res.status(500).json({ error: 'Database error' });
  }
});

// ─── LOGIN (sets HttpOnly cookie) ─────────────────────────────────────────────
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  const db   = await openDb();
  const user = await db.get(
    'SELECT id, username, password FROM users WHERE username = ?',
    [username]
  );
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const match = await bcrypt.compare(password, user.password);
  if (!match) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Issue a JWT into a Secure, HttpOnly cookie:
  const token = jwt.sign(
    { id: user.id, username: user.username },
    JWT_SECRET,
    { expiresIn: '7d' }
  );
  res.cookie('token', token, COOKIE_OPTIONS);

  // Return only the balance; client will call /api/me for username
  const row = await db.get(
    'SELECT balance FROM users WHERE id = ?',
    [user.id]
  );
  return res.json({ success: true, balance: row.balance });
});

// ─── LOGOUT ───────────────────────────────────────────────────────────────────
app.post('/api/logout', (req, res) => {
  res.clearCookie('token', CLEAR_COOKIE_OPTIONS);
  return res.json({ success: true });
});

// ─── BALANCE ROUTES ───────────────────────────────────────────────────────────
app.get('/api/balance', authMiddleware, async (req, res) => {
  const db  = await openDb();
  const row = await db.get(
    'SELECT balance FROM users WHERE id = ?',
    [req.user.id]
  );
  return res.json({ balance: row.balance });
});

const PORT          = process.env.PORT || 3000;
const SPIN_DURATION = 3000;       // 3 s spin
const ROUND_PERIOD  = 15000;      // 15 s total (12 s countdown + 3 s spin)
const DATA_DIR      = path.resolve('./data');
const DAILY_FILE    = path.join(DATA_DIR, 'daily.json');
const HISTORY_FILE  = path.join(DATA_DIR, 'history.json')
const JACKPOT_FILE  = path.join(DATA_DIR, 'jackpot.json')
let jackpotPot = 0; 
let resolvedRounds = [];  

// Ensure data folder
if (!fs.existsSync(DATA_DIR)) {
  fs.mkdirSync(DATA_DIR, { recursive: true });
}


try {
  const stored = fs.readFileSync(JACKPOT_FILE, 'utf8');
  const parsed = JSON.parse(stored);
  if (typeof parsed.jackpot === 'number') {
    jackpotPot = parsed.jackpot;
    console.log('[jackpot] Loaded previous jackpot:', jackpotPot);
  }
} catch (_err) {
  // File doesn’t exist (first run) or is invalid JSON → default to 0
  jackpotPot = 0;
  console.log('[jackpot] No previous jackpot found; starting at 0.');
}

// === Helpers to read/write JSON ===
function readJson(filepath) {
  try {
    const raw = fs.readFileSync(filepath, 'utf8');
    return JSON.parse(raw);
  } catch (err) {
    return null;
  }
}
function writeJson(filepath, obj) {
  fs.writeFileSync(filepath, JSON.stringify(obj, null, 2), 'utf8');
}
// === SHA256 ===
function sha256(str) {
  return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
}

// === Daily‐seed logic with persistence ===
function nextUtcMidnight() {
  const now = new Date();
  return Date.UTC(
    now.getUTCFullYear(),
    now.getUTCMonth(),
    now.getUTCDate() + 1,
    0, 0, 0, 0
  );
}
function createNewDailySeedObject() {
  const seed = crypto.randomBytes(32).toString('hex');
  const hash = sha256(seed);
  const expires = nextUtcMidnight();
  return { seed, hash, expires };
}
function initializeDaily() {
  const saved = readJson(DAILY_FILE);
  if (saved && saved.seed && saved.hash && saved.expires) {
    if (Date.now() < saved.expires) {
      console.log('[daily] Loaded existing daily seed, expires at', new Date(saved.expires).toUTCString());
      return saved;
    }
    console.log('[daily] Found expired daily seed (expired at', new Date(saved.expires).toUTCString(), ')');
  }
  // Otherwise, create a new one
  const fresh = createNewDailySeedObject();
  writeJson(DAILY_FILE, fresh);
  console.log('[daily] Created new daily seed with hash', fresh.hash, 'expires at', new Date(fresh.expires).toUTCString());
  return fresh;
}

let daily = initializeDaily();

function revealAndRotateDaily() {
  io.emit('dailyReveal', { seed: daily.seed, hash: daily.hash });
  console.log('[daily] Revealed seed:', daily.seed);
  const fresh = createNewDailySeedObject();
  daily = fresh;
  writeJson(DAILY_FILE, fresh);
  console.log('[daily] Rotated to new daily seed with hash', fresh.hash, 'expires at', new Date(fresh.expires).toUTCString());
}

function scheduleDailyReveal() {
  const delay = daily.expires - Date.now();
  if (delay <= 0) {
    revealAndRotateDaily();
    scheduleDailyReveal();
  } else {
    setTimeout(() => {
      revealAndRotateDaily();
      scheduleDailyReveal();
    }, delay);
  }
}


app.use(express.static('../public'));  // Adjust to your public folder

// === Global state ===
const COLOURS = [
  'green',
  'black', 'red', 'black', 'red', 'black', 'red', 'black',
  'red', 'black', 'red', 'black', 'red', 'black', 'red',
  'green',
  'black', 'red', 'black', 'red', 'black', 'red', 'black',
  'red', 'black', 'red', 'black', 'red', 'black', 'red'
];
let pastColours = readJson(HISTORY_FILE) || [];
// Trim to at most 100 on startup, in case the JSON file has more
if (pastColours.length > 100) {
  pastColours = pastColours.slice(-100);
  writeJson(HISTORY_FILE, pastColours);
}


let nonce       = 0;
let currentRoundId = null;   
let roundBets = {};
const PAYOUTS = { red: 2, black: 2, green: 14 };

function createNewRoundBets(roundId) {
  roundBets[roundId] = { red: [], black: [], green: [] };
}
// === Round driver ===
function scheduleNextRound() {
  const startTime   = Date.now() + (ROUND_PERIOD - SPIN_DURATION); // countdown portion
  const colourIndex = Math.floor(Math.random() * COLOURS.length);
  const roundId     = uuid();
  createNewRoundBets(roundId);
  currentRoundId = roundId;

  io.emit('roundScheduled', {
    roundId,
    colourIndex,
    startTime,
    spinDuration: SPIN_DURATION
  });

  setTimeout(() => finishRound(roundId, colourIndex), ROUND_PERIOD);
}

async function finishRound(roundId, winningColourIndex) {
  // 1) Determine the winning colour, push to history.json, emit to clients
  const winningColour = COLOURS[winningColourIndex];
  const input         = `${daily.seed}:${nonce++}`;
  const hash          = sha256(input);

  pastColours.push(winningColour);
  if (pastColours.length > 100) {
    pastColours.shift();
  }
  writeJson(HISTORY_FILE, pastColours);

  io.emit('roundResult', { colour: winningColour, hash });

  // 2) Pay out normal winners for this round
  const winners = roundBets[roundId][winningColour] || [];
  for (const bet of winners) {
    const payoutAmount = bet.amount * PAYOUTS[winningColour];
    const db = await openDb();
    await db.run(
      'UPDATE users SET balance = balance + ? WHERE id = ?',
      [payoutAmount, bet.userId]
    );
    const newRow = await db.get(
      'SELECT balance FROM users WHERE id = ?',
      [bet.userId]
    );
    io.to(`user_${bet.userId}`).emit('balanceUpdate', { balance: newRow.balance });
  }

  // 3) Build up totalBetAmountThisRound and betsByUserThisRound
  const thisRoundBets       = roundBets[roundId];
  let totalBetAmountThisRound = 0;
  const betsByUserThisRound = {}; // { userId: totalStakedThisRound, … }

  for (const clr of ['red', 'black', 'green']) {
    for (const betObj of thisRoundBets[clr]) {
      totalBetAmountThisRound += betObj.amount;
      if (!betsByUserThisRound[betObj.userId]) {
        betsByUserThisRound[betObj.userId] = 0;
      }
      betsByUserThisRound[betObj.userId] += betObj.amount;
    }
  }

  // 4) Record this round’s info in resolvedRounds
  resolvedRounds.push({
    roundId,
    winningColour,
    totalBetAmount: totalBetAmountThisRound,
    betsByUser:     betsByUserThisRound
  });

  // 5) Triple-Red Bonus Check
  const L = pastColours.length;
  if (
    L >= 3 &&
    pastColours[L - 1] === 'green' &&
    pastColours[L - 2] === 'green' &&
    pastColours[L - 3] === 'green' &&
    resolvedRounds.length >= 3
  ) {
    const shareEach = jackpotPot / 3;
    const lastThree = resolvedRounds.slice(-3);

    // Distribute each third of jackpotPot to bettors from each of those three rounds
    for (const rnd of lastThree) {
      const totalThisRound = rnd.totalBetAmount;
      const betsMap        = rnd.betsByUser;

      for (const [userId, stakeAmt] of Object.entries(betsMap)) {
        const winFromJackpot = (stakeAmt / totalThisRound) * shareEach;
        const db = await openDb();
        await db.run(
          'UPDATE users SET balance = balance + ? WHERE id = ?',
          [winFromJackpot, userId]
        );
        const newRow = await db.get(
          'SELECT balance FROM users WHERE id = ?',
          [userId]
        );
        io.to(`user_${userId}`).emit('balanceUpdate', { balance: newRow.balance });
      }
    }

    // Reset jackpotPot = 0 and persist it
    jackpotPot = 0;
    try {
      fs.writeFileSync(
        JACKPOT_FILE,
        JSON.stringify({ jackpot: jackpotPot }, null, 2),
        'utf8'
      );
    } catch (e) {
      console.error('[jackpot] Error writing jackpot.json:', e);
    }

    // Remove those three rounds from resolvedRounds so we don’t pay them again
    resolvedRounds.splice(-3, 3);
  }

  // 6) Cleanup this round and schedule next
  delete roundBets[roundId];
  io.emit('betsUpdate', { betsByColour: { red: [], black: [], green: [] } });
  scheduleNextRound();
}


io.use(async (socket, next) => {
  try {
    const cookieHeader = socket.handshake.headers.cookie || '';
    const token = cookieHeader
      .split(';')
      .map(v => v.trim())
      .find(v => v.startsWith('token='))
      ?.split('=')[1];
    if (!token) {
      return next();  // no JWT => not logged in
    }
    const payload = jwt.verify(token, JWT_SECRET);
    socket.userId = payload.id;
    socket.username = payload.username;
    // join a room named "user_<id>"
    socket.join(`user_${socket.userId}`);
    return next();
  } catch {
    return next();
  }
});

// === On client connect ===
io.on('connection', socket => {
  console.log('Socket connected:', socket.id, 'UserID:', socket.userId);
  if (socket.userId) {
    // Optionally, immediately send them their current balance:
    (async () => {
      const db = await openDb();
      const row = await db.get(
        'SELECT balance FROM users WHERE id = ?',
        [socket.userId]
      );
      socket.emit('balanceUpdate', { balance: row.balance });
    })();
  }
  // Send the last‐100 history (this is unchanged from before)
  socket.emit('history', pastColours);
  // Send the current daily‐hash commit (unchanged)
  socket.emit('dailyCommit', {
    hash: daily.hash,
    expires: daily.expires
  });
  socket.on('placeBet', async ({ colour, amount }) => {
  if (!socket.userId || !currentRoundId) {
    return;
  }
  const db = await openDb();
  try {
    await db.run(
      'UPDATE users SET balance = balance - ? WHERE id = ? AND balance >= ?',
      [amount, socket.userId, amount]
    );
    const row = await db.get('SELECT balance FROM users WHERE id = ?', [socket.userId]);
    const newBal = row.balance;
    io.to(`user_${socket.userId}`).emit('balanceUpdate', { balance: newBal });
  } catch (e) {
    socket.emit('betError', { message: 'Insufficient funds' });
    return;
  }
  const rake = amount * 0.0075;
  jackpotPot += rake;
  jackpotPot = Math.round(jackpotPot * 100) / 100;
  try {
    fs.writeFileSync(
      JACKPOT_FILE,
      JSON.stringify({ jackpot: jackpotPot }, null, 2),
      'utf8'
    );
  } catch (e) {
    console.error('[jackpot] Error writing jackpot.json:', e);
  }

  // 2) Broadcast the updated jackpot to all clients
  io.emit('jackpotUpdate', { jackpot: jackpotPot });

  const roundArr = roundBets[currentRoundId][colour];
  const existingIndex = roundArr.findIndex(b => b.userId === socket.userId);
  if (existingIndex > -1) {
    roundArr[existingIndex].amount += amount;
  } else {
    roundArr.push({
      userId: socket.userId,
      username: socket.username,
      amount
    });
  }

  roundArr.sort((a, b) => {
    if (a.userId === socket.userId) return -1;
    if (b.userId === socket.userId) return 1;
    return b.amount - a.amount;
  });

  const betsByColour = {
    red:   roundBets[currentRoundId].red,
    black: roundBets[currentRoundId].black,
    green: roundBets[currentRoundId].green
  };
  io.emit('betsUpdate', { betsByColour });
});

  
});

// === Start everything ===
scheduleDailyReveal();   // start 24 h reveal cycle
scheduleNextRound();     // start the first 15 s round

server.listen(PORT, () =>
  console.log(`Server running on http://localhost:${PORT}`)
);

