export type ScientificRuntimeId = 'python' | 'r' | 'wasi';

export type ScientificRuntimeStatus =
  | 'not-loaded'
  | 'initializing'
  | 'ready'
  | 'running'
  | 'interrupting'
  | 'error';

export type ScientificRuntimeCapabilities = {
  packages: boolean;
  plots: boolean;
  workspaceFiles: boolean;
  interrupt: boolean;
};


export type ScientificBrowserCapabilitySnapshot = {
  webAssembly: boolean;
  worker: boolean;
  fetch: boolean;
  abortController: boolean;
  cryptoRandomUUID: boolean;
  sharedArrayBuffer: boolean;
  atomics: boolean;
  crossOriginIsolated: boolean;
  offscreenCanvas: boolean;
  domCanvas: boolean;
};

export type ScientificRuntimeCompatibilityStatus = 'supported' | 'degraded' | 'unsupported';

export type ScientificRuntimeCompatibility = {
  runtimeId: ScientificRuntimeId;
  status: ScientificRuntimeCompatibilityStatus;
  summary: string;
  blockers: readonly string[];
  degradations: readonly string[];
};

export type ScientificBrowserDiagnostics = {
  generatedAt: number;
  capabilities: ScientificBrowserCapabilitySnapshot;
  runtimes: readonly ScientificRuntimeCompatibility[];
};

function browserCapabilitySnapshot(): ScientificBrowserCapabilitySnapshot {
  const scope = globalThis as typeof globalThis & { crossOriginIsolated?: boolean; document?: Document };
  return {
    webAssembly: typeof WebAssembly !== 'undefined',
    worker: typeof Worker !== 'undefined',
    fetch: typeof fetch !== 'undefined',
    abortController: typeof AbortController !== 'undefined',
    cryptoRandomUUID: typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function',
    sharedArrayBuffer: typeof SharedArrayBuffer !== 'undefined',
    atomics: typeof Atomics !== 'undefined',
    crossOriginIsolated: scope.crossOriginIsolated === true,
    offscreenCanvas: typeof OffscreenCanvas !== 'undefined',
    domCanvas: typeof scope.document !== 'undefined' && typeof scope.document.createElement === 'function',
  };
}

/**
 * Cheap browser preflight for the scientific runtimes. This deliberately does
 * not initialize or download Pyodide, webR, Wasmer or their packages. It is
 * suitable for an in-app doctor command and release E2E assertions.
 */
export function collectScientificBrowserDiagnostics(
  overrides: Partial<ScientificBrowserCapabilitySnapshot> = {},
): ScientificBrowserDiagnostics {
  const capabilities = { ...browserCapabilitySnapshot(), ...overrides };
  const commonBlockers = [
    ...(!capabilities.webAssembly ? ['WebAssembly is unavailable'] : []),
    ...(!capabilities.worker ? ['Web Workers are unavailable'] : []),
    ...(!capabilities.fetch ? ['fetch() is unavailable'] : []),
    ...(!capabilities.abortController ? ['AbortController is unavailable'] : []),
    ...(!capabilities.cryptoRandomUUID ? ['crypto.randomUUID() is unavailable'] : []),
  ];

  const pythonBlockers = [...commonBlockers];
  const pythonDegradations = [
    ...(!capabilities.sharedArrayBuffer || !capabilities.crossOriginIsolated
      ? ['fine-grained Python interruption is unavailable; Learnitive will terminate and lazily recreate the worker']
      : []),
  ];

  const rBlockers = [...commonBlockers];
  const rDegradations = [
    ...(!capabilities.offscreenCanvas && !capabilities.domCanvas
      ? ['interactive R plot capture is unavailable; explicit file-backed plots may still work']
      : []),
  ];

  const wasiBlockers = [
    ...commonBlockers,
    ...(!capabilities.crossOriginIsolated ? ['cross-origin isolation (COOP/COEP) is required'] : []),
    ...(!capabilities.sharedArrayBuffer ? ['SharedArrayBuffer is required'] : []),
    ...(!capabilities.atomics ? ['Atomics is required'] : []),
  ];

  const compatibility = (
    runtimeId: ScientificRuntimeId,
    blockers: readonly string[],
    degradations: readonly string[],
    label: string,
  ): ScientificRuntimeCompatibility => ({
    runtimeId,
    status: blockers.length ? 'unsupported' : degradations.length ? 'degraded' : 'supported',
    summary: blockers.length
      ? `${label} cannot run in this browser context.`
      : degradations.length
        ? `${label} can run with reduced capabilities.`
        : `${label} browser prerequisites are satisfied.`,
    blockers,
    degradations,
  });

  return {
    generatedAt: Date.now(),
    capabilities,
    runtimes: [
      compatibility('python', pythonBlockers, pythonDegradations, 'Python/Pyodide'),
      compatibility('r', rBlockers, rDegradations, 'R/webR'),
      compatibility('wasi', wasiBlockers, [], 'C/C++/WASI'),
    ],
  };
}

