/**
 * Import / Export service (spec §08) — orchestration over the pure core helpers
 * and the stores. The deterministic work (shape detection, normalization, name
 * de-dupe, id reassignment, rename propagation, envelope/message building) lives
 * in `@core/import-normalize` and `@core/export-envelope`; this layer reads the
 * stores, drives the browser file adapter, commits the merge, and reports.
 *
 * Order matters on import (spec §08 → Merge): datasets and custom themes are
 * committed **before** snippets so a snippet's by-name reference resolves
 * against the just-added dataset (whose name may have been auto-suffixed to
 * avoid a clash).
 *
 * Atomicity (spec §08 → Storage limit handling / spec §10 → Non-destructive
 * import): snippet writes are performed directly to IDB before touching the
 * Zustand store. If any write fails the already-written IDB records are deleted
 * and the just-added datasets/themes are rolled back from the store, so neither
 * the store nor IDB retains a partial import. The write-through subscriber sees
 * store changes only after all IDB writes succeed; because `put` is idempotent
 * the second (subscriber) write of each record is a harmless no-op.
 */

import { buildExportEnvelope, exportFilename, transferSummaryMessage } from '@core/export-envelope';
import {
  applyDatasetRenamesToSnippets,
  dedupeIncomingNames,
  dropClashingFonts,
  normalizeImport,
  reassignCollidingSnippetIds,
} from '@core/import-normalize';
import { snippetSizeBytes } from '@core/snippet';
import { humanizeBytes } from '@core/storage-estimate';
import { downloadJson, readTextFile } from '../infrastructure/file-transfer';
import { registerFontAssets } from '../infrastructure/font-faces';
import { deleteSnippet, saveSnippet } from '../infrastructure/snippet-store';
import { StorageQuotaError } from '../infrastructure/db';
import { notify } from '../stores/NotificationStore';
import { useCustomThemeStore } from '../stores/CustomThemeStore';
import { useDatasetStore } from '../stores/DatasetStore';
import { useFontStore } from '../stores/FontStore';
import { useSnippetStore } from '../stores/SnippetStore';

/** Practical snippet-storage budget (spec §02 storage monitor / §08). */
const SNIPPET_BUDGET_BYTES = 5 * 1024 * 1024;

/**
 * Export the whole workspace to a downloaded JSON file (spec §08 → Export).
 * Flushes the live editor buffer first so in-progress draft edits are included.
 * An empty library is reported and nothing is downloaded, even if datasets exist.
 */
export function exportWorkspace(now: Date = new Date()): void {
  // Commit any valid, uncommitted buffer so the export reflects current work.
  useSnippetStore.getState().commitDraft(now);

  const snippets = useSnippetStore.getState().snippets;
  const datasets = useDatasetStore.getState().datasets;
  const themes = useCustomThemeStore.getState().themes;
  const fonts = useFontStore.getState().fonts;

  if (snippets.length === 0) {
    notify({
      kind: 'info',
      title: 'Nothing to export',
      message: 'No snippets to export. Create a snippet first, then export your workspace.',
    });
    return;
  }

  // Uploaded font faces travel base64-encoded inside the envelope, so a theme or
  // snippet referencing one renders on another machine instead of falling back to
  // a system font (spec §08, scope doc §4).
  const envelope = buildExportEnvelope(snippets, datasets, themes, fonts, { now });
  downloadJson(exportFilename(now), JSON.stringify(envelope, null, 2));

  notify({
    kind: 'success',
    title: 'Workspace exported',
    message: transferSummaryMessage(
      'Exported',
      snippets.length,
      datasets.length,
      themes.length,
      fonts.length,
    ),
  });
}

/**
 * Import a picked JSON file: normalize, merge (never overwrite), and save
 * (spec §08 → Import). The existing workspace is never lost — an unreadable or
 * unparseable file leaves it untouched, and merges only ever append.
 */
