#!/usr/bin/env node
/**
 * Excalidraw MCP stdio server
 * Provides headless .excalidraw → SVG export via @excalidraw/excalidraw.
 * Optional: diagram generation via EXCALIDRAW_GEN_PATH (excalidraw_gen.py).
 */
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod/v3";
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import { spawnSync } from 'child_process';
import { JSDOM } from 'jsdom';

// ---------------------------------------------------------------------------
// 1. DOM environment — must be established BEFORE importing excalidraw
// ---------------------------------------------------------------------------

const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
  url: 'http://localhost/',
  pretendToBeVisual: true,
});

const { window } = dom;

// Helper: define a global, overriding read-only getters (e.g. navigator in Node v22)
function defineGlobal(key, value) {
  if (value === undefined) return;
  try {
    Object.defineProperty(global, key, {
      value,
      writable: true,
      configurable: true,
    });
  } catch {
    // Already defined and non-configurable — skip
  }
}

// Core DOM globals
defineGlobal('window',    window);
defineGlobal('document',  window.document);
defineGlobal('navigator', window.navigator);
defineGlobal('location',  window.location);
defineGlobal('history',   window.history);
defineGlobal('screen',    window.screen);

// Element / event interfaces
for (const key of [
  'Node', 'Element', 'HTMLElement', 'SVGElement', 'SVGSVGElement',
  'HTMLCanvasElement', 'HTMLImageElement', 'Image',
  'Event', 'CustomEvent', 'MouseEvent', 'PointerEvent',
  'KeyboardEvent', 'TouchEvent', 'WheelEvent', 'InputEvent',
  'MutationObserver', 'ResizeObserver', 'IntersectionObserver',
  'XMLHttpRequest', 'XMLSerializer',
  'DOMParser', 'Range',
  'getComputedStyle', 'matchMedia',
]) {
  defineGlobal(key, window[key]);
}

// Animation frame stubs (jsdom doesn't implement them)
global.requestAnimationFrame  = (fn) => setTimeout(() => fn(Date.now()), 0);
global.cancelAnimationFrame   = (id) => clearTimeout(id);

// CSS Font Loading API stub — jsdom doesn't implement FontFace
class FontFaceStub {
  constructor(family, source, _descriptors) {
    this.family = family;
    this.source = source;
    this.status = 'loaded';
    this.loaded = Promise.resolve(this);
  }
  load() { return Promise.resolve(this); }
}
defineGlobal('FontFace', FontFaceStub);

// FontFaceSet stub for document.fonts
const fontFaceSet = {
  add: () => {},
  delete: () => {},
  has: () => false,
  clear: () => {},
  load: () => Promise.resolve([]),
  check: () => true,
  ready: Promise.resolve(),
  status: 'loaded',
  forEach: () => {},
  [Symbol.iterator]: function*() {},
};
Object.defineProperty(window.document, 'fonts', {
  value: fontFaceSet,
  writable: true,
  configurable: true,
});

// Canvas stub — excalidraw's exportToSvg doesn't need real canvas rendering,
// but the class must exist for isinstance checks.
if (!global.HTMLCanvasElement) {
  defineGlobal('HTMLCanvasElement', window.HTMLCanvasElement ?? class HTMLCanvasElement {});
}

// Device pixel ratio
global.devicePixelRatio = 1;

// ---------------------------------------------------------------------------
// 1b. Stub canvas getContext — excalidraw calls this at module init time.
//     jsdom throws "Not implemented" by default; we return a no-op 2D stub.
// ---------------------------------------------------------------------------

