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

import axios, { AxiosResponse } from "axios";
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 {
  DEFAULT_PASSKEY_EXPIRATION_MINUTES,
  MASTER_SETTINGS_KEY,
} from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import {
  containsHiddenDid,
  GenericCredWrapper,
  GenericVerifiableCredential,
  OfferVerifiableCredential,
} from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer";
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";

import { Buffer } from "buffer";
import { KeyMeta } from "@/libs/crypto/vc";
import { createPeerDid } from "@/libs/crypto/vc/didPeer";

export const PRIVACY_MESSAGE =
  "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
export const SHARED_PHOTO_BASE64_KEY = "shared-photo-base64";

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

/* eslint-disable prettier/prettier */
export const UNIT_LONG: Record<string, string> = {
  "BTC": "Bitcoin",
  "BX": "Buxbe",
  "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
// ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
//
export function isNumeric(str: string): boolean {
  // This ignore commentary is because typescript complains when you pass a string to isNaN.
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  return !isNaN(str) && !isNaN(parseFloat(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 isGiveAction = (
  veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
) => {
  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 = (
  isRegistered: boolean,
  veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
  activeDid: string,
  confirmerIdList: string[] = [],
) => {
  return (
    isRegistered &&
    isGiveAction(veriClaim) &&
    !confirmerIdList.includes(activeDid) &&
    veriClaim.issuer !== activeDid &&
    !containsHiddenDid(veriClaim.claim)
  );
};

export async function blobToBase64(blob: Blob): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result as string); // potential problem if it returns an ArrayBuffer?
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });
}

export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
  // Extract the content type and the Base64 data
  const [metadata, base64] = base64DataUrl.split(",");
  const contentTypeMatch = metadata.match(/data:(.*?);base64/);
  const contentType = contentTypeMatch ? contentTypeMatch[1] : "";

  const byteCharacters = atob(base64);
  const byteArrays = [];

  for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    const slice = byteCharacters.slice(offset, offset + sliceSize);

    const byteNumbers = new Array(slice.length);
    for (let i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    const byteArray = new Uint8Array(byteNumbers);
    byteArrays.push(byteArray);
  }
  return new Blob(byteArrays, { type: contentType });
}

/**
 * @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: GenericCredWrapper<OfferVerifiableCredential>,
) => 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: GenericCredWrapper<GenericVerifiableCredential>,
) => {
  return !!(
    veriClaim.claimType === "Offer" &&
    offerGiverDid(veriClaim as GenericCredWrapper<OfferVerifiableCredential>)
  );
};

// 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 interface AccountKeyInfo extends Account, KeyMeta {}

export const getAccount = async (
  activeDid: string,
): Promise<AccountKeyInfo | undefined> => {
  await accountsDB.open();
  const account = (await accountsDB.accounts
    .where("did")
    .equals(activeDid)
    .first()) as Account;
  return account;
};

/**
 * 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 registerAndSavePasskey = async (
  keyName: string,
): Promise<Account> => {
  const cred = await registerCredential(keyName);
  const publicKeyBytes = cred.publicKeyBytes;
  const did = createPeerDid(publicKeyBytes as Uint8Array);
  const passkeyCredIdHex = cred.credIdHex as string;

  const account = {
    dateCreated: new Date().toISOString(),
    did,
    passkeyCredIdHex,
    publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
  };
  await accountsDB.open();
  await accountsDB.accounts.add(account);
  return account;
};

export const registerSaveAndActivatePasskey = async (
  keyName: string,
): Promise<Account> => {
  const account = await registerAndSavePasskey(keyName);

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

  return account;
};

export const getPasskeyExpirationSeconds = async (): Promise<number> => {
  await db.open();
  const settings = await db.settings.get(MASTER_SETTINGS_KEY);
  const passkeyExpirationSeconds =
    (settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
    60;
  return passkeyExpirationSeconds;
};

export const sendTestThroughPushServer = async (
  subscriptionJSON: PushSubscriptionJSON,
  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 newPayload = {
    // eslint-disable-next-line prettier/prettier
    message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`,
    title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
    ...subscriptionJSON,
  };
  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;
};
