import * as ethers from "ethers";
import {
  CertificateDTO,
  CertificateEnvelope,
  CertificateFile,
  EntityType,
  CertificateHistoryResponseEntryInterface,
  Minespider,
  FileMetadata,
  MaterialMassBalanceCertification,
  EntityDTO,
} from "@minespider/core-sdk";
import { AllowanceEntry } from "@minespider/core-sdk/dist/types/domain/queries/getMaterialAllowancesQuery";
import {
  JSONInterface,
  BaseMetadataInterface,
} from "@minespider/core-sdk/dist/types/service/metadata-service";
import { CertificateCacheServiceAdapterInterface, CertificateCacheServiceAdapter } from "../../Communication/Adapter/CertificateCacheServiceAdapter";

const ERROR_NO_PRIVATE_METADATA = "No private metadata found for certificate";
const ERROR_NO_PUBLIC_METADATA = "No public metadata found for certificate";
const ERROR_PATH_NOT_EXIST = "Path doesn't exist";

export interface ParsedCertificate extends CertificateDTO {
  metadataFiles: {
    public: FileMetadata[];
    private: FileMetadata[];
  };
}

const certificateEnvelopesMap = new Map<string, CertificateEnvelope>();
const certificates = new Map<string, ParsedCertificate>();
const entities = new Map<string, EntityDTO>();

export class MinespiderModel {
  private cacheAdapter: CertificateCacheServiceAdapterInterface;
  private wallet: ethers.Wallet;

  constructor(
    private minespider: Minespider,
    mnemonic: string,
    certificateCacheServiceEndpoint: string
  ) {
    this.cacheAdapter = new CertificateCacheServiceAdapter(certificateCacheServiceEndpoint)
    this.wallet = ethers.Wallet.fromMnemonic(mnemonic)
  }

  /**
   * Retreive a single Certificate Data Envelope
   *
   * @param id
   */
  public async getCertificateById(id: string): Promise<any> {
    if (!certificates.has(id.toLowerCase())) {
      try {
        const certificate = await this.cacheAdapter.getCertificate(
          id.toLowerCase(),
          this.wallet.privateKey
        );

        certificates.set(id.toLowerCase(), certificate);
      } catch (error) {
        throw error;
      }
    }

    return Object.assign({}, certificates.get(id.toLowerCase()));
  }

  public async getCertificateHistoryByCertificate(
    certificateUuid: string,
    parentsDepth: number = 10,
    childrenDepth: number = 10
  ): Promise<CertificateHistoryResponseEntryInterface[]> {
    return this.cacheAdapter.getCertificateHistoryByCertificate(
      certificateUuid,
      parentsDepth,
      childrenDepth
    );
  }

  public async getCertificateHistoryByOwner(
    entityId: string
  ): Promise<CertificateHistoryResponseEntryInterface[]> {
    return this.cacheAdapter.getCertificateHistoryByOwner(entityId);
  }

  /**
   * Get all certificates for the authenticated account and store them on the runtime
   * using global constants (@TODO change this behaviour)
   */
  public async getCertificates(): Promise<any> {
    try {
      const ownerUuid = await this.minespider.getCurrentAccountAddress();
      const entries = await this.cacheAdapter.getCertificates(ownerUuid, this.wallet.privateKey)

      await this.refreshEntitiesCache();
      const certificateEnvelopes = await this.minespider.getMyCertificateEnvelopes();

      certificateEnvelopes.map((certificateEnvelope) => {
        certificateEnvelopesMap.set(
          certificateEnvelope.manifest.uuid.toLowerCase(),
          certificateEnvelope
        );
      });

      return Promise.all(
        entries.map(async (certificateDTO: CertificateDTO) => {
          const certificateEnvelope = certificateEnvelopesMap.get(
            certificateDTO.uuid.toLowerCase()
          );

          certificates.set(certificateDTO.uuid.toLowerCase(), {
            ...certificateDTO,
            metadataFiles: {
              public: certificateEnvelope.publicMetadataFiles,
              private: certificateEnvelope.privateMetadataFiles,
            },
          });

          const certificate = certificates.get(certificateDTO.uuid.toLowerCase());

          return Object.assign({}, certificate);
        })
      );
    } catch (error) {
      throw error;
    }
  }

