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

export const WEBR_VERSION = '0.6.0';
export const WEBR_BASE_URL = `https://webr.r-wasm.org/v${WEBR_VERSION}/`;
export const WEBR_MODULE_URL = `${WEBR_BASE_URL}webr.mjs`;
export const WEBR_REPO_URL = 'https://repo.r-wasm.org/';

export const R_SCIENTIFIC_PACKAGES: readonly RuntimePackageDefinition[] = [
  { runtimeId: 'r', name: 'stats', state: 'installed', source: `R / webR ${WEBR_VERSION}` },
  { runtimeId: 'r', name: 'graphics', state: 'installed', source: `R / webR ${WEBR_VERSION}` },
  { runtimeId: 'r', name: 'grDevices', state: 'installed', source: `R / webR ${WEBR_VERSION}` },
  { runtimeId: 'r', name: 'utils', state: 'installed', source: `R / webR ${WEBR_VERSION}` },
  { runtimeId: 'r', name: 'datasets', state: 'installed', source: `R / webR ${WEBR_VERSION}` },
  { runtimeId: 'r', name: 'methods', state: 'installed', source: `R / webR ${WEBR_VERSION}` },
  { runtimeId: 'r', name: 'Matrix', state: 'available', source: WEBR_REPO_URL },
  { runtimeId: 'r', name: 'ggplot2', state: 'available', source: WEBR_REPO_URL },
  { runtimeId: 'r', name: 'plotly', state: 'available', source: WEBR_REPO_URL },
  { runtimeId: 'r', name: 'jsonlite', state: 'available', source: WEBR_REPO_URL },
  { runtimeId: 'r', name: 'cli', state: 'available', source: WEBR_REPO_URL },
] as const;

type RObject = {
  toArray?(): Promise<unknown[]>;
  toBoolean?(): Promise<boolean>;
};

type WebRShelter = {
  captureR(code: string, options?: { captureGraphics?: boolean | { width: number; height: number; bg?: string } }): Promise<{
    images: ImageBitmap[];
    output: { data: unknown; type: string }[];
    result: RObject;
  }>;
  purge(): Promise<void>;
};

type WebRInstance = {
  version: string;
  versionR: string;
  FS: {
    lookupPath(path: string): Promise<unknown>;
    mkdir(path: string): Promise<unknown>;
    readFile(path: string, flags?: string): Promise<Uint8Array>;
    unlink(path: string): Promise<void>;
    rmdir(path: string): Promise<void>;
    writeFile(path: string, data: ArrayBufferView, flags?: string): Promise<void>;
  };
  Shelter: new () => Promise<WebRShelter>;
  init(): Promise<unknown>;
  evalR(code: string, options?: Record<string, unknown>): Promise<RObject>;
  evalRString(code: string, options?: Record<string, unknown>): Promise<string>;
  evalRVoid(code: string, options?: Record<string, unknown>): Promise<void>;
  destroy(object: RObject): Promise<void>;
  installPackages(packages: string | string[]): Promise<void>;
  interrupt(): void;
  close(): void;
};

type WebRModule = {
  WebR: new (options?: {
    baseUrl?: string;
    repoUrl?: string;
    interactive?: boolean;
    homedir?: string;
  }) => WebRInstance;
};

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

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('R runtime paths must stay below /workspace');
    parts.push(part);
  }
  return parts.join('/');
}

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

function rString(value: string): string {
  return `"${value
    .replace(/\\/gu, '\\\\')
    .replace(/"/gu, '\\"')
    .replace(/\r/gu, '\\r')
    .replace(/\n/gu, '\\n')
    .replace(/\t/gu, '\\t')}"`;
}

async function imageBitmapToPng(image: ImageBitmap): Promise<Uint8Array | undefined> {
  try {
    if (typeof OffscreenCanvas !== 'undefined') {
      const canvas = new OffscreenCanvas(image.width, image.height);
      const context = canvas.getContext('2d');
      if (!context) return undefined;
      context.drawImage(image, 0, 0);
      const blob = await canvas.convertToBlob({ type: 'image/png' });
      return new Uint8Array(await blob.arrayBuffer());
    }
    if (typeof document !== 'undefined') {
      const canvas = document.createElement('canvas');
      canvas.width = image.width;
      canvas.height = image.height;
      const context = canvas.getContext('2d');
      if (!context) return undefined;
      context.drawImage(image, 0, 0);
      const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'));
      return blob ? new Uint8Array(await blob.arrayBuffer()) : undefined;
    }
  } catch {
    return undefined;
  }
  return undefined;
}

