import createPlugin, { type Plugin } from '@extism/extism';
import type {
  EnclaveOptions,
  GenerateOutput,
  LoadOutput,
  ExecOutput,
  QueryOutput,
  Resource,
} from './types';

export class Enclave {
  private plugin: Plugin;
  private logger: EnclaveOptions['logger'];
  private debug: boolean;

  private constructor(plugin: Plugin, options: EnclaveOptions = {}) {
    this.plugin = plugin;
    this.logger = options.logger ?? console;
    this.debug = options.debug ?? false;
  }

  static async create(
    wasm: string | Uint8Array,
    options: EnclaveOptions = {}
  ): Promise<Enclave> {
    const manifest =
      typeof wasm === 'string'
        ? { wasm: [{ url: wasm }] }
        : { wasm: [{ data: wasm }] };

    const plugin = await createPlugin(manifest, {
      useWasi: true,
      runInWorker: true,
      logger: options.debug ? (options.logger as Console) : undefined,
    });

    return new Enclave(plugin, options);
  }

  async generate(credential: string): Promise<GenerateOutput> {
    this.log('generate: starting with credential');

    const input = JSON.stringify({ credential });
    const result = await this.plugin.call('generate', input);
    if (!result) throw new Error('generate: plugin returned no output');
    const output = result.json() as GenerateOutput;

    this.log(`generate: created DID ${output.did}`);
    return output;
  }

  async load(source: Uint8Array | number[]): Promise<LoadOutput> {
    this.log('load: loading database');

    let database: number[];

    if (source instanceof Uint8Array) {
      database = Array.from(source);
    } else if (Array.isArray(source)) {
      database = source;
    } else {
      throw new Error('load: invalid source type');
    }

    const input = JSON.stringify({ database });
    const result = await this.plugin.call('load', input);
    if (!result) throw new Error('load: plugin returned no output');
    const output = result.json() as LoadOutput;

    if (output.success) {
      this.log(`load: loaded database for DID ${output.did}`);
    } else {
      this.log(`load: failed - ${output.error}`, 'error');
    }

    return output;
  }

  async exec(filter: string, token?: string): Promise<ExecOutput> {
    this.log(`exec: executing filter "${filter}"`);

    const input = JSON.stringify({ filter, token });
    const result = await this.plugin.call('exec', input);
    if (!result) throw new Error('exec: plugin returned no output');
    const output = result.json() as ExecOutput;

    if (output.success) {
      this.log('exec: completed successfully');
    } else {
      this.log(`exec: failed - ${output.error}`, 'error');
    }

    return output;
  }

  async execute(
    resource: Resource,
    action: string,
    options: { subject?: string; token?: string } = {}
  ): Promise<ExecOutput> {
    let filter = `resource:${resource} action:${action}`;
    if (options.subject) {
      filter += ` subject:${options.subject}`;
    }
    return this.exec(filter, options.token);
  }

  async query(did: string = ''): Promise<QueryOutput> {
    this.log(`query: resolving DID ${did || '(current)'}`);

    const input = JSON.stringify({ did });
    const result = await this.plugin.call('query', input);
    if (!result) throw new Error('query: plugin returned no output');
    const output = result.json() as QueryOutput;

    this.log(`query: resolved DID ${output.did}`);
    return output;
  }

  async ping(message: string = 'hello'): Promise<{ success: boolean; message: string; echo: string }> {
    this.log(`ping: sending "${message}"`);

    const input = JSON.stringify({ message });
    const result = await this.plugin.call('ping', input);
    if (!result) throw new Error('ping: plugin returned no output');
    const output = result.json() as { success: boolean; message: string; echo: string };

    this.log(`ping: received ${output.success ? 'pong' : 'error'}`);
    return output;
  }

  async reset(): Promise<void> {
    this.log('reset: clearing plugin state');
    await this.plugin.reset();
  }

  async close(): Promise<void> {
    this.log('close: releasing plugin resources');
    await this.plugin.close();
  }

  private log(message: string, level: 'log' | 'error' | 'warn' | 'info' | 'debug' = 'debug'): void {
    if (this.debug && this.logger) {
      this.logger[level](`[Enclave] ${message}`);
    }
  }
}

export async function createEnclave(
  wasm: string | Uint8Array,
  options: EnclaveOptions = {}
): Promise<Enclave> {
  return Enclave.create(wasm, options);
}
