import { inject, Injectable } from '@angular/core';
import { LockService } from '../sync/lock.service';
import {
  COMPACTION_RETENTION_MS,
  COMPACTION_TIMEOUT_MS,
  EMERGENCY_COMPACTION_RETENTION_MS,
  LOCK_NAMES,
  SLOW_COMPACTION_THRESHOLD_MS,
} from '../core/operation-log.const';
import { OperationLogStoreService } from './operation-log-store.service';
import { StateSnapshotService } from '../backup/state-snapshot.service';
import { CURRENT_SCHEMA_VERSION } from './schema-migration.service';
import { VectorClockService } from '../sync/vector-clock.service';
import { OpLog } from '../../core/log';
import { extractEntityKeysFromState } from './extract-entity-keys';
import { CLIENT_ID_PROVIDER, ClientIdProvider } from '../util/client-id.provider';
import { limitVectorClockSize } from '../../core/util/vector-clock';
import { hasMeaningfulStateData } from '../validation/has-meaningful-state-data.util';

/**
 * Manages the compaction (garbage collection) of the operation log.
 * To prevent the log from growing indefinitely, this service periodically
 * creates a complete snapshot of the current application state and stores it
 * in IndexedDB. It then deletes old operations from the log that are already
 * reflected in the snapshot and have been successfully synced (if applicable)
 * and are older than a defined retention window.
 */
@Injectable({ providedIn: 'root' })
export class OperationLogCompactionService {
  private opLogStore = inject(OperationLogStoreService);
  private lockService = inject(LockService);
  private stateSnapshot = inject(StateSnapshotService);
  private vectorClockService = inject(VectorClockService);
  private clientIdProvider: ClientIdProvider = inject(CLIENT_ID_PROVIDER);

  async compact(): Promise<void> {
    await this._doCompact(COMPACTION_RETENTION_MS, false);
  }

  /**
   * Emergency compaction triggered when storage quota is exceeded.
   * Uses a shorter retention window (1 day instead of 7) to free more space.
   * Returns true if compaction succeeded, false otherwise.
   */
  async emergencyCompact(): Promise<boolean> {
    try {
      await this._doCompact(EMERGENCY_COMPACTION_RETENTION_MS, true);
      return true;
    } catch (e) {
      OpLog.err('OperationLogCompactionService: Emergency compaction failed', e);
      return false;
    }
  }

  /**
   * Core compaction logic shared between regular and emergency compaction.
   * @param retentionMs - How long to keep synced operations (in ms)
   * @param isEmergency - Whether this is an emergency compaction (for logging)
   */
  private async _doCompact(retentionMs: number, isEmergency: boolean): Promise<void> {
    await this.lockService.request(LOCK_NAMES.OPERATION_LOG, async () => {
      const startTime = Date.now();
      const label = isEmergency ? 'emergency ' : '';

      // 1. Get current state from NgRx store
      const currentState = this.stateSnapshot.getStateSnapshot();
      this.checkCompactionTimeout(startTime, `${label}state snapshot`);

      // GUARD (#7892): never compact against an empty/degraded state. Compaction
      // both writes the state cache AND deletes old synced ops — if the live
      // state were a transient empty/initial state, we would cache emptiness and
      // then prune the very ops needed to recover. Skipping is always safe for
      // correctness: the op-log stays the source of truth and replaying the
      // un-pruned log reconstructs the correct state, including legitimate full
      // wipes. Trade-off: a store that is *genuinely* empty-but-active (e.g. the
      // user deleted everything yet keeps generating synced ops) will never get
      // its old synced ops pruned while it stays empty, so the log can grow. That
      // is an accepted cost — preventing empty-over-good is worth more than GC for
      // this rare case, and pruning resumes as soon as real data exists again.
      if (!hasMeaningfulStateData(currentState)) {
        OpLog.warn(
          'OperationLogCompactionService: Skipping compaction — current state has no ' +
            'meaningful data (refusing to overwrite cache and prune ops against empty state)',
        );
        return;
      }

      // 2. Get current vector clock (max of all ops)
      const currentVectorClock = await this.vectorClockService.getCurrentVectorClock();
      this.checkCompactionTimeout(startTime, `${label}vector clock`);

      // Prune vector clock before persisting to prevent bloat (max 20 entries).
      // Without this, clocks can grow unbounded across sync cycles.
      const clientId = await this.clientIdProvider.loadClientId();
      const prunedClock = clientId
        ? limitVectorClockSize(currentVectorClock, clientId)
        : currentVectorClock;

      // 3. Get lastSeq IMMEDIATELY before writing cache to minimize race window
      // This ensures new ops written after this point have seq > lastSeq
      const lastSeq = await this.opLogStore.getLastSeq();

      // 4. Extract entity keys for conflict detection after compaction
      // This allows us to distinguish between entities that existed at snapshot time
      // vs new entities created later - critical for correct vector clock comparison
      const snapshotEntityKeys = extractEntityKeysFromState(currentState);

      // 5. Write to state cache with schema version and entity keys
      await this.opLogStore.saveStateCache({
        state: currentState,
        lastAppliedOpSeq: lastSeq,
        vectorClock: prunedClock,
        compactedAt: Date.now(),
        schemaVersion: CURRENT_SCHEMA_VERSION,
        snapshotEntityKeys,
      });

      // After snapshot is saved, new operations with seq > lastSeq won't be deleted

      // 6. Reset compaction counter (persistent across tabs/restarts)
      await this.opLogStore.resetCompactionCounter();

      // 7. Delete old operations (keep recent for conflict resolution window)
      // Only delete ops that have been synced to remote
      const cutoff = Date.now() - retentionMs;

      await this.opLogStore.deleteOpsWhere(
        (entry) =>
          !!entry.syncedAt && // never drop unsynced ops
          entry.appliedAt < cutoff &&
          entry.seq <= lastSeq, // keep tail for conflict frontier
      );

      // Log metrics for slow compaction or emergency compaction
      const totalDuration = Date.now() - startTime;
      if (totalDuration > SLOW_COMPACTION_THRESHOLD_MS || isEmergency) {
        OpLog.normal('OperationLogCompactionService: Compaction completed', {
          durationMs: totalDuration,
          entityCount: snapshotEntityKeys.length,
          isEmergency,
        });
      }
    });
  }

  /**
   * Checks if compaction has exceeded the timeout threshold.
   * If exceeded, throws an error to abort compaction before the lock expires.
   * This prevents data corruption from concurrent access.
   */
  private checkCompactionTimeout(startTime: number, phase: string): void {
    const elapsed = Date.now() - startTime;
    if (elapsed > COMPACTION_TIMEOUT_MS) {
      throw new Error(
        `Compaction timeout after ${elapsed}ms during ${phase}. ` +
          `Aborting to prevent lock expiration. ` +
          `Consider reducing state size or increasing timeout.`,
      );
    }
  }
}