  public async getCertificatesFromCertificateEnvelopes(): Promise<any> {
    try {
      const entries = [];
      const certificateEnvelopes = await this.minespider.getMyCertificateEnvelopes();

      await Promise.all(certificateEnvelopes.map(async (certificateEnvelope) => {
        certificateEnvelopesMap.set(
          certificateEnvelope.manifest.uuid.toLowerCase(),
          certificateEnvelope
        );

        const certificate = await this.cacheAdapter.getCertificate(
          certificateEnvelope.manifest.uuid,
          this.wallet.privateKey
        )

        entries.push(certificate)
      }));

      return entries;
    } catch (error) {
      throw error;
    }
  }

  private async refreshEntitiesCache() {
    (await this.minespider.getEntities()).map((entityDTO) => {
      entities.set(entityDTO.id.toLowerCase(), entityDTO);
    });
  }

  /**
   * General method for creation of different entities (should not be used directly, use the aliases instead)
   *
   * @param name
   * @param latitude
   * @param longitude
   * @param location
   * @param meta
   * @param entityType
   */
  public async createEntity(
    name: string,
    latitude: string,
    longitude: string,
    location: string,
    meta: object,
    entityType: number,
    materialMassBalanceCertifications?: MaterialMassBalanceCertification[]
  ) {
    try {
      const account = await this.minespider.registerEntity(
        entityType,
        name,
        latitude,
        longitude,
        location,
        materialMassBalanceCertifications
      );

      await this.cacheAdapter.registerClient({
        uuid: account.address.toString(),
        type: entityType,
        mnemonic: account.mnemonic.toString(),
        publicKey: account.keyPair.publicKey.toString(),
      });

      await this.refreshEntitiesCache();

      return account;
    } catch (error) {
      throw error;
    }
  }

  /**
   * Alias for creation of the entity type Certifier
   *
   * @param name
   * @param latitude
   * @param longitude
   * @param location
   * @param meta
   */
  public async createCertifier(
    name: string,
    latitude: string,
    longitude: string,
    location: string,
    meta = {}
  ) {
    return this.createEntity(
      name,
      latitude,
      longitude,
      location,
      meta,
      EntityType.Certifier
    );
  }

  /**
   * Alias for creation of the entity type Certifier
   *
   * @param name
   * @param latitude
   * @param longitude
   * @param location
   * @param meta
   */
  public async createProducer(
    name: string,
    latitude: string,
    longitude: string,
    location: string,
    meta = {}
  ) {
    return this.createEntity(
      name,
      latitude,
      longitude,
      location,
      meta,
      EntityType.Producer
    );
  }

  /**
   * Modify Producer entity allowance to produce materialTaxonomyUuid (this is the TOTAL allowance)
   *
   * @param producerId
   * @param materialTaxonomyUuid
   * @param amount
   */
  public async modifyMassBalanceCertification(
    producerId: string,
    materialTaxonomyUuid: string,
    measurementUnitUuid: string,
    amount: number
  ): Promise<any> {
    let result = await this.minespider.modifyMassBalanceCertification(
      producerId,
      materialTaxonomyUuid,
      measurementUnitUuid,
      amount
    );

    return result;
  }

  /**
   * Modify Producer entity allowance to produce materialTaxonomyUuid (this is the TOTAL allowance)
   *
   * @param producerId
   * @param materialTaxonomyUuid
   * @param amount
   */
  public async modifyMassBalanceCertificationForMultipleMaterials(
    producerId: string,
    materialMassBalanceCertifications: MaterialMassBalanceCertification[]
  ): Promise<any> {
    let result = await this.minespider.modifyMassBalanceCertificationForMultipleMaterials(
      producerId,
      materialMassBalanceCertifications
    );

    return result;
  }

