'use strict';

const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');

const PROJECT_ROOT = path.resolve(__dirname, '..');
const DEFAULT_REPAIR_TIMEOUT_MS = 120000;

let cachedDatabase = null;
let lastStatus = {
  ok: false,
  checked: false,
  repaired: false,
  classification: '',
  error: '',
  attempts: [],
};

function classifySqliteError(error) {
  const code = String(error?.code || '');
  const message = String(error?.message || error || '');
  const stack = String(error?.stack || '');
  const text = `${code}\n${message}\n${stack}`.toLowerCase();

  if (
    /\bnode_module_version\b/.test(text) ||
    /module version mismatch/.test(text) ||
    /compiled against a different node\.js version/.test(text) ||
    /\berr_dlopen_failed\b/.test(text) ||
    /\bdlopen\b/.test(text) ||
    /better-sqlite3.*\.node/.test(text) ||
    /\.node/.test(text) && /(wrong architecture|mach-o|elf|invalid|symbol not found|image not found|dynamic link)/.test(text) ||
    /not a valid win32 application/.test(text)
  ) {
    return 'driver_native_load_error';
  }

  if (
    /cannot find module ['"]?better-sqlite3/.test(text) ||
    /module not found/.test(text) && /better-sqlite3/.test(text)
  ) {
    return 'driver_missing';
  }

  if (
    code === 'SQLITE_CORRUPT' ||
    code === 'SQLITE_NOTADB' ||
    /database disk image is malformed/.test(text) ||
    /file is not a database/.test(text) ||
    /\bnot a database\b/.test(text) ||
    /\bmalformed\b/.test(text)
  ) {
    return 'sqlite_structural_corruption';
  }

  if (code.startsWith('SQLITE_IOERR') || /\bsqlite_ioerr\b/.test(text)) {
    return 'sqlite_io_transient';
  }

  if (
    code === 'SQLITE_CANTOPEN' ||
    /\bsqlite_cantopen\b/.test(text) ||
    /unable to open database file/.test(text) ||
    /\b(eacces|eperm|permission denied|readonly|read-only)\b/.test(text)
  ) {
    return 'permission_or_path_error';
  }

  return 'unknown';
}

function packageSpec() {
  try {
    const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
    return pkg.dependencies?.['better-sqlite3'] || pkg.devDependencies?.['better-sqlite3'] || 'latest';
  } catch {
    return 'latest';
  }
}

function repairLogPath() {
  const dataDir = process.env.CTM_DATA_DIR || path.join(os.homedir(), '.walle', 'data');
  return path.join(dataDir, 'sqlite-driver-repair.jsonl');
}

function recordDriverEvent(event, details = {}) {
  const payload = {
    ts: new Date().toISOString(),
    event,
    node: process.version,
    node_modules_abi: process.versions.modules,
    exec_path: process.execPath,
    ...details,
  };
  try {
    const logPath = repairLogPath();
    fs.mkdirSync(path.dirname(logPath), { recursive: true });
    fs.appendFileSync(logPath, `${JSON.stringify(payload)}\n`);
  } catch {}
  return payload;
}

function clearBetterSqliteRequireCache() {
  for (const key of Object.keys(require.cache)) {
    if (key.includes(`${path.sep}better-sqlite3${path.sep}`)) delete require.cache[key];
  }
  cachedDatabase = null;
}

function smokeTestDatabase(Database) {
  let d = null;
  try {
    d = new Database(':memory:');
    const rows = d.pragma('quick_check');
    const messages = (Array.isArray(rows) ? rows : [rows])
      .map(row => String(Object.values(row || {})[0] || '').trim())
      .filter(Boolean);
    if (messages.length === 1 && messages[0].toLowerCase() === 'ok') return;
    throw new Error(`better-sqlite3 smoke quick_check failed: ${messages.join('\n') || 'empty result'}`);
  } finally {
    try { if (d) d.close(); } catch {}
  }
}

function runNpm(args, label, extraEnv = {}) {
  const timeout = Number(process.env.CTM_SQLITE_REPAIR_TIMEOUT_MS || DEFAULT_REPAIR_TIMEOUT_MS);
  const started = Date.now();
  try {
    const output = execFileSync('npm', args, {
      cwd: PROJECT_ROOT,
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'pipe'],
      timeout: Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REPAIR_TIMEOUT_MS,
      env: {
        ...process.env,
        // Compile/resolve under the SAME Node that's loading the driver. Without this,
        // execFileSync('npm') resolves `npm`/`node-gyp`/`node` from the ambient PATH (often
        // a different Node major), so even a from-source rebuild targets the WRONG ABI and
        // the brick persists. Prepending process.execPath's dir pins the toolchain to the
        // running Node, so the rebuilt binary matches this process. This is the ABI-ping-pong fix.
        PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH || ''}`,
        npm_config_audit: 'false',
        npm_config_fund: 'false',
        npm_config_update_notifier: 'false',
        ...extraEnv,
      },
    });
    return { label, ok: true, ms: Date.now() - started, output: String(output || '').slice(-4000) };
  } catch (error) {
    return {
      label,
      ok: false,
      ms: Date.now() - started,
      error: String(error?.message || error),
      output: String(error?.stdout || error?.stderr || '').slice(-4000),
    };
  }
}

function defaultRepairRunner(cause) {
  const attempts = [];
  const spec = packageSpec();
  // FROM-SOURCE FIRST. The node-ABI ping-pong: when several processes on different Node
  // majors (e.g. the pinned 25.x primary + a system-26 dev/agent) share one node_modules,
  // each one's load fails the ABI smoke test and triggers repair. Plain `npm rebuild` /
  // `npm install` resolve a PREBUILT binary (or no-op "up to date") that is compiled for
  // SOME other Node ABI — so the smoke test still fails and the primary bricks on restart.
  // Forcing a source compile (`npm_config_build_from_source`) produces a binary matching
  // THIS process's Node, so every process reliably self-heals instead of bricking.
  const fromSource = { npm_config_build_from_source: 'true' };
  const commands = [
    { label: 'npm rebuild better-sqlite3 (from source)', args: ['rebuild', 'better-sqlite3', '--foreground-scripts'], env: fromSource },
    { label: 'npm rebuild better-sqlite3', args: ['rebuild', 'better-sqlite3'] },
    { label: `npm install better-sqlite3@${spec} (from source)`, args: ['install', `better-sqlite3@${spec}`, '--no-audit', '--no-fund'], env: fromSource },
  ];
  if (process.env.CTM_SQLITE_ALLOW_MAJOR_UPGRADE !== '0' && spec !== 'latest') {
    commands.push({ label: 'npm install better-sqlite3@latest', args: ['install', 'better-sqlite3@latest', '--no-audit', '--no-fund'] });
  }

  for (const command of commands) {
    recordDriverEvent('sqlite_driver_repair_attempt', {
      action: command.label,
      cause: String(cause?.message || cause || '').slice(0, 1000),
    });
    const result = runNpm(command.args, command.label, command.env || {});
    attempts.push(result);
    recordDriverEvent(result.ok ? 'sqlite_driver_repair_command_ok' : 'sqlite_driver_repair_command_failed', result);
    if (!result.ok) continue;
    clearBetterSqliteRequireCache();
    try {
      const Database = require('better-sqlite3');
      smokeTestDatabase(Database);
      cachedDatabase = Database;
      return { ok: true, action: command.label, attempts };
    } catch (retryError) {
      attempts.push({
        label: `${command.label} smoke test`,
        ok: false,
        error: String(retryError?.message || retryError),
        classification: classifySqliteError(retryError),
      });
    }
  }

  return { ok: false, attempts };
}

function shouldAttemptRepair(classification, options = {}) {
  if (options.autoRepair === false) return false;
  if (process.env.CTM_SQLITE_AUTO_REPAIR === '0') return false;
  if (process.env.CTM_SQLITE_DRIVER_REPAIR_IN_PROGRESS === '1') return false;
  return classification === 'driver_native_load_error' || classification === 'driver_missing';
}

function loadSqliteDriver(options = {}) {
  if (cachedDatabase && !options.forceReload) return cachedDatabase;
  const requireFn = options.requireFn || require;
  const repairRunner = options.repairRunner || defaultRepairRunner;
  const source = options.source || 'ctm';

  let firstError = null;
  try {
    const Database = requireFn('better-sqlite3');
    smokeTestDatabase(Database);
    cachedDatabase = Database;
    lastStatus = { ok: true, checked: true, repaired: false, classification: 'ok', error: '', attempts: [] };
    return cachedDatabase;
  } catch (error) {
    firstError = error;
  }

  const classification = classifySqliteError(firstError);
  lastStatus = {
    ok: false,
    checked: true,
    repaired: false,
    classification,
    error: String(firstError?.message || firstError),
    attempts: [],
  };
  recordDriverEvent('sqlite_driver_smoke_failed', { source, classification, error: lastStatus.error.slice(0, 1000) });

  if (!shouldAttemptRepair(classification, options)) {
    throw createDriverUnavailableError(firstError, classification, lastStatus);
  }

  console.error(`[sqlite-driver] better-sqlite3 ${classification}; attempting automatic repair...`);
  process.env.CTM_SQLITE_DRIVER_REPAIR_IN_PROGRESS = '1';
  let repair = null;
  try {
    repair = repairRunner(firstError, { classification, source });
  } finally {
    delete process.env.CTM_SQLITE_DRIVER_REPAIR_IN_PROGRESS;
  }

  lastStatus = {
    ok: !!repair?.ok,
    checked: true,
    repaired: !!repair?.ok,
    classification: repair?.ok ? 'ok' : classification,
    error: repair?.ok ? '' : String(firstError?.message || firstError),
    attempts: repair?.attempts || [],
  };

  if (repair?.ok) {
    try {
      const Database = cachedDatabase || requireFn('better-sqlite3');
      smokeTestDatabase(Database);
      cachedDatabase = Database;
      lastStatus = {
        ok: true,
        checked: true,
        repaired: true,
        classification: 'ok',
        error: '',
        attempts: repair.attempts || [],
      };
      recordDriverEvent('sqlite_driver_repair_ok', { source, action: repair.action });
      console.error(`[sqlite-driver] better-sqlite3 repaired with ${repair.action}`);
      return cachedDatabase;
    } catch (retryError) {
      lastStatus.attempts.push({
        label: 'post-repair smoke test',
        ok: false,
        error: String(retryError?.message || retryError),
        classification: classifySqliteError(retryError),
      });
    }
  }

  recordDriverEvent('sqlite_driver_repair_failed', {
    source,
    classification,
    attempts: lastStatus.attempts.map(a => ({ label: a.label, ok: a.ok, error: a.error || '' })),
  });
  throw createDriverUnavailableError(firstError, classification, lastStatus);
}

function createDriverUnavailableError(cause, classification, status = lastStatus) {
  const message =
    `SQLite driver unavailable (${classification}). ` +
    'CTM skipped database recovery so user data is not archived or replaced. ' +
    `Original error: ${String(cause?.message || cause)}`;
  const error = new Error(message);
  error.code = 'CTM_SQLITE_DRIVER_UNAVAILABLE';
  error.classification = classification;
  error.cause = cause;
  error.status = status;
  return error;
}

function ensureSqliteDriverReady(options = {}) {
  return loadSqliteDriver({ ...options, source: options.source || 'preflight' });
}

function getSqliteDriverStatus() {
  return {
    ...lastStatus,
    node: process.version,
    node_modules_abi: process.versions.modules,
    exec_path: process.execPath,
  };
}

function resetSqliteDriverForTests() {
  cachedDatabase = null;
  lastStatus = {
    ok: false,
    checked: false,
    repaired: false,
    classification: '',
    error: '',
    attempts: [],
  };
}

module.exports = {
  classifySqliteError,
  clearBetterSqliteRequireCache,
  ensureSqliteDriverReady,
  getSqliteDriverStatus,
  loadSqliteDriver,
  recordDriverEvent,
  resetSqliteDriverForTests,
  smokeTestDatabase,
};
