// many of these are also found in endorser-mobile utility.ts

import axios, { AxiosResponse } from "axios";
import { IIdentifier } from "@veramo/core";
import { useClipboard } from "@vueuse/core";

import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { GenericServerRecord, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;

// If you edit this, check that the numbers still line up on the side in the alert (on mobile, too),
// and make sure they can take all actions while the notification shows.
export const ONBOARD_MESSAGE =
  "1) Read through all their yellow prompts. 2) Add them to your Contacts by scanning with the QR icon that is by the input box. 3) Click the person icon to register them. 4) Show them your QR so they'll scan you. 5) Have them enable notifications.";

/* eslint-disable prettier/prettier */
export const UNIT_SHORT: Record<string, string> = {
  "BTC": "BTC",
  "ETH": "ETH",
  "HUR": "Hours",
  "USD": "US $",
};
/* eslint-enable prettier/prettier */

/* eslint-disable prettier/prettier */
export const UNIT_LONG: Record<string, string> = {
  "BTC": "Bitcoin",
  "ETH": "Ethereum",
  "HUR": "hours",
  "USD": "dollars",
};
/* eslint-enable prettier/prettier */

const UNIT_CODES: Record<string, Record<string, string>> = {
  BTC: {
    name: "Bitcoin",
    faIcon: "bitcoin-sign",
  },
  HUR: {
    name: "hours",
    faIcon: "clock",
  },
  USD: {
    name: "US Dollars",
    faIcon: "dollar",
  },
};

export function iconForUnitCode(unitCode: string) {
  return UNIT_CODES[unitCode]?.faIcon || "question";
}

// from https://stackoverflow.com/a/175787/845494
//
export function isNumeric(str: string): boolean {
  return !isNaN(+str);
}

export function numberOrZero(str: string): number {
  return isNumeric(str) ? +str : 0;
}

export const isGlobalUri = (uri: string) => {
  return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
};

export const giveIsConfirmable = (veriClaim: GenericServerRecord) => {
  return veriClaim.claimType === "GiveAction";
};

export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
  fn();
  useClipboard()
    .copy(text)
    .then(() => setTimeout(fn, 2000));
};

/**
 * @returns true if the user can confirm the claim
 * @param veriClaim is expected to have fields: claim, claimType, and issuer
 */
export const isGiveRecordTheUserCanConfirm = (
  veriClaim: GenericServerRecord,
  activeDid: string,
  confirmerIdList: string[] = [],
) => {
  return (
    giveIsConfirmable(veriClaim) &&
    !confirmerIdList.includes(activeDid) &&
    veriClaim.issuer !== activeDid &&
    !containsHiddenDid(veriClaim.claim)
  );
};

/**
 * @returns the DID of the person who offered, or undefined if hidden
 * @param veriClaim is expected to have fields: claim and issuer
 */
export const offerGiverDid: (
  arg0: GenericServerRecord,
) => string | undefined = (veriClaim) => {
  let giver;
  if (
    veriClaim.claim.offeredBy?.identifier &&
    !serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string)
  ) {
    giver = veriClaim.claim.offeredBy.identifier;
  } else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
    giver = veriClaim.issuer;
  }
  return giver;
};

/**
 * @returns true if the user can fulfill the offer
 * @param veriClaim is expected to have fields: claim, claimType, and issuer
 */
export const canFulfillOffer = (veriClaim: GenericServerRecord) => {
  return !!(veriClaim.claimType === "Offer" && offerGiverDid(veriClaim));
};

// return object with paths and arrays of DIDs for any keys ending in "VisibleToDid"
export function findAllVisibleToDids(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  input: any,
  humanReadable = false,
): Record<string, Array<string>> {
  if (Array.isArray(input)) {
    const result: Record<string, Array<string>> = {};
    for (let i = 0; i < input.length; i++) {
      const inside = findAllVisibleToDids(input[i], humanReadable);
      for (const key in inside) {
        const pathKey = humanReadable
          ? "#" + (i + 1) + " " + key
          : "[" + i + "]" + key;
        result[pathKey] = inside[key];
      }
    }
    return result;
  } else if (input instanceof Object) {
    // regular map (non-array) object
    const result: Record<string, Array<string>> = {};
    for (const key in input) {
      if (key.endsWith("VisibleToDids")) {
        const newKey = key.slice(0, -"VisibleToDids".length);
        const pathKey = humanReadable ? newKey : "." + newKey;
        result[pathKey] = input[key];
      } else {
        const inside = findAllVisibleToDids(input[key], humanReadable);
        for (const insideKey in inside) {
          const pathKey = humanReadable
            ? key + "'s " + insideKey
            : "." + key + insideKey;
          result[pathKey] = inside[insideKey];
        }
      }
    }
    return result;
  } else {
    return {};
  }
}

