#!/usr/bin/env bun
/**
 * Deregister a chat room and purge all managed data, keeping ONLY the chat
 * channel record (chats) and its message history (messages).
 *
 * Usage:
 *   bun scripts/deregister-room.ts [--dry-run] [--force] [--no-backup] <channelId|jid> [more...]
 *
 * What it removes for each target chat:
 *   - room registration + role/skill overrides + channel owner lease
 *   - paired tasks/turns/attempts/outputs/reservations/leases/projects/handoffs
 *   - work items, scheduled tasks, task run logs
 *   - sessions (by group folder) + router cursor entry
 *   - on-disk group/workspace/session/ipc folders (git worktrees removed cleanly)
 *
 * What it KEEPS (never touched): the `chats` row and all `messages` rows.
 *
 * It does NOT restart the service. Restart ejclaw afterwards so the room is
 * dropped from the live in-memory bindings (the router loads bindings only at
 * startup), e.g. `systemctl --user restart ejclaw.service`.
 *
 * Safety:
 *   - refuses to deregister a main room unless --force
 *   - a group folder shared by another remaining room is NOT deleted on disk
 *     (only that chat's DB rows are removed)
 *   - --dry-run reports what would change without writing anything
 *   - the DB is auto-backed up to /home/claude/ejclaw-db-backup-<ts>.db before
 *     the purge unless --no-backup is passed
 */
import { execFileSync } from 'child_process';
import fs from 'fs';
import path from 'path';

import { Database } from 'bun:sqlite';

import { DATA_DIR, GROUPS_DIR, STORE_DIR } from '../src/config.js';

const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const force = args.includes('--force');
const noBackup = args.includes('--no-backup');
const ids = args.filter((a) => !a.startsWith('--'));

if (ids.length === 0) {
  console.error(
    'Usage: bun scripts/deregister-room.ts [--dry-run] [--force] [--no-backup] <channelId|jid> [more...]',
  );
  process.exit(2);
}

function normalizeJid(id: string): string {
  if (/^(dc|tg|wa):/.test(id)) return id;
  if (/^\d+$/.test(id)) return `dc:${id}`;
  return id;
}

const jids = [...new Set(ids.map(normalizeJid))];
const repoRoot = path.resolve(GROUPS_DIR, '..');
const db = new Database(path.join(STORE_DIR, 'messages.db'));

interface Target {
  jid: string;
  folders: string[];
  isMain: boolean;
  name: string | null;
  registered: boolean;
}

// When a room was already unregistered (room_settings gone) the group folder is
// no longer stored there, so recover it from the group_folder recorded on the
// chat's leftover managed rows — otherwise sessions/disk/worktrees for an
// already-unregistered room would be missed.
function backtraceFolders(jid: string): string[] {
  const set = new Set<string>();
  for (const t of [
    'paired_tasks',
    'work_items',
    'scheduled_tasks',
    'service_handoffs',
    'paired_projects',
  ]) {
    const rows = db
      .query(
        `SELECT DISTINCT group_folder FROM ${t} WHERE chat_jid=? AND group_folder IS NOT NULL`,
      )
      .all(jid) as Array<{ group_folder: string | null }>;
    for (const r of rows) if (r.group_folder) set.add(r.group_folder);
  }
  return [...set];
}

const targets: Target[] = jids.map((jid) => {
  const r = db
    .query('SELECT folder, is_main, name FROM room_settings WHERE chat_jid=?')
    .get(jid) as
    | { folder: string | null; is_main: number | null; name: string | null }
    | undefined;
  const folders = r?.folder ? [r.folder] : backtraceFolders(jid);
  return {
    jid,
    folders,
    isMain: r?.is_main === 1,
    name: r?.name ?? null,
    registered: Boolean(r),
  };
});

const mainTargets = targets.filter((t) => t.isMain);
if (mainTargets.length && !force) {
  console.error(
    'Refusing to deregister main room(s):',
    mainTargets.map((t) => t.jid).join(', '),
    '\nPass --force only if you really intend to unregister the main control room.',
  );
  process.exit(3);
}

