/**
 * Zod schemas for the chat `MessageBlock` discriminated union (`./block.ts`).
 *
 * These are the runtime VALIDATORS used at the single registry dispatch point
 * (`messages/blocks/MessageBlocks.tsx`): every block flows through
 * `safeParseBlock(BLOCK_SCHEMAS[kind], block)` BEFORE its renderer runs, so a
 * malformed wire / hand-authored payload renders a safe `<BlockError>` card
 * instead of throwing inside a renderer and blanking the transcript.
 *
 * Design rules (mirroring `common/blocks/schemas.ts`):
 *  - The TS types in `./block.ts` stay the source of truth; these schemas are
 *    kept in sync and `Extends<…>`-checked below so they can't silently drift.
 *  - The map / link shapes are COMPOSED from the shared
 *    `MapBlockPayloadSchema` / `LinkPreviewDataSchema` in `common/blocks` — we
 *    never re-describe a map/link payload here.
 *  - LENIENT where the renderer is tolerant: optional fields stay optional,
 *    `json.data` is `unknown` (JsonTree is throw-safe), extra keys are stripped.
 *  - STRICT on the load-bearing shape: a field whose wrong type would THROW in
 *    the renderer is required + typed (gallery `items` must be an array;
 *    image/video/audio `src` must be a string; map coords are finite
 *    numbers via the shared schema).
 *  - `custom` and the prose kinds (`text`/`markdown`) carry no schema in
 *    `BLOCK_SCHEMAS` — `custom` is host-defined (don't over-constrain) and the
 *    prose kinds can't throw on a wrong-typed field worth guarding. They render
 *    as-is. (`text`/`markdown` schemas are still defined for completeness.)
 */

import { z } from 'zod';

import { MapBlockPayloadSchema, LinkPreviewDataSchema } from '../../../common/blocks';
import type {
  TextBlock,
  MarkdownBlock,
  AudioBlock,
  VideoBlock,
  ImageBlock,
  GalleryBlock,
  MapBlock,
  JsonBlock,
  MermaidBlock,
  CodeBlock,
  DiffBlock,
  LinkBlock,
  MessageBlock,
} from './block';

/**
 * Base fields shared by every block (`BlockBase` in `./block.ts`). We validate
 * the WHOLE block object (incl. `kind` / `id` / `caption`), so each schema
 * composes this. `id` is required (it is the React key + memo discriminant).
 */
const BlockBaseSchema = z.object({
  id: z.string(),
  caption: z.string().optional(),
});

const TextBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('text'),
  text: z.string(),
});

const MarkdownBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('markdown'),
  markdown: z.string(),
});

const AudioBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('audio'),
  src: z.string(),
  title: z.string().optional(),
  artist: z.string().optional(),
  cover: z.string().optional(),
  variant: z.enum(['default', 'compact']).optional(),
});

const VideoBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('video'),
  src: z.string(),
  poster: z.string().optional(),
  title: z.string().optional(),
  aspectRatio: z.union([z.number(), z.literal('auto'), z.literal('fill')]).optional(),
});

const ImageBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('image'),
  src: z.string(),
  alt: z.string().optional(),
  interactive: z.boolean().optional(),
});

const GalleryItemSchema = z.object({
  id: z.string(),
  src: z.string(),
  thumbnail: z.string().optional(),
  alt: z.string().optional(),
});

const GalleryBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('gallery'),
  // STRICT: `items` must be an array — the renderer maps over it.
  items: z.array(GalleryItemSchema),
});

/**
 * Map block = the shared serializable map payload (`MapBlockPayloadSchema`,
 * which already enforces finite-number `center.lat/lng` etc.) + the base block
 * fields + the chat-only `userLocation` opt-in flag. Composed, never
 * duplicated.
 */
const MapBlockSchema = MapBlockPayloadSchema.merge(BlockBaseSchema).extend({
  kind: z.literal('map'),
  userLocation: z.boolean().optional(),
});

const JsonBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('json'),
  // LENIENT: JsonTree renders anything safely — accept any payload.
  data: z.unknown(),
  mode: z.enum(['full', 'compact', 'inline']).optional(),
});

const MermaidBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('mermaid'),
  chart: z.string(),
});

const CodeBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('code'),
  code: z.string(),
  language: z.string(),
});

const DiffBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('diff'),
  oldCode: z.string().optional(),
  newCode: z.string().optional(),
  patch: z.string().optional(),
  language: z.string().optional(),
  layout: z.enum(['split', 'unified']).optional(),
});

/**
 * Link block = base fields + `url` + optional pre-resolved `data` (validated
 * against the shared `LinkPreviewDataSchema`). The renderer also tolerates a
 * resolver-only block (no `data`), so `data` stays optional.
 */
const LinkBlockSchema = BlockBaseSchema.extend({
  kind: z.literal('link'),
  url: z.string(),
  data: LinkPreviewDataSchema.optional(),
});

// ── Drift guards ──────────────────────────────────────────────────────────
// Type-level assertion that each schema's inferred output stays assignable to
// the canonical block type in `./block.ts`. If a schema and the type diverge,
// the corresponding assignment fails to compile.
type Extends<A, B> = A extends B ? true : never;
const _textOk: Extends<z.infer<typeof TextBlockSchema>, TextBlock> = true;
const _markdownOk: Extends<z.infer<typeof MarkdownBlockSchema>, MarkdownBlock> = true;
const _audioOk: Extends<z.infer<typeof AudioBlockSchema>, AudioBlock> = true;
const _videoOk: Extends<z.infer<typeof VideoBlockSchema>, VideoBlock> = true;
const _imageOk: Extends<z.infer<typeof ImageBlockSchema>, ImageBlock> = true;
const _galleryOk: Extends<z.infer<typeof GalleryBlockSchema>, GalleryBlock> = true;
const _mapOk: Extends<z.infer<typeof MapBlockSchema>, MapBlock> = true;
const _jsonOk: Extends<z.infer<typeof JsonBlockSchema>, JsonBlock> = true;
const _mermaidOk: Extends<z.infer<typeof MermaidBlockSchema>, MermaidBlock> = true;
const _codeOk: Extends<z.infer<typeof CodeBlockSchema>, CodeBlock> = true;
const _diffOk: Extends<z.infer<typeof DiffBlockSchema>, DiffBlock> = true;
const _linkOk: Extends<z.infer<typeof LinkBlockSchema>, LinkBlock> = true;
void [
  _textOk, _markdownOk, _audioOk, _videoOk, _imageOk, _galleryOk, _mapOk,
  _jsonOk, _mermaidOk, _codeOk, _diffOk, _linkOk,
];

/**
 * `kind` → schema. Validated at the registry dispatch point. Kinds WITHOUT an
 * entry are skipped (rendered as-is):
 *  - `custom` — host-defined `payload`; over-constraining it would break hosts.
 *
 * Every built-in data block is covered so a malformed payload is caught
 * centrally — and any FUTURE kind is guarded the moment it's added here.
 */
export const BLOCK_SCHEMAS: Partial<Record<MessageBlock['kind'], z.ZodType>> = {
  text: TextBlockSchema,
  markdown: MarkdownBlockSchema,
  audio: AudioBlockSchema,
  video: VideoBlockSchema,
  image: ImageBlockSchema,
  gallery: GalleryBlockSchema,
  map: MapBlockSchema,
  json: JsonBlockSchema,
  mermaid: MermaidBlockSchema,
  code: CodeBlockSchema,
  diff: DiffBlockSchema,
  link: LinkBlockSchema,
  // `custom` intentionally omitted — host-defined, rendered as-is.
};
