import type { AppDataStore, IDAgent, IDManagedAgent } from './agent/index.js';
import * as Sdk from '@dwn-protocol/id';
import ms from 'ms';
import { IDUserAgent } from './user-agent/index.js';

import { DwnApi } from './dwn-api.js';
import { DidApi } from './did-api.js';
import { getServiceDwnEndpoints } from './service-options.js';
import { DidKeyMethod, DidDhtMethod, DidIonMethod } from './dids/index.js';
import { Metadata } from './interfaces/metadata.js';
import { Queue } from './interfaces/queue.js';
import { Services } from './interfaces/services.js';
import { Transactions } from './interfaces/transactions.js';
import { Jose } from './crypto/index.js';
import { VcApi } from './vc-api.js';
import { Jws } from '@dwn-protocol/id';

/**
 * Override defaults.
 */
export type ServiceOptions = {
  // Override default dwnEndpoints provided.
  dwnEndpoints?: string[];
}

/**
 * Optional overrides that can be provided when calling {@link IDDwn.connect}.
 */
export type IDConnectOptions = {
  /** Provide a {@link IDAgent} implementation. Defaults to creating a local
   * {@link IDUserAgent} if one isn't provided */
  agent?: IDAgent;

  /** Provide an instance of a {@link AppDataStore} implementation. Defaults to
   * a LevelDB-backed store with an insecure, static unlock passphrase if one
   * isn't provided. To allow the app user to enter a secure passphrase of
   * their choosing, provide an initialized {@link AppDataStore} instance. */
  appData?: AppDataStore;

  // Specify an existing DID to connect to.
  connectedDid?: string;

  /** Enable synchronization of DWN records between local and remote DWNs.
   * Sync defaults to running every 30 seconds and can be set to any value accepted by `ms()`.
   * To disable sync set to 'off'. */
  sync?: string;

  /** Override defaults service options.
   * See {@link ServiceOptions} for available options. */
  serviceOptions?: ServiceOptions;

  passphrase?: string;
}

/**
 * @see {@link IDConnectOptions}
 */
type IDOptions = {
  agent: IDAgent;
  connectedDid: string;
};

type UtilsOptions = {
  DidKeyMethod: DidKeyMethod;
  DidDhtMethod: DidDhtMethod;
  DidIonMethod: DidIonMethod;
  Jose: Jose;
  Jws: Jws;
}

export class IDDwn {
  agent: IDAgent;
  private connectedDid: string;
  did: DidApi;
  dwn: DwnApi;
  metadata: Metadata;
  jose: Jose;
  protocol: any;
  queue: Queue;
  services: Services;
  transactions: Transactions;
  utils: UtilsOptions;
  vc: VcApi;

  constructor(options: IDOptions) {
    const { agent, connectedDid } = options;
    this.agent = agent;
    this.connectedDid = connectedDid;
    this.did = new DidApi({ agent, connectedDid });
    this.dwn = new DwnApi({ agent, connectedDid });
    this.metadata = new Metadata({ agent, connectedDid });
    this.protocol = Sdk;
    this.queue = new Queue({ agent, connectedDid });
    this.services = new Services({ agent, connectedDid });
    this.transactions = new Transactions({ agent, connectedDid });
    this.utils = { DidKeyMethod, DidDhtMethod, DidIonMethod, Jose, Jws } as UtilsOptions;
    this.vc = new VcApi({ agent, connectedDid });
  }

  /**
   * Connects to a {@link IDAgent}. Defaults to creating a local {@link IDUserAgent}
   * if one isn't provided.
   *
   * @param options - optional overrides
   * @returns
   */
  static async connect(options: IDConnectOptions = {}) {
    let { agent, appData, connectedDid, sync, serviceOptions, passphrase } = options;

    if (agent === undefined) {

      // Create the agent.
      const userAgent: IDManagedAgent = await IDUserAgent.create({ appData });
      agent = userAgent;

      if (passphrase === undefined) {
        passphrase = 'insecure-static-phrase';
      }

      // Start the agent.
      await userAgent.start({ passphrase });

      // Connect attempt failed or was rejected so fallback to local user agent.
      // if (IDUserAgent.isConnected() === false) {

      // Query the Agent's DWN tenant for identity records.
      const identities = await userAgent.identityManager.list();
      const storedIdentities = identities.length;

      // If an existing identity is not found, create a new one.
      if (storedIdentities === 0) {
        // Use the specified DWN endpoints or get default relayer nodes.
        const serviceEndpointNodes = serviceOptions?.dwnEndpoints ?? await getServiceDwnEndpoints();
        // Generate ION DID service and key set.
        const didOptions = await DidIonMethod.generateDwnOptions({ serviceEndpointNodes });
        // Generate a new Identity for the end-user.
        const identity = await userAgent.identityManager.create({
          name      : 'Default',
          didMethod : 'ion',
          didOptions,
          kms       : 'local'
        });
        /** Import the Identity metadata to the User Agent's tenant so that it can be restored
         * on subsequent launches or page reloads. */
        await userAgent.identityManager.import({ identity, context: userAgent.agentDid });
        // Set the newly created identity as the connected DID.
        // connectedDid = restoreDid? restoreDid : identity.did;
        connectedDid = identity.did;

      } else {
        // An existing identity was found in the User Agent's tenant.
        const [ identity ] = identities;
        // Set the stored identity as the connected DID.
        // connectedDid = restoreDid? restoreDid : identity.did;
        connectedDid = identity.did;
      }

      // }

      // Enable sync, unless disabled.
      if (sync !== 'off') {
        // First, register the user identity for sync.
        await userAgent.syncManager.registerIdentity({ did: connectedDid });

        // Enable sync using the specified interval or default.
        sync ??= '1m';
        userAgent.syncManager.startSync({ interval: ms(sync) })
          .catch(async (error: Error) => {
            console.error(`Sync failed: ${error}`);
          });
      }

    }

    const iddwn = new IDDwn({ agent, connectedDid });

    return { iddwn, did: connectedDid };
  }

}