const _canvasCtx = {
  canvas: { width: 800, height: 600 },
  fillRect: () => {}, clearRect: () => {}, strokeRect: () => {},
  getImageData: (x, y, w, h) => ({ data: new Uint8ClampedArray(w * h * 4), width: w, height: h }),
  putImageData: () => {}, createImageData: () => ({ data: new Uint8ClampedArray(0) }),
  setTransform: () => {}, resetTransform: () => {}, transform: () => {},
  drawImage: () => {}, save: () => {}, restore: () => {},
  scale: () => {}, rotate: () => {}, translate: () => {},
  beginPath: () => {}, closePath: () => {}, moveTo: () => {}, lineTo: () => {},
  bezierCurveTo: () => {}, quadraticCurveTo: () => {},
  arc: () => {}, arcTo: () => {}, ellipse: () => {}, rect: () => {},
  fill: () => {}, stroke: () => {}, clip: () => {},
  fillText: () => {}, strokeText: () => {},
  measureText: (t) => ({ width: t.length * 8, actualBoundingBoxAscent: 12, actualBoundingBoxDescent: 3, fontBoundingBoxAscent: 14, fontBoundingBoxDescent: 4 }),
  createLinearGradient: () => ({ addColorStop: () => {} }),
  createRadialGradient: () => ({ addColorStop: () => {} }),
  createPattern: () => null,
  setLineDash: () => {}, getLineDash: () => [],
  isPointInPath: () => false, isPointInStroke: () => false,
  getContextAttributes: () => ({ alpha: true, desynchronized: false }),
  font: '10px sans-serif', fillStyle: '#000', strokeStyle: '#000',
  lineWidth: 1, lineCap: 'butt', lineJoin: 'miter',
  textAlign: 'start', textBaseline: 'alphabetic',
  globalAlpha: 1, globalCompositeOperation: 'source-over',
  shadowOffsetX: 0, shadowOffsetY: 0, shadowBlur: 0, shadowColor: 'transparent',
  miterLimit: 10, lineDashOffset: 0, filter: 'none', imageSmoothingEnabled: true,
};

// Patch before excalidraw import so module-level canvas calls get the stub
if (window.HTMLCanvasElement) {
  window.HTMLCanvasElement.prototype.getContext = function (type) {
    if (type === '2d') return _canvasCtx;
    return null;
  };
}

// ---------------------------------------------------------------------------
// 2. Load excalidraw (dynamic import so globals are set first)
// ---------------------------------------------------------------------------

let exportToSvg;
try {
  const excalidraw = await import('@excalidraw/excalidraw');
  exportToSvg = excalidraw.exportToSvg;
  if (!exportToSvg) throw new Error('exportToSvg not found in package exports');
} catch (err) {
  process.stderr.write(`FATAL: Failed to load @excalidraw/excalidraw: ${err.message}\n`);
  process.exit(1);
}

// ---------------------------------------------------------------------------
// 3. SVG export helper
// ---------------------------------------------------------------------------

async function renderToSvg(elements, appState, files) {
  const svgEl = await exportToSvg({
    elements: elements ?? [],
    appState: {
      exportWithDarkMode: false,
      exportBackground: true,
      viewBackgroundColor: '#ffffff',
      ...appState,
    },
    files: files ?? {},
  });
  const serializer = new window.XMLSerializer();
  return serializer.serializeToString(svgEl);
}

// ---------------------------------------------------------------------------
// 4. Gen subprocess helper (optional — requires EXCALIDRAW_GEN_PATH)
// ---------------------------------------------------------------------------

function requireGenPath() {
  const p = process.env.EXCALIDRAW_GEN_PATH;
  if (!p) {
    return null;
  }
  return p;
}

function spawnGen(args) {
  const genPath = requireGenPath();
  if (!genPath) {
    return {
      ok: false,
      text: 'EXCALIDRAW_GEN_PATH is not set. Set it to the path of excalidraw_gen.py to use diagram generation.',
    };
  }
  const result = spawnSync('python3', [genPath, ...args], { encoding: 'utf8' });
  if (result.error) return { ok: false, text: `spawn error: ${result.error.message}` };
  if (result.status !== 0) return { ok: false, text: result.stderr || 'subprocess failed' };
  return { ok: true, text: result.stdout.trim() };
}

// ---------------------------------------------------------------------------
// 5. MCP Server
// ---------------------------------------------------------------------------

