import { ACTION } from "./consts";
import { fetchURL } from "./common";
import { EventEmitter } from "events";
import { generate } from 'hmac-auth-express';
import {v4 as uuid} from 'uuid';
import { CHAIN, CommunityUser, Network, Option, RequestResponse, SignData, Transaction } from "./types";
import { isNativeEventSourceAvailable } from "./utils";

// const {} = require('event-source-polyfill');

const EventSourcePolyfill = require('event-source-polyfill').EventSourcePolyfill;
// // global.EventSource =  ESPolyfill.NativeEventSource || ESPolyfill.EventSourcePolyfill;

export default class Community extends EventEmitter {
  private _endPoint: string;
  private _apiKey: string;
  private _apiSecret?: string;
  private MAX_COUNT: number = 60;
  private _eventDrivenMode: boolean;
  private _es: EventSource | typeof EventSourcePolyfill | null = null;
  private _esRequests: string[] = [];
  private _esRequestShowHex: Map<string, boolean> = new Map();

  constructor(option: Option) {
    super();
    this._endPoint = option.endPoint;
    this._apiKey = option.apiKey;
    this._apiSecret = option.apiSecret;
    this._eventDrivenMode = option.eventDrivenMode ?? false;
  }

  private _callApi(path: string, params: any, action: string): Promise<any> {
    let option = {
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
    };

    Object.assign(option.headers, { "x-api-key": `${this._apiKey}` });
    if (params) {
      Object.assign(option, { body: JSON.stringify(params) });
    }

    Object.assign(option, { method: action });

    return fetchURL(path, option);
  }

  private _callHMACApi(path: string, request_body: any, action: string): Promise<any> {
    let option = {};

    if (this._apiSecret == null) throw new Error ('apiSecret is required');

    let _path = path.startsWith('/') ? path : '/' + path;
    _path = _path.indexOf('?') < 0 ? `${_path}?requestId=${uuid()}` : (_path.endsWith('&') ? `${_path}requestId=${uuid()}` : `${path}&requestId=${uuid()}`);
    
    const time = Date.now().toString();
    const digest = generate(this._apiSecret, 'sha512', time, action, _path, request_body ?? {}).digest('hex'); 
    const hmac = `HMAC ${time}:${digest}`;

    // the request header should be like this
    Object.assign(option, { 
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'api-key': this._apiKey, 
            'Authorization': hmac,
        },
        'method': action
    })

    
    if (request_body && Object.keys(request_body).length > 0)
        Object.assign(option, { body: JSON.stringify(request_body) });

