/**
 * Ink Engine Server
 *
 * Serves the shared client UI and runs a compiled Ink JSON story through the
 * unified TurnResult socket protocol.
 */

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 { compileInkSource, InkEngine } from './engine/ink-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 = 3003;
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_PORT;
const PORT_RANGE = 300;
const engineConfig = loadGameConfig(
  process.env.INK_CONFIG_FILE || './config/engines/ink.json',
  'ink',
);

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));
});

const sessions = new Map<string, InkEngine>();
const saveSlots = new Map<string, Map<number, string>>();

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

function getStoryPath(): string {
  return projectPath(
    process.env.INK_STORY_FILE ||
      engineConfig.paths.inkCompiled ||
      engineConfig.paths.mainGameFile,
  );
}

function getSourcePath(): string {
  return projectPath(process.env.INK_SOURCE_FILE || engineConfig.paths.inkSource || '');
}

function compileConfiguredStory(): void {
  const sourcePath = getSourcePath();
  const outputPath = getStoryPath();
  const result = compileInkSource(sourcePath, outputPath);
  console.log(
    `[ink] Compiled ${result.sourcePath} -> ${result.outputPath}` +
      (result.warningCount > 0 ? ` (${result.warningCount} warnings)` : ''),
  );
}

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

function getOrCreateEngine(socketId: string): InkEngine {
  let engine = sessions.get(socketId);
  if (!engine) {
    engine = new InkEngine(getStoryPath());
    sessions.set(socketId, engine);
  }
  return engine;
}

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

  switch (method) {
    case 'newGame':
    case 'newGame()': {
      const engine = new InkEngine(getStoryPath());
      sessions.set(socket.id, engine);
      socket.emit('narrativeResponse', engine.newGame());
      return { success: true, result: true, running: true, canLoad: slots.size > 0 };
    }

    case 'chooseChoice':
    case 'chooseChoice()': {
      const engine = sessions.get(socket.id);
      if (!engine?.isRunning()) {
        return { success: false, error: 'game_not_running', result: false };
      }
      const choiceIndex = Number(args[0]);
      if (!Number.isInteger(choiceIndex)) {
        return { success: false, error: 'invalid_choice', result: false };
      }
      socket.emit('narrativeResponse', engine.chooseChoice(choiceIndex));
      return { success: true, result: true };
    }

    case 'loadGame':
    case 'loadGame()': {
      const slot = normalizeSaveSlot(args[0]);
      const browserSave = typeof args[1] === 'string' ? args[1] : null;
      if (!browserSave && !slots.has(slot)) {
        return { success: false, error: 'missing_save', result: false };
      }
      const engine = getOrCreateEngine(socket.id);
      socket.emit('narrativeResponse', engine.loadGame(browserSave || slots.get(slot)!));
      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 savedState = engine.saveGame();
      slots.set(slot, savedState);
      socket.emit('gameSaved', { slot });
      return { success: true, result: true, slot, savedState };
    }

    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}` };
  }
}

io.on('connection', (socket) => {
  console.log(`[ink] 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 : [],
        );
        if (typeof respond === 'function') respond(result);
      } catch (error) {
        console.error('[ink] gameApi error:', error);
        if (typeof respond === 'function') {
          respond({
            success: false,
            error: error instanceof Error ? error.message : String(error),
          });
        }
      }
    },
  );

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

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'),
  ];
  for (const dir of dirs) {
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  }
  ensureConfiguredAssetDirectories(engineConfig);
}

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

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

  compileConfiguredStory();

  if (!existsSync(getStoryPath())) {
    console.error(`[ink] Story file missing: ${getStoryPath()}`);
    console.error('[ink] Set INK_SOURCE_FILE or configure paths.inkSource in config/engines/ink.json.');
  }

  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(`[ink] Ink server running on http://localhost:${port}`);
          resolve();
        });
        server.once('error', (error: NodeJS.ErrnoException) => {
          if (error.code === 'EADDRINUSE' || error.code === 'EACCES') {
            console.log(`Port ${port} unavailable (${error.code}), trying ${port + 1}...`);
            server.close();
            port++;
            reject();
          } else {
            reject(error);
          }
        });
        server.listen(port);
      });
      return;
    } catch {
      if (port >= initialPort + range - 1) {
        throw new Error(`Failed to start server on ports ${initialPort} to ${initialPort + range - 1}`);
      }
    }
  }
}

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

export { app, server, io };