export type RuntimeWorkspaceFile = {
  path: string;
  size: number;
};

/**
 * Minimal virtual filesystem contract shared by Pyodide, webR and future
 * WASI runtimes. Paths are always project-relative and represent files under
 * the runtime's isolated /workspace mirror.
 */
export interface RuntimeWorkspacePort {
  reset(signal?: AbortSignal): Promise<void>;
  ensureDirectory(path: string, signal?: AbortSignal): Promise<void>;
  writeFile(path: string, data: Uint8Array, signal?: AbortSignal): Promise<void>;
  readFile(path: string, signal?: AbortSignal): Promise<Uint8Array | undefined>;
  listFiles(signal?: AbortSignal): Promise<readonly RuntimeWorkspaceFile[]>;
}

export type RuntimePlot = {
  path?: string;
  mimeType?: string;
  title?: string;
};

export type RuntimeExecutionRequest =
  | {
      mode: 'file';
      path: string;
      args?: readonly string[];
      cwd?: string;
    }
  | {
      mode: 'code';
      code: string;
      args?: readonly string[];
      cwd?: string;
      sourceName?: string;
    }
  | {
      /**
       * Runtime-owned structured tool invocation. This is intentionally not a
       * shell string: each runtime validates the finite tool names it exposes.
       */
      mode: 'tool';
      tool: string;
      args?: readonly string[];
      cwd?: string;
      /** Optional bounded stdin supplied by Learnitive pipelines to finite WASI tools. */
      stdin?: string;
    };

export type RuntimeExecutionResult = {
  runtime: ScientificRuntimeId;
  exitCode: number;
  stdout: string;
  stderr: string;
  startedAt: number;
  durationMs: number;
  plots?: readonly RuntimePlot[];
  metadata?: Readonly<Record<string, string | number | boolean>>;
};

export interface ScientificRuntimeSession {
  readonly id: ScientificRuntimeId;
  readonly label: string;
  readonly capabilities: ScientificRuntimeCapabilities;
  readonly workspace: RuntimeWorkspacePort;
  initialize(signal?: AbortSignal): Promise<void>;
  execute(
    request: RuntimeExecutionRequest,
    signal?: AbortSignal,
  ): Promise<RuntimeExecutionResult>;
  interrupt?(): Promise<void>;
  installPackages?(packages: readonly string[], signal?: AbortSignal): Promise<readonly string[] | void>;
  dispose(): Promise<void> | void;
}

export type ScientificRuntimeFactory = {
  id: ScientificRuntimeId;
  label: string;
  create(input: { workspaceId: string }): Promise<ScientificRuntimeSession> | ScientificRuntimeSession;
};

export type ScientificRuntimeInstanceState = {
  runtimeId: ScientificRuntimeId;
  workspaceId: string;
  status: ScientificRuntimeStatus;
  label: string;
  error?: string;
  initializedAt?: number;
  lastUsedAt?: number;
};

type ManagedRuntime = {
  session: ScientificRuntimeSession;
  state: ScientificRuntimeInstanceState;
};

function runtimeKey(workspaceId: string, runtimeId: ScientificRuntimeId): string {
  return `${workspaceId}::${runtimeId}`;
}

/**
 * Owns lazy runtime/session lifecycle. The concrete sessions are expected to
 * be worker-backed by their platform adapters; this manager deliberately has
 * no dependency on Worker so it also works in tests and future desktop shells.
 */
export const DEFAULT_MAX_MANAGED_SCIENTIFIC_RUNTIMES = 3;

