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";
export const SERVICE_ID = "endorser.ch";

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 GenericClaim {
  "@context": string;
  "@type": string;
  issuedAt: string;
  // "any" because arbitrary objects can be subject of agreement
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  claim: Record<any, any>;
}

export interface GiveServerRecord {
  agentDid: string;
  amount: number;
  amountConfirmed: number;
  description: string;
  fullClaim: GiveVerifiableCredential;
  handleId: string;
  issuedAt: string;
  recipientDid: string;
  unit: string;
}

export interface GiveVerifiableCredential {
  "@context"?: string; // optional when embedded, eg. in an Agree
  "@type": string;
  agent?: { identifier: string };
  description?: string;
  fulfills?: { "@type": string; identifier: string };
  identifier?: string;
  object?: { amountOfThisGood: number; unitCode: string };
  recipient?: { identifier: string };
}

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

/**
 always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
 **/
export function didInfo(
  did: string,
  activeDid: string,
  allMyDids: Array<string>,
  contacts: Array<Contact>,
): string {
  const myId: string | undefined = R.find(R.equals(did), allMyDids);
  if (myId) {
    return "You" + (myId !== activeDid ? " (Alt ID)" : "");
  } else {
    const contact: Contact | undefined = R.find((c) => c.did === did, contacts);
    if (contact) {
      return contact.name || "Someone Unnamed in Contacts";
    } else if (!did) {
      return "Unspecified Person";
    } else if (isHiddenDid(did)) {
      return "Someone Not In Network";
    } else {
      return "Someone Not In Contacts";
    }
  }
}

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

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

export type CreateAndSubmitGiveResult = 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,
  fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitGiveResult> {
  try {
    // Make a claim
    const vcClaim: GiveVerifiableCredential = {
      "@context": "https://schema.org",
      "@type": "GiveAction",
    };
    if (toDid) {
      vcClaim.recipient = { identifier: toDid };
    }
    if (fromDid) {
      vcClaim.agent = { identifier: fromDid };
    }
    if (description) {
      vcClaim.description = description;
    }
    if (hours) {
      vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
    }
    if (fulfillsProjectHandleId) {
      vcClaim.fulfills = {
        "@type": "PlanAction",
        identifier: fulfillsProjectHandleId,
      };
    }
    // Make a payload for the claim
    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];
    if (!firstKey || !firstKey.privateKeyHex) {
      throw {
        error: "No private key",
        message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
      };
    }

    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);
    const alg = undefined;

    // Create a JWT for the request

    const vcJwt: string = await didJwt.createJWT(vcPayload, {
      alg: alg,
      issuer: identity.did,
      signer: signer,
    });

    // Make the xhr request payload

    const payload = JSON.stringify({ jwtEncoded: vcJwt });
    const url = apiServer + "/api/v2/claim";
    const token = await accessToken(identity);
    const headers = {
      "Content-Type": "application/json",
      Authorization: "Bearer " + token,
    };

    const response = await axios.post(url, payload, { headers });
    return {
      type: "success",
      response,
    };
  } catch (error: unknown) {
    let errorMessage: string;

    if (error instanceof Error) {
      // If it's a JavaScript Error object
      errorMessage = error.message;
    } else if (
      typeof error === "object" &&
      error !== null &&
      "message" in error
    ) {
      // If it's an object that has a 'message' property
      errorMessage = (error as { message: string }).message;
    } else {
      // Unknown error shape, default message
      errorMessage = "Unknown error";
    }

    return {
      type: "error",
      error: {
        error: errorMessage,
        userMessage: "Failed to create and submit the claim",
      },
    };
  }
}

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