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

/**
 * Wasmer's browser SDK is loaded lazily from the same CDN already permitted by
 * the Learnitive CSP. Pinning avoids an unreviewed runtime upgrade changing the
 * browser compiler semantics beneath a saved research project.
 */
export const WASMER_SDK_VERSION = '0.10.0';
export const WASMER_SDK_MODULE_URL = `https://cdn.jsdelivr.net/npm/@wasmer/sdk@${WASMER_SDK_VERSION}/dist/index.mjs`;
export const CLANG_WASMER_PACKAGE = 'clang/clang@0.160000.1';

/**
 * R14.28.9 certified browser utility catalogue. These are deliberately finite,
 * pinned Wasmer Registry packages; arbitrary registry package names are never
 * accepted from terminal/AI execution.
 */
export const WASI_UTILITY_CATALOG = {
  pandoc: { package: 'wasmer/pandoc@0.0.1', command: 'pandoc', description: 'Document conversion (Markdown/HTML/LaTeX and other Pandoc formats)' },
  jaq: { package: 'jaq/jaq@0.1.3', command: 'jaq', description: 'jq-compatible JSON querying and transformation' },
  sqlite: { package: 'sqlite/sqlite@0.2.2', command: 'sqlite', description: 'SQLite database CLI' },
} as const;

export type WasiUtilityName = keyof typeof WASI_UTILITY_CATALOG;
export type WasiToolName = 'clang' | 'clang++' | 'install-clang' | WasiUtilityName | 'install-utility';

type DirectoryEntry = { name: string; type: 'file' | 'dir' | 'directory' | string };
type WasmerDirectory = {
  createDir(path: string): Promise<void>;
  readDir(path: string): Promise<readonly DirectoryEntry[]>;
  readFile(path: string): Promise<Uint8Array>;
  removeDir(path: string): Promise<void>;
  removeFile(path: string): Promise<void>;
  writeFile(path: string, data: Uint8Array | string): Promise<void>;
};
type WasmerOutput = {
  ok: boolean;
  code: number;
  stdout: string;
  stderr: string;
};
type WasmerStdinWriter = { write(data: Uint8Array): Promise<void>; close(): Promise<void> };
type WasmerInstance = {
  wait(): Promise<WasmerOutput>;
  stdin?: { getWriter(): WasmerStdinWriter };
};
type WasmerCommand = {
  run(options?: {
    args?: readonly string[];
    cwd?: string;
    mount?: Readonly<Record<string, WasmerDirectory>>;
    env?: Readonly<Record<string, string>>;
  }): Promise<WasmerInstance>;
};
type WasmerPackage = {
  entrypoint?: WasmerCommand;
  commands?: Readonly<Record<string, WasmerCommand>>;
};
type WasmerApi = {
  init(options?: unknown): Promise<unknown>;
  Directory: new () => WasmerDirectory;
  Wasmer: {
    fromRegistry(specifier: string): Promise<WasmerPackage>;
    fromFile(bytes: Uint8Array): Promise<WasmerPackage>;
  };
};

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

function requireCrossOriginIsolation(): void {
  // The current Wasmer JS SDK uses SharedArrayBuffer internally, including for
  // single-threaded WASIX programs. Fail clearly rather than hanging or
  // pretending a compiler exists when the deployment headers are incomplete.
  if (typeof crossOriginIsolated !== 'undefined' && !crossOriginIsolated) {
    throw new Error(
      'WASI/WASIX requires a cross-origin-isolated Learnitive deployment (COOP/COEP). ' +
      'Use the supplied Apache configuration or equivalent headers.',
    );
  }
  if (typeof SharedArrayBuffer === 'undefined') {
    throw new Error('WASI/WASIX requires SharedArrayBuffer support in this browser context.');
  }
}

function normalizeRelativePath(input: string): string {
  const raw = input.replace(/^\/workspace\/?/u, '').replace(/^\/+/, '');
  const parts: string[] = [];
  for (const part of raw.split('/')) {
    if (!part || part === '.') continue;
    if (part === '..') throw new Error('WASI paths must stay below /workspace');
    parts.push(part);
  }
  return parts.join('/');
}

function directoryPath(input: string): string {
  const normalized = normalizeRelativePath(input);
  return normalized ? `/${normalized}` : '/';
}