class BrowserRClient {
  private webR: WebRInstance | undefined;
  private initializing: Promise<void> | undefined;

  async initialize(signal?: AbortSignal): Promise<void> {
    signal?.throwIfAborted();
    if (this.webR) return;
    if (this.initializing) return this.initializing;
    const run = (async () => {
      const module = await import(/* @vite-ignore */ WEBR_MODULE_URL) as unknown as WebRModule;
      signal?.throwIfAborted();
      const webR = new module.WebR({
        baseUrl: WEBR_BASE_URL,
        repoUrl: WEBR_REPO_URL,
        interactive: false,
      });
      await webR.init();
      signal?.throwIfAborted();
      this.webR = webR;
      await this.resetWorkspace();
    })();
    this.initializing = run;
    try {
      await run;
    } finally {
      if (this.initializing === run) this.initializing = undefined;
    }
  }

  private require(): WebRInstance {
    if (!this.webR) throw new Error('webR is not initialized');
    return this.webR;
  }

  async resetWorkspace(signal?: AbortSignal): Promise<void> {
    signal?.throwIfAborted();
    const webR = this.require();
    await webR.evalRVoid("unlink('/workspace', recursive=TRUE, force=TRUE); dir.create('/workspace', recursive=TRUE, showWarnings=FALSE)");
  }

  async ensureDirectory(path: string, signal?: AbortSignal): Promise<void> {
    signal?.throwIfAborted();
    const target = workspacePath(path);
    if (target === '/workspace') return;
    try {
      await this.require().FS.lookupPath(target);
    } catch {
      await this.require().FS.mkdir(target);
    }
  }

  async writeFile(path: string, data: Uint8Array, signal?: AbortSignal): Promise<void> {
    signal?.throwIfAborted();
    await this.require().FS.writeFile(workspacePath(path), data);
  }

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

  async listFiles(signal?: AbortSignal): Promise<readonly RuntimeWorkspaceFile[]> {
    signal?.throwIfAborted();
    const webR = this.require();
    const object = await webR.evalR("list.files('/workspace', recursive=TRUE, all.files=TRUE, full.names=FALSE, include.dirs=FALSE)");
    try {
      const values = object.toArray ? await object.toArray() : [];
      const files: RuntimeWorkspaceFile[] = [];
      for (const value of values) {
        signal?.throwIfAborted();
        if (typeof value !== 'string' || !value) continue;
        const bytes = await this.readFile(value, signal);
        if (bytes) files.push({ path: normalizeRelativePath(value), size: bytes.byteLength });
      }
      return files;
    } finally {
      await webR.destroy(object).catch(() => undefined);
    }
  }

  async environment(signal?: AbortSignal): Promise<Record<string, string>> {
    signal?.throwIfAborted();
    const webR = this.require();
    return {
      webR: webR.version || WEBR_VERSION,
      R: webR.versionR || 'unknown',
      loadedPackages: await webR.evalRString("paste(.packages(), collapse=',')"),
    };
  }

  async installPackages(packages: readonly string[], signal?: AbortSignal): Promise<readonly string[]> {
    signal?.throwIfAborted();
    if (!packages.length) return [];
    await this.require().installPackages([...packages]);
    signal?.throwIfAborted();
    return packages;
  }

