import type { ProjectEditableState } from "@dada/shared-contracts";

export type ProjectSaveStatus = "dirty" | "saving" | "saved" | "failed" | "conflicted";

export class ProjectStateConflict extends Error {
  readonly latestStateVersion: number;

  constructor(latestStateVersion: number) {
    super("project_state_conflict");
    this.latestStateVersion = latestStateVersion;
  }
}

export class ConflictExportGuard {
  used = false;

  async run(action: () => Promise<void>) {
    if (this.used) return false;
    this.used = true;
    await action();
    return true;
  }
}

export class SessionHistory<T> {
  private current: T;
  private readonly past: T[] = [];
  private readonly future: T[] = [];

  constructor(initial: T) {
    this.current = structuredClone(initial);
  }

  get canRedo() { return this.future.length > 0; }
  get canUndo() { return this.past.length > 0; }
  get value() { return structuredClone(this.current); }

  commit(next: T) {
    this.past.push(structuredClone(this.current));
    this.current = structuredClone(next);
    this.future.length = 0;
  }

  undo() {
    const previous = this.past.pop();
    if (previous === undefined) return undefined;
    this.future.push(structuredClone(this.current));
    this.current = previous;
    return structuredClone(this.current);
  }

  redo() {
    const next = this.future.pop();
    if (next === undefined) return undefined;
    this.past.push(structuredClone(this.current));
    this.current = next;
    return structuredClone(this.current);
  }
}

interface PendingSave {
  operationId: string;
  snapshot: ProjectEditableState;
}

export class ProjectAutoSaveQueue {
  status: ProjectSaveStatus = "saved";
  stateVersion: number;
  conflictVersion?: number;

  private disposed = false;
  private inFlight: Promise<boolean> | undefined;
  private lastSaved: ProjectEditableState;
  private pending: PendingSave | undefined;
  private retryIndex = 0;
  private timer: ReturnType<typeof setTimeout> | undefined;
  private readonly debounceMs: number;
  private readonly onConflict: ((latestVersion: number) => void) | undefined;
  private readonly onSaved: ((snapshot: ProjectEditableState, stateVersion: number) => void) | undefined;
  private readonly onStatus: ((status: ProjectSaveStatus) => void) | undefined;
  private readonly retryDelaysMs: number[];
  private readonly save: (snapshot: ProjectEditableState, stateVersion: number, operationId: string) => Promise<{ stateVersion: number }>;

  constructor(input: {
    debounceMs?: number;
    initialState: ProjectEditableState;
    initialVersion: number;
    onConflict?: (latestVersion: number) => void;
    onSaved?: (snapshot: ProjectEditableState, stateVersion: number) => void;
    onStatus?: (status: ProjectSaveStatus) => void;
    retryDelaysMs?: number[];
    save: (snapshot: ProjectEditableState, stateVersion: number, operationId: string) => Promise<{ stateVersion: number }>;
  }) {
    this.debounceMs = input.debounceMs ?? 1_000;
    this.lastSaved = structuredClone(input.initialState);
    this.onConflict = input.onConflict;
    this.onSaved = input.onSaved;
    this.onStatus = input.onStatus;
    this.retryDelaysMs = input.retryDelaysMs ?? [1_000, 2_000, 4_000, 8_000, 15_000];
    this.save = input.save;
    this.stateVersion = input.initialVersion;
  }

  commit(snapshot: ProjectEditableState) {
    if (this.disposed || this.status === "conflicted") return;
    this.pending = { operationId: crypto.randomUUID(), snapshot: structuredClone(snapshot) };
    if (!this.inFlight) {
      this.setStatus("dirty");
      this.schedule(this.debounceMs);
    }
  }

  async saveNow() {
    if (this.disposed || (this.status as ProjectSaveStatus) === "conflicted") return false;
    this.clearTimer();
    if (this.inFlight) await this.inFlight;
    if (this.disposed || this.status === "conflicted") return false;
    if (!this.pending) return this.status === "saved";
    return this.startSave();
  }

  dispose() {
    this.disposed = true;
    this.clearTimer();
  }

  private clearTimer() {
    if (this.timer) clearTimeout(this.timer);
    this.timer = undefined;
  }

  private schedule(delay: number) {
    this.clearTimer();
    this.timer = setTimeout(() => {
      this.timer = undefined;
      void this.startSave();
    }, delay);
  }

  private startSave() {
    if (this.inFlight || !this.pending || this.disposed || this.status === "conflicted") {
      return this.inFlight ?? Promise.resolve(false);
    }
    const request = this.pending;
    this.pending = undefined;
    this.setStatus("saving");
    const attempt = this.save(structuredClone(request.snapshot), this.stateVersion, request.operationId)
      .then((result) => {
        this.stateVersion = result.stateVersion;
        this.lastSaved = structuredClone(request.snapshot);
        this.retryIndex = 0;
        this.onSaved?.(structuredClone(request.snapshot), this.stateVersion);
        if (this.pending) {
          this.setStatus("dirty");
          this.schedule(this.debounceMs);
        } else {
          this.setStatus("saved");
        }
        return true;
      })
      .catch((error: unknown) => {
        if (error instanceof ProjectStateConflict) {
          this.pending = undefined;
          this.conflictVersion = error.latestStateVersion;
          this.setStatus("conflicted");
          this.onConflict?.(error.latestStateVersion);
          return false;
        }
        if (!this.pending) this.pending = request;
        this.setStatus("failed");
        const delay = this.retryDelaysMs[Math.min(this.retryIndex, this.retryDelaysMs.length - 1)] ?? 15_000;
        this.retryIndex += 1;
        this.schedule(delay);
        return false;
      })
      .finally(() => {
        if (this.inFlight === attempt) this.inFlight = undefined;
      });
    this.inFlight = attempt;
    return attempt;
  }

  private setStatus(status: ProjectSaveStatus) {
    this.status = status;
    this.onStatus?.(status);
  }
}