export class RuntimeWorkerManager {
  private readonly factories = new Map<ScientificRuntimeId, ScientificRuntimeFactory>();
  private readonly instances = new Map<string, ManagedRuntime>();
  private readonly pending = new Map<string, Promise<ScientificRuntimeSession>>();
  private readonly listeners = new Set<() => void>();

  constructor(private readonly maxManagedInstances = DEFAULT_MAX_MANAGED_SCIENTIFIC_RUNTIMES) {
    if (!Number.isInteger(maxManagedInstances) || maxManagedInstances < 1) {
      throw new Error('Scientific runtime manager capacity must be a positive integer');
    }
  }

  private async evictExcessIdleInstances(exceptKey: string): Promise<void> {
    while (this.instances.size > this.maxManagedInstances) {
      const candidate = [...this.instances.entries()]
        .filter(([key, managed]) =>
          key !== exceptKey &&
          managed.state.status !== 'running' &&
          managed.state.status !== 'interrupting' &&
          managed.state.status !== 'initializing')
        .sort(([, a], [, b]) =>
          (a.state.lastUsedAt ?? a.state.initializedAt ?? 0) -
          (b.state.lastUsedAt ?? b.state.initializedAt ?? 0))[0];
      if (!candidate) return;
      const [key, managed] = candidate;
      this.instances.delete(key);
      this.emit();
      try {
        await managed.session.dispose();
      } catch {
        // Eviction is best-effort: a stale runtime must not make the newly
        // initialized runtime unusable. The instance is already detached.
      }
    }
  }

  register(factory: ScientificRuntimeFactory): () => void {
    this.factories.set(factory.id, factory);
    this.emit();
    return () => {
      if (this.factories.get(factory.id) === factory) {
        this.factories.delete(factory.id);
        this.emit();
      }
    };
  }

  has(runtimeId: ScientificRuntimeId): boolean {
    return this.factories.has(runtimeId);
  }

  listAvailable(): readonly Pick<ScientificRuntimeFactory, 'id' | 'label'>[] {
    return [...this.factories.values()].map(({ id, label }) => ({ id, label }));
  }

