#!/usr/bin/env node
/**
 * repair-crosslinks — one-shot repair of agent_sessions rows whose slug
 * does not match the canonical slug of their claimed ctm_session (and
 * whose file is not a --resume continuation via prefix-copy lineage).
 *
 * How it runs:
 *   - Called from db.js migrateToV2() on first boot after the v2 schema bump.
 *   - Also runnable standalone for diagnostics:
 *       node scripts/repair-crosslinks.js --dry-run
 *       node scripts/repair-crosslinks.js --db /path/to/task-manager.db
 *
 * Idempotent: on re-run, cross-links have already been removed, so the query
 * returns nothing and the function is a no-op.
 *
 * Backward-compat:
 *   - Agent rows with empty slug (un-backfilled, e.g. JSONL deleted) are
 *     LEFT ALONE. We don't unlink a row we can't verify — safer to keep the
 *     stale link than to silently break a tab.
 *   - Agent rows whose slug matches canonical are kept. So are rows whose
 *     slug differs BUT whose JSONL prefix-copy contains a sessionId that
 *     belongs to this tab's known chain.
 */

'use strict';

const path = require('path');
const fs = require('fs');

/**
 * Repair cross-links in place.
 *
 * @param {import('better-sqlite3').Database} db
 * @param {Object} [opts]
 * @param {boolean} [opts.dryRun=false] — report what would change, don't write
 * @param {Function} [opts.log=console.log] — override for tests
 * @returns {{ removed: number, kept: number, startupResets: number, repairs: Array }}
 */
