/**
 * Zork LLM Server
 *
 * Starts an Express + Socket.IO server that runs Zork I through the
 * ZorkLlmEngine and serves the same shared client UI as the YAML engine.
 *
 * Usage:
 *   npm run dev:zork      (development, with file watching)
 *   npm run start:zork    (production, from compiled dist/)
 *
 * Environment variables:
 *   PORT             – HTTP port (default: 3002)
 *   ZORK_STORY_FILE  – path to the story file (default: ./data/z-code/zork1.bin)
 *   OPENROUTER_API_KEY, OPENROUTER_MODEL – required
 */

import path from 'path';
import http from 'http';
import express from 'express';
import { Server as SocketIOServer } from 'socket.io';
import * as dotenv from 'dotenv';
import { existsSync, mkdirSync, copyFileSync } from 'fs';
import { ZorkLlmEngine, ZorkTurnResult } from './engine/zork-llm-engine';
import {
  clientGameConfig,
  ensureConfiguredAssetDirectories,
  loadGameConfig,
  projectPath,
} from './config/game-config';

dotenv.config();

const app = express();
const server = http.createServer(app);
const io = new SocketIOServer(server);

const DEFAULT_PORT = 3002;
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_PORT;
const PORT_RANGE = 300;
const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZORK_DEBUG ?? '');
const engineConfig = loadGameConfig(
  process.env.ZORK_CONFIG_FILE || './config/engines/zork.json',
  'zork',
);

function debugLog(message: string, details?: unknown): void {
  if (!DEBUG_ENABLED) return;
  if (typeof details === 'undefined') {
    console.log(`[zork:debug] ${message}`);
    return;
  }
  console.log(`[zork:debug] ${message}`, details);
}

// Serve the same shared client UI
app.use(
  express.static(path.join(__dirname, '../public'), {
    etag: false,
    lastModified: false,
    setHeaders: (res) => {
      res.setHeader(
        'Cache-Control',
        'no-store, no-cache, must-revalidate, proxy-revalidate',
      );
      res.setHeader('Pragma', 'no-cache');
      res.setHeader('Expires', '0');
    },
  }),
);

app.get('/api/game-config', (_req, res) => {
  res.json(clientGameConfig(engineConfig));
});

// One engine instance per connected socket
const sessions = new Map<string, ZorkLlmEngine>();
// Save-game slot maps: socketId → Map<slotNumber, serialisedJson>
const saveSlots = new Map<string, Map<number, string>>();

function toClientTurn(turn: ZorkTurnResult): ZorkTurnResult {
  return {
    ...turn,
    gameState: {
      ...turn.gameState,
      currentRoomId: turn.gameState?.statusLine,
      statusLine: turn.gameState?.statusLine,
    },
  };
}

function normalizeSaveSlot(slot: unknown): number {
  const n = Number(slot);
  return Number.isInteger(n) && n > 0 ? n : 1;
}

function getOrCreateEngine(socketId: string): ZorkLlmEngine {
  let engine = sessions.get(socketId);
  if (!engine) {
    engine = new ZorkLlmEngine({
      storyPath: projectPath(process.env.ZORK_STORY_FILE || engineConfig.paths.mainGameFile),
      promptDir: projectPath(engineConfig.paths.promptDir || 'data/zork-prompts'),
    });
    sessions.set(socketId, engine);
  }
  return engine;
}

function getSlots(socketId: string): Map<number, string> {
  let slots = saveSlots.get(socketId);
  if (!slots) {
    slots = new Map();
    saveSlots.set(socketId, slots);
  }
  return slots;
}

async function handleGameApi(
  socket: ReturnType<SocketIOServer['sockets']['sockets']['get']> & {
    id: string;
  },
  method: string,
  args: unknown[],
): Promise<object> {
  const slots = getSlots(socket.id);
  debugLog(`gameApi request from ${socket.id}: ${method}`, { args });

  switch (method) {
    case 'newGame':
    case 'newGame()': {
      const engine = getOrCreateEngine(socket.id);
      const turn = await engine.newGame();
      socket.emit('narrativeResponse', toClientTurn(turn));
      return {
        success: true,
        result: true,
        running: true,
        canLoad: slots.size > 0,
      };
    }

    case 'loadGame':
    case 'loadGame()': {
      const slot = normalizeSaveSlot(args[0]);
      if (!slots.has(slot)) {
        return { success: false, error: 'missing_save', result: false };
      }
      const engine = getOrCreateEngine(socket.id);
      const turn = await engine.loadGame(slots.get(slot)!);
      socket.emit('narrativeResponse', toClientTurn(turn));
      socket.emit('gameLoaded', { slot });
      return { success: true, result: true, running: true, slot };
    }

    case 'saveGame':
    case 'saveGame()': {
      const engine = sessions.get(socket.id);
      if (!engine?.isRunning()) {
        return { success: false, error: 'game_not_running', result: false };
      }
      const slot = normalizeSaveSlot(args[0]);
      const savedJson = await engine.saveGame();
      slots.set(slot, savedJson);
      socket.emit('gameSaved', { slot });
      return { success: true, result: true, slot };
    }

    case 'hasSaveGame':
    case 'hasSaveGame()': {
      const slot = normalizeSaveSlot(args[0]);
      return { success: true, result: slots.has(slot), slot };
    }

    case 'getSaveGames':
    case 'getSaveGames()':
      return {
        success: true,
        result: Array.from(slots.keys()).sort((a, b) => a - b),
      };

    case 'isGameRunning':
    case 'isGameRunning()':
      return {
        success: true,
        result: sessions.get(socket.id)?.isRunning() ?? false,
      };

    default:
      return { success: false, error: `unknown_method:${method}` };
  }
}