function mountedPath(input: string): string {
  const normalized = normalizeRelativePath(input);
  return normalized ? `/workspace/${normalized}` : '/workspace';
}

function resolveRelative(cwd: string | undefined, input: string): string {
  if (input.startsWith('/workspace/')) return normalizeRelativePath(input);
  if (input.startsWith('/')) throw new Error('WASI project arguments may only use /workspace absolute paths');
  const base = normalizeRelativePath(cwd ?? '');
  return normalizeRelativePath(base ? `${base}/${input}` : input);
}

async function ensureDirectoryTree(directory: WasmerDirectory, path: string): Promise<void> {
  const normalized = normalizeRelativePath(path);
  if (!normalized) return;
  let current = '';
  for (const part of normalized.split('/')) {
    current += `/${part}`;
    try { await directory.createDir(current); } catch { /* already exists */ }
  }
}

class BrowserWasixClient {
  private api: WasmerApi | undefined;
  private initializing: Promise<void> | undefined;
  private project: WasmerDirectory | undefined;
  private clangPackage: Promise<WasmerPackage> | undefined;
  private readonly utilityPackages = new Map<WasiUtilityName, Promise<WasmerPackage>>();

  async initialize(signal?: AbortSignal): Promise<void> {
    signal?.throwIfAborted();
    if (this.api && this.project) return;
    if (this.initializing) return this.initializing;
    const run = (async () => {
      requireCrossOriginIsolation();
      const module = await import(/* @vite-ignore */ WASMER_SDK_MODULE_URL) as unknown as WasmerApi;
      signal?.throwIfAborted();
      await module.init();
      signal?.throwIfAborted();
      this.api = module;
      this.project = new module.Directory();
    })();
    this.initializing = run;
    try {
      await run;
    } finally {
      if (this.initializing === run) this.initializing = undefined;
    }
  }

  private requireApi(): WasmerApi {
    if (!this.api) throw new Error('Wasmer SDK is not initialized');
    return this.api;
  }

  private requireProject(): WasmerDirectory {
    if (!this.project) throw new Error('WASI workspace is not initialized');
    return this.project;
  }

  async resetWorkspace(signal?: AbortSignal): Promise<void> {
    await this.initialize(signal);
    const api = this.requireApi();
    this.project = new api.Directory();
  }

  async ensureDirectory(path: string, signal?: AbortSignal): Promise<void> {
    await this.initialize(signal);
    signal?.throwIfAborted();
    await ensureDirectoryTree(this.requireProject(), path);
  }

  async writeFile(path: string, data: Uint8Array, signal?: AbortSignal): Promise<void> {
    await this.initialize(signal);
    signal?.throwIfAborted();
    const normalized = normalizeRelativePath(path);
    const parent = normalized.includes('/') ? normalized.slice(0, normalized.lastIndexOf('/')) : '';
    await ensureDirectoryTree(this.requireProject(), parent);
    await this.requireProject().writeFile(directoryPath(normalized), data);
  }

  async readFile(path: string, signal?: AbortSignal): Promise<Uint8Array | undefined> {
    await this.initialize(signal);
    signal?.throwIfAborted();
    try {
      return await this.requireProject().readFile(directoryPath(path));
    } catch {
      return undefined;
    }
  }

  async listFiles(signal?: AbortSignal): Promise<readonly RuntimeWorkspaceFile[]> {
    await this.initialize(signal);
    signal?.throwIfAborted();
    const directory = this.requireProject();
    const result: RuntimeWorkspaceFile[] = [];
    const visit = async (path: string) => {
      signal?.throwIfAborted();
      let entries: readonly DirectoryEntry[];
      try { entries = await directory.readDir(path || '/'); } catch { return; }
      for (const entry of entries) {
        signal?.throwIfAborted();
        const child = path === '/' || !path ? `/${entry.name}` : `${path}/${entry.name}`;
        if (entry.type === 'dir' || entry.type === 'directory') {
          await visit(child);
          continue;
        }
        try {
          const bytes = await directory.readFile(child);
          result.push({ path: normalizeRelativePath(child), size: bytes.byteLength });
        } catch { /* entry changed during listing */ }
      }
    };
    await visit('/');
    return result.sort((a, b) => a.path.localeCompare(b.path));
  }

