import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { Axios, AxiosResponse } from "axios";
import { Contact } from "@/db/tables/contacts";

export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims
export const SERVICE_ID = "endorser.ch";
// the header line for contacts exported via Endorser Mobile
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
// the prefix for the contact URL
export const CONTACT_URL_PREFIX = "https://endorser.ch";
// the suffix for the contact URL
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
// the prefix for handle IDs, the permanent ID for claims on Endorser
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";

export interface AgreeVerifiableCredential {
  "@context": string;
  "@type": string;
  // "any" because arbitrary objects can be subject of agreement
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  object: Record<string, any>;
}

export interface GiverInputInfo {
  did?: string;
  name?: string;
}

export interface GiverOutputInfo {
  action: string;
  giver?: GiverInputInfo;
  description?: string;
  amount?: number;
  unitCode?: string;
}

export interface ClaimResult {
  success: { claimId: string; handleId: string };
  error: { code: string; message: string };
}

export interface GenericVerifiableCredential {
  "@context": string;
  "@type": string;
  [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}

export interface GenericServerRecord extends GenericVerifiableCredential {
  handleId?: string;
  id: string;
  issuedAt: string;
  issuer: string;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  claim: Record<string, any>;
  claimType?: string;
}
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
  "@context": SCHEMA_ORG_CONTEXT,
  "@type": "",
  claim: {},
  id: "",
  issuedAt: "",
  issuer: "",
};

// a summary record; the VC is found the fullClaim field
export interface GiveServerRecord {
  agentDid: string;
  amount: number;
  amountConfirmed: number;
  description: string;
  fullClaim: GiveVerifiableCredential;
  fulfillsPlanHandleId: string;
  handleId: string;
  issuedAt: string;
  jwtId: string;
  recipientDid: string;
  unit: string;
}

// a summary record; the VC is found the fullClaim field
export interface OfferServerRecord {
  amount: number;
  amountGiven: number;
  amountGivenConfirmed: number;
  fullClaim: OfferVerifiableCredential;
  fulfillsPlanHandleId: string;
  handleId: string;
  jwtId: string;
  nonAmountGivenConfirmed: number;
  objectDescription: string;
  offeredByDid: string;
  recipientDid: string;
  requirementsMet: boolean;
  unit: string;
  validThrough: string;
}

// a summary record; the VC is not currently part of this record
export interface PlanServerRecord {
  agentDid?: string; // optional, if the issuer wants someone else to manage as well
  description: string;
  endTime?: string;
  fulfillsPlanHandleId: string;
  handleId: string;
  issuerDid: string;
  locLat?: number;
  locLon?: number;
  startTime?: string;
  url?: string;
}

// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4
export interface GiveVerifiableCredential {
  "@context"?: string; // optional when embedded, eg. in an Agree
  "@type": "GiveAction";
  agent?: { identifier: string };
  description?: string;
  fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
  identifier?: string;
  image?: string;
  object?: { amountOfThisGood: number; unitCode: string };
  recipient?: { identifier: string };
}

// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id8
export interface OfferVerifiableCredential {
  "@context"?: string; // optional when embedded, eg. in an Agree
  "@type": "Offer";
  description?: string;
  includesObject?: { amountOfThisGood: number; unitCode: string };
  itemOffered?: {
    description?: string;
    isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
  };
  offeredBy?: { identifier: string };
  validThrough?: string;
}

// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential {
  "@context": "https://schema.org";
  "@type": "PlanAction";
  name: string;
  agent?: { identifier: string };
  description?: string;
  identifier?: string;
  lastClaimId?: string;
  location?: {
    geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
  };
}

/**
 * Represents data about a project
 *
 * @deprecated
 * We should use PlanServerRecord instead.
 **/
export interface PlanData {
  /**
   * Name of the project
   **/
  name: string;
  /**
   * Description of the project
   **/
  description: string;
  /**
   * URL referencing information about the project
   **/
  handleId: string;
  /**
   * The DID of the issuer
   */
  issuerDid: string;
  /**
   * The Identier of the project -- different from jwtId, needs to be fixed
   **/
  rowid?: string;
}