const targetJidSet = new Set(jids);
function folderShared(folder: string): boolean {
  const rows = db
    .query('SELECT chat_jid FROM room_settings WHERE folder=?')
    .all(folder) as Array<{ chat_jid: string }>;
  return rows.some((r) => !targetJidSet.has(r.chat_jid));
}

const safeFolders = new Set<string>();
const sharedFolders = new Set<string>();
for (const t of targets) {
  for (const folder of t.folders) {
    if (folderShared(folder)) sharedFolders.add(folder);
    else safeFolders.add(folder);
  }
}

console.log('=== targets ===');
for (const t of targets) {
  console.log(
    `  ${t.jid} | name=${t.name ?? '(unregistered)'} | folder=${t.folders.length ? t.folders.join(',') : '-'} | registered=${t.registered}${t.isMain ? ' | MAIN' : ''}`,
  );
}
if (sharedFolders.size) {
  console.log(
    '  NOTE shared folders kept on disk (used by another room):',
    [...sharedFolders].join(', '),
  );
}

const qJ = jids.map(() => '?').join(',');
const taskIds = (
  db
    .query(`SELECT id FROM paired_tasks WHERE chat_jid IN (${qJ})`)
    .all(...jids) as Array<{
    id: string;
  }>
).map((r) => r.id);
const qT = taskIds.map(() => '?').join(',');
// task_run_logs.task_id references scheduled_tasks.id (NOT paired_tasks.id), so
// its rows must be pruned by the chat's scheduled task ids.
const scheduledTaskIds = (
  db
    .query(`SELECT id FROM scheduled_tasks WHERE chat_jid IN (${qJ})`)
    .all(...jids) as Array<{ id: string }>
).map((r) => r.id);
const qS = scheduledTaskIds.map(() => '?').join(',');
const safeFolderList = [...safeFolders];
const qF = safeFolderList.map(() => '?').join(',');

const count = (t: string, where: string, a: unknown[]): number =>
  (
    db.query(`SELECT COUNT(*) n FROM ${t} WHERE ${where}`).get(...a) as {
      n: number;
    }
  ).n;

// Report / gather counts.
const plan: Array<{ table: string; n: number; run: () => void }> = [];
const addJid = (t: string) =>
  plan.push({
    table: t,
    n: count(t, `chat_jid IN (${qJ})`, jids),
    run: () =>
      db.query(`DELETE FROM ${t} WHERE chat_jid IN (${qJ})`).run(...jids),
  });
const addTask = (t: string) => {
  if (!taskIds.length) return;
  plan.push({
    table: t,
    n: count(t, `task_id IN (${qT})`, taskIds),
    run: () =>
      db.query(`DELETE FROM ${t} WHERE task_id IN (${qT})`).run(...taskIds),
  });
};

addTask('paired_turn_attempts');
addTask('paired_turn_outputs');
addTask('paired_turns');
// Prune scheduled-task run logs before the scheduled_tasks rows they belong to.
if (scheduledTaskIds.length) {
  plan.push({
    table: 'task_run_logs',
    n: count('task_run_logs', `task_id IN (${qS})`, scheduledTaskIds),
    run: () =>
      db
        .query(`DELETE FROM task_run_logs WHERE task_id IN (${qS})`)
        .run(...scheduledTaskIds),
  });
}
for (const t of [
  'paired_task_execution_leases',
  'paired_turn_reservations',
  'paired_tasks',
  'paired_projects',
  'service_handoffs',
  'work_items',
  'scheduled_tasks',
  'channel_owner',
  'room_role_overrides',
  'room_skill_overrides',
  'room_settings',
]) {
  addJid(t);
}
// sessions are keyed by group folder — only purge folders not shared with a
// surviving room.
if (safeFolderList.length) {
  plan.push({
    table: 'sessions',
    n: count('sessions', `group_folder IN (${qF})`, safeFolderList),
    run: () =>
      db
        .query(`DELETE FROM sessions WHERE group_folder IN (${qF})`)
        .run(...safeFolderList),
  });
}

