import { inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { OperationLogStoreService } from './operation-log-store.service';
import { loadAllData } from '../../root-store/meta/load-all-data.action';
import { OperationLogMigrationService } from './operation-log-migration.service';
import {
  CURRENT_SCHEMA_VERSION,
  SchemaMigrationService,
} from './schema-migration.service';
import { OperationLogSnapshotService } from './operation-log-snapshot.service';
import { OperationLogRecoveryService } from './operation-log-recovery.service';
import { SyncHydrationService } from './sync-hydration.service';
import { ArchiveMigrationService } from './archive-migration.service';
import { OpLog } from '../../core/log';
import { StateSnapshotService, AppStateSnapshot } from '../backup/state-snapshot.service';
import { Operation, OpType, RepairPayload } from '../core/operation.types';
import { SnackService } from '../../core/snack/snack.service';
import { T } from '../../t.const';
import { IndexedDBOpenError } from '../core/errors/indexed-db-open.error';
import { alertDialog } from '../../util/native-dialogs';
import { ValidateStateService } from '../validation/validate-state.service';
import { OperationApplierService } from '../apply/operation-applier.service';
import { HydrationStateService } from '../apply/hydration-state.service';
import { bulkApplyOperations } from '../apply/bulk-hydration.action';
import { getFailedOpIdsFromBatch } from '../apply/failed-op-ids.util';
import { VectorClockService } from '../sync/vector-clock.service';
import { MAX_CONFLICT_RETRY_ATTEMPTS } from '../core/operation-log.const';
import { AppDataComplete } from '../model/model-config';
import { CLIENT_ID_PROVIDER, ClientIdProvider } from '../util/client-id.provider';
import { limitVectorClockSize } from '../../core/util/vector-clock';
import { IS_ELECTRON } from '../../app.constants';

/**
 * sessionStorage key used to track auto-reload attempts after IndexedDB backing store errors.
 * Exported for use in tests.
 */
export const IDB_OPEN_ERROR_RELOAD_KEY = 'sp_idb_open_reload_attempt';

/**
 * Handles the hydration (loading) of the application state from the operation log
 * during application startup. It first attempts to load a saved state snapshot,
 * and then replays any subsequent operations from the log to bring the application
 * state up to date. This approach optimizes startup performance by avoiding a full
 * replay of all historical operations.
 */
@Injectable({ providedIn: 'root' })
export class OperationLogHydratorService {
  private store = inject(Store);
  private opLogStore = inject(OperationLogStoreService);
  private migrationService = inject(OperationLogMigrationService);
  private schemaMigrationService = inject(SchemaMigrationService);
  private stateSnapshotService = inject(StateSnapshotService);
  private snackService = inject(SnackService);
  private validateStateService = inject(ValidateStateService);
  private vectorClockService = inject(VectorClockService);
  private operationApplierService = inject(OperationApplierService);
  private hydrationStateService = inject(HydrationStateService);
  private clientIdProvider: ClientIdProvider = inject(CLIENT_ID_PROVIDER);

  // Extracted services
  private snapshotService = inject(OperationLogSnapshotService);
  private recoveryService = inject(OperationLogRecoveryService);
  private syncHydrationService = inject(SyncHydrationService);
  private archiveMigrationService = inject(ArchiveMigrationService);

  // Track if schema migration ran during this hydration (requires validation)
  private _migrationRanDuringHydration = false;

  async hydrateStore(): Promise<void> {
    OpLog.normal('OperationLogHydratorService: Starting hydration...');

    try {
      // PERF: Parallel startup operations - all access different IndexedDB stores
      // and don't depend on each other's results, so they can run concurrently.
      const [, , hasBackup] = await Promise.all([
        // Check for pending remote ops from crashed sync (touches 'ops' store)
        this.recoveryService.recoverPendingRemoteOps(),
        // Legacy migration placeholder - kept for future DB migrations if needed
        this._runLegacyMigrationIfNeeded(),
        // A.7.12: Check for interrupted migration (touches 'state_cache' store)
        this.opLogStore.hasStateCacheBackup(),
      ]);

      // Clean up corrupt operations (e.g., with undefined entityId) that cause
      // infinite rejection loops during sync. Must run after recoverPendingRemoteOps.
      await this.recoveryService.cleanupCorruptOps();

      // Migrate archives from legacy 'pf' database to SUP_OPS if needed.
      // This is idempotent - skips if archives already exist in SUP_OPS.
      await this.archiveMigrationService.migrateArchivesIfNeeded();

      if (hasBackup) {
        OpLog.warn(
          'OperationLogHydratorService: Found migration backup - previous migration may have crashed. Restoring...',
        );
        await this.opLogStore.restoreStateCacheFromBackup();
        OpLog.normal('OperationLogHydratorService: Restored from backup.');
      }

      // 1. Load snapshot
      let snapshot = await this.opLogStore.loadStateCache();

      if (!snapshot) {
        OpLog.normal(
          'OperationLogHydratorService: No snapshot found. Checking for migration...',
        );
        // Fresh install or migration - no snapshot exists
        await this.migrationService.checkAndMigrate();
        // Try loading again after potential migration
        snapshot = await this.opLogStore.loadStateCache();
      }

      // 2. Run schema migration if needed (A.7.12: with backup safety)
      if (snapshot && this.schemaMigrationService.needsMigration(snapshot)) {
        snapshot = await this.snapshotService.migrateSnapshotWithBackup(snapshot);
        this._migrationRanDuringHydration = true;
      }

      // 3. Validate snapshot if it exists
      if (snapshot && !this.snapshotService.isValidSnapshot(snapshot)) {
        OpLog.warn('OperationLogHydratorService: Snapshot is invalid/corrupted.');
        // The snapshot is only a load-time cache — the op-log is the source of
        // truth. Before surrendering to recovery (which, with no legacy data and
        // no sync, drops to an EMPTY store), check whether the op-log itself is
        // intact and rebuild from it (#7892).
        //
        // Correctness: compaction only ever prunes *synced* ops, so for a
        // no-sync client the entire log survives and replay-from-0 fully
        // reconstructs state; for a synced client any pruned ops still live on
        // the remote and a subsequent sync restores them. Either way, replaying
        // the surviving log is strictly better than discarding it for empty.
        const lastSeq = await this.opLogStore.getLastSeq();
        if (lastSeq > 0) {
          OpLog.warn(
            `OperationLogHydratorService: Discarding corrupt snapshot and replaying ` +
              `the op-log from the start (lastSeq=${lastSeq}).`,
          );
          // Fall through to the "no snapshot → replay all operations" branch.
          snapshot = null;
        } else {
          OpLog.warn(
            'OperationLogHydratorService: No op-log to replay. Attempting recovery...',
          );
          await this.recoveryService.attemptRecovery();
          sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
          return;
        }
      }

      if (snapshot) {
        OpLog.normal('OperationLogHydratorService: Snapshot found. Hydrating state...', {
          lastAppliedOpSeq: snapshot.lastAppliedOpSeq,
        });

        // CHECKPOINT B: Schema-version trust optimization
        // Skip synchronous validation if schema version matches current - the snapshot
        // was validated before being saved in the previous session. Only validate
        // synchronously if a migration ran (schema changed).
        // TODO: Consider removing this validation after ops-log testing phase.
        // Checkpoint C validates the final state anyway, making this redundant.
        const stateToLoad = snapshot.state as AppStateSnapshot;
        const snapshotSchemaVersion = (snapshot as { schemaVersion?: number })
          .schemaVersion;
        const needsSyncValidation =
          this._migrationRanDuringHydration ||
          snapshotSchemaVersion !== CURRENT_SCHEMA_VERSION;

        if (needsSyncValidation) {
          OpLog.normal(
            'OperationLogHydratorService: Running synchronous validation (migration ran or schema mismatch)',
          );
          await this._validateStateForHydration(
            stateToLoad as unknown as Record<string, unknown>,
            'snapshot',
          );
        } else {
          OpLog.normal(
            'OperationLogHydratorService: Trusting snapshot (schema version matches, no migration)',
          );
        }

        // CRITICAL: Restore snapshot's vector clock to the vector_clock store.
        // This is necessary because:
        // 1. hydrateFromRemoteSync saves the clock in the snapshot but NOT in the store
        // 2. When user creates new ops, incrementAndStoreVectorClock reads from the store
        // 3. Without this, new ops would have clocks missing entries from the SYNC_IMPORT
        // 4. Those ops would be CONCURRENT with the SYNC_IMPORT and get filtered on sync
        if (snapshot.vectorClock && Object.keys(snapshot.vectorClock).length > 0) {
          // Prune vector clock before restoring to prevent bloat from old snapshots
          // that were saved before pruning was added to saveCurrentStateAsSnapshot().
          const clientId = await this.clientIdProvider.loadClientId();
          const clockToRestore = clientId
            ? limitVectorClockSize(snapshot.vectorClock, clientId)
            : snapshot.vectorClock;
          await this.opLogStore.setVectorClock(clockToRestore);
          OpLog.normal(
            'OperationLogHydratorService: Restored vector clock from snapshot',
            { clockSize: Object.keys(clockToRestore).length },
          );
        }

        // 3. Hydrate NgRx with (possibly repaired) snapshot
        // stateToLoad is AppStateSnapshot which is runtime-compatible but TypeScript can't verify
        this.store.dispatch(
          loadAllData({
            appDataComplete: stateToLoad as unknown as AppDataComplete,
          }),
        );

        // 4. Replay tail operations (A.7.13: with operation migration)
        const tailOps = await this.opLogStore.getOpsAfterSeq(snapshot.lastAppliedOpSeq);

        if (tailOps.length > 0) {
          // Optimization: If last op is SyncImport or Repair, skip replay and load directly
          const lastOp = tailOps[tailOps.length - 1].op;
          const appData = this._extractFullStateFromOp(lastOp);
          if (appData) {
            OpLog.normal(
              `OperationLogHydratorService: Last of ${tailOps.length} tail ops is ${lastOp.opType}, loading directly`,
            );

            // Validate the full-state data before loading to NgRx.
            // The check is non-fatal: we log issues but still dispatch so the user
            // sees their data rather than a half-loaded UI. Repair is intentionally
            // not attempted here (it requires a confirm dialog that breaks Electron
            // focus on Windows — see issue #7631).
            await this._validateStateForHydration(
              appData as Record<string, unknown>,
              'tail-full-state-op-load',
            );
            // FIX: Merge vector clock BEFORE dispatching loadAllData
            // This ensures any operations created synchronously during loadAllData
            // (e.g., TODAY_TAG repair) will have the correct merged clock.
            // Without this, those operations get superseded clocks and are rejected by the server.
            await this.opLogStore.mergeRemoteOpClocks([lastOp]);
            this.store.dispatch(
              loadAllData({
                appDataComplete: appData as unknown as AppDataComplete,
              }),
            );
            // No snapshot save needed - full state ops already contain complete state
            // Snapshot will be saved after next batch of regular operations
          } else {
            // A.7.13: Migrate tail operations before replay
            const opsToReplay = this._migrateTailOps(tailOps.map((e) => e.op));

            const droppedCount = tailOps.length - opsToReplay.length;
            OpLog.normal(
              `OperationLogHydratorService: Replaying ${opsToReplay.length} tail ops ` +
                `(${droppedCount} dropped during migration).`,
            );
            // PERF: Use bulk dispatch to apply all operations in a single NgRx update.
            // This reduces 500 dispatches to 1, dramatically improving startup performance.
            // The bulkHydrationMetaReducer iterates through ops and applies each action.
            // Lenient (no throw) so a cold-boot IndexedDB hiccup can't block
            // startup. A null clientId leaves the bulk-apply flag unset, which
            // defaults to own-op semantics (apply faithfully) — the safe
            // direction for the common case (replaying THIS device's own ops).
            // See bulkOperationsMetaReducer.
            const localClientId =
              (await this.clientIdProvider.loadClientId()) ?? undefined;
            this.hydrationStateService.startApplyingRemoteOps();
            this.store.dispatch(
              bulkApplyOperations({ operations: opsToReplay, localClientId }),
            );
            this.hydrationStateService.endApplyingRemoteOps();

            // Merge replayed ops' clocks into local clock
            // This ensures subsequent ops have clocks that dominate these tail ops
            await this.opLogStore.mergeRemoteOpClocks(opsToReplay);

            // CHECKPOINT C: Validate state after replaying tail operations.
            // If invalid, we keep the data on screen but skip the snapshot save so
            // we don't cache corrupted state for next boot.
            const isStateValid =
              await this._validateCurrentStateForHydration('tail-replay');

            // 5. If we replayed many ops AND state is valid, save a new snapshot
            // for faster future loads.
            if (isStateValid && opsToReplay.length > 10) {
              OpLog.normal(
                `OperationLogHydratorService: Saving new snapshot after replaying ${opsToReplay.length} ops`,
              );
              await this.snapshotService.saveCurrentStateAsSnapshot();
            }
          }
        }

        OpLog.normal('OperationLogHydratorService: Hydration complete.');
      } else {
        OpLog.warn(
          'OperationLogHydratorService: No snapshot found. Replaying all operations from start.',
        );
        // No snapshot means we might be in a fresh install state or post-migration-check with no legacy data.
        // We must replay ALL operations from the beginning of the log.
        const allOps = await this.opLogStore.getOpsAfterSeq(0);

        if (allOps.length === 0) {
          // Fresh install - no data at all
          OpLog.normal(
            'OperationLogHydratorService: Fresh install detected. No data to load.',
          );
          sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
          return;
        }

        // Optimization: If last op is SyncImport or Repair, skip replay and load directly
        const lastOp = allOps[allOps.length - 1].op;
        const appData = this._extractFullStateFromOp(lastOp);
        if (appData) {
          OpLog.normal(
            `OperationLogHydratorService: Last of ${allOps.length} ops is ${lastOp.opType}, loading directly`,
          );

          // Validate the full-state data before loading to NgRx (non-fatal).
          await this._validateStateForHydration(
            appData as Record<string, unknown>,
            'full-state-op-load',
          );
          // FIX: Merge vector clock BEFORE dispatching loadAllData
          // Same fix as the tail ops branch - prevents superseded clock bug
          await this.opLogStore.mergeRemoteOpClocks([lastOp]);
          this.store.dispatch(
            loadAllData({
              appDataComplete: appData as unknown as AppDataComplete,
            }),
          );
          // No snapshot save needed - full state ops already contain complete state
        } else {
          // A.7.13: Migrate all operations before replay
          const opsToReplay = this._migrateTailOps(allOps.map((e) => e.op));

          const droppedCount = allOps.length - opsToReplay.length;
          OpLog.normal(
            `OperationLogHydratorService: Replaying all ${opsToReplay.length} ops ` +
              `(${droppedCount} dropped during migration).`,
          );
          // PERF: Use bulk dispatch to apply all operations in a single NgRx update.
          // This reduces 500 dispatches to 1, dramatically improving startup performance.
          // The bulkHydrationMetaReducer iterates through ops and applies each action.
          // Lenient (no throw) so a cold-boot IndexedDB hiccup can't block
          // startup. A null clientId leaves the bulk-apply flag unset, which
          // defaults to own-op semantics (apply faithfully) — the safe direction
          // for the common case (replaying THIS device's own ops). See
          // bulkOperationsMetaReducer.
          const localClientId = (await this.clientIdProvider.loadClientId()) ?? undefined;
          this.hydrationStateService.startApplyingRemoteOps();
          this.store.dispatch(
            bulkApplyOperations({ operations: opsToReplay, localClientId }),
          );
          this.hydrationStateService.endApplyingRemoteOps();

          // Merge replayed ops' clocks into local clock
          await this.opLogStore.mergeRemoteOpClocks(opsToReplay);

          // CHECKPOINT C: Validate state after replaying all operations.
          // If invalid, we still proceed but skip the snapshot save so we don't
          // cache corrupted state for next boot.
          const isStateValid =
            await this._validateCurrentStateForHydration('full-replay');

          // Save snapshot after replay for faster future loads (only when valid).
          if (isStateValid) {
            OpLog.normal(
              `OperationLogHydratorService: Saving snapshot after replaying ${opsToReplay.length} ops`,
            );
            await this.snapshotService.saveCurrentStateAsSnapshot();
          }
        }

        OpLog.normal('OperationLogHydratorService: Full replay complete.');
      }

      // Legacy cleanup placeholder - kept for future maintenance operations if needed
      await this._runLegacyCleanupIfNeeded();

      // Retry any failed remote ops from previous conflict resolution attempts
      // Now that state is fully hydrated, dependencies might be resolved
      await this.retryFailedRemoteOps();

      // Clear the auto-reload guard so that a fresh backing-store error in the same
      // tab session gets the auto-reload treatment again rather than going straight
      // to the manual recovery dialog.
      sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
    } catch (e) {
      OpLog.err('OperationLogHydratorService: Error during hydration', e);

      // Handle IndexedDB open failure with specific guidance
      if (e instanceof IndexedDBOpenError) {
        this._showIndexedDBOpenError(e);
        throw e;
      }

      try {
        await this.recoveryService.attemptRecovery();
      } catch (recoveryErr) {
        OpLog.err('OperationLogHydratorService: Recovery also failed', recoveryErr);

        // Check if recovery failed due to IndexedDB issue
        if (recoveryErr instanceof IndexedDBOpenError) {
          this._showIndexedDBOpenError(recoveryErr);
          throw recoveryErr;
        }

        this.snackService.open({
          type: 'ERROR',
          msg: T.F.SYNC.S.HYDRATION_FAILED,
          actionStr: T.PS.RELOAD,
          actionFn: (): void => {
            window.location.reload();
          },
        });
        throw recoveryErr;
      }
    }
  }

  /**
   * Extracts full application state from operations that contain complete state.
   * Returns undefined for operations that don't contain full state (normal CRUD ops).
   *
   * Operations that contain full state:
   * - OpType.SyncImport: Full state from remote sync
   * - OpType.Repair: Full repaired state from auto-repair
   * - OpType.BackupImport: Full state from backup file restore
   */
  private _extractFullStateFromOp(op: Operation): unknown | undefined {
    if (!op.payload) {
      return undefined;
    }

    // Handle full state operations
    if (
      op.opType === OpType.SyncImport ||
      op.opType === OpType.BackupImport ||
      op.opType === OpType.Repair
    ) {
      const payload = op.payload as
        | { appDataComplete?: unknown }
        | RepairPayload
        | unknown;

      // Check if payload has appDataComplete wrapper
      if (
        typeof payload === 'object' &&
        payload !== null &&
        'appDataComplete' in payload
      ) {
        return (payload as { appDataComplete: unknown }).appDataComplete;
      }

      // Legacy format: payload IS the appDataComplete
      return payload;
    }

    return undefined;
  }

  // ============================================================
  // A.7.13 Tail Ops Migration
  // ============================================================

  /**
   * Migrates tail operations to current schema version (A.7.13).
   * Operations that should be dropped (e.g., for removed features) are filtered out.
   *
   * @param ops - The operations to migrate
   * @returns Array of migrated operations
   */
  private _migrateTailOps(ops: Operation[]): Operation[] {
    // Check if any ops need migration
    const needsMigration = ops.some((op) =>
      this.schemaMigrationService.operationNeedsMigration(op),
    );

    if (!needsMigration) {
      return ops;
    }

    OpLog.normal(
      `OperationLogHydratorService: Migrating ${ops.length} tail ops to current schema version...`,
    );

    return this.schemaMigrationService.migrateOperations(ops);
  }

  /**
   * Handles hydration after a remote sync download.
   * Delegates to SyncHydrationService.
   *
   * @param downloadedMainModelData - Entity models from remote meta file.
   *   These are NOT stored in IndexedDB (only archives are) so must be passed explicitly.
   * @param remoteVectorClock - Vector clock from the downloaded snapshot.
   *   Merged into the SYNC_IMPORT's clock to prevent mutual discarding during provider switch.
   */
  async hydrateFromRemoteSync(
    downloadedMainModelData?: Record<string, unknown>,
    remoteVectorClock?: Record<string, number>,
  ): Promise<void> {
    return this.syncHydrationService.hydrateFromRemoteSync(
      downloadedMainModelData,
      remoteVectorClock,
    );
  }

  /**
   * Validates a state object during hydration without attempting repair.
   *
   * Repair is intentionally not run here: it requires a native `confirm()` dialog
   * which steals focus from the renderer on Windows and leaves the UI unresponsive
   * to keyboard/mouse input until the window is refocused (issue #7631). Validation
   * failures are logged but non-fatal — the caller continues with the original state
   * so the user sees their data rather than a half-loaded UI.
   *
   * @returns Whether the state is valid.
   */
  private async _validateStateForHydration(
    state: Record<string, unknown>,
    context: string,
  ): Promise<boolean> {
    const result = await this.validateStateService.validateState(state);
    if (!result.isValid) {
      OpLog.err(`[OperationLogHydratorService] Validation failed for ${context}`, {
        typiaErrorCount: result.typiaErrors.length,
        crossModelError: result.crossModelError,
      });
      return false;
    }
    return true;
  }

  /**
   * Validates the current NgRx state after replay.
   * Used to gate the snapshot save — we must not persist a corrupted snapshot.
   *
   * @param context - Context string for logging
   * @returns Whether the current state is valid.
   */
  private async _validateCurrentStateForHydration(context: string): Promise<boolean> {
    const currentState = this.stateSnapshotService.getStateSnapshot();
    return this._validateStateForHydration(
      currentState as unknown as Record<string, unknown>,
      context,
    );
  }

  /**
   * Legacy cleanup placeholder.
   * Kept for future maintenance operations if needed.
   */
  private async _runLegacyCleanupIfNeeded(): Promise<void> {
    // No-op: placeholder for future cleanup operations
  }

  /**
   * Retries failed remote operations from previous conflict resolution attempts.
   * Called after hydration to give failed ops another chance to apply now that
   * more state might be available (e.g., dependencies resolved by sync).
   *
   * Failed ops are ops that previously failed during conflict resolution
   * but may succeed now that more state has been loaded.
   */
  async retryFailedRemoteOps(): Promise<void> {
    const failedOps = await this.opLogStore.getFailedRemoteOps();

    if (failedOps.length === 0) {
      return;
    }

    OpLog.normal(
      `OperationLogHydratorService: Retrying ${failedOps.length} previously failed remote ops...`,
    );

    // Retry as ONE seq-ordered batch, not one op at a time. A per-op retry turns
    // every applyOperations() call into a single-op batch, and the same-batch
    // archive pre-scan (collectArchivingOrDeletingEntityIdsFromBatch) returns an
    // empty set for single-op batches — silently weakening the #7330
    // orphan-resurrection guard. Batching restores that protection and matches
    // how the primary remote-apply path (applyRemoteOperations) applies ops.
    // getFailedRemoteOps() reads from an index whose result order isn't part of
    // its contract, so sort by seq explicitly to keep causal order. See #8305.
    const orderedFailedOps = [...failedOps].sort((a, b) => a.seq - b.seq);
    const opsToApply = orderedFailedOps.map((e) => e.op);
    const opIdToSeq = new Map(orderedFailedOps.map((e) => [e.op.id, e.seq]));

    const result = await this.operationApplierService.applyOperations(opsToApply);

    // Mark successfully applied ops.
    const appliedSeqs = result.appliedOps
      .map((op) => opIdToSeq.get(op.id))
      .filter((seq): seq is number => seq !== undefined);
    if (appliedSeqs.length > 0) {
      await this.opLogStore.markApplied(appliedSeqs);
      OpLog.normal(
        `OperationLogHydratorService: Successfully retried ${appliedSeqs.length} failed ops`,
      );
    }

    // On a partial failure the batch applier stops at the first op whose archive
    // side effect throws and returns it; that op and every op after it in seq
    // order stay unapplied (slice-from-failure, shared with the primary path).
    // markFailed bumps the retry count (rejecting ops past
    // MAX_CONFLICT_RETRY_ATTEMPTS), so a permanently-failing op can't be retried
    // forever.
    if (result.failedOp) {
      const stillFailedOpIds = getFailedOpIdsFromBatch(opsToApply, result.failedOp.op);

      OpLog.warn(
        `OperationLogHydratorService: Failed to retry op ${result.failedOp.op.id}`,
        result.failedOp.error,
      );
      await this.opLogStore.markFailed(stillFailedOpIds, MAX_CONFLICT_RETRY_ATTEMPTS);
      OpLog.warn(
        `OperationLogHydratorService: ${stillFailedOpIds.length} ops still failing after retry`,
      );
    }
  }

  /**
   * Legacy migration placeholder.
   * Kept for future DB migrations if needed.
   */
  private async _runLegacyMigrationIfNeeded(): Promise<void> {
    // No-op: placeholder for future migrations
  }

  /**
   * Shows a helpful error dialog when IndexedDB fails to open.
   * Provides platform-specific guidance for "backing store" errors.
   * Also logs full error details to console for debugging.
   *
   * @see https://github.com/johannesjo/super-productivity/issues/6255
   */
  private _showIndexedDBOpenError(error: IndexedDBOpenError): void {
    // Log full error details to console for debugging (can be copied by users)
    OpLog.err(
      'IndexedDB open failed after all retries. Original error:',
      error.originalError,
    );

    const originalMsg =
      error.originalError instanceof Error
        ? error.originalError.message
        : String(error.originalError);

    // Hoist platform detection — used in both branches below to avoid computing twice
    const isFlatpak = IS_ELECTRON && window.ea?.isFlatpak?.();
    const isSnap = !isFlatpak && IS_ELECTRON && window.ea?.isSnap?.();

    // For backing-store errors (common during Linux session startup with autostart),
    // auto-reload once after the user dismisses the dialog. By the time the dialog
    // is dismissed the OS / Flatpak sandbox will usually have finished initializing.
    // A sessionStorage counter prevents an infinite reload loop on genuine errors.
    if (error.isBackingStoreError) {
      const reloadCount = +(sessionStorage.getItem(IDB_OPEN_ERROR_RELOAD_KEY) || '0');
      if (reloadCount === 0) {
        // Silent auto-reload on first occurrence — most likely a transient startup
        // timing issue (Flatpak sandbox not ready, stale LOCK file). The user is
        // typically not watching (autostart scenario), so a blocking dialog requiring
        // a click before the reload is unnecessary friction. If the reload fixes it,
        // the user never needs to know. If it fails again, the dialog below runs.
        OpLog.warn(
          'IndexedDB backing-store error on first attempt — triggering silent auto-reload',
        );
        sessionStorage.setItem(IDB_OPEN_ERROR_RELOAD_KEY, '1');
        this._triggerReload();
        return;
      }
    }

    // Second failure, or non-backing-store error: show full manual recovery instructions.
    let message =
      'Database Error - Cannot Load Data\n\n' +
      'Super Productivity cannot open its database. ' +
      'This may be caused by:\n\n' +
      '- Low disk space\n' +
      '- Temporary file lock (try closing other tabs)\n' +
      '- Storage corruption\n\n';

    if (error.isBackingStoreError) {
      message +=
        'Recovery steps:\n' +
        '1. Close ALL browser tabs and windows\n' +
        '2. Restart the app\n' +
        (isFlatpak
          ? '3. If using Linux Flatpak with autostart, try disabling autostart and launching manually\n'
          : isSnap
            ? '3. If using Linux Snap, try: snap set core experimental.refresh-app-awareness=true\n'
            : '3. If using Linux with autostart, try disabling autostart and launching manually\n') +
        '4. If issue persists, check available disk space\n\n';
    }

    message +=
      'If the problem continues after restart, your browser storage may need to be cleared.\n\n' +
      `Technical details: ${originalMsg}\n\n` +
      '(Check browser console for full error details)';

    alertDialog(message);
  }

  /**
   * Triggers an app reload. Uses Electron IPC in Electron context, browser reload otherwise.
   * Extracted as a method to allow spying in unit tests.
   */
  private _triggerReload(): void {
    if (IS_ELECTRON) {
      window.ea.reloadMainWin();
    } else {
      window.location.reload();
    }
  }
}