  public async getEntityDetails(entityId: string) {
    try {
      return await this.minespider.getEntityDetails(entityId);
    } catch (error) {
      throw error;
    }
  }
  
  public async getEntitiesByName(entityName: string): Promise<EntityDTO[]> {
    try {
      const entities = await this.getEntities();
      return entities.filter(entity => entity.name.toLowerCase().indexOf(entityName.toLowerCase()) > -1);
    } catch (error) {
      throw error;
    }
  }

  public async getEntities(): Promise<EntityDTO[]> {
    if (entities.size == 0) {
      await this.refreshEntitiesCache();
    }

    return Array.from(entities.values());
  }

  /**
   * @TODO
   * @param producerId
   */
  public async getProducerMBCertification(
    producerId: string,
    materialTaxonomyUuid: string,
    measurementUnitUuid: string
  ): Promise<any> {
    try {
      return await this.minespider.getProducerMBCertification(
        producerId,
        materialTaxonomyUuid,
        measurementUnitUuid
      );
    } catch (error) {
      throw error;
    }
  }

  public async downloadCertificateEnvelope(
    address: string
  ): Promise<CertificateEnvelope> {
    return this.minespider.downloadCertificateEnvelope(address);
  }

  /**
   * Create a Certified Data Package (Certificate) with Minespider Protocol into the Blockchain
   *
   * @param certificate
   */
  public async createCertificate(certificate: any): Promise<string> {
    try {
      const response = await this.minespider.createCertificate(
        certificate.certificateAmount,
        certificate.certificateRawAmount,
        certificate.certificateGrade,
        certificate.certificateUnit,
        certificate.certificateProducer,
        certificate.materialTaxonomyUuid,
        certificate.materialTypeUuid,
        certificate.publicFiles,
        certificate.privateFiles,
        certificate.meta
      );

      await this.getCertificates();

      return response;
    } catch (error) {
      throw error;
    }
  }

  /**
   * Sell a Certified Data Package (Certificate) with Minespider Protocol and assign it to a given address
   *
   * @param packetId
   * @param cmw
   * @param newOwner
   * @param publicFilesList
   * @param privateFilesList
   */
  public async sellCertificate(
    certificateUuid: string,
    amount: number,
    newOwnerAddress: string,
    publicFilesList: CertificateFile[],
    privateFilesList: CertificateFile[],
    newCertificateType?: number,
    meta?: BaseMetadataInterface
  ): Promise<any> {
    try {
      if (
        certificates.size <= 0 ||
        !certificates.has(certificateUuid.toLowerCase())
      ) {
        await this.getCertificates();
      } //@TODO this is a workaround remove for getting the certificate envelope directly instead

      const certificateEnvelope = certificateEnvelopesMap.get(
        certificateUuid.toLowerCase()
      );
      const sellCertificateResponse = await this.minespider.sellCertificate(
        certificateEnvelopesMap.get(certificateUuid.toLowerCase()),
        amount,
        newOwnerAddress,
        publicFilesList,
        privateFilesList,
        newCertificateType,
        meta
      );

      const certificateDTO = await this.cacheAdapter.getCertificate(
        certificateEnvelope.manifest.uuid.toLowerCase(),
        this.wallet.privateKey
      );

      certificates.set(certificateEnvelope.manifest.uuid.toLowerCase(), {
        ...certificateDTO,
        metadataFiles: {
          public: certificateEnvelope.publicMetadataFiles,
          private: certificateEnvelope.privateMetadataFiles,
        },
      });

      return sellCertificateResponse;
    } catch (error) {
      throw error;
    }
  }

  /**
   * Retrieve the list of all public files for a given Certificate Id and returns it's data
   *
   * @param certificateUuid
   */
  public async getCertificatePublicFiles(
    certificateUuid: string
  ): Promise<any> {
    if (
      certificates.size <= 0 ||
      !certificates.has(certificateUuid.toLowerCase())
    ) {
      await this.getCertificates();
    } //@TODO this is a workaround remove for getting the certificate envelope directly instead

    const dataPacketEnvelope = certificateEnvelopesMap.get(
      certificateUuid.toLowerCase()
    );

    return dataPacketEnvelope.publicMetadataFiles;
  }