function checkRuntimeConfiguration(): void {
  const storyPath = projectPath(process.env.ZORK_STORY_FILE ?? engineConfig.paths.mainGameFile);
  const promptDir = projectPath(engineConfig.paths.promptDir || 'data/zork-prompts');
  const promptFiles = [
    'character-generation.yml',
    'text-rewriter.yml',
    'command-translator.yml',
    'output-evaluator.yml',
  ];

  const missingPrompts = promptFiles
    .map((file) => path.join(promptDir, file))
    .filter((filePath) => !existsSync(filePath));

  if (!process.env.OPENROUTER_API_KEY) {
    console.error('[zork] Missing OPENROUTER_API_KEY in environment.');
  }
  if (!process.env.OPENROUTER_MODEL) {
    console.error('[zork] Missing OPENROUTER_MODEL in environment.');
  }
  if (!existsSync(storyPath)) {
    console.error(`[zork] Story file missing: ${storyPath}`);
    console.error('[zork] Place zork1.bin in ./data/z-code/ or set ZORK_STORY_FILE.');
  }
  if (missingPrompts.length > 0) {
    console.error('[zork] Missing prompt files:');
    for (const filePath of missingPrompts) {
      console.error(`  - ${filePath}`);
    }
  }

  debugLog('runtime configuration', {
    storyPath,
    promptDir,
    debug: DEBUG_ENABLED,
    hasApiKey: Boolean(process.env.OPENROUTER_API_KEY),
    model: process.env.OPENROUTER_MODEL ?? null,
  });
}

io.on('connection', (socket) => {
  console.log(`[zork] Client connected: ${socket.id}`);
  socket.emit('gameConfig', clientGameConfig(engineConfig));

  socket.on(
    'gameApi',
    async (
      request: { method?: string; args?: unknown[] },
      respond: (result: object) => void,
    ) => {
      try {
        const result = await handleGameApi(
          socket as Parameters<typeof handleGameApi>[0],
          String(request?.method ?? ''),
          Array.isArray(request?.args) ? request.args : [],
        );
        debugLog(`gameApi response to ${socket.id}`, result);
        if (typeof respond === 'function') respond(result);
      } catch (error) {
        console.error('[zork] gameApi error:', error);
        if (typeof respond === 'function') {
          respond({
            success: false,
            error: error instanceof Error ? error.message : String(error),
          });
        }
      }
    },
  );

  socket.on(
    'playerCommand',
    async (data: { command?: string }) => {
      const engine = sessions.get(socket.id);
      if (!engine?.isRunning()) {
        socket.emit('error', {
          message: 'No active game. Start or load a game first.',
        });
        return;
      }

      const input = String(data?.command ?? '').trim();
      if (!input) return;
      debugLog(`playerCommand from ${socket.id}: ${input}`);

      try {
        const turn: ZorkTurnResult = await engine.processInput(input);
        debugLog(`narrativeResponse to ${socket.id}`, {
          inputMode: turn.inputMode,
          paragraphs: turn.paragraphs.length,
          statusLine: turn.gameState?.statusLine,
        });
        socket.emit('narrativeResponse', toClientTurn(turn));
      } catch (error) {
        console.error('[zork] playerCommand error:', error);
        socket.emit('error', {
          message:
            error instanceof Error ? error.message : 'An error occurred.',
        });
      }
    },
  );

  socket.on('disconnect', () => {
    console.log(`[zork] Client disconnected: ${socket.id}`);
    sessions.delete(socket.id);
    saveSlots.delete(socket.id);
  });
});

// ---------------------------------------------------------------------------
// Startup helpers
// ---------------------------------------------------------------------------

function ensureDirectories(): void {
  const dirs = [
    path.join(__dirname, '../public'),
    path.join(__dirname, '../public/js'),
    path.join(__dirname, '../public/css'),
    path.join(__dirname, '../public/images'),
    path.join(__dirname, '../public/music'),
    path.join(__dirname, '../public/sounds'),
    path.join(__dirname, '../public/fonts'),
    path.join(__dirname, '../data/z-code'),
    path.join(__dirname, '../data/zork-prompts'),
  ];
  for (const dir of dirs) {
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  }
  ensureConfiguredAssetDirectories(engineConfig);
}

function ensureKokoroJs(): void {
  const src = path.join(__dirname, '../node_modules/kokoro-js/dist/index.js');
  const dst = path.join(__dirname, '../public/js/kokoro-js.js');
  if (existsSync(src) && !existsSync(dst)) copyFileSync(src, dst);
}

async function startServer(initialPort: number, range: number): Promise<void> {
  ensureDirectories();
  try { ensureKokoroJs(); } catch { /* optional */ }
  checkRuntimeConfiguration();

  let port = initialPort;
  while (port < initialPort + range) {
    try {
      await new Promise<void>((resolve, reject) => {
        server.removeAllListeners('error');
        server.removeAllListeners('listening');
        server.once('listening', () => {
          console.log(
            `[zork] Zork Narrator server running on http://localhost:${port}`,
          );
          resolve();
        });
        server.once('error', (err: NodeJS.ErrnoException) => {
          if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
            console.log(`Port ${port} unavailable (${err.code}), trying ${port + 1}...`);
            server.close();
            port++;
            reject();
          } else {
            reject(err);
          }
        });
        server.listen(port);
      });
      return;
    } catch {
      if (port >= initialPort + range - 1) {
        throw new Error(
          `Failed to start server on ports ${initialPort}–${initialPort + range - 1}`,
        );
      }
    }
  }
}

if (require.main === module) {
  startServer(PORT, PORT_RANGE).catch((err) => {
    console.error('[zork] Failed to start:', err);
    process.exit(1);
  });
}
