//@ts-nocheck
import type { RecordsWriteMessage } from '@dwn-protocol/id';

import { Convert } from '../common/index.js';

import type { IDManagedAgent } from './types/agent.js';
import type { ManagedDid } from './did-manager.js';

export interface ManagedDidStore {
  deleteDid(options: { did: string, agent?: IDManagedAgent, context?: string }): Promise<boolean>
  getDid(options: { did: string, agent?: IDManagedAgent, context?: string }): Promise<ManagedDid | undefined>
  findDid(options: { did: string, agent?: IDManagedAgent, context?: string }): Promise<ManagedDid | undefined>
  findDid(options: { alias: string, agent?: IDManagedAgent, context?: string }): Promise<ManagedDid | undefined>
  importDid(options: { did: ManagedDid, agent?: IDManagedAgent, context?: string }): Promise<void>
  listDids(options?: { agent?: IDManagedAgent, context?: string }): Promise<ManagedDid[]>
}

/**
 *
 */
export class DidStoreDwn implements ManagedDidStore {
  private _didRecordProperties = {
    dataFormat : 'application/json',
    schema     : 'https://identity.foundation/schemas/dwn/managed-did'
  };

  async deleteDid(options: {
    agent: IDManagedAgent,
    context?: string,
    did: string
  }): Promise<boolean> {
    const { agent, context, did } = options;

    // Determine which DID to use to author DWN messages.
    const authorDid = await this.getAuthor({ agent, context, did });

    // Query the DWN for all stored DID objects.
    const { reply: queryReply} = await agent.dwnManager.processRequest({
      author         : authorDid,
      target         : authorDid,
      messageType    : 'RecordsQuery',
      messageOptions : {
        filter: { ...this._didRecordProperties }
      }
    });

    // Loop through all of the entries and try to find a match.
    let matchingRecordId: string | undefined;
    for (const record of queryReply.entries ?? []) {
      if (record.encodedData) {
        const storedDid = Convert.base64Url(record.encodedData).toObject() as ManagedDid;
        if (storedDid && storedDid.did === did) {
          matchingRecordId = (record as RecordsWriteMessage).recordId ;
          break;
        }
      }
    }

    // Return undefined if the specified DID was not found in the store.
    if (!matchingRecordId) return false;

    // If a record for the specified DID was found, attempt to delete it.
    const { reply: { status } } = await agent.dwnManager.processRequest({
      author         : authorDid,
      target         : authorDid,
      messageType    : 'RecordsDelete',
      messageOptions : {
        recordId: matchingRecordId
      }
    });

    // If the DID was successfully deleted, return true;
    if (status.code === 202) return true;

    // If the DID could not be deleted, return false;
    return false;
  }

  async findDid(options: { agent: IDManagedAgent, context?: string, did: string }): Promise<ManagedDid | undefined>;
  async findDid(options: { agent: IDManagedAgent, context?: string, alias: string }): Promise<ManagedDid | undefined>;
  async findDid(options: { agent: IDManagedAgent, alias: string, context?: string, did: string }): Promise<ManagedDid | undefined> {
    const { agent, alias, context, did } = options;

    // Determine which DID to use to author DWN messages.
    const authorDid = await this.getAuthor({ agent, context, did });

    // Query the DWN for all stored DID objects.
    const { reply: queryReply} = await agent.dwnManager.processRequest({
      author         : authorDid,
      target         : authorDid,
      messageType    : 'RecordsQuery',
      messageOptions : {
        filter: { ...this._didRecordProperties }
      }
    });

    // Loop through all of the entries and return a match, if found.
    for (const record of queryReply.entries ?? []) {
      if (record.encodedData) {
        const storedDid = Convert.base64Url(record.encodedData).toObject() as ManagedDid;
        if (storedDid && storedDid.did === did) return storedDid;
        if (storedDid && storedDid.alias === alias) return storedDid;
      }
    }

    // Return undefined if no matches were found.
    return undefined;
  }

  async getDid(options: {
    agent: IDManagedAgent,
    context?: string,
    did: string
  }): Promise<ManagedDid | undefined> {
    const { agent, context, did } = options;

    // Determine which DID to use to author DWN messages.
    const authorDid = await this.getAuthor({ agent, context, did });

    // Query the DWN for all stored DID objects.
    const { reply: queryReply} = await agent.dwnManager.processRequest({
      author         : authorDid,
      target         : authorDid,
      messageType    : 'RecordsQuery',
      messageOptions : { filter: { ...this._didRecordProperties } }
    });

    // Loop through all of the entries and return a match, if found.
    for (const record of queryReply.entries ?? []) {
      if (record.encodedData) {
        const storedDid = Convert.base64Url(record.encodedData).toObject() as ManagedDid;
        if (storedDid && storedDid.did === did) return storedDid;
      }
    }

    // Return undefined if no matches were found.
    return undefined;
  }

