import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { logger } from "../monitoring/logger.js";
class FrameRecovery {
  constructor(db, options = {}) {
    this.db = db;
    this.orphanThresholdMs = (options.orphanThresholdHours ?? 24) * 60 * 60 * 1e3;
  }
  // Sessions older than this are considered orphaned (default: 24 hours)
  orphanThresholdMs;
  // Current session/run ID to exclude from orphan detection
  currentRunId = null;
  /**
   * Set the current run ID to exclude from orphan detection
   */
  setCurrentRunId(runId) {
    this.currentRunId = runId;
  }
  /**
   * Main recovery entry point - call on startup
   */
  async recoverOnStartup() {
    const errors = [];
    const timestamp = (/* @__PURE__ */ new Date()).toISOString();
    logger.info("Starting crash recovery check");
    const walStatus = this.checkWalStatus();
    if (walStatus.checkpointNeeded) {
      try {
        this.checkpointWal();
        logger.info("WAL checkpoint completed");
      } catch (err) {
        const msg = `WAL checkpoint failed: ${err instanceof Error ? err.message : String(err)}`;
        errors.push(msg);
        logger.warn(msg);
      }
    }
    const integrityCheck = this.runIntegrityCheck();
    if (!integrityCheck.passed) {
      logger.error("Database integrity check failed", {
        violations: integrityCheck.foreignKeyViolations,
        corrupted: integrityCheck.corruptedRows
      });
    }
    const orphanedFrames = this.recoverOrphanedFrames();
    if (orphanedFrames.detected > 0) {
      logger.info("Orphaned frames processed", {
        detected: orphanedFrames.detected,
        recovered: orphanedFrames.recovered,
        closed: orphanedFrames.closed
      });
    }
    const report = {
      timestamp,
      integrityCheck,
      orphanedFrames,
      walStatus,
      recovered: integrityCheck.passed && orphanedFrames.detected === orphanedFrames.closed,
      errors
    };
    logger.info("Crash recovery completed", {
      recovered: report.recovered,
      orphansFound: orphanedFrames.detected,
      integrityPassed: integrityCheck.passed
    });
    return report;
  }
  /**
   * Check WAL mode status
   */
  checkWalStatus() {
    try {
      const journalMode = this.db.pragma("journal_mode");
      const isWal = journalMode[0]?.journal_mode === "wal";
      if (!isWal) {
        return { enabled: false, checkpointNeeded: false, walSize: 0 };
      }
      const walInfo = this.db.pragma("wal_checkpoint(PASSIVE)");
      const walSize = walInfo[0]?.log ?? 0;
      const checkpointed = walInfo[0]?.checkpointed ?? 0;
      return {
        enabled: true,
        checkpointNeeded: walSize > 1e3,
        // Checkpoint if WAL has > 1000 pages
        walSize: walSize - checkpointed
      };
    } catch (err) {
      logger.warn("Failed to check WAL status", { error: err });
      return { enabled: false, checkpointNeeded: false, walSize: 0 };
    }
  }
  /**
   * Force WAL checkpoint
   */
  checkpointWal() {
    this.db.pragma("wal_checkpoint(TRUNCATE)");
  }
  /**
   * Run database integrity checks
   */
  runIntegrityCheck() {
    const errors = [];
    let foreignKeyViolations = 0;
    let corruptedRows = 0;
    try {
      const fkViolations = this.db.pragma(
        "foreign_key_check"
      );
      foreignKeyViolations = fkViolations.length;
      if (foreignKeyViolations > 0) {
        errors.push(`Found ${foreignKeyViolations} foreign key violations`);
        logger.warn("Foreign key violations detected", {
          count: foreignKeyViolations,
          samples: fkViolations.slice(0, 5)
        });
      }
      const integrity = this.db.pragma("integrity_check");
      const integrityResult = integrity[0]?.integrity_check;
      if (integrityResult !== "ok") {
        corruptedRows = integrity.length;
        errors.push(`Integrity check failed: ${integrityResult}`);
        logger.error("Database corruption detected", {
          result: integrityResult
        });
      }
    } catch (err) {
      errors.push(
        `Integrity check error: ${err instanceof Error ? err.message : String(err)}`
      );
    }
    return {
      passed: foreignKeyViolations === 0 && corruptedRows === 0,
      foreignKeyViolations,
      corruptedRows,
      errors
    };
  }
  /**
   * Detect and recover orphaned frames
   * Orphaned = active frames from sessions that are no longer running
   */
  recoverOrphanedFrames() {
    const orphanThreshold = Date.now() - this.orphanThresholdMs;
    const thresholdUnix = Math.floor(orphanThreshold / 1e3);
    const query = `
      SELECT frame_id, run_id, project_id, name, type, created_at, depth
      FROM frames
      WHERE state = 'active'
        AND created_at < ?
        ${this.currentRunId ? "AND run_id != ?" : ""}
      ORDER BY created_at ASC
    `;
    const params = this.currentRunId ? [thresholdUnix, this.currentRunId] : [thresholdUnix];
    const orphaned = this.db.prepare(query).all(...params);
    if (orphaned.length === 0) {
      return { detected: 0, recovered: 0, closed: 0, frameIds: [] };
    }
    const frameIds = orphaned.map((f) => f.frame_id);
    let closed = 0;
    const closeStmt = this.db.prepare(`
      UPDATE frames
      SET state = 'closed',
          closed_at = unixepoch(),
          outputs = json_set(COALESCE(outputs, '{}'), '$.recovered', true, '$.recoveryReason', 'orphan_cleanup')
      WHERE frame_id = ?
    `);
    const transaction = this.db.transaction(() => {
      for (const frame of orphaned) {
        try {
          closeStmt.run(frame.frame_id);
          closed++;
          logger.debug("Closed orphaned frame", {
            frameId: frame.frame_id,
            runId: frame.run_id,
            name: frame.name,
            age: Math.round((Date.now() / 1e3 - frame.created_at) / 3600) + "h"
          });
        } catch (err) {
          logger.warn("Failed to close orphaned frame", {
            frameId: frame.frame_id,
            error: err
          });
        }
      }
    });
    transaction();
    return {
      detected: orphaned.length,
      recovered: 0,
      // Future: could attempt to resume some frames
      closed,
      frameIds
    };
  }
  /**
   * Validate data integrity for a specific project
   */
  validateProjectIntegrity(projectId) {
    const issues = [];
    const invalidParents = this.db.prepare(
      `
      SELECT f1.frame_id, f1.parent_frame_id
      FROM frames f1
      LEFT JOIN frames f2 ON f1.parent_frame_id = f2.frame_id
      WHERE f1.project_id = ?
        AND f1.parent_frame_id IS NOT NULL
        AND f2.frame_id IS NULL
    `
    ).all(projectId);
    if (invalidParents.length > 0) {
      issues.push(
        `${invalidParents.length} frames with invalid parent references`
      );
    }
    const depthIssues = this.db.prepare(
      `
      SELECT f1.frame_id, f1.depth as child_depth, f2.depth as parent_depth
      FROM frames f1
      JOIN frames f2 ON f1.parent_frame_id = f2.frame_id
      WHERE f1.project_id = ?
        AND f1.depth != f2.depth + 1
    `
    ).all(projectId);
    if (depthIssues.length > 0) {
      issues.push(`${depthIssues.length} frames with incorrect depth values`);
    }
    const orphanEvents = this.db.prepare(
      `
      SELECT COUNT(*) as count
      FROM events e
      LEFT JOIN frames f ON e.frame_id = f.frame_id
      WHERE f.frame_id IS NULL
    `
    ).get();
    if (orphanEvents.count > 0) {
      issues.push(`${orphanEvents.count} orphaned events without valid frames`);
    }
    return {
      valid: issues.length === 0,
      issues
    };
  }
  /**
   * Clean up orphaned events (events without valid frames)
   */
  cleanupOrphanedEvents() {
    const result = this.db.prepare(
      `
      DELETE FROM events
      WHERE frame_id NOT IN (SELECT frame_id FROM frames)
    `
    ).run();
    if (result.changes > 0) {
      logger.info("Cleaned up orphaned events", { count: result.changes });
    }
    return result.changes;
  }
  /**
   * Get recovery statistics
   */
  getRecoveryStats() {
    const stats = this.db.prepare(
      `
      SELECT
        COUNT(*) as total,
        SUM(CASE WHEN state = 'active' THEN 1 ELSE 0 END) as active,
        SUM(CASE WHEN state = 'closed' THEN 1 ELSE 0 END) as closed,
        SUM(CASE WHEN json_extract(outputs, '$.recovered') = true THEN 1 ELSE 0 END) as recovered
      FROM frames
    `
    ).get();
    const oldest = this.db.prepare(
      `
      SELECT datetime(created_at, 'unixepoch') as created
      FROM frames
      WHERE state = 'active'
      ORDER BY created_at ASC
      LIMIT 1
    `
    ).get();
    return {
      totalFrames: stats.total,
      activeFrames: stats.active,
      closedFrames: stats.closed,
      recoveredFrames: stats.recovered,
      oldestActiveFrame: oldest?.created ?? null
    };
  }
}
async function recoverDatabase(db, currentRunId) {
  const recovery = new FrameRecovery(db);
  if (currentRunId) {
    recovery.setCurrentRunId(currentRunId);
  }
  return recovery.recoverOnStartup();
}
export {
  FrameRecovery,
  recoverDatabase
};
//# sourceMappingURL=frame-recovery.js.map
