import {
  computeDigest,
  type FreshPayload,
  type FreshSnapshot,
  type FreshnessPolicy,
  type InvalidationReason,
} from './model';

/**
 * Session-scoped last-known snapshot cache (RI-5-001).
 *
 * Restored snapshots are situational awareness only: they surface as `stale`
 * until a fetch re-verifies them. A cache entry that is corrupted, belongs to
 * another workspace, was written by a newer schema, or no longer validates is
 * invalidated (treated as unavailable, never rendered as current).
 */

const CACHE_PREFIX = 'mosaic:freshness:v1';

interface StoredSnapshot {
  data: unknown;
  source: string;
  workspace: string;
  version: number;
  schemaVersion: number;
  fetchedAt: number;
  digest: string;
}

export type SnapshotCacheRead<T> =
  | { readonly outcome: 'hit'; readonly snapshot: FreshSnapshot<T> }
  | { readonly outcome: 'miss' }
  | { readonly outcome: 'invalidated'; readonly reason: InvalidationReason };

export interface ReadSnapshotCacheOptions<T> {
  readonly key: string;
  readonly workspace: string;
  readonly policy: FreshnessPolicy;
  readonly validate: (value: unknown) => FreshPayload<T> | null;
}

function cacheKey(key: string): string {
  return `${CACHE_PREFIX}:${key}`;
}

function isStoredSnapshot(value: unknown): value is StoredSnapshot {
  if (typeof value !== 'object' || value === null) return false;
  const candidate = value as Record<string, unknown>;
  return (
    typeof candidate['data'] === 'object' &&
    candidate['data'] !== null &&
    typeof candidate['source'] === 'string' &&
    typeof candidate['workspace'] === 'string' &&
    typeof candidate['version'] === 'number' &&
    typeof candidate['schemaVersion'] === 'number' &&
    typeof candidate['fetchedAt'] === 'number' &&
    typeof candidate['digest'] === 'string'
  );
}

function getStorage(): Storage | null {
  try {
    return globalThis.sessionStorage ?? null;
  } catch {
    return null;
  }
}

/**
 * Restore a cached snapshot under the active workspace scope. Every failure
 * mode maps to an explicit invalidation reason or a miss — never to data
 * that renders as current.
 */
export function readSnapshotCache<T>(options: ReadSnapshotCacheOptions<T>): SnapshotCacheRead<T> {
  const storage = getStorage();
  if (storage === null) return { outcome: 'miss' };

  let raw: string | null;
  try {
    raw = storage.getItem(cacheKey(options.key));
  } catch {
    return { outcome: 'miss' };
  }
  if (raw === null) return { outcome: 'miss' };

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch {
    return { outcome: 'invalidated', reason: 'cache-corruption' };
  }
  if (!isStoredSnapshot(parsed)) {
    return { outcome: 'invalidated', reason: 'cache-corruption' };
  }
  if (parsed.workspace !== options.workspace) {
    return { outcome: 'invalidated', reason: 'cross-workspace' };
  }
  if (parsed.schemaVersion > options.policy.schemaVersion) {
    // Written by a newer build than the running client: version regression.
    return { outcome: 'invalidated', reason: 'version-regression' };
  }

  const payload = options.validate(parsed.data);
  if (payload === null) {
    return { outcome: 'invalidated', reason: 'schema-mismatch' };
  }
  if (computeDigest(payload.data) !== parsed.digest) {
    return { outcome: 'invalidated', reason: 'cache-corruption' };
  }

  return {
    outcome: 'hit',
    snapshot: {
      data: payload.data,
      source: parsed.source,
      workspace: parsed.workspace,
      version: parsed.version,
      schemaVersion: parsed.schemaVersion,
      fetchedAt: parsed.fetchedAt,
      digest: parsed.digest,
    },
  };
}

/** Persist a verified snapshot. Failures are non-fatal (cache is best-effort). */
export function writeSnapshotCache<T>(key: string, snapshot: FreshSnapshot<T>): void {
  const storage = getStorage();
  if (storage === null) return;
  const stored: StoredSnapshot = {
    data: snapshot.data,
    source: snapshot.source,
    workspace: snapshot.workspace,
    version: snapshot.version,
    schemaVersion: snapshot.schemaVersion,
    fetchedAt: snapshot.fetchedAt,
    digest: snapshot.digest,
  };
  try {
    storage.setItem(cacheKey(key), JSON.stringify(stored));
  } catch {
    // Quota or serialization failures simply skip caching.
  }
}

/** Drop a cached snapshot (used when a surface invalidates its cache entry). */
export function clearSnapshotCache(key: string): void {
  const storage = getStorage();
  if (storage === null) return;
  try {
    storage.removeItem(cacheKey(key));
  } catch {
    // Ignorable: a wedged storage entry is detected as corruption on read.
  }
}