    return fetchURL(this._endPoint + _path, option);  
  }

  public signalPull(requestId: string): void {
    this.emit(`ready-to-pull-${requestId}`);
  }

  public setEventDrivenMode(mode: boolean): void {
    this._eventDrivenMode = mode;
  }

  public async registerEventListener (projectId: string, session: string): Promise<void> {
    if(this._eventDrivenMode) {
      var _es: any;
      if(this._es == null) {
        if(isNativeEventSourceAvailable()) {
          _es = new EventSource(this._endPoint + `/v0/community2/events?session=${session}&projectId=${projectId}`);
        }
        else {
          _es = new EventSourcePolyfill(this._endPoint + `/v0/community2/events?session=${session}&projectId=${projectId}`);
        }

        if (_es) {
          _es.onmessage = (e: any) => {
            try {
              console.log("EventSource message:", e.data);
              let requestDone = true;
              let data = JSON.parse(e.data);
              let showHex = this._esRequestShowHex.get(data.id);
              if (["completed"].includes(data.txnStatus)) {
                this.emit("completed", data.id, showHex ? data.hex :  data.txnId);
              } else if (["sent"].includes(data.txnStatus)) {
                this.emit("sent", data.id, showHex ? data.hex :  data.txnId);
              } else if (["error", "sent_error"].includes(data.txnStatus)) {
                this.emit("error", data.id, data.errorReason ?? data.txnErrorReason);
              } else if (data.status === "cancelled") {
                this.emit("cancelled", data.id);
              } else if (["warning", "error"].includes(data.status)) {
                this.emit("error", data.id, data.errorReason ?? data.txnErrorReason);
              } else if (data.address) {
                this.emit("address_generated", data.id, data.address);
              } else if (data.newAddress) {
                this.emit("new_address_generated", data.id, data.newAddress);
              }
              else {
                requestDone = false;
              }

              //remove request id from _esRequests and
              if(requestDone) {
                 this._esRequests = this._esRequests.filter((id) => id !== data.id);
                 this._esRequestShowHex.delete(data.id);
              }
              // determine if all the requests are responded
              if (this._esRequests.length === 0) {
                // console.log("Closing EventSource")
                _es.close();
                this._es = null;
              }
            }
            catch (ex) {
              console.error(ex);
            }
          };
          this._es = _es;
        }
      }
    }
  }

  private _pull(requestId: string, session: string, showHex: boolean = false): void {
    if(this._eventDrivenMode) {
      // this.once(`ready-to-pull-${requestId}`, () => {
      //   this._pullRequest(requestId, session, showHex);
      // });
      this._esRequests.push(requestId);
      this._esRequestShowHex.set(requestId, showHex);
      var _es: any;
      if(this._es == null ) {
        if(isNativeEventSourceAvailable()) {
          _es = new EventSource(this._endPoint + `/v0/community2/events?session=${session}&requestId=${requestId}`);
        }
        else {
          _es = new EventSourcePolyfill(this._endPoint + `/v0/community2/events?session=${session}&requestId=${requestId}`);
        }

        if (_es) {
          _es.onmessage = (e: any) => {
            try {
              console.log("EventSource message:", e.data);
              let requestDone = true;
              let data = JSON.parse(e.data);
              if (["completed"].includes(data.txnStatus)) {
                this.emit("completed", data.id, showHex ? data.hex :  data.txnId);
              } else if (["sent"].includes(data.txnStatus)) {
                this.emit("sent", data.id, showHex ? data.hex :  data.txnId);
              } else if (["error", "sent_error"].includes(data.txnStatus)) {
                this.emit("error", data.id, data.errorReason ?? data.txnErrorReason);
              } else if (data.status === "cancelled") {
                this.emit("cancelled", data.id);
              } else if (["warning", "error"].includes(data.status)) {
                this.emit("error", data.id, data.errorReason ?? data.txnErrorReason);
              } else if (data.address) {
                this.emit("address_generated", data.id, data.address);
              } else if (data.newAddress) {
                this.emit("new_address_generated", data.id, data.newAddress);
              }
              else {
                requestDone = false;
              }

              //remove request id from _esRequests and
              if(requestDone) {
                 this._esRequests = this._esRequests.filter((id) => id !== data.id);
                 this._esRequestShowHex.delete(data.id);
              }
              // determine if all the requests are responded
              if (this._esRequests.length === 0) {
                // console.log("Closing EventSource")
                _es.close();
                this._es = null;
              }
            }
            catch (ex) {
              console.error(ex);
            }
          };
          this._es = _es;
        }
      }
    } else {
      this._pullRequest(requestId, session, showHex);
    }
  }

  private _pullRequest(requestId: string, session: string, showHex: boolean): void {
    let count = 0;
    const interval = setInterval(() => {
      if (count >= this.MAX_COUNT) {
        clearInterval(interval);
      }
      count++;

      this._callApi(this._endPoint + `/v0/community2/status_pull/requests/${requestId}?session=${session}`, null, ACTION.GET)
        .then((res2) => {
          if (["completed"].includes(res2.body.txnStatus)) {
            this.emit("completed", res2.body.id, showHex ? res2.body.hex :  res2.body.txnId);
            clearInterval(interval);
          } else if (["error", "sent_error"].includes(res2.body.txnStatus)) {
            this.emit("error", res2.body.id, res2.body.errorReason ?? res2.body.txnErrorReason);
            clearInterval(interval);
          } else if (res2.body.status === "cancelled") {
            this.emit("cancelled", res2.body.id);
            clearInterval(interval);
          } else if (["warning", "error"].includes(res2.body.status)) {
            this.emit("error", res2.body.id, res2.body.errorReason ?? res2.body.txnErrorReason);
            clearInterval(interval);
          } else if (res2.body.address) {
            this.emit("address_generated", res2.body.id, res2.body.address);
            clearInterval(interval);
          } else if (res2.body.newAddress) {
            this.emit("new_address_generated", res2.body.id, res2.body.newAddress);
            clearInterval(interval);
          }
        })
        .catch((ex) => {
          console.error(ex);
        });
    }, 3000);
  }

  /**
   * Create Community User in Custonomy Community
   * @param projectId - The UUID of the project
   * @param session - The external user id of the project site
   * @param chain - either ETH or SOL 
   * @returns The status and callback url for user pin input
   */
  createUser(projectId: string, session: string, chain: CHAIN = CHAIN.ETH): Promise<RequestResponse> {
    return this._callApi(
      this._endPoint + "/v0/community2/requests",
      {
        projectId: projectId,
        session: session,
        type: "createaccount",
        params: {chain: chain}
      },
      ACTION.POST
    ).then((response: any) => {
      let count = 0;
      const { body, status } = response;

      if (status != 200) {
        throw new Error(JSON.stringify({ ...body, status }));
      } else {
        //TODO: temparay fake the procdess
        this._pull(body.id, session);       
        return {
          id: body.id,
          status: body.status,
          callbackURL: body.callbackURL,
        } as RequestResponse;
      }
    });
  }

  /**
   * Get User Address
   * @param projectId - The UUID of the project
   * @param session - The external user id
   * @param chain - either ETH or SOL 
   * @returns
   */
  getUserInfo(projectId: string, session: string, chain?: CHAIN): Promise<CommunityUser | null> {
    return this._callApi(this._endPoint + `/v0/community2/users?projectId=${projectId}&session=${session}${chain ? `&chain=${chain}` : ''}`, null, ACTION.GET).then((response: any) => {
      if (response?.body?.error != null) throw new Error(JSON.stringify(response.body));
      if (response == null) return null;
      let user = response.body as CommunityUser;
      return user;
    });
  }

  /**
   * Get User Address
   * @param projectId - The UUID of the project
   * @param session - The external user id
   * @returns
   */
  getAddress(projectId: string, session: string, chain?: CHAIN): Promise<string | null> {
    return this._callApi(this._endPoint + `/v0/community2/users?projectId=${projectId}&session=${session}${chain ? `&chain=${chain}` : ''}`, null, ACTION.GET).then((response: any) => {
      if (response?.body?.error != null) throw new Error(JSON.stringify(response.body));
      if (response == null) return null;
      let user = response.body as CommunityUser;
      return user.addresses?.length > 0 ? user.addresses[0]?.address : null;
    });
  }

  /**
   * Get User Address
   * @param projectId - The UUID of the project
   * @param session - The external user id
   * @returns
   */
  getAddressWithPubkey(projectId: string, session: string, chain?: CHAIN): Promise<{address: string, pubkey: string} | null> {
    return this._callApi(this._endPoint + `/v0/community2/users?projectId=${projectId}&session=${session}${chain ? `&chain=${chain}` : ''}`, null, ACTION.GET).then((response: any) => {
      if (response?.body?.error != null) throw new Error(JSON.stringify(response.body));
      if (response == null) return null;
      let user = response.body as CommunityUser;
      return user.addresses?.length > 0 ? {address: user.addresses[0]?.address, pubkey: user.addresses[0]?.pubkey} : null;
    });
  }

  /**
   * Submit transaction to Custonomy Community
   * @param projectId - The UUID of the project
   * @param externalUserId - The external user id of the project site for signing
   * @param transaction - The transaction detail that rqeuired to be signed by Custonomy Community
   * @returns The status and callback url for user pin input
   */
  submitTransaction(projectId: string, session: string, transaction: Transaction, showHex: boolean = false): Promise<RequestResponse> {
    return this._callApi(
      this._endPoint + "/v0/community2/requests",
      {
        projectId: projectId,
        session: session,
        type: "createtxn",
        params: transaction,
      },
      ACTION.POST
    )
      .then((response: any) => {
        const { body, status } = response;
        let count = 0;
        if (status != 200) {
          throw new Error(JSON.stringify({ ...body, status }));
        } else {
          //TODO: temparay fake the procdess
          this._pull(body.id, session, showHex);         
          return {
            id: body.id,
            status: body.status,
            callbackURL: body.callbackURL,
          } as RequestResponse;
        }
      })      
  }

  public async signTransaction(projectId: string, session: string, data: Transaction | SignData) {
    let txn: Transaction = data as Transaction;
    if (txn.gas) txn.gasLimit = txn.gas;
    let obj: Transaction | SignData = txn;

    return this._callApi(
      this._endPoint + "/v0/community2/requests",
      {
        projectId: projectId,
        session: session,
        type: "createtxn",
        params: { ...obj, method: obj.method ?? "eth_signTransaction" },
      },
      ACTION.POST
    ).then((response: any) => {
      // console.log({response})
      const { body, status } = response;
      let count = 0;
      if (status != 200) {
        throw new Error(JSON.stringify({ ...body, status }));
      } else {
        //TODO: temparay fake the procdess
        this._pull(body.id, session, true);
        
        return {
          id: body.id,
          status: body.status,
          callbackURL: body.callbackURL,
        } as RequestResponse;
      }
    });
  }

  public async signData(projectId: string, session: string, data: SignData) {
    return this.signTransaction(projectId, session, {
      type: 9999,
      chainId: data.chainId,
      from: data.from,
      method: data.method,
      data: data.data,
      sourceImage: data.sourceImage,
      sourceName: data.sourceName,
      sourceUrl: data.sourceUrl,
    } as SignData);
  }

  public async listNetworks(projectId: string, session: string) {
    return this._callApi(this._endPoint + `/v0/community2/networks?projectId=${projectId}&session=${session}`, null, ACTION.GET).then((response: any) => {
      if (response?.body?.error != null) throw new Error(JSON.stringify(response.body));
      if (response == null) return null;
      let networks = response.body as Network[];
      return networks?.length > 0 ? networks : [];
    });
  }

  public async submitGenPasskeyOptRequest(projectId: string, session: string) {
    return this._callApi(
      this._endPoint + "/v0/community2/requests",
      {
        projectId: projectId,
        session: session,
        type: "genpasskeyopt",
      },
      ACTION.POST
    )
      .then((response: any) => {
        const { body, status } = response;
        if (status != 200) {
          throw new Error(JSON.stringify({ ...body, status }));
        } else {
          return {
            id: body.id,
            status: body.status,
            callbackURL: body.callbackURL,
          } as RequestResponse;
        }
      })
      .catch((ex) => {
        console.error(ex);
        throw ex;
      });
  }


  /**
   * Submit transaction to Custonomy Community using HMAC
   * @param projectId - The UUID of the project
   * @param externalUserId - The external user id of the project site for signing
   * @param transaction - The transaction detail that rqeuired to be signed by Custonomy Community
   * @returns The status and callback url for user pin input
   */
  submitAPITransaction(projectId: string, session: string, data: Transaction | SignData) {

    return this._callHMACApi(
       "/v0/community2/apirequests",
      {
        projectId: projectId,
        session: session,
        type: "createtxn",
        params: {type: 9999, ...data },
      },
      ACTION.POST
    ).then((response: any) => {
      // console.log({response})
      const { body, status } = response;
      let count = 0;
      if (status != 200) {
        throw new Error(JSON.stringify({ ...body, status }));
      } else {
        //TODO: temparay fake the procdess
        this._pull(body.id, session, data.type == 9999 || data.method == 'eth_signTransaction' );
        
        return {
          id: body.id,
          status: body.status,
        } as RequestResponse;
      }
    });
  }

}