const server = new McpServer({
  name: "excalidraw",
  version: "1.0.0",
});

// --- Tool: excalidraw_to_svg ---

server.tool(
  "excalidraw_to_svg",
  "Convert Excalidraw elements JSON to SVG string",
  {
    elements: z.string().describe("JSON string of Excalidraw elements array"),
    app_state: z.string().optional().describe("JSON string of appState overrides"),
  },
  async ({ elements, app_state }) => {
    let parsed;
    try {
      parsed = JSON.parse(elements);
    } catch (err) {
      throw new Error(`Invalid elements JSON: ${err.message}`);
    }
    const appState = app_state ? JSON.parse(app_state) : {};
    const svg = await renderToSvg(parsed, appState, {});
    return { content: [{ type: "text", text: svg }] };
  }
);

// --- Tool: excalidraw_file_to_svg ---

server.tool(
  "excalidraw_file_to_svg",
  "Convert an .excalidraw file to SVG (writes .svg alongside the input file)",
  {
    file_path: z.string().describe("Absolute or relative path to .excalidraw file"),
  },
  async ({ file_path }) => {
    const absPath = resolve(file_path);
    if (!existsSync(absPath)) {
      throw new Error(`File not found: ${absPath}`);
    }
    const raw = JSON.parse(readFileSync(absPath, 'utf8'));
    const svg = await renderToSvg(raw.elements, raw.appState, raw.files);
    const outPath = absPath.replace(/\.excalidraw$/, '.svg');
    writeFileSync(outPath, svg, 'utf8');
    return {
      content: [{ type: "text", text: `SVG written to: ${outPath}\n\n${svg}` }],
    };
  }
);

// --- Tool: list_diagrams ---

server.tool(
  "list_diagrams",
  "List available diagram templates from the DIAGRAMS registry (requires EXCALIDRAW_GEN_PATH)",
  {},
  async () => {
    const res = spawnGen(['--list']);
    return { content: [{ type: "text", text: res.text }] };
  }
);

// --- Tool: generate_diagram ---

server.tool(
  "generate_diagram",
  "Generate an .excalidraw file from a named diagram template (requires EXCALIDRAW_GEN_PATH)",
  {
    name: z.string().describe("Diagram template name (from list_diagrams)"),
    output_path: z.string().optional().describe("Output path for the .excalidraw file"),
  },
  async ({ name, output_path }) => {
    const args = [name];
    if (output_path) args.push('--output', output_path);
    const res = spawnGen(args);
    if (!res.ok) throw new Error(res.text);
    return { content: [{ type: "text", text: res.text }] };
  }
);

// --- Tool: generate_and_export ---

server.tool(
  "generate_and_export",
  "Generate an .excalidraw file and immediately export it to SVG (requires EXCALIDRAW_GEN_PATH)",
  {
    name: z.string().describe("Diagram template name (from list_diagrams)"),
    output_path: z.string().optional().describe("Output path for the .excalidraw file (SVG written alongside)"),
  },
  async ({ name, output_path }) => {
    const genArgs = [name];
    if (output_path) genArgs.push('--output', output_path);
    const genRes = spawnGen(genArgs);
    if (!genRes.ok) throw new Error(genRes.text);

    const excalidrawPath = genRes.text;
    if (!existsSync(excalidrawPath)) {
      throw new Error(`Generated file not found: ${excalidrawPath}`);
    }

    const raw = JSON.parse(readFileSync(excalidrawPath, 'utf8'));
    const svg = await renderToSvg(raw.elements, raw.appState, raw.files);
    const svgPath = excalidrawPath.replace(/\.excalidraw$/, '.svg');
    writeFileSync(svgPath, svg, 'utf8');

    return {
      content: [{ type: "text", text: `Generated: ${excalidrawPath}\nExported SVG: ${svgPath}` }],
    };
  }
);

// --- Start ---
const transport = new StdioServerTransport();
await server.connect(transport);
