'use client';

/**
 * `<MessageBlocks>` — walks `message.blocks`, dispatches each through the
 * registry, and renders it under the bubble (text/markdown in a bubble
 * wrapper, media full-width). Per-block error isolation + unknown-kind
 * fallback keep one bad block from blanking the transcript.
 */

import { Component, type ReactNode } from 'react';

import { cn } from '@djangocfg/ui-core/lib';

import { BlockError, safeParseBlock } from '../../../../common/blocks';
import type { MessageBlock } from '../../types/block';
import { BLOCK_SCHEMAS } from '../../types/blockSchemas';
import { useChatBubbleStyles, BLOCK_SURFACE } from '../../styles';
import { BUILTIN_BLOCK_REGISTRY, type BlockRegistry } from './registry';

export interface MessageBlocksProps {
  blocks?: MessageBlock[];
  /** Registry of `kind` → renderer. Defaults to `BUILTIN_BLOCK_REGISTRY`. */
  registry?: BlockRegistry;
  /** Spaciousness forwarded to renderers. */
  appearance?: 'compact' | 'full';
  isUser?: boolean;
  /**
   * When true, an unknown `kind` renders a visible dev notice. When
   * false (default) it is skipped silently — production-lenient.
   */
  strict?: boolean;
}

/** Block kinds rendered inside a chat-bubble surface (bubble-tinted, inline). */
const PROSE_KINDS = new Set<MessageBlock['kind']>(['text', 'markdown']);

/**
 * Block kinds wrapped in the shared media-card surface (`BLOCK_SURFACE`) so
 * every media block shares one radius / border / shadow. `prose` kinds get
 * the bubble surface instead (above).
 *
 * Bare kinds skip the light card and own their frame:
 *  - `custom` — host-owned chrome.
 *  - `link`   — the LinkPreviewCard ships its own frame.
 *  - `audio`  — the player draws its own card AND a scrub-time tooltip that
 *    sits ABOVE the bar; a clipping card would crop that tooltip, so the
 *    player rounds itself to the shared 2xl radius instead (see AudioBlock).
 *  - `diff`   — the DiffViewer ships its own bordered panel + a floating
 *    copy button that a clip would crop; it re-rounds to 2xl (see DiffBlock).
 *
 * `code` is NOT bare: its dark editor panel can't be reliably re-rounded
 * from outside (PrettyCode rounds its inner layers to `rounded-lg`), so we
 * let the shared `overflow-hidden rounded-2xl` card CLIP it to the uniform
 * corner instead. (Code has no overflowing tooltip, so clipping is safe.)
 */
const BARE_KINDS = new Set<MessageBlock['kind']>(['custom', 'link', 'audio', 'diff']);

interface BoundaryProps {
  kind: string;
  children: ReactNode;
}
interface BoundaryState {
  failed: boolean;
}

/** Minimal per-block error boundary. */
class BlockErrorBoundary extends Component<BoundaryProps, BoundaryState> {
  state: BoundaryState = { failed: false };

  static getDerivedStateFromError(): BoundaryState {
    return { failed: true };
  }

  render() {
    if (this.state.failed) {
      return (
        <div className="rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-muted-foreground">
          Failed to render a {this.props.kind} block.
        </div>
      );
    }
    return this.props.children;
  }
}

function UnknownBlock({ kind }: { kind: string }) {
  return (
    <div className="rounded-lg border border-dashed border-border px-3 py-2 text-xs text-muted-foreground">
      Unknown block kind: <code className="font-mono">{kind}</code>
    </div>
  );
}

export function MessageBlocks({
  blocks,
  registry = BUILTIN_BLOCK_REGISTRY,
  appearance = 'compact',
  isUser = false,
  strict = false,
}: MessageBlocksProps) {
  const { surface: bubbleSurface } = useChatBubbleStyles(
    isUser ? 'user' : 'assistant',
    false,
  );

  if (!blocks?.length) return null;

  return (
    <div
      className={cn('mt-1.5 flex w-full flex-col gap-2', isUser && 'items-end')}
    >
      {blocks.map((block) => {
        const renderer = registry[block.kind] as
          | ((b: MessageBlock, c: { appearance: 'compact' | 'full'; isUser: boolean }) => ReactNode)
          | undefined;

        // Central payload validation — every block flows through here, so a
        // malformed payload (wrong-typed `src`, non-array gallery `items`, bad
        // map coords) renders a safe `<BlockError>` instead of throwing inside
        // its renderer and blanking the transcript. Kinds without a schema
        // (e.g. host-defined `custom`) skip this and render as-is. The renderer
        // can therefore trust its input. The error boundary below stays as a
        // last-resort runtime guard.
        const schema = BLOCK_SCHEMAS[block.kind];

        let body: ReactNode;
        if (schema) {
          const parsed = safeParseBlock(schema, block);
          if (!parsed.ok) {
            body = <BlockError label={block.kind} reason={parsed.error} />;
          } else if (renderer) {
            body = renderer(block, { appearance, isUser });
          } else if (strict) {
            body = <UnknownBlock kind={block.kind} />;
          } else {
            return null;
          }
        } else if (renderer) {
          body = renderer(block, { appearance, isUser });
        } else if (strict) {
          body = <UnknownBlock kind={block.kind} />;
        } else {
          return null;
        }

        const isProse = PROSE_KINDS.has(block.kind);
        const isBare = BARE_KINDS.has(block.kind);

        // Surface selection — one consistent frame per category:
        //   prose  → tinted chat bubble (inline)
        //   bare   → no frame (host-owned `custom` block)
        //   media  → shared `BLOCK_SURFACE` card (image/video/map/code/…)
        let framed: ReactNode;
        if (isProse) {
          framed = (
            <div
              className={cn(
                'inline-block max-w-full rounded-2xl px-3.5 py-2 text-sm',
                bubbleSurface,
              )}
            >
              {body}
            </div>
          );
        } else if (isBare) {
          framed = body;
        } else {
          framed = <div className={cn('w-full', BLOCK_SURFACE)}>{body}</div>;
        }

        return (
          <div key={block.id} data-block-kind={block.kind} className="w-full">
            {block.caption ? (
              <div className="mb-1 text-xs font-medium text-muted-foreground">
                {block.caption}
              </div>
            ) : null}
            <BlockErrorBoundary kind={block.kind}>{framed}</BlockErrorBoundary>
          </div>
        );
      })}
    </div>
  );
}
