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";

const persistentWorkers = new Map<string, Worker>();

export function disposePersistentWorker(pluginId: string): boolean {
  const worker = persistentWorkers.get(pluginId);
  if (!worker) return false;
  persistentWorkers.delete(pluginId);
  worker.terminate();
  return true;
}

function createPluginWorker<TOptions>(plugin: PlimiPlugin<TOptions>): Worker {
  if (!plugin.worker) {
    throw new Error("Worker is not defined for this plugin");
  }

  if (!plugin.capabilities?.persistentWorker) {
    return plugin.worker();
  }

  const pluginId = plugin.manifest.id;
  const existing = persistentWorkers.get(pluginId);
  if (existing) return existing;

  const worker = plugin.worker();
  persistentWorkers.set(pluginId, worker);
  return worker;
}

function discardPluginWorker<TOptions>(
  plugin: PlimiPlugin<TOptions>,
  worker: Worker
): void {
  if (persistentWorkers.get(plugin.manifest.id) === worker) {
    persistentWorkers.delete(plugin.manifest.id);
  }
  worker.terminate();
}

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) => {
    let worker: Worker;
    try {
      worker = createPluginWorker(plugin);
    } catch (error) {
      reject(error);
      return;
    }
    const jobId = Math.random().toString(36).substring(7);

    const cleanup = () => {
      context.signal.removeEventListener("abort", onAbort);
      worker.removeEventListener("message", onMessage);
      worker.removeEventListener("error", onError);
    };

    const onAbort = () => {
      cleanup();
      discardPluginWorker(plugin, worker);
      reject(new DOMException("Operation cancelled by user.", "AbortError"));
    };

    const 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") {
        cleanup();
        if (!plugin.capabilities?.persistentWorker) {
          worker.terminate();
        }
        resolve(e.data.result);
      } else if (type === "error") {
        cleanup();
        discardPluginWorker(plugin, worker);
        reject(new Error(e.data.error));
      }
    };

    const onError = (e: ErrorEvent) => {
      cleanup();
      discardPluginWorker(plugin, worker);
      const location = e.filename
        ? ` (${e.filename}${e.lineno ? `:${e.lineno}:${e.colno}` : ""})`
        : "";
      reject(
        new Error(
          `Worker failed to load or execute${location}: ${
            e.message || "check worker, CORS, and cross-origin isolation headers"
          }`
        )
      );
    };

    context.signal.addEventListener("abort", onAbort);
    worker.addEventListener("message", onMessage);
    worker.addEventListener("error", onError);

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

    worker.postMessage(request);
  });
}