export async function importWorkspace(file: File): Promise<void> {
  let text: string;
  try {
    text = await readTextFile(file);
  } catch (err) {
    notify({
      kind: 'error',
      title: "Couldn't read the file",
      message: 'The selected file could not be read. Choose the file again and retry.',
      detail: err instanceof Error ? `${err.name}: ${err.message}` : String(err),
    });
    return;
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(text);
  } catch {
    notify({
      kind: 'error',
      title: 'Import failed',
      message: 'Failed to import. Please check that the file is valid JSON.',
    });
    return;
  }

  const {
    snippets: normSnippets,
    datasets: normDatasets,
    themes: normThemes,
    fonts: normFonts,
  } = normalizeImport(parsed);

  if (normSnippets.length === 0) {
    notify({
      kind: 'info',
      title: 'Nothing imported',
      message: 'No snippets found in file.',
    });
    return;
  }

  // Datasets first: de-dupe their names against the library (and within the batch),
  // then propagate any rename into the imported snippets so their references still
  // resolve (spec §08 → Name conflicts; docs/architecture/07 §5–6).
  const existingDatasetNames = useDatasetStore.getState().datasets.map((d) => d.name);
  const { records: dedupedDatasets, renames } = dedupeIncomingNames(
    existingDatasetNames,
    normDatasets,
  );
  const renamedSnippets = applyDatasetRenamesToSnippets(normSnippets, renames);

  // Themes key on a unique name too, but nothing references them by name, so a
  // rename needs no propagation — only reporting (spec §08 → Name conflicts).
  const existingThemeNames = useCustomThemeStore.getState().themes.map((t) => t.name);
  const { records: dedupedThemes, renames: themeRenames } = dedupeIncomingNames(
    existingThemeNames,
    normThemes,
  );

  // Fonts key on a unique family. A clash skips the incoming face (kept existing —
  // a same-named face already satisfies the reference), so no propagation either,
  // just reporting (spec §08 → Name conflicts; scope doc §4).
  const existingFontFamilies = useFontStore.getState().fonts.map((f) => f.family);
  const { records: newFonts, skipped: skippedFonts } = dropClashingFonts(
    existingFontFamilies,
    normFonts,
  );

  // Reassign incoming snippet ids that clash with the library (spec §08 → ID collisions).
  const existingSnippetIds = useSnippetStore.getState().snippets.map((s) => s.id);
  const finalSnippets = reassignCollidingSnippetIds(existingSnippetIds, renamedSnippets);

  // Commit datasets and themes BEFORE snippets so by-name references resolve
  // (spec §08). Record which ids existed before the add so we can roll them back
  // if the subsequent snippet writes fail.
  const datasetIdsBefore = new Set(useDatasetStore.getState().datasets.map((d) => d.id));
  useDatasetStore.getState().addDatasets(dedupedDatasets);
  const themeIdsBefore = new Set(useCustomThemeStore.getState().themes.map((t) => t.id));
  useCustomThemeStore.getState().addThemes(dedupedThemes);
  // Fonts join the same pre-snippet commit so they roll back together on failure.
  // Live `FontFace` registration is deferred to the success path below.
  const fontIdsBefore = new Set(useFontStore.getState().fonts.map((f) => f.id));
  useFontStore.getState().addFonts(newFonts);

  // Storage budget pre-check (spec §08 → Storage limit handling): warn on overage
  // but still attempt the save.
  const existingBytes = useSnippetStore
    .getState()
    .snippets.reduce((n, s) => n + snippetSizeBytes(s), 0);
  const incomingBytes = finalSnippets.reduce((n, s) => n + snippetSizeBytes(s), 0);
  const overage = existingBytes + incomingBytes - SNIPPET_BUDGET_BYTES;

  // Atomic snippet write (spec §08 → Storage limit handling / spec §10 →
  // Non-destructive import): write every snippet to IDB before updating the
  // Zustand store. A quota failure mid-write is caught here, the already-written
  // records are deleted, the just-added datasets are removed from the store, and
  // we surface a clear actionable error — leaving the workspace exactly as before.
  // After all writes succeed, `addSnippets` makes them visible in the store; the
  // write-through subscriber re-runs `saveSnippet` for each, but `put` is
  // idempotent so those second writes are harmless.
  const writtenSnippetIds: string[] = [];
  try {
    for (const snippet of finalSnippets) {
      await saveSnippet(snippet);
      writtenSnippetIds.push(snippet.id);
    }
  } catch (err) {
    // Roll back: delete every IDB record we already wrote.
    await Promise.allSettled(writtenSnippetIds.map((id) => deleteSnippet(id)));

    // Roll back: remove the datasets and themes we just added to the store so
    // neither store nor IDB retains any trace of this import.
    const addedDatasets = useDatasetStore
      .getState()
      .datasets.filter((d) => !datasetIdsBefore.has(d.id));
    for (const d of addedDatasets) {
      useDatasetStore.getState().remove(d.id);
    }
    const addedThemes = useCustomThemeStore
      .getState()
      .themes.filter((t) => !themeIdsBefore.has(t.id));
    for (const t of addedThemes) {
      useCustomThemeStore.getState().remove(t.id);
    }
    const addedFonts = useFontStore.getState().fonts.filter((f) => !fontIdsBefore.has(f.id));
    for (const f of addedFonts) {
      useFontStore.getState().remove(f.id);
    }

    // Surface a clear, actionable error (spec §08 "Quota failure"; NN/g #9 /
    // GOV.UK plain language — no codes, tell the user what to do next).
    if (err instanceof StorageQuotaError) {
      notify({
        kind: 'error',
        title: 'Import failed — storage full',
        message:
          'The import could not be saved because snippet storage is full. ' +
          'Delete snippets you no longer need to free up space, then try importing again.',
      });
    } else {
      notify({
        kind: 'error',
        title: 'Import failed',
        message:
          'A storage error prevented the import from completing. The workspace was not changed. ' +
          'If this keeps happening, your browser may be blocking local storage.',
        detail:
          err instanceof Error ? `Snippet save failed: ${err.name}: ${err.message}` : String(err),
      });
    }
    return;
  }

  // All IDB writes succeeded — now make the snippets visible in the store.
  useSnippetStore.getState().addSnippets(finalSnippets);

  // Register the imported faces on document.fonts so charts using them render
  // immediately (mirrors the upload path) — only now that the import has committed.
  const addedFonts = useFontStore.getState().fonts.filter((f) => !fontIdsBefore.has(f.id));
  registerFontAssets(addedFonts);

  // Feedback (spec §08 → Feedback): one summary toast; a warning when records were
  // renamed, fonts were skipped, or storage is over budget, otherwise a success.
  const summary = transferSummaryMessage(
    'Imported',
    finalSnippets.length,
    dedupedDatasets.length,
    dedupedThemes.length,
    newFonts.length,
  );
  const clauses: string[] = [];
  const allRenames = [...renames, ...themeRenames];
  if (allRenames.length > 0) {
    clauses.push(
      `Renamed to avoid clashes: ${allRenames.map((r) => `${r.from} → ${r.to}`).join(', ')}.`,
    );
  }
  if (skippedFonts.length > 0) {
    clauses.push(`Skipped fonts already in your library: ${skippedFonts.join(', ')}.`);
  }
  if (overage > 0) {
    clauses.push(
      `This puts snippet storage about ${humanizeBytes(overage)} over the ~5 MB budget; ` +
        `consider deleting some snippets.`,
    );
  }

  if (clauses.length > 0) {
    notify({
      kind: 'warning',
      title: 'Import complete',
      message: `${summary}. ${clauses.join(' ')}`,
    });
  } else {
    notify({ kind: 'success', title: 'Import complete', message: summary });
  }
}