  subscribe(listener: () => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  getState(
    workspaceId: string,
    runtimeId: ScientificRuntimeId,
  ): ScientificRuntimeInstanceState | undefined {
    return this.instances.get(runtimeKey(workspaceId, runtimeId))?.state;
  }

  getCapabilities(
    workspaceId: string,
    runtimeId: ScientificRuntimeId,
  ): ScientificRuntimeCapabilities | undefined {
    return this.instances.get(runtimeKey(workspaceId, runtimeId))?.session.capabilities;
  }

  listStates(workspaceId?: string): readonly ScientificRuntimeInstanceState[] {
    return [...this.instances.values()]
      .map((managed) => managed.state)
      .filter((state) => !workspaceId || state.workspaceId === workspaceId);
  }

  async ensureReady(
    workspaceId: string,
    runtimeId: ScientificRuntimeId,
    signal?: AbortSignal,
  ): Promise<ScientificRuntimeSession> {
    signal?.throwIfAborted();
    const key = runtimeKey(workspaceId, runtimeId);
    const inFlight = this.pending.get(key);
    if (inFlight) return inFlight;

    const existing = this.instances.get(key);
    if (existing) {
      if (existing.state.status === 'error') {
        await this.dispose(workspaceId, runtimeId);
      } else {
        return existing.session;
      }
    }

    const factory = this.factories.get(runtimeId);
    if (!factory) throw new Error(`Scientific runtime is not registered: ${runtimeId}`);

    const creation = (async () => {
      const session = await factory.create({ workspaceId });
      if (session.id !== runtimeId) {
        await session.dispose();
        throw new Error(`Runtime factory ${runtimeId} returned ${session.id}`);
      }
      const managed: ManagedRuntime = {
        session,
        state: {
          runtimeId,
          workspaceId,
          label: session.label || factory.label,
          status: 'initializing',
        },
      };
      this.instances.set(key, managed);
      this.emit();

      try {
        await session.initialize(signal);
        managed.state = {
          ...managed.state,
          status: 'ready',
          initializedAt: Date.now(),
          lastUsedAt: Date.now(),
        };
        this.emit();
        await this.evictExcessIdleInstances(key);
        return session;
      } catch (error) {
        managed.state = {
          ...managed.state,
          status: 'error',
          error: error instanceof Error ? error.message : String(error),
        };
        this.emit();
        throw error;
      }
    })();

    this.pending.set(key, creation);
    try {
      return await creation;
    } finally {
      if (this.pending.get(key) === creation) this.pending.delete(key);
    }
  }

  async execute(
    workspaceId: string,
    runtimeId: ScientificRuntimeId,
    request: RuntimeExecutionRequest,
    signal?: AbortSignal,
  ): Promise<RuntimeExecutionResult> {
    const session = await this.ensureReady(workspaceId, runtimeId, signal);
    const key = runtimeKey(workspaceId, runtimeId);
    const managed = this.instances.get(key)!;
    if (managed.state.status === 'running' || managed.state.status === 'interrupting') {
      throw new Error(`${managed.state.label} is already running for this workspace`);
    }

    managed.state = { ...managed.state, status: 'running', error: undefined, lastUsedAt: Date.now() };
    this.emit();

    const onAbort = () => {
      if (!session.interrupt) return;
      managed.state = { ...managed.state, status: 'interrupting' };
      this.emit();
      void session.interrupt().catch(() => undefined);
    };
    signal?.addEventListener('abort', onAbort, { once: true });

    try {
      const result = await session.execute(request, signal);
      if (result.runtime !== runtimeId) {
        throw new Error(`Runtime ${runtimeId} returned a result for ${result.runtime}`);
      }
      managed.state = { ...managed.state, status: 'ready', lastUsedAt: Date.now() };
      this.emit();
      return result;
    } catch (error) {
      managed.state = signal?.aborted
        ? { ...managed.state, status: 'ready', lastUsedAt: Date.now() }
        : {
            ...managed.state,
            status: 'error',
            error: error instanceof Error ? error.message : String(error),
            lastUsedAt: Date.now(),
          };
      this.emit();
      throw error;
    } finally {
      signal?.removeEventListener('abort', onAbort);
    }
  }

  async interrupt(workspaceId: string, runtimeId: ScientificRuntimeId): Promise<void> {
    const managed = this.instances.get(runtimeKey(workspaceId, runtimeId));
    if (!managed?.session.interrupt) return;
    managed.state = { ...managed.state, status: 'interrupting' };
    this.emit();
    try {
      await managed.session.interrupt();
    } finally {
      managed.state = { ...managed.state, status: 'ready', lastUsedAt: Date.now() };
      this.emit();
    }
  }

  async dispose(workspaceId: string, runtimeId: ScientificRuntimeId): Promise<void> {
    const key = runtimeKey(workspaceId, runtimeId);
    const inFlight = this.pending.get(key);
    if (inFlight) {
      try {
        await inFlight;
      } catch {
        // A failed initialization still leaves an error instance to dispose.
      }
    }
    const managed = this.instances.get(key);
    if (!managed) return;
    this.instances.delete(key);
    this.emit();
    await managed.session.dispose();
  }

  async disposeWorkspace(workspaceId: string): Promise<void> {
    const ids = new Set(this.listStates(workspaceId).map((state) => state.runtimeId));
    const prefix = `${workspaceId}::`;
    for (const key of this.pending.keys()) {
      if (key.startsWith(prefix)) ids.add(key.slice(prefix.length) as ScientificRuntimeId);
    }
    await Promise.allSettled([...ids].map((id) => this.dispose(workspaceId, id)));
  }

  async disposeAll(): Promise<void> {
    const pending = [...this.pending.values()];
    if (pending.length) await Promise.allSettled(pending);
    const entries = [...this.instances.values()];
    this.instances.clear();
    this.pending.clear();
    this.emit();
    await Promise.allSettled(entries.map((managed) => managed.session.dispose()));
  }

  private emit(): void {
    for (const listener of this.listeners) listener();
  }
}

export type RuntimePackageState = 'available' | 'installed' | 'unsupported';

export type RuntimePackageDefinition = {
  runtimeId: ScientificRuntimeId;
  name: string;
  version?: string;
  state: RuntimePackageState;
  source?: string;
  estimatedDownloadBytes?: number;
};

/** Shared package metadata registry; concrete runtime adapters own installation. */
export class RuntimePackageRegistry {
  private readonly packages = new Map<string, RuntimePackageDefinition>();

