// Idempotent migration from the single-user schema to the multi-user schema.
//
// Used both at server boot (server.js calls runMigration after opening the DB)
// and by the standalone scripts/migrate-to-multiuser.js. Safe to run repeatedly:
// every step guards itself with "already done?" checks.
//
// What it does, in order:
//   1. Create the `users` + `sessions` tables.
//   2. Ensure an `admin` user exists (password "admin", scrypt hash). Record its id.
//   3. Add a `user_id` column to every per-user table (ALTER ADD COLUMN, nullable
//      for upgraded installs — fresh installs create it NOT NULL directly) and
//      backfill every existing row to the admin id.
//   4. Recreate the tables whose PRIMARY KEY / UNIQUE must include user_id so the
//      constraint becomes per-user: app_state, settings, prompts, design,
//      jobangebote. Old rows are copied to the admin user.
//   5. Move on-disk attachment files into a per-user subdirectory for the admin.
//   6. One-time migration of any still-present .env values into the admin's cfg.
//
// After this, the app boots against a fully multi-user schema and all pre-existing
// data is owned by the admin user.

const fs = require('fs');
const path = require('path');
const password = require('./password');
const config = require('./config');

// Tables that need a `user_id` column added (legacy upgrades). Fresh installs
// create these with user_id NOT NULL directly in initializeDatabase().
const USER_TABLES = [
  'bewerbungen', 'status_verlauf', 'anhaenge', 'interne_anhaenge',
  'emails', 'email_anhaenge', 'basis_dokumente', 'basis_anhaenge',
  'termine', 'chat_threads', 'chat_messages',
  'jobangebote_blacklist',
];
// jobangebote is NOT in USER_TABLES: its user_id column + per-user UNIQUE
// constraint are installed by the recreate in step 4 (which needs user_id to be
// absent so the rebuild fires). Adding it here first would make step 4's
// !hasColumn guard short-circuit and leave the legacy single-user UNIQUE in
// place, breaking per-user isolation on ingest.

const ADMIN_USERNAME = 'admin';
const ADMIN_DEFAULT_PASSWORD = 'admin';

