import { DestroyRef, effect, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Result } from '@shared/kernel/fp';
import { runSubmit, SUBMIT_FAILED } from '@shared/application/submit';
import { SubmitApplicationRequest, SubmitApplicationResponse } from '@shared/infrastructure/api-client';
import { AanvraagType } from '@registratie/domain/aanvraag';
import { ApplicationsAdapter, parseApplications } from '@registratie/infrastructure/applications.adapter';

/** What a wizard persists per step: the opaque machine snapshot + progress + docs. */
export interface DraftSnapshot {
  draft: unknown;
  stepIndex: number;
  stepCount: number;
  documentIds: string[];
}

export interface DraftSyncDeps {
  type: AanvraagType;
  /** The machine snapshot while it's worth persisting; null when not (pristine/done). */
  snapshot: () => DraftSnapshot | null;
  /** Seed the machine from a resumed draft. Called at most once, on init, and ONLY
      with a real draft on a still-pristine machine — see `applyResume`. */
  onResume: (draft: unknown) => void;
  /** Draft-sync only runs in the real app — false in Storybook/tests (explicit seed). */
  enabled: () => boolean;
}

const DEBOUNCE_MS = 600; // ponytail: fixed debounce; tune if the sync feels laggy/chatty.

/**
 * The effectful glue that replaces per-wizard sessionStorage with a backend-owned
 * Concept (PRD 0001, phase D). Instantiated in a field initializer (like
 * `createStore`/`createUploadController`). Responsibilities:
 *
 *  - resume: a `?aanvraag=<id>` link wins; otherwise resume the ONE existing Concept of
 *    this type (at most one per type), seeding the machine from its saved draft;
 *  - create-on-first-progress: when no Concept exists, one is created lazily the first
 *    time the wizard reports a non-null snapshot, and its id is stamped into the URL;
 *  - debounced draft sync on every subsequent change.
 *
 * Inert without a Router (stories) or when `enabled()` is false — no network, no resume.
 */
export function createDraftSync(deps: DraftSyncDeps) {
  const adapter = inject(ApplicationsAdapter);
  const router = inject(Router, { optional: true });
  const route = inject(ActivatedRoute, { optional: true });
  const active = () => deps.enabled() && !!router && !!route;

  let id: string | undefined;
  let ensuring: Promise<string> | undefined; // in-flight create, so we never create twice
  let timer: ReturnType<typeof setTimeout> | undefined;
  // Resolves once resume() has decided whether a Concept of this type already exists;
  // gates ensureId so a fast typist can't create a duplicate before that lookup lands.
  let resumeGate: Promise<unknown> = Promise.resolve();

  const ensureId = async (): Promise<string> => {
    await resumeGate;
    if (id) return id;
    ensuring ??= adapter.create(deps.type).then((newId) => {
      id = newId;
      // Stamp the id into the URL (no navigation) so a reload resumes this Concept.
      void router!.navigate([], { relativeTo: route!, queryParams: { aanvraag: newId }, queryParamsHandling: 'merge', replaceUrl: true });
      return newId;
    });
    return ensuring;
  };

  // Apply a resumed draft only when it's safe to: a late lookup must never clobber
  // progress the user already made while it was in flight, and "start fresh" needs no
  // dispatch (the machine already starts fresh). snapshot() is non-null once the user
  // has real progress.
  const applyResume = (draft: unknown | null) => {
    if (draft == null || deps.snapshot() != null) return;
    deps.onResume(draft);
  };

  const flush = async () => {
    const snap = deps.snapshot();
    if (!snap) return;
    const theId = await ensureId();
    await adapter.syncDraft(theId, { draft: snap.draft, stepIndex: snap.stepIndex, stepCount: snap.stepCount, documentIds: snap.documentIds });
  };

  // One effect watches the snapshot; each change resets a debounce timer. The timer's
  // callback only does network I/O (never dispatch), so it can't livelock the store.
  effect(() => {
    if (!active()) return;
    const snap = deps.snapshot(); // tracked: fires on every machine change
    if (!snap) return;
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => void flush(), DEBOUNCE_MS);
  });

  inject(DestroyRef).onDestroy(() => timer && clearTimeout(timer));

  // Attach to a specific Concept id and seed the machine from its draft. A non-Concept
  // (submitted/gone) id is treated as fresh so it can't reopen as an editable draft.
  const load = (linked: string): Promise<void> => {
    id = linked;
    return adapter
      .detail(linked)
      .then((dto) => {
        if (dto.status && dto.status.tag !== 'Concept') {
          id = undefined;
          applyResume(null);
          return;
        }
        applyResume(dto.draft ?? null);
      })
      .catch(() => {
        id = undefined;
        applyResume(null); // unknown/deleted id → start fresh
      });
  };

  // Find the user's existing Concept of this type (at most one), if any.
  const findConcept = async (): Promise<string | undefined> => {
    try {
      const parsed = parseApplications(await adapter.list());
      return parsed.ok ? parsed.value.find((a) => a.type === deps.type && a.status.tag === 'Concept')?.id : undefined;
    } catch {
      return undefined;
    }
  };

  return {
    /** Resolve the initial state: a `?aanvraag` link wins; else resume this type's
        existing Concept; else start fresh (a Concept is created on first progress). */
    async resume() {
      let release!: () => void;
      resumeGate = new Promise<void>((r) => (release = r));
      try {
        if (!active()) {
          applyResume(null);
          return;
        }
        const linked = route!.snapshot.queryParamMap.get('aanvraag');
        if (linked) {
          await load(linked);
          return;
        }
        const existing = await findConcept();
        if (existing) {
          await load(existing);
          // Stamp the id into the URL so a reload resumes the same Concept.
          void router!.navigate([], { relativeTo: route!, queryParams: { aanvraag: existing }, queryParamsHandling: 'merge', replaceUrl: true });
          return;
        }
        applyResume(null);
      } finally {
        release();
      }
    },

    /** Submit through the aanvraag lifecycle: ensure the Concept exists, then
        `POST /applications/{id}/submit` (server sets autoApprovable + transitions).
        Folded into a Result like the old submit-* commands. */
    submit(body: SubmitApplicationRequest): Promise<Result<string, SubmitApplicationResponse>> {
      return runSubmit(async () => adapter.submit(await ensureId(), body), SUBMIT_FAILED);
    },

    /** Restart: discard the current in-progress Concept (delete it) and detach, so a
        fresh one is created on next progress. Keeps the one-per-type invariant. A
        submitted id can't be deleted (409, caught) — that submission correctly remains,
        and detaching still lets the user start a new Concept. */
    reset() {
      if (id) {
        void adapter.cancel(id).catch(() => {}); // Concept → deleted; submitted → 409, kept
        id = undefined;
        ensuring = undefined;
      }
      if (active()) void router!.navigate([], { relativeTo: route!, queryParams: { aanvraag: null }, queryParamsHandling: 'merge', replaceUrl: true });
    },
  };
}
