/**
 * Chart export — pure helpers for the single-chart export affordance
 * (spec §08 → Per-chart export). Distinct from the workspace export envelope
 * (`export-envelope.ts`): that backs up the whole library as one JSON file; this
 * turns *one* snippet into a shareable artifact — its spec as `.vl.json`, or its
 * rendered image as PNG/SVG.
 *
 * The naming is deterministic, and so is producing a **self-contained** spec —
 * one with every saved-dataset reference replaced by its inline data, so the
 * exported file renders without Astrolabe. Both live here and are tested. The
 * clipboard write, the file download, and the image rasterization are browser-side
 * and stay in `infrastructure/file-transfer` and the chart renderer.
 */

import { collectFontFamilies } from './custom-theme';
import {
  fontDataUri,
  primaryFamilyName,
  variableFontDescriptors,
  type FontAsset,
  type FontFormat,
} from './font-asset';
import { prepareSpecForRender, type ResolvableDataset } from './rendering';
import { extractDatasetRefs } from './spec-refs';

/** The formats a single chart can be exported as (file extension == the value). */
export type ChartExportFormat = 'vl.json' | 'png' | 'svg';

/** Cap on the derived base name so a very long snippet title can't blow up the
 *  filename (filesystems and download shelves both balk past ~255 chars). */
export const MAX_BASENAME_LEN = 60;

/** C0 control characters + DEL, built from a string so no literal control byte
 *  ever lands in this source file. Stripped from filenames. */
// eslint-disable-next-line no-control-regex -- intentional: scrub control chars from names
const CONTROL_CHARS = new RegExp('[\\u0000-\\u001f\\u007f]', 'g');