  async execute(request: RuntimeExecutionRequest, signal?: AbortSignal): Promise<RuntimeExecutionResult> {
    if (request.mode === 'tool') throw new Error(`R runtime does not expose structured tool: ${request.tool}`);
    signal?.throwIfAborted();
    const webR = this.require();
    const startedAt = Date.now();
    const cwd = workspacePath(request.cwd ?? '');
    const args = [...(request.args ?? [])];
    const argumentVector = args.length ? `c(${args.map(rString).join(',')})` : 'character()';
    const execution = request.mode === 'file'
      ? `sys.source(${rString(workspacePath(request.path))}, envir=environment(), keep.source=TRUE)`
      : `source(textConnection(${rString(request.code)}), local=environment(), echo=FALSE, print.eval=TRUE)`;
    const programName = request.mode === 'file' ? workspacePath(request.path) : request.sourceName ?? '<command>';
    const wrapper = `local({\n  setwd(${rString(cwd)})\n  .learnitive_args <- ${argumentVector}\n  commandArgs <- function(trailingOnly=FALSE) {\n    if (trailingOnly) return(.learnitive_args)\n    c(${rString('Rscript')}, ${rString(`--file=${programName}`)}, .learnitive_args)\n  }\n  tryCatch({\n    ${execution}\n    TRUE\n  }, error=function(e) {\n    message(paste0('Error: ', conditionMessage(e)))\n    FALSE\n  })\n})`;

    const shelter = await new webR.Shelter();
    let stdout = '';
    let stderr = '';
    let exitCode = 0;
    const plots: RuntimePlot[] = [];
    try {
      const capture = await shelter.captureR(wrapper, {
        captureGraphics: { width: 1008, height: 756, bg: 'white' },
      });
      for (const item of capture.output) {
        const text = typeof item.data === 'string' ? item.data : String(item.data ?? '');
        if (!text) continue;
        if (item.type === 'stdout') stdout += `${text}${text.endsWith('\n') ? '' : '\n'}`;
        else stderr += `${text}${text.endsWith('\n') ? '' : '\n'}`;
      }
      if (capture.result.toBoolean) exitCode = (await capture.result.toBoolean()) ? 0 : 1;

      let plotIndex = 0;
      for (const image of capture.images) {
        plotIndex += 1;
        const png = await imageBitmapToPng(image);
        try { image.close(); } catch { /* browser may already have released it */ }
        if (!png) continue;
        await this.ensureDirectory('figures', signal);
        const path = `figures/interactive-r-plot-${startedAt}-${plotIndex}.png`;
        await this.writeFile(path, png, signal);
        plots.push({ path, mimeType: 'image/png', title: `R plot ${plotIndex}` });
      }
    } catch (error) {
      if (signal?.aborted) throw abortError();
      exitCode = 1;
      stderr += `${error instanceof Error ? error.message : String(error)}\n`;
    } finally {
      await shelter.purge().catch(() => undefined);
    }

    const environment = await this.environment(signal).catch(() => ({ webR: WEBR_VERSION, R: 'unknown', loadedPackages: '' }));
    return {
      runtime: 'r',
      exitCode,
      stdout,
      stderr,
      startedAt,
      durationMs: Date.now() - startedAt,
      ...(plots.length ? { plots } : {}),
      metadata: {
        webRVersion: environment.webR,
        rVersion: environment.R,
        loadedPackages: environment.loadedPackages,
      },
    };
  }

  interrupt(): void {
    this.webR?.interrupt();
  }

  dispose(): void {
    this.webR?.close();
    this.webR = undefined;
    this.initializing = undefined;
  }
}

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

class BrowserRRuntimeSession implements ScientificRuntimeSession {
  readonly id = 'r' as const;
  readonly label = `R · webR ${WEBR_VERSION}`;
  readonly capabilities = { packages: true, plots: true, workspaceFiles: true, interrupt: true } as const;
  private readonly client = new BrowserRClient();
  readonly workspace: RuntimeWorkspacePort = new BrowserRWorkspace(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); }
  interrupt(): Promise<void> { this.client.interrupt(); return Promise.resolve(); }
  installPackages(packages: readonly string[], signal?: AbortSignal): Promise<readonly string[]> { return this.client.installPackages(packages, signal); }
  dispose(): void { this.client.dispose(); }
}

export function createBrowserRRuntimeFactory(): ScientificRuntimeFactory {
  return {
    id: 'r',
    label: `R · webR ${WEBR_VERSION}`,
    create: () => new BrowserRRuntimeSession(),
  };
}
