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 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<any, any>;
}

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

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

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

export interface GenericVerifiableCredential {
  "@context": string;
  "@type": string;
}

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

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;
}

export interface OfferServerRecord {
  amount: number;
  amountGiven: number;
  fullClaim: OfferVerifiableCredential;
  fulfillsPlanHandleId: string;
  handleId: string;
  offeredByDid: string;
  recipientDid: string;
  requirementsMet: boolean;
  unit: string;
  validThrough: string;
}

export interface PlanServerRecord {
  agentDid?: string; // optional, if the issuer wants someone else to manage as well
  description: string;
  endTime?: string;
  fulfillsPlanHandleId: string;
  issuerDid: string;
  handleId: 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;
  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;
  location?: {
    geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
  };
}

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

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

// 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 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 testRecursivelyOnString(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 (testRecursivelyOnString(func, input[key])) {
          return true;
        }
      }
    } else {
      // it's an array
      for (const value of input) {
        if (testRecursivelyOnString(func, value)) {
          return true;
        }
      }
    }
    return false;
  } else {
    return false;
  }
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function containsHiddenDid(obj: any) {
  return testRecursivelyOnString(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 logic is found 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")) {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          result[key] = removeVisibleToDids(R.clone(input[key]));
        }
      }
      return result;
    } else {
      // it's an array
      return R.map(removeVisibleToDids, input);
    }
    return false;
  } else {
    return input;
  }
}

/**
 always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY

 Similar logic is found in endorser-mobile.
 **/
export function didInfo(
  did: string | undefined,
  activeDid: string | undefined,
  allMyDids: string[],
  contacts: Contact[],
): string {
  if (!did) return "Someone Anonymous";

  const myId = R.find(R.equals(did), allMyDids);
  if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`;

  const contact = R.find((c) => c.did === did, contacts);
  return contact
    ? contact.name || "Contact With No Name"
    : isHiddenDid(did)
      ? "Someone Not In Network"
      : "Someone Not In Contacts";
}

export interface ResultWithType {
  type: string;
}

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

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

export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;

/**
 * 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 hours
 * @param hours may be null; should have this or description
 */
export async function createAndSubmitGive(
  axios: Axios,
  apiServer: string,
  identity: IIdentifier,
  fromDid?: string,
  toDid?: string,
  description?: string,
  hours?: number,
  unitCode?: string,
  fulfillsProjectHandleId?: string,
  fulfillsOfferHandleId?: string,
  isTrade: boolean = false,
): 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: hours
      ? { amountOfThisGood: hours, 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,
    });
  }
  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 hours
 * @param hours 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,
  hours?: number,
  expirationDate?: string,
  fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitClaimResult> {
  const vcClaim: OfferVerifiableCredential = {
    "@context": "https://schema.org",
    "@type": "Offer",
    offeredBy: { identifier: identity.did },
    validThrough: expirationDate || undefined,
  };
  if (hours) {
    vcClaim.includesObject = {
      amountOfThisGood: hours,
      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,
  );
}

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,
      },
    };
  }
}

// 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 interface ErrorResponse {
  error?: {
    message?: string;
  };
}

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

/**
 * Represents data about a project
 **/
export interface ProjectData {
  /**
   * 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
   **/
  rowid: string;
}

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

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