  /**
   * Retrieve the list of all private files for a given CertificateUuid and returns it's data
   *
   * @param certificateUuid
   */
  public async getCertificatePrivateFiles(
    certificateUuid: string
  ): Promise<any> {
    if (
      certificates.size <= 0 ||
      !certificates.has(certificateUuid.toLowerCase())
    ) {
      await this.getCertificates();
    } //@TODO this is a workaround remove for getting the certificate envelope directly instead

    const dataPacketEnvelope = certificateEnvelopesMap.get(
      certificateUuid.toLowerCase()
    );

    return dataPacketEnvelope.privateMetadataFiles;
  }

  public async downloadCertificatePublicFile(
    certificateUuid: string,
    fileAddress: string
  ) {
    const dataPacketEnvelope = certificateEnvelopesMap.get(
      certificateUuid.toLowerCase()
    );

    const fileMetadata = dataPacketEnvelope.publicMetadataFiles.find((file) => {
      return file.target === fileAddress;
    });

    if (!fileMetadata) {
      throw new Error(`No file metadata found for the address ${fileAddress}`);
    }

    return this.minespider.downloadFile(
      dataPacketEnvelope.publicFilesKey,
      fileMetadata
    );
  }

  public async downloadCertificatePrivateFile(
    certificateUuid: string,
    fileAddress: string
  ) {
    const dataPacketEnvelope = certificateEnvelopesMap.get(
      certificateUuid.toLowerCase()
    );

    const fileMetadata = dataPacketEnvelope.privateMetadataFiles.find(
      (file) => {
        return file.target === fileAddress;
      }
    );

    if (!fileMetadata) {
      throw new Error(`No file metadata found for the address ${fileAddress}`);
    }

    return this.minespider.downloadFile(
      dataPacketEnvelope.privateFilesKey,
      fileMetadata
    );
  }

  public async getCurrentAccountAddress() {
    return await this.minespider.getCurrentAccountAddress();
  }

  public async getCertificateManifest(certificateUuid: string) {
    const dataPacketEnvelope = certificateEnvelopesMap.get(
      certificateUuid.toLowerCase()
    );

    return dataPacketEnvelope.manifest;
  }

  public async readCertificateFileList(
    fileList: FileList
  ): Promise<CertificateFile[]> {
    var certificateFiles: CertificateFile[] = [];

    for (let i = 0; i < fileList.length; i++) {
      var fileContent = await new Promise<Buffer>((resolve, reject) => {
        var reader = new FileReader();

        reader.onload = function (event: any) {
          resolve(Buffer.from(event.target.result));
        };

        reader.readAsArrayBuffer(fileList[i]);
      });

      certificateFiles.push(
        new CertificateFile(fileContent, fileList[i].name, {
          type: fileList[i].type,
        })
      );
    }

    return certificateFiles;
  }

  public async getMaterialAllowances(
    balanceHolderUuid: string,
    materialTypeUuid: string,
    measurementUnitUuid: string
  ): Promise<AllowanceEntry[]> {
    return this.minespider.getMaterialAllowances(
      balanceHolderUuid,
      materialTypeUuid,
      measurementUnitUuid
    );
  }

  public async getMaterialTaxonomies() {
    return this.minespider.getMaterialTaxonomies();
  }

  public async getMaterialTypes() {
    return this.minespider.getMaterialTypes();
  }

  public async getMeasurementUnits() {
    return this.minespider.getMeasurementUnits();
  }

  public async getMeasurementUnitTypes() {
    return this.minespider.getMeasurementUnitTypes();
  }