/** Characters illegal on Windows or awkward across filesystems and URLs. */
const ILLEGAL_CHARS = /[\\/:*?"<>|]/g;

/**
 * Turn a snippet's display name into a filesystem- and URL-safe base name.
 *
 * Keeps the user's words and **case**, and keeps letters of *any* script (a
 * Cyrillic or CJK title stays itself — we never ASCII-fold, matching the app's
 * full-script support). Swaps whitespace and dot runs for single dashes (so the
 * extension stays unambiguous), drops control + illegal characters, collapses and
 * trims dashes, and caps the length. Falls back to `"chart"` when nothing usable
 * remains (a name of only punctuation or whitespace).
 */
export function snippetFileBasename(name: string): string {
  const cleaned = name
    .normalize('NFC')
    .replace(CONTROL_CHARS, '')
    .replace(ILLEGAL_CHARS, '')
    .replace(/[.\s]+/g, '-') // dot and whitespace runs → one dash boundary
    .replace(/-+/g, '-')
    .replace(/^-+|-+$/g, '');
  const capped = cleaned.slice(0, MAX_BASENAME_LEN).replace(/-+$/g, '');
  return capped || 'chart';
}

/**
 * Download filename for a single chart export, e.g. `sales-by-region.png`. The
 * base is derived from the snippet name; `format` is both the extension and the
 * artifact kind. No date or `astrolabe-` prefix (unlike the workspace export) —
 * the user is exporting *one named chart* and wants its name on the file.
 */
export function chartExportFilename(name: string, format: ChartExportFormat): string {
  return `${snippetFileBasename(name)}.${format}`;
}

/**
 * The saved-dataset names a spec references via `{ data: { name } }` (deduped).
 * A name the spec defines for itself via top-level `datasets` is excluded — those
 * are already self-contained. Returns `[]` for spec text that doesn't parse. Drives
 * whether the export offers an "inline referenced data" option at all.
 */
export function referencedDatasetNames(specText: string): string[] {
  // extractDatasetRefs already safe-parses a string (→ [] on bad JSON) and dedups.
  return extractDatasetRefs(specText);
}

/**
 * Re-serialize a spec with every saved-dataset reference replaced by its inline
 * data, so the exported file renders standalone (outside Astrolabe). Sizing is
 * left exactly as authored — unlike the preview, no fit-mode is applied. Throws
 * `DatasetNotFoundError` (from `prepareSpecForRender`) if the spec references a
 * name not present in `datasets`; the caller surfaces that. `specText` must be
 * valid JSON (it is the spec the editor is showing).
 */
export function inlineReferencedDatasets(
  specText: string,
  datasets: ReadonlyArray<ResolvableDataset>,
): string {
  const parsed: unknown = JSON.parse(specText);
  const resolved = prepareSpecForRender(parsed, { datasets, fitMode: 'default' });
  return JSON.stringify(resolved, null, 2);
}

// --- SVG font embedding (spec §08; scope doc §4, item 7) --------------------
//
// `view.toSVG()` writes only the `font-family` name, so an exported SVG falls
// back to a system font wherever the user's uploaded face isn't installed. We
// embed the referenced uploaded faces as base64 `@font-face` rules so the SVG
// renders the right type anywhere. Only uploaded faces are embedded — the
// roster/system stacks are decoration with their own fallbacks, and we don't
// hold their bytes here.

/** CSS `format()` keyword for an `@font-face` src — distinct from the file extension. */
const CSS_FORMAT: Record<FontFormat, string> = {
  woff2: 'woff2',
  woff: 'woff',
  ttf: 'truetype',
  otf: 'opentype',
};

/**
 * The uploaded fonts a chart actually references — its spec + the active chart
 * config, matched by primary family against the font library. Returns the
 * matched `FontAsset`s (the bytes to embed); `[]` when none are referenced or
 * the library is empty. Case-insensitive, like the naming helpers.
 */
export function referencedUploadedFonts(
  specText: string,
  config: unknown,
  fonts: ReadonlyArray<FontAsset>,
): FontAsset[] {
  if (fonts.length === 0) return [];
  let spec: unknown;
  try {
    spec = JSON.parse(specText);
  } catch {
    spec = null; // an unparseable spec still has a config that may name a font
  }
  const stacks = new Set<string>([...collectFontFamilies(spec), ...collectFontFamilies(config)]);
  const used = new Set<string>([...stacks].map((s) => primaryFamilyName(s).toLowerCase()));
  return fonts.filter((f) => used.has(f.family.toLowerCase()));
}

/** Escape a family name for use inside a double-quoted CSS string. */
function cssQuote(family: string): string {
  return family.replace(/[\\"]/g, '\\$&');
}

/** Build one `@font-face` rule embedding a face's bytes as a data-URI `src`. */
function fontFaceRule(font: FontAsset): string {
  const desc = variableFontDescriptors(font.axes);
  const weight = desc.weight ? `font-weight:${desc.weight};` : '';
  const stretch = desc.stretch ? `font-stretch:${desc.stretch};` : '';
  return (
    `@font-face{font-family:"${cssQuote(font.family)}";${weight}${stretch}` +
    `src:url(${fontDataUri(font)}) format("${CSS_FORMAT[font.format]}");}`
  );
}

/**
 * Inject a `<style>` of `@font-face` rules (base64 `src`) as the first child of
 * the root `<svg>`, so the serialized chart carries the uploaded faces it uses.
 * Returns `svg` unchanged when there are no fonts. The CSS is wrapped in CDATA
 * (an SVG is XML; a family name could contain `&`/`<`), with the one sequence
 * that can't appear in CDATA — `]]>` — split defensively.
 */
export function embedFontsInSvg(svg: string, fonts: ReadonlyArray<FontAsset>): string {
  if (fonts.length === 0) return svg;
  const css = fonts.map(fontFaceRule).join('').replace(/]]>/g, ']]]]><![CDATA[>');
  const style = `<style type="text/css"><![CDATA[${css}]]></style>`;
  return svg.replace(/(<svg\b[^>]*>)/, `$1${style}`);
}