  private async installClang(signal?: AbortSignal): Promise<WasmerPackage> {
    signal?.throwIfAborted();
    this.clangPackage ??= this.requireApi().Wasmer.fromRegistry(CLANG_WASMER_PACKAGE);
    try {
      const pkg = await this.clangPackage;
      signal?.throwIfAborted();
      return pkg;
    } catch (error) {
      this.clangPackage = undefined;
      throw error;
    }
  }


  private utilityDefinition(name: string): (typeof WASI_UTILITY_CATALOG)[WasiUtilityName] & { name: WasiUtilityName } {
    const key = name.toLowerCase() as WasiUtilityName;
    const definition = WASI_UTILITY_CATALOG[key];
    if (!definition) throw new Error(`WASI utility is not in the Learnitive certified catalogue: ${name}`);
    return { name: key, ...definition };
  }

  private async installUtility(name: string, signal?: AbortSignal): Promise<{ name: WasiUtilityName; pkg: WasmerPackage }> {
    signal?.throwIfAborted();
    const definition = this.utilityDefinition(name);
    let pending = this.utilityPackages.get(definition.name);
    if (!pending) {
      pending = this.requireApi().Wasmer.fromRegistry(definition.package);
      this.utilityPackages.set(definition.name, pending);
    }
    try {
      const pkg = await pending;
      signal?.throwIfAborted();
      return { name: definition.name, pkg };
    } catch (error) {
      if (this.utilityPackages.get(definition.name) === pending) this.utilityPackages.delete(definition.name);
      throw error;
    }
  }

  private async utility(name: WasiUtilityName, signal?: AbortSignal): Promise<WasmerPackage> {
    signal?.throwIfAborted();
    const pending = this.utilityPackages.get(name);
    if (!pending) {
      throw new Error(`${name}: utility is not loaded. Run \`wasix install ${name}\` first (network approval required).`);
    }
    return pending;
  }

  private async writeStdin(instance: WasmerInstance, stdin: string | undefined): Promise<void> {
    if (!instance.stdin) return;
    const writer = instance.stdin.getWriter();
    if (stdin) await writer.write(new TextEncoder().encode(stdin));
    await writer.close();
  }

  private async runUtility(
    name: WasiUtilityName,
    args: readonly string[],
    cwd: string | undefined,
    stdin: string | undefined,
    signal?: AbortSignal,
  ): Promise<RuntimeExecutionResult> {
    const startedAt = Date.now();
    const definition = this.utilityDefinition(name);
    const pkg = await this.utility(name, signal);
    if (name === 'pandoc') {
      const outputIndex = args.findIndex((arg) => arg === '-o' || arg === '--output');
      const inlineOutput = args.find((arg) => arg.startsWith('--output='))?.slice('--output='.length);
      const output = outputIndex >= 0 ? args[outputIndex + 1] : inlineOutput;
      if (output && !output.startsWith('-')) {
        const resolved = resolveRelative(cwd, output);
        const parent = resolved.includes('/') ? resolved.slice(0, resolved.lastIndexOf('/')) : '';
        await ensureDirectoryTree(this.requireProject(), parent);
      }
    } else if (name === 'sqlite') {
      const database = args.find((arg) => !arg.startsWith('-') && /\.(?:sqlite|sqlite3|db)$/iu.test(arg));
      if (database) {
        const resolved = resolveRelative(cwd, database);
        const parent = resolved.includes('/') ? resolved.slice(0, resolved.lastIndexOf('/')) : '';
        await ensureDirectoryTree(this.requireProject(), parent);
      }
    }
    const command = pkg.commands?.[definition.command] ?? pkg.entrypoint ?? Object.values(pkg.commands ?? {})[0];
    if (!command) throw new Error(`${definition.package} does not expose a runnable ${definition.command} command`);
    const instance = await command.run({
      args,
      cwd: mountedPath(cwd ?? ''),
      mount: { '/workspace': this.requireProject() },
    });
    await this.writeStdin(instance, stdin);
    const output = await instance.wait();
    signal?.throwIfAborted();
    return {
      runtime: 'wasi',
      exitCode: output.code,
      stdout: output.stdout ?? '',
      stderr: output.stderr ?? '',
      startedAt,
      durationMs: Date.now() - startedAt,
      metadata: {
        tool: name,
        registryPackage: definition.package,
        wasmerSdkVersion: WASMER_SDK_VERSION,
      },
    };
  }