  /**
   * Get private metadata information
   * @param certificateUuid
   * @param path, ex: 'header.sdkVersion'
   */
  public async getPrivateMetadata(
    certificateUuid: string,
    path?: string
  ): Promise<unknown> {
    const dataPacketEnvelope = certificateEnvelopesMap.get(
      certificateUuid.toLowerCase()
    );

    const fileMetadata = dataPacketEnvelope.privateMetadataFiles
      .filter((file) => {
        return file.filename === "minespider.json";
      })
      .pop();

    if (!fileMetadata) {
      throw new Error(`${ERROR_NO_PRIVATE_METADATA} ${certificateUuid}`);
    }

    const file = await this.minespider.downloadFile(
      dataPacketEnvelope.privateFilesKey,
      fileMetadata
    );

    try {
      const metadata = JSON.parse(file.buffer.toString());
      return this.getObjectPath(metadata, path);
    } catch (e) {
      throw e;
    }
  }

  /**
   * Get public metadata information
   * @param certificateUuid
   * @param path, ex: 'header.sdkVersion'
   */
  public async getPublicMetadata(
    certificateUuid: string,
    path?: string
  ): Promise<unknown> {
    const dataPacketEnvelope = certificateEnvelopesMap.get(
      certificateUuid.toLowerCase()
    );

    const fileMetadata = dataPacketEnvelope.publicMetadataFiles
      .filter((file) => {
        return file.filename === "minespider.json";
      })
      .pop();

    if (!fileMetadata) {
      throw new Error(`${ERROR_NO_PUBLIC_METADATA} ${certificateUuid}`);
    }

    const file = await this.minespider.downloadFile(
      dataPacketEnvelope.publicFilesKey,
      fileMetadata
    );

    try {
      const metadata: JSONInterface = JSON.parse(file.buffer.toString());
      return this.getObjectPath(metadata, path);
    } catch (e) {
      throw e;
    }
  }

  private getObjectPath(obj: JSONInterface, path: string): unknown {
    if (!path) {
      return obj;
    }
    return path.split(".").reduce((acc, key) => {
      if (!acc[key]) {
        throw new Error(ERROR_PATH_NOT_EXIST);
      }
      acc = acc[key];
      return acc;
    }, obj);
  }

  public async getCertificateOwner(certificateId: string): Promise<EntityDTO> {
    if (
      certificates.size <= 0 ||
      !certificates.has(certificateId.toLowerCase())
    ) {
      await this.getCertificates();
    }

    const certificate = await this.cacheAdapter.getCertificate(
      certificateId.toLowerCase(),
      this.wallet.privateKey
    )

    if (!certificate) {
      return;
    }

    if (entities.has(certificate.owner.uuid.toLowerCase())) {
      const entity = entities.get(certificate.owner.uuid.toLowerCase())

      return entity;
    }

    const entity = {
      ...(await this.minespider.getEntityDetails(certificate.owner.uuid.toLowerCase())),
      entityType: certificate.owner.type
    }

    if (!entity.id) {
      return;
    }

    entities.set(entity.id.toLowerCase(), entity);

    return entity;
  }

  public async getCertificateParentOwner(
    certificateId: string
  ): Promise<EntityDTO> {
    if (
      certificates.size <= 0 ||
      !certificates.has(certificateId.toLowerCase())
    ) {
      await this.getCertificates();
    }

    const certificate = certificates.get(certificateId.toLowerCase())
    const parentCertificate = await this.cacheAdapter.getCertificate(
      certificate.details.parent.toLowerCase(),
      this.wallet.privateKey
    )

    if (!parentCertificate) {
      return;
    }

    if (entities.has(parentCertificate.owner.uuid.toLowerCase())) {
      return entities.get(parentCertificate.owner.uuid.toLowerCase());
    }

    const entity = {
      ...(await this.minespider.getEntityDetails(parentCertificate.owner.uuid.toLowerCase())),
      entityType: parentCertificate.owner.type
    }
    entities.set(entity.id.toLowerCase(), entity);

    return entity;
  }

  public async getBalanceHolderUuid() {
    return this.minespider.getBalanceHolderUuid();
  }

  public async acceptCertificate(certificateUuid: string): Promise<void> {
    return this.minespider.acceptCertificate(certificateUuid);
  }
}
