import type {
  RuntimeExecutionRequest,
  RuntimeExecutionResult,
  RuntimePackageDefinition,
  RuntimeWorkspaceFile,
  RuntimeWorkspacePort,
  ScientificRuntimeFactory,
  ScientificRuntimeSession,
} from '@learnitive/scientific-runtime';

export const PYODIDE_VERSION = '314.0.4';
export const PYODIDE_INDEX_URL = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;

export const PYTHON_SCIENTIFIC_PACKAGES: readonly RuntimePackageDefinition[] = [
  { runtimeId: 'python', name: 'numpy', state: 'available', source: `Pyodide ${PYODIDE_VERSION}` },
  { runtimeId: 'python', name: 'scipy', state: 'available', source: `Pyodide ${PYODIDE_VERSION}` },
  { runtimeId: 'python', name: 'pandas', state: 'available', source: `Pyodide ${PYODIDE_VERSION}` },
  { runtimeId: 'python', name: 'matplotlib', state: 'available', source: `Pyodide ${PYODIDE_VERSION}` },
  { runtimeId: 'python', name: 'sympy', state: 'available', source: `Pyodide ${PYODIDE_VERSION}` },
] as const;

type WorkerRequest =
  | { id: string; type: 'initialize'; interruptBuffer?: SharedArrayBuffer }
  | { id: string; type: 'workspace-reset' }
  | { id: string; type: 'workspace-mkdir'; path: string }
  | { id: string; type: 'workspace-write'; path: string; data: Uint8Array }
  | { id: string; type: 'workspace-read'; path: string }
  | { id: string; type: 'workspace-list' }
  | { id: string; type: 'execute'; request: RuntimeExecutionRequest }
  | { id: string; type: 'environment' };

type WorkerResponse =
  | { id: string; ok: true; value?: unknown }
  | { id: string; ok: false; error: string };

type WithoutId<T> = T extends { id: string } ? Omit<T, 'id'> : never;
type WorkerRequestInput = WithoutId<WorkerRequest>;
type WorkerCallRequest = Exclude<WorkerRequestInput, { type: 'initialize' }>;

type PendingRequest = {
  resolve(value: unknown): void;
  reject(reason: Error): void;
};

function abortError(): DOMException {
  return new DOMException('The operation was aborted', 'AbortError');
}

class PythonWorkerClient {
  private worker: Worker | undefined;
  private initialized = false;
  private initializing: Promise<void> | undefined;
  private readonly pending = new Map<string, PendingRequest>();
  private interruptBuffer: Int32Array | undefined;

  private ensureWorker(): Worker {
    if (this.worker) return this.worker;
    const worker = new Worker(new URL('./python.worker.ts', import.meta.url), {
      type: 'module',
      name: 'interactive-scientific-python',
    });
    worker.addEventListener('message', (event: MessageEvent<WorkerResponse>) => {
      const pending = this.pending.get(event.data.id);
      if (!pending) return;
      this.pending.delete(event.data.id);
      if ('error' in event.data) pending.reject(new Error(event.data.error));
      else pending.resolve(event.data.value);
    });
    worker.addEventListener('error', (event) => {
      this.failWorker(new Error(event.message || 'Scientific Python worker failed'));
    });
    this.worker = worker;
    return worker;
  }

  private failWorker(error: Error): void {
    for (const pending of this.pending.values()) pending.reject(error);
    this.pending.clear();
    this.worker?.terminate();
    this.worker = undefined;
    this.initialized = false;
    this.initializing = undefined;
    this.interruptBuffer = undefined;
  }

  private async rawCall<T>(request: WorkerRequestInput, signal?: AbortSignal): Promise<T> {
    signal?.throwIfAborted();
    const worker = this.ensureWorker();
    const id = crypto.randomUUID();
    return new Promise<T>((resolve, reject) => {
      const onAbort = () => {
        this.pending.delete(id);
        reject(abortError());
      };
      signal?.addEventListener('abort', onAbort, { once: true });
      this.pending.set(id, {
        resolve: (value) => {
          signal?.removeEventListener('abort', onAbort);
          resolve(value as T);
        },
        reject: (error) => {
          signal?.removeEventListener('abort', onAbort);
          reject(error);
        },
      });
      worker.postMessage({ ...request, id } as WorkerRequest);
    });
  }