export interface EndorserRateLimits {
  doneClaimsThisWeek: string;
  doneRegistrationsThisMonth: string;
  maxClaimsPerWeek: string;
  maxRegistrationsPerMonth: string;
  nextMonthBeginDateTime: string;
  nextWeekBeginDateTime: string;
}

export interface ImageRateLimits {
  doneImagesThisWeek: string;
  maxImagesPerWeek: string;
  nextWeekBeginDateTime: string;
}

export interface VerifiableCredential {
  "@context": string;
  "@type": string;
  name: string;
  description: string;
  identifier?: string;
}

export interface WorldProperties {
  startTime?: string;
  endTime?: string;
}

export interface RegisterVerifiableCredential {
  "@context": string;
  "@type": string;
  agent: { identifier: string };
  object: string;
  participant: { identifier: string };
}

// now for some of the error & other wrapper types

export interface ResultWithType {
  type: string;
}

export interface SuccessResult extends ResultWithType {
  type: "success";
  response: AxiosResponse<ClaimResult>;
}

export interface ErrorResponse {
  error?: {
    message?: string;
  };
}

export interface InternalError {
  error: string; // for system logging
  userMessage?: string; // for user display
}

export interface ErrorResult extends ResultWithType {
  type: "error";
  error: InternalError;
}

export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;

// This is used to check for hidden info.
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN";

export function isDid(did: string) {
  return did.startsWith("did:");
}

export function isHiddenDid(did: string) {
  return did === HIDDEN_DID;
}

export function isEmptyOrHiddenDid(did?: string) {
  return !did || did === HIDDEN_DID; // catching empty string as well
}

/**
 * @return true for any nested string where func(input) === true
 *
 * Similar logic is found in endorser-mobile.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) {
  if (Object.prototype.toString.call(input) === "[object String]") {
    return func(input);
  } else if (input instanceof Object) {
    if (!Array.isArray(input)) {
      // it's an object
      for (const key in input) {
        if (testRecursivelyOnStrings(func, input[key])) {
          return true;
        }
      }
    } else {
      // it's an array
      for (const value of input) {
        if (testRecursivelyOnStrings(func, value)) {
          return true;
        }
      }
    }
    return false;
  } else {
    return false;
  }
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function containsHiddenDid(obj: any) {
  return testRecursivelyOnStrings(isHiddenDid, obj);
}

export function stripEndorserPrefix(claimId: string) {
  if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
    return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
  } else {
    return claimId;
  }
}

// similar logic is found in endorser-mobile
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function removeSchemaContext(obj: any) {
  return obj["@context"] === SCHEMA_ORG_CONTEXT
    ? R.omit(["@context"], obj)
    : obj;
}

// similar logic is found in endorser-mobile
export function addLastClaimOrHandleAsIdIfMissing(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  obj: any,
  lastClaimId?: string,
  handleId?: string,
) {
  if (!obj.identifier && lastClaimId) {
    const result = R.clone(obj);
    result.lastClaimId = lastClaimId;
    return result;
  } else if (!obj.identifier && handleId) {
    const result = R.clone(obj);
    result.identifier = handleId;
    return result;
  } else {
    return obj;
  }
}

// return clone of object without any nested *VisibleToDids keys
// similar code is also contained in endorser-mobile
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function removeVisibleToDids(input: any): any {
  if (input instanceof Object) {
    if (!Array.isArray(input)) {
      // it's an object
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const result: Record<string, any> = {};
      for (const key in input) {
        if (!key.endsWith("VisibleToDids")) {
          result[key] = removeVisibleToDids(R.clone(input[key]));
        }
      }
      return result;
    } else {
      // it's an array
      return R.map(removeVisibleToDids, input);
    }
  } else {
    return input;
  }
}

export function contactForDid(
  did: string | undefined,
  contacts: Contact[],
): Contact | undefined {
  return isEmptyOrHiddenDid(did)
    ? undefined
    : R.find((c) => c.did === did, contacts);
}

/**
 *
 * Similar logic is found in endorser-mobile.
 *
 * @param did
 * @param activeDid
 * @param contact
 * @param allMyDids
 * @return { known: boolean, displayName: string } where known is true if the display name is some easily-recogizable name, false if it's a generic name like "Someone Anonymous"
 */
