import type { SaveSlotId } from "./types";

interface StorageAdapter {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
}

type TimerHandle = ReturnType<typeof setTimeout>;
type UploadSave = (slotId: SaveSlotId) => Promise<boolean>;

const PENDING_KEY = "i-want-to-heal:saves:pending-sync:v1";
export const SAVE_SYNC_RETRY_DELAYS_MS = [2_000, 10_000, 30_000, 120_000] as const;
const fallbackMemory = new Map<string, string>();
const fallbackStorage: StorageAdapter = {
  getItem: (key) => fallbackMemory.get(key) ?? null,
  setItem: (key, value) => { fallbackMemory.set(key, value); },
};

function browserStorage(): StorageAdapter {
  try {
    if (typeof localStorage !== "undefined") return localStorage;
  } catch {
    // Android WebView can deny storage before its host is ready.
  }
  return fallbackStorage;
}

function isSlotId(value: number): value is SaveSlotId {
  return value === 1 || value === 2 || value === 3;
}

export function networkAppearsOnline() {
  return typeof navigator === "undefined" || navigator.onLine !== false;
}

export class PendingSaveSyncRepository {
  constructor(private readonly storage: StorageAdapter = browserStorage()) {}

  list(): SaveSlotId[] {
    try {
      const parsed = JSON.parse(this.storage.getItem(PENDING_KEY) ?? "[]") as unknown;
      if (!Array.isArray(parsed)) return [];
      return [...new Set(parsed.map(Number).filter(isSlotId))];
    } catch {
      return [];
    }
  }

  has(slotId: SaveSlotId) {
    return this.list().includes(slotId);
  }

  mark(slotId: SaveSlotId) {
    const slots = this.list();
    if (!slots.includes(slotId)) this.write([...slots, slotId]);
  }

  clear(slotId: SaveSlotId) {
    this.write(this.list().filter((candidate) => candidate !== slotId));
  }

  private write(slots: readonly SaveSlotId[]) {
    this.storage.setItem(PENDING_KEY, JSON.stringify(slots));
  }
}

export class SaveSyncRetryCoordinator {
  private readonly attempts = new Map<SaveSlotId, number>();
  private readonly timers = new Map<SaveSlotId, TimerHandle>();
  private readonly active = new Set<SaveSlotId>();
  private disposed = false;

  constructor(
    private readonly pending: PendingSaveSyncRepository,
    private readonly upload: UploadSave,
    private readonly isOnline: () => boolean = networkAppearsOnline,
    private readonly schedule: (run: () => void, delay: number) => TimerHandle = setTimeout,
    private readonly cancel: (timer: TimerHandle) => void = clearTimeout,
  ) {}

  flush(resetAttempts = false) {
    if (resetAttempts) this.attempts.clear();
    if (!this.isOnline()) return;
    for (const slotId of this.pending.list()) this.run(slotId);
  }

  failed(slotId: SaveSlotId) {
    if (this.disposed || !this.pending.has(slotId) || !this.isOnline() || this.timers.has(slotId)) return;
    const attempt = this.attempts.get(slotId) ?? 0;
    const delay = SAVE_SYNC_RETRY_DELAYS_MS[attempt];
    if (delay === undefined) return;
    this.attempts.set(slotId, attempt + 1);
    const timer = this.schedule(() => {
      this.timers.delete(slotId);
      this.run(slotId);
    }, delay);
    this.timers.set(slotId, timer);
  }

  succeeded(slotId: SaveSlotId) {
    this.attempts.delete(slotId);
    const timer = this.timers.get(slotId);
    if (timer !== undefined) this.cancel(timer);
    this.timers.delete(slotId);
  }

  dispose() {
    this.disposed = true;
    for (const timer of this.timers.values()) this.cancel(timer);
    this.timers.clear();
    this.active.clear();
  }

  private run(slotId: SaveSlotId) {
    if (this.disposed || this.active.has(slotId) || this.timers.has(slotId) || !this.pending.has(slotId) || !this.isOnline()) return;
    this.active.add(slotId);
    void this.upload(slotId).finally(() => this.active.delete(slotId));
  }
}

const pendingSaveSync = new PendingSaveSyncRepository();
let activeCoordinator: SaveSyncRetryCoordinator | null = null;

export function markSaveSyncPending(slotId: SaveSlotId) {
  pendingSaveSync.mark(slotId);
}

export function hasPendingSaveSync(slotId: SaveSlotId) {
  return pendingSaveSync.has(slotId);
}

export function clearSaveSyncPending(slotId: SaveSlotId) {
  pendingSaveSync.clear(slotId);
  activeCoordinator?.succeeded(slotId);
}

export function scheduleSaveSyncRetry(slotId: SaveSlotId) {
  activeCoordinator?.failed(slotId);
}

export function startSaveSyncCoordinator(upload: UploadSave) {
  const coordinator = new SaveSyncRetryCoordinator(pendingSaveSync, upload);
  activeCoordinator?.dispose();
  activeCoordinator = coordinator;
  const onOnline = () => coordinator.flush(true);
  window.addEventListener("online", onOnline);
  coordinator.flush();
  return () => {
    window.removeEventListener("online", onOnline);
    coordinator.dispose();
    if (activeCoordinator === coordinator) activeCoordinator = null;
  };
}