  async importDid(options: {
    agent: IDManagedAgent,
    context?: string,
    did: ManagedDid
  }) {
    const { agent, context, did: importDid } = options;

    // Determine which DID to use to author DWN messages.
    const authorDid = await this.getAuthor({ agent, context, did: importDid.did });

    // Check if the DID being imported is already present in the store.
    const duplicateFound = await this.getDid({ agent, context, did: importDid.did });
    if (duplicateFound) {
      throw new Error(`DidStoreDwn: DID with ID already exists: '${importDid.did}'`);
    }

    // Encode the ManagedDid as bytes.
    const importDidU8A = Convert.object(importDid).toUint8Array();

    const { reply: { status } } = await agent.dwnManager.processRequest({
      author         : authorDid,
      target         : authorDid,
      messageType    : 'RecordsWrite',
      messageOptions : { ...this._didRecordProperties },
      dataStream     : new Blob([importDidU8A])
    });

    // If the write fails, throw an error.
    if (status.code !== 202) {
      throw new Error('DidStoreDwn: Failed to write imported DID to store.');
    }
  }

  async listDids(options: {
    agent: IDManagedAgent,
    context?: string
  }): Promise<ManagedDid[]> {
    const { agent, context } = options;

    // Determine which DID to use to author DWN messages.
    const authorDid = await this.getAuthor({ agent, context });

    // Query the DWN for all stored DID objects.
    const { reply: queryReply} = await agent.dwnManager.processRequest({
      author         : authorDid,
      target         : authorDid,
      messageType    : 'RecordsQuery',
      messageOptions : {
        filter: { ...this._didRecordProperties }
      }
    });

    // Loop through all of the entries and accumulate the DID objects.
    let storedDids: ManagedDid[] = [];
    for (const record of queryReply.entries ?? []) {
      if (record.encodedData) {
        const storedDid = Convert.base64Url(record.encodedData).toObject() as ManagedDid;
        storedDids.push(storedDid);
      }
    }

    return storedDids;
  }

  private async getAuthor(options: {
    context?: string,
    did?: string,
    agent: IDManagedAgent
  }): Promise<string> {
    const { context, did, agent } = options;

    // If `context` is specified, DWN messages will be signed by this DID.
    if (context) return context;

    // If Agent has an agentDid, use it to sign DWN messages.
    if (agent.agentDid) return agent.agentDid;

    // If `context`, `agent.agentDid`, and `did` are undefined, throw error.
    if (!did) {
      throw new Error(`DidStoreDwn: Agent property 'agentDid' is undefined.`);
    }

    /** Lacking a context and agentDid DID, check whether KeyManager has
     * a key pair for the given `did` value.*/
    const signingKeyId = await agent.didManager.getDefaultSigningKey({ did });
    const keyPair = (signingKeyId)
      ? await agent.keyManager.getKey({ keyRef: signingKeyId })
      : undefined;

    // If a key pair is found, use the `did` to sign messages.
    if (keyPair) return did;

    // If all else fails, throw an error.
    throw new Error(`DidStoreDwn: Agent property 'agentDid' is undefined and no keys were found for: '${did}'`);
  }
}

/**
 *
 */
export class DidStoreMemory implements ManagedDidStore {
  /**
   * A private field that contains the Map used as the in-memory key-value store.
   */
  private store: Map<string, ManagedDid> = new Map();

  async deleteDid({ did }: { did: string; }): Promise<boolean> {
    if (this.store.has(did)) {
      // DID with given identifier exists so proceed with delete.
      this.store.delete(did);
      return true;
    }

    // DID with given identifier not present so delete operation not possible.
    return false;
  }

  async getDid({ did }: { did: string; }): Promise<ManagedDid | undefined> {
    return this.store.get(did);
  }

  async findDid(options: { did: string }): Promise<ManagedDid | undefined>;
  async findDid(options: { alias: string }): Promise<ManagedDid | undefined>;
  async findDid(options: { alias?: string, did?: string}): Promise<ManagedDid | undefined> {
    let { alias, did } = options;

    // Get DID by identifier.
    if (did) return this.store.get(did);

    if (alias) {
      // Search through the store to find a matching entry
      for (const did of this.store.values()) {
        if (did.alias === alias) return did;
      }
    }

    return undefined;
  }

  async importDid(options: { did: ManagedDid }) {
    const { did: importDid } = options;

    if (this.store.has(importDid.did)) {
      // DID with given identifier already exists so import operation cannot proceed.
      throw new Error(`DidStoreMemory: DID with ID already exists: '${importDid.did}'`);
    }

    // Make a deep copy of the DID so that the object stored does not share the same references as the input.
    const clonedDid = structuredClone(importDid);
    this.store.set(importDid.did, clonedDid);
  }

  async listDids(): Promise<ManagedDid[]> {
    return Array.from(this.store.values());
  }
}
