import { config } from './config.js';
import { createRedis } from './redisClient.js';
import { createPool } from './db.js';
import { initMySqlSupport, createMatchRow, updateMatchPerSecond, endMatch, insertMatchTick } from './mysqlWriter.js';
import { verifyTicket } from './ticket.js';
import { enqueue, leaveQueue, dequeuePair } from './matchmaking.js';
import { createInitialState, step, isSetOver, resetBall } from './physics.js';
import { randomId } from './crypto.js';
import { saveMatchSnapshot, loadMatchSnapshot } from './matchStore.js';
import { sendRewardsRequest } from './rewardsClient.js';
import { createHttpAndWs } from './server.js';

let startupError = null;
let redis = null;
let pool = null;
let mysqlSupport = null;
let mysqlStartupError = null;

const connections = new Map(); // ws -> session
const userSockets = new Map(); // userId -> ws
const activeMatches = new Map(); // matchId -> match
const WS_CONNECTING = 0;
const WS_OPEN = 1;

function nowUtc() {
  // MySQL expects UTC string
  const d = new Date();
  return d.toISOString().slice(0, 19).replace('T', ' ');
}

function safeSend(ws, msg) {
  if (ws.readyState !== ws.OPEN) return;
  ws.send(JSON.stringify(msg));
}

function isSocketActive(ws) {
  return !!ws && (ws.readyState === WS_CONNECTING || ws.readyState === WS_OPEN);
}

function getStatus() {
  return {
    ok: startupError === null,
    startupError,
    redisReady: !!redis,
    redisMode: redis?.mode ?? null,
    mysqlReady: !!pool && !!mysqlSupport,
    mysqlStartupError,
    connections: connections.size,
    activeMatches: activeMatches.size,
  };
}

function isRuntimeReady() {
  return startupError === null && !!redis;
}

function getUsernameByUserId(userId) {
  const ws = userSockets.get(userId);
  const session = ws ? connections.get(ws) : null;
  return session?.username || null;
}

function attachWsHandlers(ws) {
  ws.on('message', async (buf) => {
    let msg;
    try {
      msg = JSON.parse(buf.toString('utf8'));
    } catch {
      safeSend(ws, { type: 'error', error: 'invalid_json' });
      return;
    }

    const session = connections.get(ws) ?? {};

    if (msg?.type === 'hello') {
      if (!isRuntimeReady()) {
        safeSend(ws, { type: 'hello.error', error: startupError || 'server_not_ready' });
        ws.close(1013, 'Server unavailable');
        return;
      }

      const result = verifyTicket(config.sharedSecret, msg.ticket);
      if (!result.ok) {
        safeSend(ws, { type: 'hello.error', error: result.error });
        ws.close(1008, 'Auth failed');
        return;
      }

      const { userId, username } = result.payload;
      session.userId = Number(userId);
      session.username = username;

      const existingWs = userSockets.get(session.userId);
      if (existingWs && existingWs !== ws && isSocketActive(existingWs)) {
        safeSend(ws, { type: 'hello.error', error: 'duplicate_session' });
        ws.close(1008, 'Duplicate session');
        return;
      }

      connections.set(ws, session);
      userSockets.set(session.userId, ws);

      // Try resume if client provides matchId hint
      if (msg.matchId) {
        const match = activeMatches.get(msg.matchId);
        if (match) {
          match.reconnect(session.userId, ws);
        } else if (redis) {
          // try load snapshot for UI (client can rejoin later)
          const snap = await loadMatchSnapshot(redis, msg.matchId);
          if (snap) safeSend(ws, { type: 'match.snapshot', snapshot: snap });
        }
      }

      safeSend(ws, { type: 'hello.ok', userId: session.userId });
      return;
    }

    if (!session.userId) {
      safeSend(ws, { type: 'error', error: 'not_authenticated' });
      return;
    }

    if (msg?.type === 'queue.join') {
      if (!isRuntimeReady()) {
        safeSend(ws, { type: 'error', error: startupError || 'server_not_ready' });
        return;
      }

      const currentWs = userSockets.get(session.userId);
      if (currentWs && currentWs !== ws && isSocketActive(currentWs)) {
        safeSend(ws, { type: 'error', error: 'duplicate_session' });
        return;
      }

      if (session.matchId) {
        safeSend(ws, { type: 'error', error: 'already_in_match' });
        return;
      }

      const queueSize = await enqueue(redis, session.userId);
      session.inQueue = true;
      connections.set(ws, session);
      safeSend(ws, { type: 'queue.status', status: 'searching', queueSize });
      return;
    }

    if (msg?.type === 'queue.leave') {
      if (!redis) {
        safeSend(ws, { type: 'queue.status', status: 'idle' });
        return;
      }

      await leaveQueue(redis, session.userId);
      session.inQueue = false;
      connections.set(ws, session);
      safeSend(ws, { type: 'queue.status', status: 'idle' });
      return;
    }

    if (msg?.type === 'match.input') {
      const matchId = session.matchId;
      if (!matchId) return;
      const match = activeMatches.get(matchId);
      if (!match) return;
      match.onInput(session.userId, msg);
      return;
    }

    safeSend(ws, { type: 'error', error: 'unknown_type' });
  });

  ws.on('close', () => {
    const session = connections.get(ws);
    connections.delete(ws);
    if (session?.userId) {
      const cur = userSockets.get(session.userId);
      if (cur === ws) userSockets.delete(session.userId);
      if (!session.matchId && redis) {
        void leaveQueue(redis, session.userId).catch(() => {});
      }
      if (session.matchId) {
        const match = activeMatches.get(session.matchId);
        if (match) match.onDisconnect(session.userId);
      }
    }
  });
}