  async initialize(signal?: AbortSignal): Promise<void> {
    if (this.initialized) return;
    if (this.initializing) return this.initializing;
    const run = (async () => {
      let buffer: SharedArrayBuffer | undefined;
      if (typeof SharedArrayBuffer !== 'undefined' && globalThis.crossOriginIsolated) {
        buffer = new SharedArrayBuffer(4);
        this.interruptBuffer = new Int32Array(buffer);
      }
      await this.rawCall<void>({ type: 'initialize', ...(buffer ? { interruptBuffer: buffer } : {}) }, signal);
      this.initialized = true;
    })();
    this.initializing = run;
    try {
      await run;
    } finally {
      if (this.initializing === run) this.initializing = undefined;
    }
  }

  async call<T>(request: WorkerCallRequest, signal?: AbortSignal): Promise<T> {
    await this.initialize(signal);
    return this.rawCall<T>(request, signal);
  }

  async interrupt(): Promise<void> {
    if (this.interruptBuffer) {
      Atomics.store(this.interruptBuffer, 0, 2);
      Atomics.notify(this.interruptBuffer, 0);
      return;
    }
    // Without cross-origin isolation SharedArrayBuffer is unavailable. Hard
    // termination is the only reliable way to stop synchronous Wasm work;
    // the session lazily reinitializes on the next workspace operation.
    this.failWorker(abortError());
  }

  dispose(): void {
    this.failWorker(new Error('Scientific Python runtime was disposed'));
  }
}

class BrowserPythonWorkspace implements RuntimeWorkspacePort {
  constructor(private readonly client: PythonWorkerClient) {}

  reset(signal?: AbortSignal): Promise<void> {
    return this.client.call<void>({ type: 'workspace-reset' }, signal);
  }

  ensureDirectory(path: string, signal?: AbortSignal): Promise<void> {
    return this.client.call<void>({ type: 'workspace-mkdir', path }, signal);
  }

  writeFile(path: string, data: Uint8Array, signal?: AbortSignal): Promise<void> {
    return this.client.call<void>({ type: 'workspace-write', path, data }, signal);
  }

  readFile(path: string, signal?: AbortSignal): Promise<Uint8Array | undefined> {
    return this.client.call<Uint8Array | undefined>({ type: 'workspace-read', path }, signal);
  }

  listFiles(signal?: AbortSignal): Promise<readonly RuntimeWorkspaceFile[]> {
    return this.client.call<readonly RuntimeWorkspaceFile[]>({ type: 'workspace-list' }, signal);
  }
}

class BrowserPythonRuntimeSession implements ScientificRuntimeSession {
  readonly id = 'python' as const;
  readonly label = `Python · Pyodide ${PYODIDE_VERSION}`;
  readonly capabilities = {
    packages: true,
    plots: true,
    workspaceFiles: true,
    interrupt: true,
  } as const;
  readonly workspace: RuntimeWorkspacePort;
  private readonly client = new PythonWorkerClient();

  constructor() {
    this.workspace = new BrowserPythonWorkspace(this.client);
  }

  initialize(signal?: AbortSignal): Promise<void> {
    return this.client.initialize(signal);
  }

  execute(request: RuntimeExecutionRequest, signal?: AbortSignal): Promise<RuntimeExecutionResult> {
    return this.client.call<RuntimeExecutionResult>({ type: 'execute', request }, signal);
  }

  interrupt(): Promise<void> {
    return this.client.interrupt();
  }

  dispose(): void {
    this.client.dispose();
  }
}

export function createBrowserPythonRuntimeFactory(): ScientificRuntimeFactory {
  return {
    id: 'python',
    label: `Python · Pyodide ${PYODIDE_VERSION}`,
    create: () => new BrowserPythonRuntimeSession(),
  };
}