export function didInfoForContact(
  did: string | undefined,
  activeDid: string | undefined,
  contact?: Contact,
  allMyDids: string[] = [],
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
): { known: boolean; displayName: string } {
  if (!did) return { displayName: "Someone Anonymous", known: false };
  if (contact) {
    return {
      displayName: contact.name || "Contact With No Name",
      known: !!contact.name,
    };
  } else if (did === activeDid) {
    return { displayName: "You", known: true };
  } else {
    const myId = R.find(R.equals(did), allMyDids);
    return myId
      ? { displayName: "You (Alt ID)", known: true }
      : isHiddenDid(did)
        ? { displayName: "Someone Outside Your Network", known: false }
        : { displayName: "Someone Outside Contacts", known: false };
  }
}

/**
 always returns text, maybe something like "unnamed" or "unknown"

 Now that we're using more informational didInfoForContact under the covers, we might want to consolidate.
 **/
export function didInfo(
  did: string | undefined,
  activeDid: string | undefined,
  allMyDids: string[],
  contacts: Contact[],
): string {
  const contact = contactForDid(did, contacts);
  return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
}

/**
 * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
 *
 * @param identity
 * @param fromDid may be null
 * @param toDid
 * @param description may be null; should have this or amount
 * @param amount may be null; should have this or description
 */
export async function createAndSubmitGive(
  axios: Axios,
  apiServer: string,
  identity: IIdentifier,
  fromDid?: string | null,
  toDid?: string,
  description?: string,
  amount?: number,
  unitCode?: string,
  fulfillsProjectHandleId?: string,
  fulfillsOfferHandleId?: string,
  isTrade: boolean = false,
  imageUrl?: string,
): Promise<CreateAndSubmitClaimResult> {
  const vcClaim: GiveVerifiableCredential = {
    "@context": "https://schema.org",
    "@type": "GiveAction",
    recipient: toDid ? { identifier: toDid } : undefined,
    agent: fromDid ? { identifier: fromDid } : undefined,
    description: description || undefined,
    object: amount
      ? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
      : undefined,
    fulfills: [{ "@type": isTrade ? "TradeAction" : "DonateAction" }],
  };
  if (fulfillsProjectHandleId) {
    vcClaim.fulfills = vcClaim.fulfills || []; // weird that it won't typecheck without this
    vcClaim.fulfills.push({
      "@type": "PlanAction",
      identifier: fulfillsProjectHandleId,
    });
  }
  if (fulfillsOfferHandleId) {
    vcClaim.fulfills = vcClaim.fulfills || []; // weird that it won't typecheck without this
    vcClaim.fulfills.push({
      "@type": "Offer",
      identifier: fulfillsOfferHandleId,
    });
  }
  if (imageUrl) {
    vcClaim.image = imageUrl;
  }
  return createAndSubmitClaim(
    vcClaim as GenericServerRecord,
    identity,
    apiServer,
    axios,
  );
}

/**
 * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
 *
 * @param identity
 * @param description may be null; should have this or amount
 * @param amount may be null; should have this or description
 * @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null)
 * @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
 */