async function runMigration({ db, dbAll, dbGet, dbRun }) {
  // Wire the config store to the same DB so config.setForUser works during the
  // one-time .env migration below.
  await config.init({ dbAll, dbRun });
  const exec = (sql) => new Promise((resolve, reject) => db.run(sql, (err) => err ? reject(err) : resolve()));

  const tableExists = async (name) => {
    const row = await dbGet("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [name]);
    return !!row;
  };

  const columnsOf = async (table) => {
    const rows = await dbAll(`PRAGMA table_info(${table})`);
    return rows.map((r) => r.name);
  };

  const hasColumn = async (table, col) => (await columnsOf(table)).includes(col);

  // 1. users + sessions -------------------------------------------------
  await exec(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      username TEXT NOT NULL UNIQUE,
      password_hash TEXT NOT NULL,
      is_admin INTEGER NOT NULL DEFAULT 0,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);
  await exec(`
    CREATE TABLE IF NOT EXISTS sessions (
      token TEXT PRIMARY KEY,
      user_id INTEGER NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      last_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
    )
  `);
  await exec('CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)');
  // Impersonation support: the admin's session keeps its token but switches
  // user_id to the target, storing the original admin in impersonator_id so the
  // admin can switch back. Added after the fact for existing installs.
  if (!(await hasColumn('sessions', 'impersonator_id'))) {
    await exec('ALTER TABLE sessions ADD COLUMN impersonator_id INTEGER');
  }
  // "Angemeldet bleiben": vorher galten alle Sessions 30 Tage. Bestehende
  // Sessions behalten dieses Verhalten (Default 1) — niemand wird durch das
  // Update ausgeloggt.
  if (!(await hasColumn('sessions', 'dauerhaft'))) {
    await exec('ALTER TABLE sessions ADD COLUMN dauerhaft INTEGER NOT NULL DEFAULT 1');
  }

  // 2. Ensure admin user (idempotent) -----------------------------------
  let admin = await dbGet('SELECT id, password_hash FROM users WHERE username = ?', [ADMIN_USERNAME]);
  if (!admin) {
    const hash = password.hash(ADMIN_DEFAULT_PASSWORD);
    const res = await dbRun(
      'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 1)',
      [ADMIN_USERNAME, hash]
    );
    admin = { id: res.lastID };
    console.log(`Multi-User-Migration: Admin-Benutzer „${ADMIN_USERNAME}“ angelegt (Passwort „${ADMIN_DEFAULT_PASSWORD}“). Bitte nach erstem Login ändern.`);
  }
  const adminId = admin.id;

  // 3. Add user_id column + backfill ------------------------------------
  for (const tbl of USER_TABLES) {
    if (!(await tableExists(tbl))) continue;          // table may not exist yet on a partial install
    if (!(await hasColumn(tbl, 'user_id'))) {
      await exec(`ALTER TABLE ${tbl} ADD COLUMN user_id INTEGER`);
    }
    // Backfill every row that is not yet assigned (NULL) to the admin.
    await dbRun(`UPDATE ${tbl} SET user_id = ? WHERE user_id IS NULL`, [adminId]);
  }

  // 4. Recreate tables whose PK/UNIQUE must be per-user -----------------
  // app_state: PK(key) -> PK(user_id, key)
  if (await tableExists('app_state')) {
    if (!(await hasColumn('app_state', 'user_id'))) {
      await recreate(db, dbAll, dbRun, 'app_state',
        `CREATE TABLE app_state (
            user_id INTEGER NOT NULL,
            key TEXT NOT NULL,
            value TEXT,
            PRIMARY KEY (user_id, key),
            FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
        )`,
        `INSERT INTO app_state (user_id, key, value) SELECT ?, key, value FROM app_state_old`,
        [adminId]
      );
    } else {
      await dbRun('UPDATE app_state SET user_id = ? WHERE user_id IS NULL', [adminId]);
    }
  }

  // prompts: PK(key) -> PK(user_id, key)
  if (await tableExists('prompts') && !(await hasColumn('prompts', 'user_id'))) {
    await recreate(db, dbAll, dbRun, 'prompts',
      `CREATE TABLE prompts (
          user_id INTEGER NOT NULL,
          key TEXT NOT NULL,
          inhalt TEXT NOT NULL,
          updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          PRIMARY KEY (user_id, key),
          FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      )`,
      `INSERT INTO prompts (user_id, key, inhalt, updated_at) SELECT ?, key, inhalt, updated_at FROM prompts_old`,
      [adminId]
    );
  } else if (await tableExists('prompts')) {
    await dbRun('UPDATE prompts SET user_id = ? WHERE user_id IS NULL', [adminId]);
  }

  // design: PK(key) -> PK(user_id, key)
  if (await tableExists('design') && !(await hasColumn('design', 'user_id'))) {
    await recreate(db, dbAll, dbRun, 'design',
      `CREATE TABLE design (
          user_id INTEGER NOT NULL,
          key TEXT NOT NULL,
          value TEXT NOT NULL,
          updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          PRIMARY KEY (user_id, key),
          FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      )`,
      `INSERT INTO design (user_id, key, value, updated_at) SELECT ?, key, value, updated_at FROM design_old`,
      [adminId]
    );
  } else if (await tableExists('design')) {
    await dbRun('UPDATE design SET user_id = ? WHERE user_id IS NULL', [adminId]);
  }

  // settings: single row CHECK(id=1) -> per-user row (user_id UNIQUE)
  if (await tableExists('settings') && !(await hasColumn('settings', 'user_id'))) {
    await recreate(db, dbAll, dbRun, 'settings',
      `CREATE TABLE settings (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id INTEGER NOT NULL UNIQUE,
          name TEXT,
          adresse TEXT,
          kundennummer TEXT,
          ort TEXT,
          webseite TEXT,
          email TEXT,
          telefon TEXT,
          geburtsdatum TEXT,
          FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      )`,
      `INSERT INTO settings (user_id, name, adresse, kundennummer, ort, webseite, email, telefon, geburtsdatum)
       SELECT ?, name, adresse, kundennummer, ort, webseite, email, telefon, geburtsdatum FROM settings_old`,
      [adminId]
    );
  } else if (await tableExists('settings')) {
    await dbRun('UPDATE settings SET user_id = ? WHERE user_id IS NULL', [adminId]);
  }

  // jobangebote: UNIQUE(quelle, external_id) -> UNIQUE(user_id, quelle, external_id)
  if (await tableExists('jobangebote') && !(await hasColumn('jobangebote', 'user_id'))) {
    // user_id was added in step 3 already; this branch only recreates for the
    // per-user UNIQUE constraint. Rebuild preserving all columns.
    const cols = (await columnsOf('jobangebote')).filter((c) => c !== 'user_id');
    const colList = cols.join(', ');
    await recreate(db, dbAll, dbRun, 'jobangebote',
      `CREATE TABLE jobangebote (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id INTEGER NOT NULL,
          external_id TEXT,
          quelle TEXT NOT NULL DEFAULT 'drittanbieter',
          firma TEXT NOT NULL,
          stelle TEXT NOT NULL,
          ort TEXT,
          adresse TEXT,
          ansprechpartner TEXT,
          gehalt TEXT,
          beschreibung TEXT,
          quelle_url TEXT,
          art TEXT,
          anzeige_datum DATE,
          kontakt_email TEXT,
          status TEXT NOT NULL DEFAULT 'offen',
          verknuepfte_bewerbung_id INTEGER,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          url_norm TEXT,
          firma_slug TEXT,
          labels TEXT,
          UNIQUE (user_id, quelle, external_id),
          FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
          FOREIGN KEY (verknuepfte_bewerbung_id) REFERENCES bewerbungen(id) ON DELETE SET NULL
      )`,
      `INSERT INTO jobangebote (user_id, ${colList}) SELECT ?, ${colList} FROM jobangebote_old`,
      [adminId]
    );
    await exec('CREATE INDEX IF NOT EXISTS idx_jobangebote_url_norm ON jobangebote(url_norm)');
    await exec('CREATE INDEX IF NOT EXISTS idx_jobangebote_firma_slug ON jobangebote(firma_slug)');
  }

  // Ensure a settings row exists for the admin (idempotent). On a fresh install
  // the settings table does not exist yet at this point (it is created by the
  // server's CREATE TABLE IF NOT EXISTS right after the migration), so skip —
  // the row is upserted on the admin's first /vorlagen save anyway.
  if (await tableExists('settings')) {
    const srow = await dbGet('SELECT id FROM settings WHERE user_id = ?', [adminId]);
    if (!srow) {
      await dbRun(
        `INSERT INTO settings (user_id, name, adresse, kundennummer, ort, webseite, email, telefon, geburtsdatum)
         VALUES (?, 'Max Mustermann', 'Musterstraße 1, 12345 Musterstadt', '', '', '', '', '', '')`,
        [adminId]
      );
    }
  }

  // 5. Move on-disk files into a per-user subdirectory for the admin -----
  moveFilesIntoUserSubdir(adminId);

  // 6. One-time env -> admin cfg import ----------------------------------
  // On an upgraded install app_state already exists here, so this fills the
  // admin's rows right away. On a *fresh* install the table is only created
  // after this migration returns, so this is a no-op — server.js therefore
  // calls importEnvIntoAdmin() again once the schema is complete. Idempotent
  // either way (it only fills keys the admin has not stored).
  await migrateEnvForAdmin(dbAll, adminId);

  return { adminId };
}

// Import any still-present env config into the admin's rows. Called at boot from
// server.js, after initializeDatabase() has created app_state — this is what
// makes a deployment that passes its config as container env vars (as ours does)
// end up with those values owned by the admin, instead of leaking to every user
// through a read-time process.env fallback (which config.get() no longer has).
async function importEnvIntoAdmin({ dbAll, dbGet }) {
  const admin = await dbGet('SELECT id FROM users WHERE username = ?', [ADMIN_USERNAME]);
  if (!admin) return;
  await migrateEnvForAdmin(dbAll, admin.id);
}

// Rename `table` to `table_old`, create the new table from `newSchemaSql`,
// copy rows via `copySql` (with `copyParams`), then drop `table_old`.
async function recreate(db, dbAll, dbRun, table, newSchemaSql, copySql, copyParams) {
  const exec = (sql) => new Promise((resolve, reject) => db.run(sql, (err) => err ? reject(err) : resolve()));
  await exec(`ALTER TABLE ${table} RENAME TO ${table}_old`);
  await exec(newSchemaSql);
  await dbRun(copySql, copyParams || []);
  await exec(`DROP TABLE ${table}_old`);
}

// Move every file in each per-user storage directory into a `<userId>/` subdir.
// Idempotent: if the subdir already contains files (already migrated), leave the
// top-level files alone (they would be a re-run left-overs) — but on first run the
// top level holds the legacy flat files, which we move in.
const STORAGE_DIRS = ['anhaenge', 'basis_anhaenge', 'interne_anhaenge', 'email_anhaenge', 'signatur', 'bewerberfoto'];
function moveFilesIntoUserSubdir(userId) {
  const dataDir = path.join(__dirname, '..', 'data');
  for (const dir of STORAGE_DIRS) {
    const base = path.join(dataDir, dir);
    if (!fs.existsSync(base)) continue;
    const userDir = path.join(base, String(userId));
    if (!fs.existsSync(userDir)) fs.mkdirSync(userDir, { recursive: true });
    for (const name of fs.readdirSync(base)) {
      if (name === String(userId)) continue;
      const src = path.join(base, name);
      if (!fs.statSync(src).isFile()) continue;   // skip subdirectories
      const dst = path.join(userDir, name);
      if (!fs.existsSync(dst)) fs.renameSync(src, dst);
    }
  }
}

// Move any still-present .env value for a DEFAULT key that the admin hasn't got
// stored yet into the admin's cfg rows. Idempotent: only fills keys that are not
// yet present for the admin.
async function migrateEnvForAdmin(dbAll, adminId) {
  // On a fresh install app_state does not exist yet at this point (created by
  // the server right after the migration), so there is nothing to read from or
  // write to — skip. The .env values, if any, then fall back via config.get().
  try {
    await dbAll('SELECT 1 FROM app_state LIMIT 1');
  } catch (e) {
    return;
  }
  const storedRows = await dbAll('SELECT key FROM app_state WHERE user_id = ?', [adminId]);
  const stored = new Set(storedRows.map((r) => r.key.slice(config.PREFIX.length)));
  const migrated = [];
  for (const key of Object.keys(config.DEFAULTS)) {
    if (stored.has(key)) continue;             // already in the DB, never overwrite from .env
    const envVal = process.env[key];
    if (envVal && envVal.length) {
      await config.setForUser(adminId, key, envVal);
      migrated.push(key);
    }
  }
  if (migrated.length) {
    console.log(`Multi-User-Migration: ${migrated.length} Konfigurationswerte aus .env in den Admin-Benutzer migriert — .env wird nicht mehr benötigt.`);
  }
}

module.exports = { runMigration, importEnvIntoAdmin, moveFilesIntoUserSubdir, STORAGE_DIRS, ADMIN_USERNAME, ADMIN_DEFAULT_PASSWORD };