/**
 * InstanceSpawner.js
 * Multi-School Instance Orchestrator for SuperAdmin.
 * Spawns, provisions, monitors, and stops isolated server & client instances
 * for each subscribing school.
 */

const { spawn, execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const Database = require('better-sqlite3');
const { v4: uuidv4 } = require('uuid');
const LicenseTokenGenerator = require('./LicenseTokenGenerator');

const BASE_SERVER_DIR = path.join(__dirname, '../../../../server');
const BASE_DATA_DIR = path.join(BASE_SERVER_DIR, 'data', 'schools');
const REGISTRY_DB_PATH = path.join(__dirname, '../../data/spawner-registry.db');
const BASE_CLIENT_DIR = path.join(__dirname, '../../../../client');

class InstanceSpawner {
  constructor() {
    this.instances = new Map();

    if (!fs.existsSync(BASE_DATA_DIR)) {
      fs.mkdirSync(BASE_DATA_DIR, { recursive: true });
    }

    const registryDir = path.dirname(REGISTRY_DB_PATH);
    if (!fs.existsSync(registryDir)) {
      fs.mkdirSync(registryDir, { recursive: true });
    }

    this.db = new Database(REGISTRY_DB_PATH);
    this.db.pragma('journal_mode = WAL');
    this.db.pragma('foreign_keys = ON');
    this.initializeRegistry();

    const orphanedRows = this.db.prepare("SELECT * FROM spawned_instances WHERE status = 'RUNNING'").all();
    const markOrphaned = this.db.transaction((rows) => {
      const statement = this.db.prepare(
        "UPDATE spawned_instances SET status = 'STOPPED', pid = NULL, client_pid = NULL WHERE school_id = ?"
      );
      for (const row of rows) {
        statement.run(row.school_id);
      }
    });
    markOrphaned(orphanedRows);

    for (const row of orphanedRows) {
      this.instances.set(row.school_id, this.registryRowToMeta(row, 'STOPPED'));
    }

    const ports = this.db.prepare(`
      SELECT MAX(server_port) AS max_server_port, MAX(client_port) AS max_client_port
      FROM spawned_instances
    `).get();
    const maxServerPort = Number(ports.max_server_port);
    const maxClientPort = Number(ports.max_client_port);
    this.nextServerPort = Math.max(3010, Number.isFinite(maxServerPort) ? maxServerPort + 1 : 3010);
    this.nextClientPort = Math.max(5180, Number.isFinite(maxClientPort) ? maxClientPort + 1 : 5180);
  }

  initializeRegistry() {
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS spawned_instances (
        school_id TEXT PRIMARY KEY,
        name TEXT,
        code TEXT,
        contact_email TEXT,
        server_port INTEGER UNIQUE,
        client_port INTEGER UNIQUE,
        db_path TEXT,
        license_key TEXT,
        license_token TEXT,
        spawned_at TEXT,
        status TEXT,
        pid INTEGER
      );

      CREATE TABLE IF NOT EXISTS instance_logs (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        school_id TEXT,
        stream TEXT,
        line TEXT,
        ts TEXT,
        FOREIGN KEY (school_id) REFERENCES spawned_instances(school_id) ON DELETE CASCADE
      );

      CREATE INDEX IF NOT EXISTS idx_instance_logs_school ON instance_logs(school_id, id DESC);
    `);

    const columns = this.db.pragma('table_info(spawned_instances)');
    if (!columns.some(column => column.name === 'pid')) {
      this.db.prepare('ALTER TABLE spawned_instances ADD COLUMN pid INTEGER').run();
    }
    if (!columns.some(column => column.name === 'client_pid')) {
      this.db.prepare('ALTER TABLE spawned_instances ADD COLUMN client_pid INTEGER').run();
    }
  }

  registryRowToMeta(row, status = row.status) {
    const clientStatus = row.status === 'RUNNING' && row.client_pid != null
      ? 'RUNNING'
      : 'STOPPED';
    return {
      id: row.school_id,
      name: row.name,
      code: row.code,
      contact_email: row.contact_email,
      server_port: row.server_port,
      client_port: row.client_port,
      db_path: row.db_path,
      license_key: row.license_key,
      license_token: row.license_token,
      spawned_at: row.spawned_at,
      pid: status === 'RUNNING' ? row.pid : null,
      status,
      process: null,
      client_pid: clientStatus === 'RUNNING' ? row.client_pid : null,
      client_status: clientStatus,
      client_process: null,
      logs: []
    };
  }

  launchSchoolProcess(instanceData) {
    const env = {
      ...process.env,
      PORT: `${instanceData.server_port}`,
      DB_PATH: instanceData.db_path,
      TENANT_ID: instanceData.id,
      TENANT_CODE: instanceData.code,
      LICENSE_KEY: instanceData.license_token
    };

    const serverProcess = spawn('node', ['src/index.js'], {
      cwd: BASE_SERVER_DIR,
      env,
      stdio: ['ignore', 'pipe', 'pipe']
    });

    const clientProcess = spawn(
      /^win/.test(process.platform) ? 'npx.cmd' : 'npx',
      ['vite'],
      {
        cwd: BASE_CLIENT_DIR,
        env: {
          ...process.env,
          VITE_PORT: `${instanceData.client_port}`,
          VITE_API_TARGET: `http://127.0.0.1:${instanceData.server_port}`,
          VITE_WS_TARGET: `ws://127.0.0.1:${instanceData.server_port}`,
          TENANT_ID: instanceData.id,
          TENANT_CODE: instanceData.code
        },
        stdio: ['ignore', 'pipe', 'pipe']
      }
    );

    const instanceMeta = {
      ...instanceData,
      pid: serverProcess.pid || null,
      status: 'RUNNING',
      process: serverProcess,
      client_pid: clientProcess.pid || null,
      client_status: 'RUNNING',
      client_process: clientProcess,
      logs: []
    };

    this.instances.set(instanceMeta.id, instanceMeta);
    this.attachProcessListeners(instanceMeta, serverProcess, 'STDOUT', 'STDERR', 'server');
    this.attachProcessListeners(instanceMeta, clientProcess, 'CLIENT_STDOUT', 'CLIENT_STDERR', 'client');
    return instanceMeta;
  }

  attachProcessListeners(instanceMeta, childProcess, stdoutLabel, stderrLabel, kind) {
    childProcess.stdout.on('data', (data) => {
      this.captureLogs(instanceMeta.id, instanceMeta.logs, stdoutLabel, data);
    });

    childProcess.stderr.on('data', (data) => {
      this.captureLogs(instanceMeta.id, instanceMeta.logs, stderrLabel, data);
    });

    childProcess.on('error', (err) => {
      this.captureLogs(instanceMeta.id, instanceMeta.logs, stderrLabel, `Process error: ${err.message}`);
      this.markProcessStopped(instanceMeta.id, childProcess, kind);
    });

    childProcess.on('exit', (code) => {
      console.log(`[Spawner] School instance ${instanceMeta.id} (${kind}) exited with code ${code}`);
      this.markProcessStopped(instanceMeta.id, childProcess, kind);
    });
  }

  captureLogs(schoolId, logs, stream, data) {
    const lines = data.toString().split(/\r?\n/);
    if (lines[lines.length - 1] === '') lines.pop();
    if (lines.length === 0) return;

    const entries = lines.map(line => ({ line, ts: new Date().toISOString() }));
    for (const entry of entries) {
      logs.push(`[${stream}] ${entry.line}`);
      if (logs.length > 200) logs.shift();
    }

    try {
      const persistLogs = this.db.transaction((logEntries) => {
        const insert = this.db.prepare(`
          INSERT INTO instance_logs (school_id, stream, line, ts)
          VALUES (?, ?, ?, ?)
        `);
        for (const entry of logEntries) {
          insert.run(schoolId, stream, entry.line, entry.ts);
        }
        this.db.prepare(`
          DELETE FROM instance_logs
          WHERE school_id = ?
            AND id NOT IN (
              SELECT id FROM instance_logs
              WHERE school_id = ?
              ORDER BY id DESC
              LIMIT 500
            )
        `).run(schoolId, schoolId);
      });
      persistLogs(entries);
    } catch (err) {
      console.error(`[Spawner] Failed to persist logs for ${schoolId}: ${err.message}`);
    }
  }

  killIfAlive(child) {
    if (child && child.pid && child.exitCode === null && !child.killed) {
      try {
        child.kill('SIGTERM');
      } catch (err) {
        console.error(`[Spawner] Failed to kill child (pid ${child.pid}): ${err.message}`);
      }
    }
  }

  markProcessStopped(schoolId, childProcess, kind) {
    const meta = this.instances.get(schoolId);
    if (!meta) return;
    if (kind === 'server' && meta.process !== childProcess) return;
    if (kind === 'client' && meta.client_process !== childProcess) return;

    if (kind === 'server') {
      meta.status = 'STOPPED';
      meta.pid = null;
      meta.process = null;
    } else if (kind === 'client') {
      meta.client_status = 'STOPPED';
      meta.client_pid = null;
      meta.client_process = null;
    }

    this.killIfAlive(meta.process);
    this.killIfAlive(meta.client_process);

    try {
      if (kind === 'server') {
        this.db.prepare(`
          UPDATE spawned_instances
          SET status = 'STOPPED', pid = NULL
          WHERE school_id = ?
        `).run(schoolId);
      } else if (kind === 'client') {
        this.db.prepare(`
          UPDATE spawned_instances
          SET client_pid = NULL
          WHERE school_id = ?
        `).run(schoolId);
      }
    } catch (err) {
      console.error(`[Spawner] Failed to persist stopped state for ${schoolId}: ${err.message}`);
    }
  }

  toPublicInstance(meta) {
    return {
      id: meta.id,
      name: meta.name,
      code: meta.code,
      server_port: meta.server_port,
      client_port: meta.client_port,
      db_path: meta.db_path,
      license_key: meta.license_key,
      pid: meta.pid,
      status: meta.status,
      client_pid: meta.client_pid,
      client_status: meta.client_status,
      spawned_at: meta.spawned_at
    };
  }

  /**
   * Spawn and provision a new school instance
   */
  async spawnSchoolInstance(schoolData) {
    const schoolId = schoolData.id || uuidv4();
    const schoolCode = schoolData.code || `SCH-${Math.floor(1000 + Math.random() * 9000)}`;
    const serverPort = schoolData.port_server || this.nextServerPort++;
    const clientPort = schoolData.port_client || this.nextClientPort++;
    const numericServerPort = Number(serverPort);
    const numericClientPort = Number(clientPort);
    if (Number.isFinite(numericServerPort)) {
      this.nextServerPort = Math.max(this.nextServerPort, numericServerPort + 1);
    }
    if (Number.isFinite(numericClientPort)) {
      this.nextClientPort = Math.max(this.nextClientPort, numericClientPort + 1);
    }

    const schoolDir = path.join(BASE_DATA_DIR, schoolId);
    if (!fs.existsSync(schoolDir)) {
      fs.mkdirSync(schoolDir, { recursive: true });
    }

    const schoolDbPath = path.join(schoolDir, 'school.db');

    const now = new Date();
    const termEnd = new Date(now.getFullYear(), now.getMonth() + 4, now.getDate());
    const licenseSigned = LicenseTokenGenerator.generateToken({
      tenant_id: schoolId,
      tenant_code: schoolCode,
      term_name: schoolData.term_name || 'Term 1',
      academic_year: `${now.getFullYear()}`,
      start_date: now.toISOString().slice(0, 10),
      end_date: termEnd.toISOString().slice(0, 10),
      max_students: schoolData.max_students || 1000,
      max_staff: schoolData.max_staff || 200
    });

    this.initializeSchoolDatabase(schoolDbPath, schoolId, schoolData.name, schoolCode, licenseSigned.token);

    const instanceMeta = this.launchSchoolProcess({
      id: schoolId,
      name: schoolData.name,
      code: schoolCode,
      contact_email: schoolData.contact_email || 'admin@school.ac.zw',
      server_port: serverPort,
      client_port: clientPort,
      db_path: schoolDbPath,
      license_key: licenseSigned.license_key,
      license_token: licenseSigned.token,
      spawned_at: new Date().toISOString()
    });

    try {
      this.db.prepare(`
        INSERT INTO spawned_instances (
          school_id, name, code, contact_email, server_port, client_port,
          db_path, license_key, license_token, spawned_at, status, pid, client_pid
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'RUNNING', ?, ?)
      `).run(
        instanceMeta.id,
        instanceMeta.name,
        instanceMeta.code,
        instanceMeta.contact_email,
        instanceMeta.server_port,
        instanceMeta.client_port,
        instanceMeta.db_path,
        instanceMeta.license_key,
        instanceMeta.license_token,
        instanceMeta.spawned_at,
        instanceMeta.pid,
        instanceMeta.client_pid
      );
    } catch (err) {
      console.error(`[Spawner] Failed to persist instance ${schoolId}: ${err.message}`);
    }

    return {
      id: instanceMeta.id,
      name: instanceMeta.name,
      code: instanceMeta.code,
      server_port: instanceMeta.server_port,
      client_port: instanceMeta.client_port,
      db_path: instanceMeta.db_path,
      pid: instanceMeta.pid,
      status: instanceMeta.status,
      client_pid: instanceMeta.client_pid,
      client_status: instanceMeta.client_status,
      license_key: instanceMeta.license_key,
      license_token: instanceMeta.license_token
    };
  }

  /**
   * Initialize a fresh school database instance with schema
   */
  initializeSchoolDatabase(dbPath, tenantId, schoolName, schoolCode, licenseToken) {
    const db = new Database(dbPath);
    db.pragma('foreign_keys = ON');

    try {
      db.prepare(`
        CREATE TABLE IF NOT EXISTS tenants (
          id TEXT PRIMARY KEY,
          name TEXT NOT NULL,
          code TEXT UNIQUE NOT NULL,
          contact_email TEXT,
          status TEXT DEFAULT 'active',
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        );
      `).run();

      db.prepare(`
        CREATE TABLE IF NOT EXISTS tenant_licenses (
          id TEXT PRIMARY KEY,
          tenant_id TEXT NOT NULL,
          license_key TEXT UNIQUE NOT NULL,
          term_name TEXT NOT NULL,
          academic_year TEXT NOT NULL,
          start_date DATE NOT NULL,
          end_date DATE NOT NULL,
          grace_period_days INTEGER DEFAULT 7,
          status TEXT DEFAULT 'active',
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        );
      `).run();

      db.prepare(`
        INSERT OR REPLACE INTO tenants (id, name, code, status)
        VALUES (?, ?, ?, 'active')
      `).run(tenantId, schoolName, schoolCode);

      const now = new Date();
      const end = new Date(now.getFullYear(), now.getMonth() + 4, now.getDate());

      db.prepare(`
        INSERT OR REPLACE INTO tenant_licenses (id, tenant_id, license_key, term_name, academic_year, start_date, end_date)
        VALUES (?, ?, ?, 'Term 1', '2026', ?, ?)
      `).run(`lic-${tenantId}`, tenantId, licenseToken, now.toISOString().slice(0, 10), end.toISOString().slice(0, 10));

    } finally {
      db.close();
    }
  }

  /**
   * Stop a running school instance
   */
  stopSchoolInstance(schoolId) {
    const meta = this.instances.get(schoolId);
    const persisted = this.db.prepare('SELECT school_id FROM spawned_instances WHERE school_id = ?').get(schoolId);
    if (!meta && !persisted) throw new Error('Instance not found');

    if (meta) {
      if (meta.process && meta.status === 'RUNNING') {
        this.killIfAlive(meta.process);
      }
      if (meta.client_process && meta.client_status === 'RUNNING') {
        this.killIfAlive(meta.client_process);
      }
      meta.status = 'STOPPED';
      meta.pid = null;
      meta.process = null;
      meta.client_status = 'STOPPED';
      meta.client_pid = null;
      meta.client_process = null;
    }

    this.db.prepare(`
      UPDATE spawned_instances
      SET status = 'STOPPED', pid = NULL, client_pid = NULL
      WHERE school_id = ?
    `).run(schoolId);

    return { id: schoolId, status: 'STOPPED' };
  }

  startSchoolInstance(schoolId) {
    const row = this.db.prepare('SELECT * FROM spawned_instances WHERE school_id = ?').get(schoolId);
    if (!row) throw new Error('Instance not found');

    const current = this.instances.get(schoolId);
    if (
      current &&
      current.status === 'RUNNING' &&
      current.process &&
      !current.process.killed &&
      current.process.exitCode === null
    ) {
      return this.toPublicInstance(current);
    }

    this.db.prepare(`
      UPDATE spawned_instances
      SET status = 'STARTING', pid = NULL, client_pid = NULL
      WHERE school_id = ?
    `).run(schoolId);

    let instanceMeta;
    try {
      instanceMeta = this.launchSchoolProcess({
        id: row.school_id,
        name: row.name,
        code: row.code,
        contact_email: row.contact_email,
        server_port: row.server_port,
        client_port: row.client_port,
        db_path: row.db_path,
        license_key: row.license_key,
        license_token: row.license_token,
        spawned_at: row.spawned_at
      });

      this.db.prepare(`
        UPDATE spawned_instances
        SET status = 'RUNNING', pid = ?, client_pid = ?
        WHERE school_id = ?
      `).run(instanceMeta.pid, instanceMeta.client_pid, schoolId);
    } catch (err) {
      if (instanceMeta) {
        if (instanceMeta.process) this.killIfAlive(instanceMeta.process);
        if (instanceMeta.client_process) this.killIfAlive(instanceMeta.client_process);
        instanceMeta.status = 'STOPPED';
        instanceMeta.pid = null;
        instanceMeta.process = null;
        instanceMeta.client_status = 'STOPPED';
        instanceMeta.client_pid = null;
        instanceMeta.client_process = null;
      }
      this.db.prepare(`
        UPDATE spawned_instances
        SET status = 'STOPPED', pid = NULL, client_pid = NULL
        WHERE school_id = ?
      `).run(schoolId);
      throw err;
    }

    return this.toPublicInstance(instanceMeta);
  }

  restartSchoolInstance(schoolId) {
    this.stopSchoolInstance(schoolId);
    return this.startSchoolInstance(schoolId);
  }

  /**
   * List all spawned instances
   */
  listInstances() {
    const persistedRows = this.db.prepare('SELECT * FROM spawned_instances ORDER BY spawned_at DESC').all();
    const instances = new Map();

    for (const row of persistedRows) {
      instances.set(row.school_id, this.registryRowToMeta(row));
    }
    for (const meta of this.instances.values()) {
      instances.set(meta.id, meta);
    }

    return Array.from(instances.values()).map(meta => this.toPublicInstance(meta));
  }

  /**
   * Get logs for a specific instance
   */
  getInstanceLogs(schoolId) {
    const rows = this.db.prepare(`
      SELECT stream, line
      FROM instance_logs
      WHERE school_id = ?
      ORDER BY id DESC
      LIMIT 200
    `).all(schoolId);

    return rows.reverse().map(row => `[${row.stream}] ${row.line}`);
  }
}

module.exports = new InstanceSpawner();
