import { useCoreServices } from '@learnitive/context';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  toast,
} from '@learnitive/ui-components';
import { WsPath } from '@learnitive/ws-path';
import React from 'react';
import type { LatexDocumentController } from '../latex-document-controller';

type TikzDrawImage = {
  filename: string;
  mimeType: string;
  dataUrl: string;
};

type TikzDrawPayload = {
  tikz: string;
  packages?: readonly string[];
  libraries?: readonly string[];
  images?: readonly TikzDrawImage[];
};

type TikzDrawMessage =
  | { source: 'interactive-tikz-draw'; type: 'ready' }
  | { source: 'interactive-tikz-draw'; type: 'insert'; payload: TikzDrawPayload };

function isTikzDrawMessage(value: unknown): value is TikzDrawMessage {
  if (!value || typeof value !== 'object') return false;
  const message = value as Partial<TikzDrawMessage>;
  if (message.source !== 'interactive-tikz-draw') return false;
  return message.type === 'ready' || message.type === 'insert';
}

function dataUrlBytes(dataUrl: string): Uint8Array {
  const comma = dataUrl.indexOf(',');
  if (comma < 0) throw new Error('TikZ image data is malformed.');
  const header = dataUrl.slice(0, comma);
  const data = dataUrl.slice(comma + 1);
  if (/;base64$/iu.test(header)) {
    const raw = atob(data);
    const bytes = new Uint8Array(raw.length);
    for (let index = 0; index < raw.length; index += 1) bytes[index] = raw.charCodeAt(index);
    return bytes;
  }
  return new TextEncoder().encode(decodeURIComponent(data));
}

function packageDeclared(source: string, packageName: string): boolean {
  const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
  return new RegExp(`\\\\usepackage(?:\\[[^\\]]*\\])?\\{[^}]*\\b${escaped}\\b[^}]*\\}`, 'u').test(source);
}

function libraryDeclared(source: string, libraryName: string): boolean {
  const escaped = libraryName.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
  return new RegExp(`\\\\usetikzlibrary\\{[^}]*\\b${escaped}\\b[^}]*\\}`, 'u').test(source);
}

function provisionEdit(
  source: string,
  packages: readonly string[],
  libraries: readonly string[],
): { from: number; to: number; text: string } | undefined {
  const beginDocument = source.search(/\\begin\s*\{document\}/u);
  if (beginDocument < 0) return undefined;
  const missingPackages = [...new Set(['tikz', ...packages])].filter(
    (packageName) => !packageDeclared(source, packageName),
  );
  const missingLibraries = [...new Set(libraries)].filter(
    (libraryName) => !libraryDeclared(source, libraryName),
  );
  if (!missingPackages.length && !missingLibraries.length) return undefined;
  const lines = [
    ...missingPackages.map((packageName) => `\\usepackage{${packageName}}`),
    ...(missingLibraries.length
      ? [`\\usetikzlibrary{${missingLibraries.join(',')}}`]
      : []),
  ];
  return { from: beginDocument, to: beginDocument, text: `${lines.join('\n')}\n` };
}

function insertionOffset(source: string, requested: number | undefined): number | undefined {
  const beginMatch = /\\begin\s*\{document\}/u.exec(source);
  const endMatch = /\\end\s*\{document\}/u.exec(source);
  if (!beginMatch || !endMatch || beginMatch.index >= endMatch.index) return undefined;
  const bodyStart = beginMatch.index + beginMatch[0].length;
  const bodyEnd = endMatch.index;
  if (requested !== undefined && requested >= bodyStart && requested <= bodyEnd) return requested;
  return bodyEnd;
}

