import type { Game } from "./types";
import { promises as fsp } from "fs";
import path from "path";
import { transientState } from "./sessionState";

interface GameDB {
  db: any | null;
  init(): Promise<void>;
  getGameById(id: string): Promise<Game | null>;
  saveGame(game: Game): Promise<void>;
  deleteGame?(id: string): Promise<void>;
}

export const gameDB: GameDB = {
  db: null,
  init: async () => {
    let mod: any;
    try {
      // Use runtime require to load the DB module. This runs under Node (ts-node)
      // so a direct require is appropriate and avoids relying on globalThis.
      // eslint-disable-next-line @typescript-eslint/no-var-requires
      mod = require("../../db/games");
    } catch (e) {
      // DB-only mode: fail fast so callers know persistence is required.
      throw new Error("Game DB module could not be loaded: " + String(e));
    }
    // If the module uses default export, prefer it
    let db = mod.default || mod;
    // If the required module returned a Promise (the db initializer may), await it.
    if (db && typeof db.then === "function") {
      try {
        db = await db;
      } catch (e) {
        throw new Error("Game DB initializer promise rejected: " + String(e));
      }
    }

    gameDB.db = db;
    return db;
  },

  getGameById: async (id: string | number): Promise<Game | null> => {
    if (!gameDB.db) {
      await gameDB.init();
    }
    const db = gameDB.db;
    // fallback: try to query by id using raw SQL if sequelize is available
    if (db && db.sequelize) {
      try {
        const rows = await db.sequelize.query("SELECT state FROM games WHERE id=:id", {
          replacements: { id },
          type: db.Sequelize.QueryTypes.SELECT,
        });
        if (rows && rows.length) {
          const r = rows[0];
          // state may be stored as text or JSON
          if (typeof r.state === "string") {
            try {
              return JSON.parse(r.state) as Game;
            } catch (e) {
              return null;
            }
          }
          return r.state as Game;
        }
      } catch (e) {
        // ignore and fallthrough
      }
    }
    // If DB didn't have a state or query failed, attempt to read from the
    // filesystem copy at db/games/<id> or <id>.json so the state remains editable.
    try {
      const gamesDir = path.resolve(__dirname, "../../../db/games");
      const candidates = [path.join(gamesDir, String(id)), path.join(gamesDir, String(id) + ".json")];
      for (const filePath of candidates) {
        try {
          const raw = await fsp.readFile(filePath, "utf8");
          return JSON.parse(raw) as Game;
        } catch (e) {
          // try next candidate
        }
      }
      return null;
    } catch (err) {
      return null;
    }
  },
  saveGame: async (game: Game): Promise<void> => {
    if (!gameDB.db) {
      await gameDB.init();
    }
    const db = gameDB.db;
    /* Shallow copy game, filling its sessions with a shallow copy of sessions so we can then
     * delete the player field from them */
    const reducedGame = Object.assign({}, game, { sessions: {} }),
      reducedSessions = [];

    for (let id in game.sessions) {
      const reduced = Object.assign({}, game.sessions[id]);

      // Automatically remove all transient fields (uses TRANSIENT_SESSION_SCHEMA as source of truth)
      transientState.stripSessionTransients(reduced);

      reducedGame.sessions[id] = reduced;

      /* Do not send session-id as those are secrets */
      reducedSessions.push(reduced);
    }

    // Automatically remove all game-level transient fields (uses TRANSIENT_GAME_SCHEMA)
    transientState.stripGameTransients(reducedGame);

    /* Save per turn while debugging... */
    game.step = game.step ? game.step : 0;

    // Always persist a JSON file so game state is inspectable/editable.
    try {
      const gamesDir = path.resolve(__dirname, "../../../db/games");
      await fsp.mkdir(gamesDir, { recursive: true });
      // Write extensionless filename to match existing files
      const filePath = path.join(gamesDir, reducedGame.id);
      const tmpPath = `${filePath}.tmp`;
      await fsp.writeFile(tmpPath, JSON.stringify(reducedGame, null, 2), "utf8");
      await fsp.rename(tmpPath, filePath);
    } catch (err) {
      // Log but continue to attempt DB persistence
      // eslint-disable-next-line no-console
      console.error("Failed to write game JSON file for", reducedGame.id, err);
    }

    // Now attempt DB persistence if sequelize is present.
    if (db && db.sequelize) {
      const payload = JSON.stringify(reducedGame);
      // Try an UPDATE; if it errors due to missing column, try to add the
      // column and retry. If update affects no rows, try INSERT.
      try {
        try {
          await db.sequelize.query("UPDATE games SET state=:state WHERE id=:id", {
            replacements: { id: reducedGame.id, state: payload },
          });
          // Some dialects don't return affectedRows consistently; we'll
          // still attempt insert if no row exists by checking select.
          const check = await db.sequelize.query("SELECT id FROM games WHERE id=:id", {
            replacements: { id: reducedGame.id },
            type: db.Sequelize.QueryTypes.SELECT,
          });
          if (!check || check.length === 0) {
            await db.sequelize.query("INSERT INTO games (id, state) VALUES(:id, :state)", {
              replacements: { id: reducedGame.id, state: payload },
            });
          }
        } catch (e: any) {
          const msg = String(e && e.message ? e.message : e);
          // If the column doesn't exist (SQLite: no such column: state), add it.
          if (
            /no such column: state/i.test(msg) ||
            /has no column named state/i.test(msg) ||
            /unknown column/i.test(msg)
          ) {
            try {
              await db.sequelize.query("ALTER TABLE games ADD COLUMN state TEXT");
              // retry insert/update after adding column
              await db.sequelize.query("UPDATE games SET state=:state WHERE id=:id", {
                replacements: { id: reducedGame.id, state: payload },
              });
              const check = await db.sequelize.query("SELECT id FROM games WHERE id=:id", {
                replacements: { id: reducedGame.id },
                type: db.Sequelize.QueryTypes.SELECT,
              });
              if (!check || check.length === 0) {
                await db.sequelize.query("INSERT INTO games (id, state) VALUES(:id, :state)", {
                  replacements: { id: reducedGame.id, state: payload },
                });
              }
            } catch (inner) {
              // swallow; callers should handle missing persistence
            }
          } else {
            // For other errors, attempt insert as a fallback
            try {
              await db.sequelize.query("INSERT INTO games (id, state) VALUES(:id, :state)", {
                replacements: { id: reducedGame.id, state: payload },
              });
            } catch (err) {
              // swallow; callers should handle missing persistence
            }
          }
        }
      } catch (finalErr) {
        // swallow; we don't want persistence errors to crash the server
      }
    }
  },
  deleteGame: async (id: string | number): Promise<void> => {
    if (!gameDB.db) {
      await gameDB.init();
    }
    const db = gameDB.db;
    if (db && db.sequelize) {
      try {
        await db.sequelize.query("DELETE FROM games WHERE id=:id", {
          replacements: { id },
        });
      } catch (e) {
        // swallow; callers should handle missing persistence
      }
    }
  },
};

export const games: Record<string, Game> = {};