class Match {
  constructor({ matchId, leftUserId, rightUserId, mysqlMatchId }) {
    const leftUsername = getUsernameByUserId(leftUserId);
    const rightUsername = getUsernameByUserId(rightUserId);
    if (!leftUsername || !rightUsername) {
      throw new Error('match_requires_valid_usernames');
    }

    this.matchId = matchId;
    this.mysqlMatchId = mysqlMatchId ?? null;
    this.players = {
      left: {
        userId: leftUserId,
        username: leftUsername,
        ws: userSockets.get(leftUserId) ?? null,
        input: { move: 0, targetY: null },
        disconnectedAt: null,
      },
      right: {
        userId: rightUserId,
        username: rightUsername,
        ws: userSockets.get(rightUserId) ?? null,
        input: { move: 0, targetY: null },
        disconnectedAt: null,
      },
    };
    this.seed = Math.floor(Math.random() * 1e9);
    this.state = createInitialState(this.seed);
    this.pointsToWin = 11;
    this.setsToWin = 3;
    this.sets = { left: 0, right: 0 };
    this.currentSet = 1;
    this.warmupMs = 10_000;
    this.warmupEndsAt = Date.now() + this.warmupMs;

    this._lastTickMs = Date.now();
    this._lastRedisSnapshotMs = 0;
    this._lastMysqlUpdateMs = 0;
    this._ended = false;

    this._broadcast(this._matchFoundPayload('left'), 'left');
    this._broadcast(this._matchFoundPayload('right'), 'right');

    // attach matchId to sessions
    for (const side of ['left', 'right']) {
      const userId = this.players[side].userId;
      const ws = userSockets.get(userId);
      if (ws) {
        const s = connections.get(ws) ?? {};
        s.matchId = this.matchId;
        s.inQueue = false;
        connections.set(ws, s);
      }
    }
  }

  reconnect(userId, ws) {
    const side = this._sideOf(userId);
    if (!side) return;
    this.players[side].ws = ws;
    this.players[side].disconnectedAt = null;

    const session = connections.get(ws) ?? {};
    if (session.username) {
      this.players[side].username = session.username;
    }

    const s = session;
    s.matchId = this.matchId;
    connections.set(ws, s);

    safeSend(ws, {
      type: 'match.reconnected',
      matchId: this.matchId,
      side,
      opponentUserId: this.players[side === 'left' ? 'right' : 'left'].userId,
      opponentUsername: this.players[side === 'left' ? 'right' : 'left'].username,
      warmupEndsAt: this.warmupEndsAt,
      pointsToWin: this.pointsToWin,
      setsToWin: this.setsToWin,
    });
    safeSend(ws, { type: 'match.state', matchId: this.matchId, state: this._publicState() });
  }

  onInput(userId, msg) {
    const side = this._sideOf(userId);
    if (!side) return;
    const move = Number(msg.move ?? 0);
    if (![ -1, 0, 1 ].includes(move)) return;

    let targetY = null;
    if (msg.targetY !== null && msg.targetY !== undefined) {
      const parsedTargetY = Number(msg.targetY);
      if (!Number.isFinite(parsedTargetY)) return;
      targetY = Math.max(0, Math.min(1, parsedTargetY));
    }

    this.players[side].input = { move, targetY };
  }

  onDisconnect(userId) {
    const side = this._sideOf(userId);
    if (!side) return;
    this.players[side].disconnectedAt = Date.now();
  }

