import type { PlimiPlugin, ToolContext } from "./plugin-types";
import type { ToolInput } from "../io/input-types";
import type { ToolResult } from "../io/output-types";
import type { ToolWorkerRequest, ToolWorkerResponse } from "./worker-protocol";
import { normalizeToolOptions, validateToolInput } from "./plugin-validation";

export async function runPlugin<TOptions>(
  plugin: PlimiPlugin<TOptions>,
  input: ToolInput,
  options: TOptions,
  context: ToolContext
): Promise<ToolResult> {
  validateToolInput(plugin.manifest.input, input);
  const normalizedOptions = plugin.optionsSchema
    ? normalizeToolOptions(plugin.optionsSchema, options) as TOptions
    : options;

  if (plugin.capabilities?.worker && plugin.worker) {
    return runPluginInWorker(plugin, input, normalizedOptions, context);
  }

  if (!plugin.run) {
    throw new Error(`Plugin ${plugin.manifest.id} has no runner`);
  }

  return plugin.run(input, normalizedOptions, context);
}

function runPluginInWorker<TOptions>(
  plugin: PlimiPlugin<TOptions>,
  input: ToolInput,
  options: TOptions,
  context: ToolContext
): Promise<ToolResult> {
  return new Promise((resolve, reject) => {
    if (!plugin.worker) {
      return reject(new Error("Worker is not defined for this plugin"));
    }

    const worker = plugin.worker();
    const jobId = Math.random().toString(36).substring(7);

    // Handle cancellation
    const onAbort = () => {
      worker.terminate();
      reject(new DOMException("Operation cancelled by user.", "AbortError"));
    };
    context.signal.addEventListener("abort", onAbort);

    worker.onmessage = (e: MessageEvent<ToolWorkerResponse>) => {
      const { type, id } = e.data;

      if (id !== jobId) return;

      if (type === "progress") {
        context.reportProgress(e.data.progress);
      } else if (type === "success") {
        context.signal.removeEventListener("abort", onAbort);
        worker.terminate();
        resolve(e.data.result);
      } else if (type === "error") {
        context.signal.removeEventListener("abort", onAbort);
        worker.terminate();
        reject(new Error(e.data.error));
      }
    };

    worker.onerror = (e) => {
      context.signal.removeEventListener("abort", onAbort);
      worker.terminate();
      reject(new Error("Worker error: " + e.message));
    };

    // Serialize input payload (can't send DOM elements or complex functions)
    const request: ToolWorkerRequest<TOptions> = {
      type: "run",
      id: jobId,
      input,
      options,
    };

    worker.postMessage(request);
  });
}