  private key(runtimeId: ScientificRuntimeId, name: string): string {
    return `${runtimeId}:${name.toLowerCase()}`;
  }

  upsert(definition: RuntimePackageDefinition): void {
    this.packages.set(this.key(definition.runtimeId, definition.name), { ...definition });
  }

  get(runtimeId: ScientificRuntimeId, name: string): RuntimePackageDefinition | undefined {
    return this.packages.get(this.key(runtimeId, name));
  }

  list(runtimeId?: ScientificRuntimeId): readonly RuntimePackageDefinition[] {
    return [...this.packages.values()].filter((item) => !runtimeId || item.runtimeId === runtimeId);
  }

  setState(runtimeId: ScientificRuntimeId, name: string, state: RuntimePackageState): void {
    const current = this.get(runtimeId, name);
    if (!current) throw new Error(`Unknown ${runtimeId} package: ${name}`);
    this.upsert({ ...current, state });
  }
}

export type RuntimeFileFingerprint = {
  path: string;
  size: number;
  hash: string;
};

export type RuntimeFileDelta = {
  created: readonly RuntimeFileFingerprint[];
  modified: readonly RuntimeFileFingerprint[];
  deleted: readonly RuntimeFileFingerprint[];
};

export function hashRuntimeBytes(bytes: Uint8Array): string {
  // 32-bit FNV-1a is intentionally non-cryptographic: it is used only to
  // detect runtime workspace changes, never for trust or integrity decisions.
  let hash = 0x811c9dc5;
  for (const byte of bytes) {
    hash ^= byte;
    hash = Math.imul(hash, 0x01000193);
  }
  return (hash >>> 0).toString(16).padStart(8, '0');
}

export function diffRuntimeFiles(
  before: readonly RuntimeFileFingerprint[],
  after: readonly RuntimeFileFingerprint[],
): RuntimeFileDelta {
  const oldMap = new Map(before.map((item) => [item.path, item]));
  const newMap = new Map(after.map((item) => [item.path, item]));
  const created: RuntimeFileFingerprint[] = [];
  const modified: RuntimeFileFingerprint[] = [];
  const deleted: RuntimeFileFingerprint[] = [];

  for (const item of after) {
    const previous = oldMap.get(item.path);
    if (!previous) created.push(item);
    else if (previous.size !== item.size || previous.hash !== item.hash) modified.push(item);
  }
  for (const item of before) {
    if (!newMap.has(item.path)) deleted.push(item);
  }
  return { created, modified, deleted };
}

export type ScientificArtifactKind = 'plot' | 'dataset' | 'table' | 'report' | 'binary' | 'other';

const PLOT_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'webp', 'svg', 'eps']);
const DATASET_EXTENSIONS = new Set(['csv', 'tsv', 'parquet', 'arrow', 'feather', 'rds', 'sqlite', 'sqlite3', 'db']);
const TABLE_EXTENSIONS = new Set(['xlsx', 'ods']);
const BINARY_EXTENSIONS = new Set(['wasm', 'o', 'a']);
const REPORT_EXTENSIONS = new Set(['html', 'htm', 'md', 'docx', 'odt', 'rtf', 'epub']);

export function classifyScientificArtifact(
  path: string,
  plotPaths: ReadonlySet<string> = new Set(),
): ScientificArtifactKind {
  const normalized = path.replace(/^\/+/, '');
  const lower = normalized.toLowerCase();
  const extension = lower.includes('.') ? lower.slice(lower.lastIndexOf('.') + 1) : '';
  if (plotPaths.has(normalized) || lower.startsWith('figures/') || PLOT_EXTENSIONS.has(extension)) {
    return 'plot';
  }
  if (DATASET_EXTENSIONS.has(extension)) return 'dataset';
  if (lower.startsWith('tables/') || TABLE_EXTENSIONS.has(extension)) return 'table';
  if (lower.startsWith('reports/') || REPORT_EXTENSIONS.has(extension)) return 'report';
  if (extension === 'pdf') return lower.startsWith('figures/') ? 'plot' : 'report';
  if (!extension || BINARY_EXTENSIONS.has(extension)) return 'binary';
  return 'other';
}