  tick() {
    if (this._ended) return;

    const now = Date.now();
    const dt = Math.min(0.05, Math.max(0.001, (now - this._lastTickMs) / 1000));
    this._lastTickMs = now;

    // forfeit if disconnect too long
    for (const side of ['left', 'right']) {
      const p = this.players[side];
      if (p.disconnectedAt && now - p.disconnectedAt > config.reconnectGraceMs) {
        const winner = side === 'left' ? 'right' : 'left';
        this._end(`forfeit_${side}`, winner);
        return;
      }
    }

    if (now >= this.warmupEndsAt) {
      step(this.state, dt, this.players.left.input, this.players.right.input);

      if (isSetOver(this.state, this.pointsToWin, 2)) {
        const setWinner = this.state.scoreL > this.state.scoreR ? 'left' : 'right';
        this.sets[setWinner] += 1;

        if (this.sets[setWinner] >= this.setsToWin) {
          this._end('sets', setWinner);
          return;
        }

        this.currentSet += 1;
        this.state.scoreL = 0;
        this.state.scoreR = 0;
        resetBall(this.state, setWinner === 'left' ? -1 : 1);
      }
    }

    // Broadcast state at tick rate
    this._broadcast({ type: 'match.state', matchId: this.matchId, state: this._publicState() });

    // Redis snapshot (for reconnect)
    if (redis && now - this._lastRedisSnapshotMs >= config.redisSnapshotMs) {
      this._lastRedisSnapshotMs = now;
      void saveMatchSnapshot(redis, this.matchId, this._snapshot(), 60 * 30);
    }

    // MySQL update (score/time)
    if (this.mysqlMatchId && pool && mysqlSupport && now - this._lastMysqlUpdateMs >= config.mysqlUpdateIntervalMs) {
      this._lastMysqlUpdateMs = now;
      const score = this._formatScore();
      const participantsJson = JSON.stringify([this.players.left.userId, this.players.right.userId]);
      void updateMatchPerSecond(pool, mysqlSupport, this.mysqlMatchId, { status: 'live', score, participantsJson })
        .catch((e) => console.error('[mysql] per-second update failed', e));

      if (mysqlSupport.hasMatchTicks) {
        const tickTs = nowUtc();
        const stateJson = JSON.stringify(this._snapshot());
        void insertMatchTick(pool, this.mysqlMatchId, tickTs, stateJson)
          .catch((e) => console.error('[mysql] tick insert failed', e));
      }
    }
  }

  _publicState() {
    const now = Date.now();
    return {
      t: this.state.t,
      scoreL: this.state.scoreL,
      scoreR: this.state.scoreR,
      setsL: this.sets.left,
      setsR: this.sets.right,
      currentSet: this.currentSet,
      pointsToWin: this.pointsToWin,
      setsToWin: this.setsToWin,
      isWarmup: now < this.warmupEndsAt,
      warmupEndsAt: this.warmupEndsAt,
      warmupRemainingMs: Math.max(0, this.warmupEndsAt - now),
      ball: this.state.ball,
      paddleL: { y: this.state.paddleL.y },
      paddleR: { y: this.state.paddleR.y },
    };
  }

  _matchFoundPayload(side) {
    const opponentSide = side === 'left' ? 'right' : 'left';
    return {
      type: 'match.found',
      matchId: this.matchId,
      side,
      opponentUserId: this.players[opponentSide].userId,
      opponentUsername: this.players[opponentSide].username,
      warmupEndsAt: this.warmupEndsAt,
      pointsToWin: this.pointsToWin,
      setsToWin: this.setsToWin,
    };
  }

  _snapshot() {
    return {
      matchId: this.matchId,
      mysqlMatchId: this.mysqlMatchId,
      players: { left: this.players.left.userId, right: this.players.right.userId },
      seed: this.seed,
      state: this._publicState(),
      updatedAtMs: Date.now(),
    };
  }

  _broadcast(msg, onlySide = null) {
    if (!onlySide) {
      for (const side of ['left', 'right']) this._broadcast(msg, side);
      return;
    }
    const ws = this.players[onlySide].ws;
    if (ws) safeSend(ws, msg);
  }

  _sideOf(userId) {
    if (this.players.left.userId === userId) return 'left';
    if (this.players.right.userId === userId) return 'right';
    return null;
  }