export async function createAndSubmitOffer(
  axios: Axios,
  apiServer: string,
  identity: IIdentifier,
  description?: string,
  amount?: number,
  unitCode?: string,
  expirationDate?: string,
  fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitClaimResult> {
  const vcClaim: OfferVerifiableCredential = {
    "@context": "https://schema.org",
    "@type": "Offer",
    offeredBy: { identifier: identity.did },
    validThrough: expirationDate || undefined,
  };
  if (amount) {
    vcClaim.includesObject = {
      amountOfThisGood: amount,
      unitCode: unitCode || "HUR",
    };
  }
  if (description) {
    vcClaim.itemOffered = { description };
  }
  if (fulfillsProjectHandleId) {
    vcClaim.itemOffered = vcClaim.itemOffered || {};
    vcClaim.itemOffered.isPartOf = {
      "@type": "PlanAction",
      identifier: fulfillsProjectHandleId,
    };
  }
  return createAndSubmitClaim(
    vcClaim as GenericServerRecord,
    identity,
    apiServer,
    axios,
  );
}

// similar logic is found in endorser-mobile
export const createAndSubmitConfirmation = async (
  identifier: IIdentifier,
  claim: GenericVerifiableCredential,
  lastClaimId: string, // used to set the lastClaimId
  handleId: string | undefined,
  apiServer: string,
  axios: Axios,
) => {
  const goodClaim = removeSchemaContext(
    removeVisibleToDids(
      addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),
    ),
  );
  const confirmationClaim: GenericVerifiableCredential = {
    "@context": "https://schema.org",
    "@type": "AgreeAction",
    object: goodClaim,
  };
  return createAndSubmitClaim(confirmationClaim, identifier, apiServer, axios);
};

export async function createAndSubmitClaim(
  vcClaim: GenericVerifiableCredential,
  identity: IIdentifier,
  apiServer: string,
  axios: Axios,
): Promise<CreateAndSubmitClaimResult> {
  try {
    const vcPayload = {
      vc: {
        "@context": ["https://www.w3.org/2018/credentials/v1"],
        type: ["VerifiableCredential"],
        credentialSubject: vcClaim,
      },
    };

    // Create a signature using private key of identity
    const firstKey = identity.keys[0];
    const privateKeyHex = firstKey?.privateKeyHex;

    if (!privateKeyHex) {
      throw {
        error: "No private key",
        message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
      };
    }

    const signer = await SimpleSigner(privateKeyHex);

    // Create a JWT for the request
    const vcJwt: string = await didJwt.createJWT(vcPayload, {
      issuer: identity.did,
      signer,
    });

    // Make the xhr request payload
    const payload = JSON.stringify({ jwtEncoded: vcJwt });
    const url = `${apiServer}/api/v2/claim`;
    const token = await accessToken(identity);

    const response = await axios.post(url, payload, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
    });

    return { type: "success", response };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  } catch (error: any) {
    console.error("Error creating claim:", error);
    const errorMessage: string =
      error.response?.data?.error?.message ||
      error.message ||
      "Got some error submitting the claim. Check your permissions, network, and error logs.";

    return {
      type: "error",
      error: {
        error: errorMessage,
      },
    };
  }
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isAccept = (claim: Record<string, any>) => {
  return (
    claim &&
    claim["@context"] === SCHEMA_ORG_CONTEXT &&
    claim["@type"] === "AcceptAction"
  );
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isOffer = (claim: Record<string, any>) => {
  return (
    claim &&
    claim["@context"] === SCHEMA_ORG_CONTEXT &&
    claim["@type"] === "Offer"
  );
};

export function currencyShortWordForCode(unitCode: string, single: boolean) {
  return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}

export function displayAmount(code: string, amt: number) {
  return "" + amt + " " + currencyShortWordForCode(code, amt === 1);
}

// insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case)
export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
  return !text
    ? ""
    : text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
};

/**
 return readable summary of claim, or something generic

 similar code is also contained in endorser-mobile
 **/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const claimSummary = (claim: Record<string, any>) => {
  if (!claim) {
    // to differentiate from "something" above
    return "something";
  }
  if (claim.claim) {
    // probably a Verified Credential
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    claim = claim.claim as Record<string, any>;
  }
  if (Array.isArray(claim)) {
    if (claim.length === 1) {
      claim = claim[0];
    } else {
      return "multiple claims";
    }
  }
  const type = claim["@type"];
  if (!type) {
    return "a claim";
  } else {
    let typeExpl = capitalizeAndInsertSpacesBeforeCaps(type);
    if (typeExpl === "Person") {
      typeExpl += " claim";
    }
    return "a " + typeExpl;
  }
};