// router cursor prune count
const ras = db
  .query('SELECT value FROM router_state WHERE key=?')
  .get('last_agent_seq') as { value: string } | undefined;
let routerPrune = 0;
if (ras) {
  const obj = JSON.parse(ras.value) as Record<string, unknown>;
  routerPrune = jids.filter((j) => j in obj).length;
}

console.log('\n=== DB rows to delete ===');
for (const p of plan) if (p.n) console.log(`  ${p.table}: ${p.n}`);
if (routerPrune) console.log(`  router_state.last_agent_seq: ${routerPrune}`);

// disk targets
const diskDirs: string[] = [];
for (const folder of safeFolders) {
  for (const d of [
    path.join(GROUPS_DIR, folder),
    path.join(DATA_DIR, 'workspaces', folder),
    path.join(DATA_DIR, 'sessions', folder),
    path.join(DATA_DIR, 'ipc', folder),
  ]) {
    if (fs.existsSync(d)) diskDirs.push(d);
  }
}
console.log('\n=== disk dirs to delete ===');
for (const d of diskDirs) console.log(`  ${d}`);

if (dryRun) {
  console.log('\n[dry-run] no changes made.');
  process.exit(0);
}

// --- execute ---
// Auto-backup the DB before the irreversible purge (skip with --no-backup).
if (!noBackup) {
  const dbFile = path.join(STORE_DIR, 'messages.db');
  const stamp = new Date()
    .toISOString()
    .replace(/[-:]/g, '')
    .replace(/\..+$/, '')
    .replace('T', '-');
  const backup = path.join('/home/claude', `ejclaw-db-backup-${stamp}.db`);
  fs.copyFileSync(dbFile, backup);
  console.log(`DB backed up to ${backup}`);
}

db.exec('BEGIN');
try {
  for (const p of plan) p.run();
  if (ras && routerPrune) {
    const obj = JSON.parse(ras.value) as Record<string, unknown>;
    for (const j of jids) delete obj[j];
    db.query('UPDATE router_state SET value=? WHERE key=?').run(
      JSON.stringify(obj),
      'last_agent_seq',
    );
  }
  db.exec('COMMIT');
} catch (e) {
  db.exec('ROLLBACK');
  console.error('ROLLBACK due to error:', (e as Error).message);
  process.exit(1);
}

// remove git worktrees that live under a safe workspace folder, then rm -rf.
function listWorktrees(): string[] {
  try {
    const out = execFileSync('git', ['worktree', 'list', '--porcelain'], {
      cwd: repoRoot,
      encoding: 'utf-8',
    });
    return out
      .split('\n')
      .filter((l) => l.startsWith('worktree '))
      .map((l) => l.slice('worktree '.length).trim());
  } catch {
    return [];
  }
}
const worktrees = listWorktrees();
for (const folder of safeFolders) {
  const wsDir = path.join(DATA_DIR, 'workspaces', folder);
  for (const wt of worktrees) {
    if (wt === wsDir || wt.startsWith(wsDir + path.sep)) {
      try {
        execFileSync('git', ['worktree', 'remove', '--force', wt], {
          cwd: repoRoot,
          stdio: 'inherit',
        });
        console.log(`  git worktree removed: ${wt}`);
      } catch (e) {
        console.warn(
          `  worktree remove failed (${wt}): ${(e as Error).message}`,
        );
      }
    }
  }
}
for (const d of diskDirs) {
  fs.rmSync(d, { recursive: true, force: true });
  console.log(`  removed ${d}`);
}
try {
  execFileSync('git', ['worktree', 'prune'], { cwd: repoRoot });
} catch {
  /* ignore */
}

console.log('\nDone. Restart ejclaw to drop the room(s) from live bindings:');
console.log('  systemctl --user restart ejclaw.service');