  async _end(reason, winnerSide) {
    if (this._ended) return;
    this._ended = true;

    const score = this._formatScore();
    const endTime = nowUtc();

    try {
      if (!pool || !this.mysqlMatchId) {
        throw new Error('mysql_not_ready');
      }
      await endMatch(pool, this.mysqlMatchId, { score, endTimeUtc: endTime });
    } catch (e) {
      console.error('[mysql] end match failed', e);
    }

    const payload = {
      discipline: 'ping-pong',
      mode: '1v1',
      matchId: this.mysqlMatchId,
      matchKey: this.matchId,
      reason,
      score,
      sets: { left: this.sets.left, right: this.sets.right },
      points: { left: this.state.scoreL, right: this.state.scoreR },
      pointsToWin: this.pointsToWin,
      setsToWin: this.setsToWin,
      winnerUserId: this.players[winnerSide].userId,
      winnerUsername: this.players[winnerSide].username,
      loserUserId: this.players[winnerSide === 'left' ? 'right' : 'left'].userId,
      loserUsername: this.players[winnerSide === 'left' ? 'right' : 'left'].username,
      endedAt: endTime,
    };

    this._broadcast({ type: 'match.end', matchId: this.matchId, payload });

    // Save final snapshot and let it expire
    try {
      if (!redis) {
        throw new Error('redis_not_ready');
      }
      await saveMatchSnapshot(redis, this.matchId, { ...this._snapshot(), ended: true, payload }, 60 * 5);
    } catch (e) {
      console.error('[redis] final snapshot failed', e);
    }

    // Fire-and-forget rewards request with retry later (for now simple one-shot)
    sendRewardsRequest(payload)
      .then((resp) => {
        // optional: notify players
        this._broadcast({ type: 'rewards.queued', matchId: this.matchId, response: resp });
      })
      .catch((e) => {
        console.error('[rewards] request failed', e);
        this._broadcast({ type: 'rewards.error', matchId: this.matchId });
      })
      .finally(() => {
        activeMatches.delete(this.matchId);
      });
  }

  _formatScore() {
    return `sety ${this.sets.left}:${this.sets.right} | punkty ${this.state.scoreL}:${this.state.scoreR}`;
  }
}

// Matchmaking loop
setInterval(async () => {
  if (!isRuntimeReady()) return;

  try {
    const pair = await dequeuePair(redis);
    if (!pair) return;

    const [u1, u2] = pair;
    const matchId = randomId('m_');

    let mysqlMatchId = null;
    if (pool && mysqlSupport) {
      try {
        const participantsJson = JSON.stringify([u1, u2]);
        mysqlMatchId = await createMatchRow(pool, mysqlSupport, {
          team1Id: u1,
          team2Id: u2,
          startTimeUtc: nowUtc(),
          platform: 'PC',
          matchType: 'przyjacielski',
          participantsJson,
          discipline: 'ping-pong',
        });
      } catch (e) {
        console.error('[matchmaking] mysql createMatchRow failed, starting match without DB row', e);
      }
    }

    const match = new Match({ matchId, leftUserId: u1, rightUserId: u2, mysqlMatchId });
    activeMatches.set(matchId, match);
  } catch (e) {
    console.error('[matchmaking] error', e);
  }
}, 150);

// Physics tick loop (single scheduler for all matches)
const tickMs = Math.max(5, Math.floor(1000 / config.matchTickHz));
setInterval(() => {
  for (const match of activeMatches.values()) {
    try { match.tick(); } catch (e) { console.error('[match] tick error', e); }
  }
}, tickMs);

const { server } = createHttpAndWs({
  onConnection: (ws) => {
    if (!isRuntimeReady()) {
      safeSend(ws, { type: 'hello.error', error: startupError || 'server_not_ready' });
      ws.close(1013, 'Server unavailable');
      return;
    }
    connections.set(ws, {});
    attachWsHandlers(ws);
    safeSend(ws, { type: 'hello', message: 'send hello ticket' });
  },
  getStatus,
});

server.listen(config.port, async () => {
  console.log(`[ping-pong-1v1] listening on :${config.port}`);
  try {
    redis = await createRedis();
    try {
      pool = createPool();
      mysqlSupport = await initMySqlSupport(pool);
      mysqlStartupError = null;
    } catch (error) {
      mysqlStartupError = error instanceof Error ? error.message : String(error);
      pool = null;
      mysqlSupport = null;
      console.warn('[mysql] unavailable, continuing without DB persistence:', error);
    }
    startupError = null;
    console.log(`[ping-pong-1v1] startup dependencies ready (redis mode: ${redis?.mode ?? 'unknown'})`);
  } catch (error) {
    startupError = error instanceof Error ? error.message : String(error);
    console.error('[ping-pong-1v1] startup failed:', error);
  }
});

process.on('unhandledRejection', (error) => {
  console.error('[ping-pong-1v1] unhandled rejection:', error);
});

process.on('uncaughtException', (error) => {
  console.error('[ping-pong-1v1] uncaught exception:', error);
});