/**
 return readable description of claim if possible, as a past-tense action

 identifiers is a list of objects with a 'did' field, each representing the user
 contacts is a list of objects with a 'did' field for others and a 'name' field for their name

 similar code is also contained in endorser-mobile
 **/
export const claimSpecialDescription = (
  record: GenericServerRecord,
  activeDid: string,
  identifiers: Array<string>,
  contacts: Array<Contact>,
) => {
  let claim = record.claim;
  if (claim.claim) {
    // it's probably a Verified Credential
    claim = claim.claim;
  }

  const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
  const type = claim["@type"] || "UnknownType";

  if (type === "AgreeAction") {
    return issuer + " agreed with " + claimSummary(claim.object);
  } else if (isAccept(claim)) {
    return issuer + " accepted " + claimSummary(claim.object);
  } else if (type === "GiveAction") {
    // agent.did is for legacy data, before March 2023
    const giver = claim.agent?.identifier || claim.agent?.did;
    const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
    let gaveAmount = claim.object?.amountOfThisGood
      ? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
      : "";
    if (claim.description) {
      if (gaveAmount) {
        gaveAmount = gaveAmount + ", and also: ";
      }
      gaveAmount = gaveAmount + claim.description;
    }
    if (!gaveAmount) {
      gaveAmount = "something not described";
    }
    // recipient.did is for legacy data, before March 2023
    const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
    const gaveRecipientInfo = gaveRecipientId
      ? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
      : "";
    return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
  } else if (type === "JoinAction") {
    // agent.did is for legacy data, before March 2023
    const agent = claim.agent?.identifier || claim.agent?.did;
    const contactInfo = didInfo(agent, activeDid, identifiers, contacts);

    let eventOrganizer =
      claim.event && claim.event.organizer && claim.event.organizer.name;
    eventOrganizer = eventOrganizer || "";
    let eventName = claim.event && claim.event.name;
    eventName = eventName ? " " + eventName : "";
    let fullEvent = eventOrganizer + eventName;
    fullEvent = fullEvent ? " attended the " + fullEvent : "";

    let eventDate = claim.event && claim.event.startTime;
    eventDate = eventDate ? " at " + eventDate : "";
    return contactInfo + fullEvent + eventDate;
  } else if (isOffer(claim)) {
    const offerer = claim.offeredBy?.identifier;
    const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
    let offering = "";
    if (claim.includesObject) {
      offering +=
        " " +
        displayAmount(
          claim.includesObject.unitCode,
          claim.includesObject.amountOfThisGood,
        );
    }
    if (claim.itemOffered?.description) {
      offering += ", saying: " + claim.itemOffered?.description;
    }
    // recipient.did is for legacy data, before March 2023
    const offerRecipientId =
      claim.recipient?.identifier || claim.recipient?.did;
    const offerRecipientInfo = offerRecipientId
      ? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts)
      : "";
    return contactInfo + " offered" + offering + offerRecipientInfo;
  } else if (type === "PlanAction") {
    const claimer = claim.agent?.identifier || record.issuer;
    const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
    return claimerInfo + " announced a project: " + claim.name;
  } else if (type === "Tenure") {
    // party.did is for legacy data, before March 2023
    const claimer = claim.party?.identifier || claim.party?.did;
    const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
    const polygon = claim.spatialUnit?.geo?.polygon || "";
    return (
      contactInfo +
      " possesses [" +
      polygon.substring(0, polygon.indexOf(" ")) +
      "...]"
    );
  } else {
    return issuer + " declared " + claimSummary(claim as GenericServerRecord);
  }
};

export const BVC_MEETUPS_PROJECT_CLAIM_ID =
  process.env.VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID ||
  "https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK"; // this won't resolve as a URL on production; it's a URN only found in the test system

export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
  return {
    "@context": SCHEMA_ORG_CONTEXT,
    "@type": "JoinAction",
    agent: {
      identifier: did,
    },
    event: {
      organizer: {
        name: "Bountiful Voluntaryist Community",
      },
      name: "Saturday Morning Meeting",
      startTime: startTime,
    },
  };
};
