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=";

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 PlanVerifiableCredential {
  "@context": "https://schema.org";
  "@type": "PlanAction";
  name: string;
  description: string;
  identifier?: string;
  location?: {
    geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
  };
}

export interface PlanServerRecord {
  agentDid?: string; // optional, if the issuer wants someone else to manage as well
  description: string;
  endTime?: string;
  issuerDid: string;
  handleId: string;
  locLat?: number;
  locLon?: number;
  startTime?: string;
  url?: 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

 Similar logic is found in endorser-mobile.
 **/
export function didInfo(
  did: string,
  activeDid: string,
  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 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 {
    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: "HUR" } : undefined,
      fulfills: fulfillsProjectHandleId
        ? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
        : undefined,
    };

    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 };
  } catch (error: unknown) {
    const errorMessage: string =
      error === null
        ? "Null error"
        : error instanceof Error
        ? error.message
        : typeof error === "object" && error !== null && "message" in error
        ? (error as { message: string }).message
        : "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;
}