  private async clang(signal?: AbortSignal): Promise<WasmerPackage> {
    signal?.throwIfAborted();
    if (!this.clangPackage) {
      throw new Error('Clang toolchain is not loaded. Run `wasix install clang` first (network approval required).');
    }
    return this.clangPackage;
  }

  private compilerArgs(tool: WasiToolName, args: readonly string[], cwd: string | undefined): string[] {
    if (!args.length) throw new Error(`${tool}: expected a C/C++ source file or compiler arguments`);
    const result = [...args];
    if (tool === 'clang++') result.unshift('--driver-mode=g++');

    // Clang is a registry package with its own system root. Only rewrite paths
    // that Learnitive can positively identify as project files or output targets;
    // flags such as -isysroot must remain owned by the compiler package.
    for (let i = 0; i < result.length; i += 1) {
      const value = result[i]!;
      if (value === '-o') {
        const target = result[i + 1];
        if (!target) throw new Error(`${tool}: -o requires an output path`);
        result[i + 1] = mountedPath(resolveRelative(cwd, target));
        i += 1;
        continue;
      }
      if (/^-I[^=].+/u.test(value)) {
        const include = value.slice(2);
        if (include && !include.startsWith('/')) result[i] = `-I${mountedPath(resolveRelative(cwd, include))}`;
        continue;
      }
      if (value === '-I') {
        const include = result[i + 1];
        if (include && !include.startsWith('/')) result[i + 1] = mountedPath(resolveRelative(cwd, include));
        i += 1;
        continue;
      }
      if (!value.startsWith('-') && /\.(?:c|cc|cpp|cxx|h|hh|hpp|a|o)$/iu.test(value)) {
        result[i] = mountedPath(resolveRelative(cwd, value));
      }
    }
    return result;
  }

  private async runTool(
    tool: WasiToolName,
    args: readonly string[],
    cwd: string | undefined,
    signal?: AbortSignal,
    stdin?: string,
  ): Promise<RuntimeExecutionResult> {
    const startedAt = Date.now();
    signal?.throwIfAborted();
    if (tool === 'install-utility') {
      const name = args[0];
      if (!name) throw new Error('install-utility: expected a certified utility name');
      const installed = await this.installUtility(name, signal);
      const definition = this.utilityDefinition(installed.name);
      return {
        runtime: 'wasi',
        exitCode: 0,
        stdout: `Loaded browser WASI utility: ${installed.name} (${definition.package})\n`,
        stderr: '',
        startedAt,
        durationMs: Date.now() - startedAt,
        metadata: { tool, utility: installed.name, registryPackage: definition.package, wasmerSdkVersion: WASMER_SDK_VERSION },
      };
    }
    if (tool === 'pandoc' || tool === 'jaq' || tool === 'sqlite') {
      return this.runUtility(tool, args, cwd, stdin, signal);
    }
    if (tool === 'install-clang') {
      await this.installClang(signal);
      return {
        runtime: 'wasi',
        exitCode: 0,
        stdout: `Loaded browser Clang toolchain: ${CLANG_WASMER_PACKAGE}\n`,
        stderr: '',
        startedAt,
        durationMs: Date.now() - startedAt,
        metadata: { tool, clangPackage: CLANG_WASMER_PACKAGE, wasmerSdkVersion: WASMER_SDK_VERSION },
      };
    }
    const pkg = await this.clang(signal);
    const command = pkg.commands?.[tool] ?? pkg.commands?.clang ?? pkg.entrypoint;
    if (!command) throw new Error(`${CLANG_WASMER_PACKAGE} does not expose a clang entrypoint`);
    const compilerArgs = this.compilerArgs(tool, args, cwd);
    const instance = await command.run({
      args: compilerArgs,
      cwd: mountedPath(cwd ?? ''),
      mount: { '/workspace': this.requireProject() },
    });
    const output = await instance.wait();
    signal?.throwIfAborted();
    return {
      runtime: 'wasi',
      exitCode: output.code,
      stdout: output.stdout ?? '',
      stderr: output.stderr ?? '',
      startedAt,
      durationMs: Date.now() - startedAt,
      metadata: {
        tool,
        clangPackage: CLANG_WASMER_PACKAGE,
        wasmerSdkVersion: WASMER_SDK_VERSION,
      },
    };
  }