export function TikzVisualEditor({
  wsPath,
  open,
  onOpenChange,
  documentController,
}: {
  wsPath: string;
  open: boolean;
  onOpenChange: (open: boolean) => void;
  documentController: LatexDocumentController;
}) {
  const { fileSystem, latexSource, projectAssets, projectCollaboration } = useCoreServices();
  const iframeRef = React.useRef<HTMLIFrameElement>(null);
  const [ready, setReady] = React.useState(false);
  const [inserting, setInserting] = React.useState(false);

  React.useEffect(() => {
    if (!open) setReady(false);
  }, [open]);

  React.useEffect(() => {
    if (!open) return;
    const receive = (event: MessageEvent<unknown>) => {
      if (event.origin !== window.location.origin) return;
      if (event.source !== iframeRef.current?.contentWindow) return;
      if (!isTikzDrawMessage(event.data)) return;
      if (event.data.type === 'ready') {
        setReady(true);
        return;
      }

      const payload = event.data.payload;
      if (!payload || typeof payload.tikz !== 'string' || !payload.tikz.includes('\\begin{tikzpicture}')) {
        toast.error('TikZ Visual Editor returned invalid TikZ source.');
        return;
      }
      if (inserting) return;
      setInserting(true);
      void (async () => {
        try {
          const parsed = WsPath.assertFile(wsPath);
          const snapshot = latexSource.getSnapshot(wsPath);
          if (!snapshot) throw new Error('The current LaTeX source is not loaded.');
          const canonicalText = projectCollaboration.getCanonicalSourceText(parsed.wsName, parsed.path) ?? snapshot.text;
          let tikz = payload.tikz.trim();
          const images = payload.images ?? [];
          const importedByName = new Map<string, string>();
          for (const image of images) {
            if (!image.filename || !image.dataUrl) continue;
            let projectPath = importedByName.get(image.filename);
            if (!projectPath) {
              const imported = await projectAssets.importImage(parsed.wsName, {
                fileName: image.filename,
                data: dataUrlBytes(image.dataUrl),
              });
              projectPath = imported.path;
              importedByName.set(image.filename, projectPath);
            }
            tikz = tikz.split(`{${image.filename}}`).join(`{${projectPath}}`);
          }

          const cursor = latexSource.getCursorLocation(wsPath)?.offset;
          const at = insertionOffset(canonicalText, cursor);
          if (at === undefined) {
            throw new Error('TikZ insertion requires a LaTeX document with \\begin{document} and \\end{document}.');
          }
          const preamble = provisionEdit(
            canonicalText,
            payload.packages ?? [],
            payload.libraries ?? [],
          );
          const block = `${at > 0 && canonicalText[at - 1] !== '\n' ? '\n' : ''}\n${tikz}\n\n`;
          const edits = [
            { from: at, to: at, text: block },
            ...(preamble ? [preamble] : []),
          ];
          if (projectCollaboration.isSourcePathCollaborative(parsed.wsName, parsed.path)) {
            projectCollaboration.applyTextEdits(parsed.wsName, parsed.path, edits, { reviewSource: 'tikz', semanticKind: 'tikz-edit' });
          } else {
            // LatexSourceService remains the canonical browser-local mutation
            // boundary for TikZ (BF14/BF17). Keep the insertion revision-safe
            // by proving that both the active document controller and source
            // service still point at the snapshot used to calculate offsets.
            // The controller subscribes to LatexSourceService, so this source
            // mutation immediately refreshes Source and Visual without a
            // second competing write path.
            const activeDocument = documentController.getSnapshot();
            const latestSnapshot = latexSource.getSnapshot(wsPath);
            if (
              activeDocument.status !== 'ready' ||
              activeDocument.activePath !== wsPath ||
              activeDocument.requestedPath !== wsPath ||
              !latestSnapshot
            ) {
              throw new Error('TikZ insertion requires the current LaTeX document to be active.');
            }
            if (
              latestSnapshot.revision !== snapshot.revision ||
              activeDocument.revision !== snapshot.revision
            ) {
              throw new Error(
                'The LaTeX source changed while the TikZ editor was open. Insert again into the current document.',
              );
            }
            latexSource.applyEdits(wsPath, edits, 'visual-structure');
            // Make the canonical source revision visible to the mounted Visual
            // projection in the same interaction. The normal source-service
            // subscription remains authoritative; this explicit reconciliation
            // prevents a TikZ block from waiting for Source -> Visual toggling.
            documentController.refreshActiveSource(wsPath);
          }
          if (images.length) fileSystem.refreshFileTree();
          onOpenChange(false);
          toast.success('TikZ drawing inserted into the current document.');
        } catch (error) {
          console.error('Unable to insert TikZ drawing', error);
          toast.error(error instanceof Error ? error.message : 'Unable to insert TikZ drawing.');
        } finally {
          setInserting(false);
        }
      })();
    };
    window.addEventListener('message', receive);
    return () => window.removeEventListener('message', receive);
  }, [documentController, fileSystem, inserting, latexSource, onOpenChange, open, projectAssets, projectCollaboration, wsPath]);

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent
        className="flex h-[92dvh] w-[97vw] max-w-[1680px] flex-col gap-0 overflow-hidden p-0 sm:max-w-[1680px] sm:rounded-xl"
        data-testid="tikz-visual-editor-dialog"
        style={{ width: '97vw', maxWidth: '1680px', height: '92dvh' }}
      >
        <DialogHeader className="shrink-0 border-b bg-background px-4 py-3">
          <div className="flex items-center gap-3">
            <div className="min-w-0 flex-1">
              <DialogTitle>TikZ Visual Editor</DialogTitle>
              <DialogDescription>
                Draw visually, then use Insert TikZ inside the editor. Everything stays in Learnitive and project images are imported locally.
              </DialogDescription>
            </div>
          </div>
        </DialogHeader>
        <div className="min-h-0 flex-1 bg-muted/20 p-2 sm:p-3">
          <iframe
            ref={iframeRef}
            className="h-full w-full rounded-lg border bg-background shadow-sm"
            src="/tikz-draw/index.html"
            title="TikZ Visual Editor drawing canvas"
            data-testid="tikz-draw-frame"
            sandbox="allow-scripts allow-same-origin allow-downloads allow-modals"
          />
        </div>
      </DialogContent>
    </Dialog>
  );
}
