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

function extractErrorMessage(err: unknown): string {
  if (err instanceof Error) {
    return err.message;
  }
  if (typeof err === 'object' && err !== null) {
    const obj = err as Record<string, unknown>;
    if (typeof obj.message === 'string') return obj.message;
    if (typeof obj.error === 'string') return obj.error;
    if (typeof obj.msg === 'string') return obj.msg;
    const allKeys = [...Object.keys(obj), ...Object.getOwnPropertyNames(obj)];
    for (const key of allKeys) {
      const val = obj[key];
      if (typeof val === 'string' && val.length > 0) {
        return `${key}: ${val}`;
      }
    }
    try {
      const str = JSON.stringify(err, Object.getOwnPropertyNames(err));
      if (str !== '{}') return str;
    } catch {}
    return `[error object with keys: ${allKeys.join(', ') || 'none'}]`;
  }
  return String(err);
}

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 });
    
    try {
      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;
    } catch (err) {
      console.error('[DEBUG] Raw error:', err);
      console.error('[DEBUG] typeof:', typeof err);
      console.error('[DEBUG] constructor:', err?.constructor?.name);
      if (typeof err === 'object' && err !== null) {
        console.error('[DEBUG] keys:', Object.keys(err));
        console.error('[DEBUG] ownProps:', Object.getOwnPropertyNames(err));
        console.error('[DEBUG] prototype:', Object.getPrototypeOf(err));
      }
      const errMsg = extractErrorMessage(err);
      this.log(`generate: failed - ${errMsg}`, 'error');
      throw new Error(`generate: ${errMsg}`);
    }
  }

  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 {
      return { success: false, error: 'invalid source type' };
    }

    const input = JSON.stringify({ database });
    
    try {
      const result = await this.plugin.call('load', input);
      if (!result) {
        return { success: false, error: '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;
    } catch (err) {
      const errMsg = extractErrorMessage(err);
      this.log(`load: failed - ${errMsg}`, 'error');
      return { success: false, error: errMsg };
    }
  }

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

    const input = JSON.stringify({ filter, token });
    
    try {
      const result = await this.plugin.call('exec', input);
      if (!result) {
        return { success: false, error: '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;
    } catch (err) {
      const errMsg = extractErrorMessage(err);
      this.log(`exec: failed - ${errMsg}`, 'error');
      return { success: false, error: errMsg };
    }
  }

  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 });
    
    try {
      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;
    } catch (err) {
      const errMsg = extractErrorMessage(err);
      this.log(`query: failed - ${errMsg}`, 'error');
      throw new Error(`query: ${errMsg}`);
    }
  }

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

    const input = JSON.stringify({ message });
    
    try {
      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;
    } catch (err) {
      const errMsg = err instanceof Error ? err.message : String(err);
      this.log(`ping: failed - ${errMsg}`, 'error');
      throw new Error(`ping: ${errMsg}`);
    }
  }

  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);
}