  private async runWasm(
    path: string,
    args: readonly string[],
    cwd: string | undefined,
    signal?: AbortSignal,
  ): Promise<RuntimeExecutionResult> {
    const startedAt = Date.now();
    signal?.throwIfAborted();
    if (!/\.wasm$/iu.test(path)) throw new Error('WASI runtime can execute .wasm files only');
    const bytes = await this.readFile(path, signal);
    if (!bytes) throw new Error(`WASI program not found: ${path}`);
    const pkg = await this.requireApi().Wasmer.fromFile(bytes);
    const command = pkg.entrypoint;
    if (!command) throw new Error(`${path} does not expose a runnable WASI/WASIX entrypoint`);
    const instance = await command.run({
      args,
      cwd: mountedPath(cwd ?? ''),
      mount: { '/workspace': this.requireProject() },
    });
    const output = await instance.wait();
    signal?.throwIfAborted();
    return {
      runtime: 'wasi',
      exitCode: output.code,
      stdout: output.stdout ?? '',
      stderr: output.stderr ?? '',
      startedAt,
      durationMs: Date.now() - startedAt,
      metadata: {
        program: path,
        wasmerSdkVersion: WASMER_SDK_VERSION,
      },
    };
  }

  async execute(request: RuntimeExecutionRequest, signal?: AbortSignal): Promise<RuntimeExecutionResult> {
    await this.initialize(signal);
    if (request.mode === 'tool') {
      if (!['clang', 'clang++', 'install-clang', 'install-utility', 'pandoc', 'jaq', 'sqlite'].includes(request.tool)) {
        throw new Error(`Unsupported WASI tool: ${request.tool}`);
      }
      return this.runTool(request.tool as WasiToolName, request.args ?? [], request.cwd, signal, request.stdin);
    }
    if (request.mode === 'file') {
      return this.runWasm(request.path, request.args ?? [], request.cwd, signal);
    }
    throw new Error('WASI/WASIX executes compiled .wasm programs; use clang/clang++ to build C/C++ source.');
  }

  dispose(): void {
    this.project = undefined;
    this.api = undefined;
    this.initializing = undefined;
    this.clangPackage = undefined;
    this.utilityPackages.clear();
  }
}

class BrowserWasiWorkspace implements RuntimeWorkspacePort {
  constructor(private readonly client: BrowserWasixClient) {}
  reset(signal?: AbortSignal): Promise<void> { return this.client.resetWorkspace(signal); }
  ensureDirectory(path: string, signal?: AbortSignal): Promise<void> { return this.client.ensureDirectory(path, signal); }
  writeFile(path: string, data: Uint8Array, signal?: AbortSignal): Promise<void> { return this.client.writeFile(path, data, signal); }
  readFile(path: string, signal?: AbortSignal): Promise<Uint8Array | undefined> { return this.client.readFile(path, signal); }
  listFiles(signal?: AbortSignal): Promise<readonly RuntimeWorkspaceFile[]> { return this.client.listFiles(signal); }
}

class BrowserWasiRuntimeSession implements ScientificRuntimeSession {
  readonly id = 'wasi' as const;
  readonly label = 'C/C++ · Wasmer WASIX';
  readonly capabilities = {
    packages: false,
    plots: false,
    workspaceFiles: true,
    interrupt: false,
  } as const;
  private readonly client = new BrowserWasixClient();
  readonly workspace: RuntimeWorkspacePort = new BrowserWasiWorkspace(this.client);

  initialize(signal?: AbortSignal): Promise<void> { return this.client.initialize(signal); }
  execute(request: RuntimeExecutionRequest, signal?: AbortSignal): Promise<RuntimeExecutionResult> { return this.client.execute(request, signal); }
  dispose(): void { this.client.dispose(); }
}

export function createBrowserWasiRuntimeFactory(): ScientificRuntimeFactory {
  return {
    id: 'wasi',
    label: 'C/C++ · Wasmer WASIX',
    create: () => new BrowserWasiRuntimeSession(),
  };
}