/**
 * Test findAllVisibleToDids
 *

 pkgx +deno.land sh

 deno

 import * as R from 'ramda';
 //import { findAllVisibleToDids } from './src/libs/util'; // doesn't work because other dependencies fail so gotta copy-and-paste function

 console.log(R.equals(findAllVisibleToDids(null), {}));
 console.log(R.equals(findAllVisibleToDids(9), {}));
 console.log(R.equals(findAllVisibleToDids([]), {}));
 console.log(R.equals(findAllVisibleToDids({}), {}));
 console.log(R.equals(findAllVisibleToDids({ issuer: "abc" }), {}));
 console.log(R.equals(findAllVisibleToDids({ issuerVisibleToDids: ["abc"] }), { ".issuer": ["abc"] }));
 console.log(R.equals(findAllVisibleToDids([{ issuerVisibleToDids: ["abc"] }]), { "[0].issuer": ["abc"] }));
 console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] } }]), { "[1].fluff.issuer": ["abc"] }));
 console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] }, stuff: [ { did: "HIDDEN", agentDidVisibleToDids: ["def", "ghi"] } ] }]), { "[1].fluff.issuer": ["abc"], "[1].stuff[0].agentDid": ["def", "ghi"] }));

 *
 **/

export const getIdentity = async (activeDid: string): Promise<IIdentifier> => {
  await accountsDB.open();
  const account = (await accountsDB.accounts
    .where("did")
    .equals(activeDid)
    .first()) as Account;
  const identity = JSON.parse(account?.identity || "null");

  if (!identity) {
    throw new Error(
      `Attempted to load Offer records for DID ${activeDid} but no identifier was found`,
    );
  }
  return identity;
};

/**
 * Generates a new identity, saves it to the database, and sets it as the active identity.
 * @return {Promise<string>} with the DID of the new identity
 */
export const generateSaveAndActivateIdentity = async (): Promise<string> => {
  const mnemonic = generateSeed();
  // address is 0x... ETH address, without "did:eth:"
  const [address, privateHex, publicHex, derivationPath] =
    deriveAddress(mnemonic);

  const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
  const identity = JSON.stringify(newId);

  await accountsDB.open();
  await accountsDB.accounts.add({
    dateCreated: new Date().toISOString(),
    derivationPath: derivationPath,
    did: newId.did,
    identity: identity,
    mnemonic: mnemonic,
    publicKeyHex: newId.keys[0].publicKeyHex,
  });

  await db.settings.update(MASTER_SETTINGS_KEY, {
    activeDid: newId.did,
  });

  return newId.did;
};

export const sendTestThroughPushServer = async (
  subscription: PushSubscription,
  skipFilter: boolean,
): Promise<AxiosResponse> => {
  await db.open();
  const settings = await db.settings.get(MASTER_SETTINGS_KEY);
  let pushUrl: string = DEFAULT_PUSH_SERVER as string;
  if (settings?.webPushServer) {
    pushUrl = settings.webPushServer;
  }

  // This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
  // This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
  // Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
  const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";

  const auth = Buffer.from(subscription.getKey("auth"));
  const authB64 = auth
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
  const p256dh = Buffer.from(subscription.getKey("p256dh"));
  const p256dhB64 = p256dh
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
  const newPayload = {
    endpoint: subscription.endpoint,
    keys: {
      auth: authB64,
      p256dh: p256dhB64,
    },
    message: `Test, where you will see this message ${
      skipFilter ? "un" : ""
    }filtered.`,
    title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
  };
  console.log("Sending a test web push message:", newPayload);
  const payloadStr = JSON.stringify(newPayload);
  const response = await axios.post(
    pushUrl + "/web-push/send-test",
    payloadStr,
    {
      headers: {
        "Content-Type": "application/json",
      },
    },
  );

  console.log("Got response from web push server:", response);
  return response;
};