function repairCrosslinks(db, opts = {}) {
  const dryRun = !!opts.dryRun;
  const log = opts.log || ((..._a) => {});

  // Lazy-require to avoid circular deps at init time.
  const {
    readFirstSlug,
    readPrefixSessionIds,
    canonicalSlugForTab,
    knownAgentIdsForTab,
  } = require('../lib/session-lineage');

  // Load every ctm_session that has >1 agent_session row (cheap filter;
  // single-row tabs can't be cross-linked to themselves).
  const multiTabs = db.prepare(
    `SELECT ctm_session_id, COUNT(*) AS n
       FROM agent_sessions
      WHERE ctm_session_id IS NOT NULL AND ctm_session_id != ''
      GROUP BY ctm_session_id
      HAVING n > 1`
  ).all();

  const repairs = [];
  let removed = 0;
  let kept = 0;
  let startupResets = 0;

  for (const { ctm_session_id: ctmId } of multiTabs) {
    const canonical = canonicalSlugForTab(db, ctmId);
    if (!canonical) {
      // No canonical slug → can't make a slug-based judgment. Skip this tab.
      // The relink code has a backward-compat refuse path; this is safe.
      kept += 1;
      continue;
    }

    const known = knownAgentIdsForTab(db, ctmId);
    const rows = db.prepare(
      "SELECT agent_session_id, slug, jsonl_path FROM agent_sessions WHERE ctm_session_id = ? ORDER BY created_at ASC"
    ).all(ctmId);

    for (const row of rows) {
      // Self-ref is always kept (it IS the canonical).
      if (row.agent_session_id === ctmId) { kept += 1; continue; }

      // Rows with matching slug are legitimate continuations.
      if (row.slug && row.slug === canonical) { kept += 1; continue; }

      // Rows with empty slug: try to backfill from the JSONL so we don't
      // over-unlink. If the file's gone, leave the row alone (conservative).
      let candidateSlug = row.slug;
      if (!candidateSlug && row.jsonl_path && fs.existsSync(row.jsonl_path)) {
        const hdr = readFirstSlug(row.jsonl_path);
        if (hdr && hdr.slug) {
          candidateSlug = hdr.slug;
          // Persist the backfill as we go — unless this is a dry run.
          if (!dryRun) {
            db.prepare('UPDATE agent_sessions SET slug = ? WHERE agent_session_id = ?')
              .run(hdr.slug, row.agent_session_id);
          }
        }
      }

      // If we now have a slug and it matches canonical, keep it.
      if (candidateSlug && candidateSlug === canonical) { kept += 1; continue; }

      // If slug still empty (file missing or no slug in JSONL), be conservative
      // and KEEP the row. Relink's backward-compat layer will refuse to
      // extend this link further, but we won't proactively break it.
      if (!candidateSlug) { kept += 1; continue; }

      // Slugs mismatch. Last chance: check for prefix-copy lineage (the
      // --resume case where Claude Code's slug somehow drifted).
      let prefixRescue = false;
      if (row.jsonl_path && fs.existsSync(row.jsonl_path)) {
        const prefixIds = readPrefixSessionIds(row.jsonl_path);
        for (const sid of prefixIds) {
          if (sid !== row.agent_session_id && known.has(sid)) {
            prefixRescue = true;
            break;
          }
        }
      }
      if (prefixRescue) { kept += 1; continue; }

      // Definitively cross-linked. Unlink.
      repairs.push({
        ctm_session_id: ctmId,
        removed_agent_session_id: row.agent_session_id,
        canonical_slug: canonical,
        wrong_slug: candidateSlug,
      });
      log(`[repair-crosslinks] unlink ctm=${ctmId.slice(0, 8)} agent=${row.agent_session_id.slice(0, 8)} canonical=${canonical} wrong=${candidateSlug}`);

      if (!dryRun) {
        db.prepare('DELETE FROM agent_sessions WHERE agent_session_id = ? AND ctm_session_id = ?')
          .run(row.agent_session_id, ctmId);
        removed += 1;

        // If startup_tasks pointed at the removed row, reset it to the
        // canonical-slug row (self-ref preferred, else earliest matching).
        const stRow = db.prepare(
          'SELECT agent_session_id FROM startup_tasks WHERE ctm_session_id = ?'
        ).get(ctmId);
        if (stRow && stRow.agent_session_id === row.agent_session_id) {
          // Find replacement: prefer self-ref; else earliest row with canonical slug.
          const selfRef = db.prepare(
            'SELECT agent_session_id FROM agent_sessions WHERE ctm_session_id = ? AND agent_session_id = ?'
          ).get(ctmId, ctmId);
          const replacement = selfRef
            ? ctmId
            : (db.prepare(
                "SELECT agent_session_id FROM agent_sessions WHERE ctm_session_id = ? AND slug = ? ORDER BY created_at ASC LIMIT 1"
              ).get(ctmId, canonical) || {}).agent_session_id;
          if (replacement) {
            db.prepare('UPDATE startup_tasks SET agent_session_id = ? WHERE ctm_session_id = ?')
              .run(replacement, ctmId);
            startupResets += 1;
            log(`[repair-crosslinks] startup_tasks reset: ${ctmId.slice(0, 8)} -> agent=${replacement.slice(0, 8)}`);
          } else {
            // No replacement available. Null it out — better than pointing at
            // a deleted row.
            db.prepare('UPDATE startup_tasks SET agent_session_id = NULL WHERE ctm_session_id = ?')
              .run(ctmId);
            startupResets += 1;
            log(`[repair-crosslinks] startup_tasks nulled for ${ctmId.slice(0, 8)} — no matching agent remains`);
          }
        }

        // The removed agent_session may still have a JSONL on disk. Create
        // a standalone ctm_sessions + agent_sessions pair for it so the
        // conversation doesn't vanish from the UI — unless one already exists.
        const orphanCtm = db.prepare('SELECT id FROM ctm_sessions WHERE id = ?').get(row.agent_session_id);
        if (!orphanCtm && row.jsonl_path && fs.existsSync(row.jsonl_path)) {
          // Minimal insert; background jobs will fill in metadata later.
          db.prepare(
            "INSERT OR IGNORE INTO ctm_sessions (id, provider, created_at, updated_at) VALUES (?, 'claude', datetime('now'), datetime('now'))"
          ).run(row.agent_session_id);
          db.prepare(
            "INSERT OR IGNORE INTO agent_sessions (agent_session_id, ctm_session_id, jsonl_path, slug, created_at, updated_at) VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))"
          ).run(row.agent_session_id, row.agent_session_id, row.jsonl_path, candidateSlug);
          log(`[repair-crosslinks] recreated standalone ctm for orphaned ${row.agent_session_id.slice(0, 8)}`);
        }
      } else {
        removed += 1;
      }
    }
  }

  return { removed, kept, startupResets, repairs };
}

module.exports = { repairCrosslinks };

// --- CLI entry point ---
if (require.main === module) {
  const args = process.argv.slice(2);
  const dryRun = args.includes('--dry-run');
  let dbPath = null;
  const dbIdx = args.indexOf('--db');
  if (dbIdx >= 0) dbPath = args[dbIdx + 1];
  if (!dbPath) {
    // Default to the Dropbox path used by CTM in production.
    const home = process.env.HOME;
    dbPath = path.join(home, 'Library', 'CloudStorage', 'Dropbox', 'work', 'claude', 'task-manager', 'task-manager.db');
  }
  if (!fs.existsSync(dbPath)) {
    console.error(`DB not found: ${dbPath}`);
    process.exit(1);
  }
  const Database = require('better-sqlite3');
  const db = new Database(dbPath, { readonly: dryRun });
  const result = repairCrosslinks(db, { dryRun, log: console.log });
  console.log(`\n=== ${dryRun ? 'DRY RUN' : 'REPAIR'} COMPLETE ===`);
  console.log(`removed=${result.removed} kept=${result.kept} startupResets=${result.startupResets}`);
  if (result.repairs.length > 0) {
    console.log('\nRepairs:');
    for (const r of result.repairs) {
      console.log(`  ctm=${r.ctm_session_id.slice(0, 8)} removed=${r.removed_agent_session_id.slice(0, 8)} canonical=${r.canonical_slug} wrong=${r.wrong_slug}`);
    }
  }
  db.close();
}
