import {
  Agent,
  AutoAcceptCredential,
  AutoAcceptProof,
  BaseEvent,
  ConnectionsModule,
  ConnectionStateChangedEvent,
  CredentialsModule,
  DidExchangeRole,
  DidsModule,
  EncryptedMessage,
  Key,
  KeyDidResolver,
  KeyType,
  PeerDidResolver,
  ProofEventTypes,
  ProofsModule,
  ProofState,
  ProofStateChangedEvent,
  TypedArrayEncoder,
  V2CredentialProtocol,
  V2ProofProtocol,
  WalletError,
  WalletKeyExistsError,
  OutOfBandState,
} from "@aries-framework/core";
import {
  AnonCredsCredentialFormatService,
  AnonCredsModule,
  AnonCredsProof,
  AnonCredsProofFormatService,
} from "@aries-framework/anoncreds";
import {
  IndyVdrAnonCredsRegistry,
  IndyVdrIndyDidRegistrar,
  IndyVdrIndyDidResolver,
  IndyVdrModule,
} from "@aries-framework/indy-vdr";
import { AnonCredsRsModule } from "@aries-framework/anoncreds-rs";
import { anoncreds } from "@hyperledger/anoncreds-nodejs";
import { indyVdr } from "@hyperledger/indy-vdr-nodejs";
import { AskarModule } from "@aries-framework/askar";
import { ariesAskar } from "@hyperledger/aries-askar-nodejs";
import { Key as C, KeyAlgs } from "@hyperledger/aries-askar-shared";
import { IConfAgent } from "@ocm-engine/config";
import axios from "axios";
import {
  catchError,
  filter,
  lastValueFrom,
  map,
  Observable,
  ReplaySubject,
  Subject,
  take,
  timeout,
} from "rxjs";
import { SubjectInboundTransport } from "./askar/transports/agent.subject.inbound.transport";
import { SubjectOutboundTransport } from "./askar/transports/agent.subject.outbound.transport";

export type EventReplaySubject = ReplaySubject<BaseEvent>;
export type SubjectMessage = {
  message: EncryptedMessage;
  replySubject?: Subject<SubjectMessage>;
};
import { Request, Response, Express } from "express";

export const importDidsToWallet = async (
  agent: Agent,
  dids: Array<string>,
): Promise<void> => {
  for (const did in dids) {
    try {
      await agent.dids.import({
        did: dids[did],
        overwrite: false,
      });
    } catch (e) {
      console.log((e as Error).message);
    }
  }
};

export const generateKey = async ({
  seed,
  agent,
}: {
  seed: string;
  agent: Agent;
}): Promise<Key> => {
  if (!seed) {
    throw Error("No seed provided");
  }

  const privateKey = TypedArrayEncoder.fromString(seed);

  try {
    return await agent.wallet.createKey({
      seed: privateKey,
      keyType: KeyType.Ed25519,
    });
  } catch (e) {
    if (e instanceof WalletKeyExistsError) {
      const c = C.fromSecretBytes({
        algorithm: KeyAlgs.Ed25519,
        secretKey: TypedArrayEncoder.fromString(seed),
      });

      return Key.fromPublicKey(c.publicBytes, KeyType.Ed25519);
    }

    if (e instanceof WalletError) {
      throw new Error(`Failed to create key - ${e.message}`);
    }

    throw new Error("Unknown");
  }
};

export const generateDidFromKey = (key: Key): string => {
  if (!key) {
    throw new Error("Key not provided");
  }

  return TypedArrayEncoder.toBase58(key.publicKey.slice(0, 16));
};

//eslint-disable-next-line
export const getAskarAnonCredsIndyModules = (networks: any) => {
  return {
    connections: new ConnectionsModule({
      autoAcceptConnections: true,
    }),
    credentials: new CredentialsModule({
      autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
      credentialProtocols: [
        new V2CredentialProtocol({
          credentialFormats: [new AnonCredsCredentialFormatService()],
        }),
      ],
    }),
    proofs: new ProofsModule({
      autoAcceptProofs: AutoAcceptProof.ContentApproved,
      proofProtocols: [
        new V2ProofProtocol({
          proofFormats: [new AnonCredsProofFormatService()],
        }),
      ],
    }),
    anoncreds: new AnonCredsModule({
      registries: [new IndyVdrAnonCredsRegistry()],
    }),
    anoncredsRs: new AnonCredsRsModule({
      anoncreds,
    }),
    indyVdr: new IndyVdrModule({
      indyVdr,
      networks,
    }),
    dids: new DidsModule({
      registrars: [new IndyVdrIndyDidRegistrar()],
      resolvers: [
        new IndyVdrIndyDidResolver(),
        new KeyDidResolver(),
        new PeerDidResolver(),
      ],
    }),
    askar: new AskarModule({
      ariesAskar,
    }),
  } as const;
};

export const setupEventReplaySubjects = (
  agents: Agent[],
  eventTypes: string[],
): ReplaySubject<BaseEvent>[] => {
  const replaySubjects: EventReplaySubject[] = [];

  for (const agent of agents) {
    const replaySubject = new ReplaySubject<BaseEvent>();

    for (const eventType of eventTypes) {
      agent.events.observable(eventType).subscribe(replaySubject);
    }

    replaySubjects.push(replaySubject);
  }

  return replaySubjects;
};

export const setupSubjectTransports = (agents: Agent[]) => {
  const subjectMap: Record<string, Subject<SubjectMessage>> = {};

  for (const agent of agents) {
    const messages = new Subject<SubjectMessage>();
    subjectMap[agent.config.endpoints[0]] = messages;
    agent.registerInboundTransport(new SubjectInboundTransport(messages));
    agent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap));
  }
};

export const svdxProofStateChangeHandler = async (
  ev: ProofStateChangedEvent,
  agent: Agent,
  config?: IConfAgent,
) => {
  if (ProofState.Done !== ev.payload.proofRecord.state) {
    return;
  }

  const presentationMessage = await agent.proofs.findPresentationMessage(
    ev.payload.proofRecord.id,
  );

  console.log(JSON.stringify(presentationMessage, null, 2));
  if (!presentationMessage) {
    console.log("No presentation message found");
    return;
  }

  const attachmentId = presentationMessage.formats[0].attachmentId;

  const attachment =
    presentationMessage.getPresentationAttachmentById(attachmentId);

  console.log(JSON.stringify(attachment, null, 2));
  if (!attachment) {
    console.log("No attachment found");
    return;
  }

  const email =
    attachment.getDataAsJson<AnonCredsProof>()?.requested_proof.revealed_attrs[
      "email"
    ].raw;

  if (!config?.agentSVDXWebHook) {
    console.log("Agent SVDX web hook not set");
    return;
  }

  try {
    await axios.post(
      config?.agentSVDXWebHook,
      {
        email,
        connectionId: ev.payload.proofRecord.connectionId,
      },
      {
        auth: {
          username: config?.agentSVDXBasicUser,
          password: config?.agentSVDXBasicPass,
        },
      },
    );
  } catch (e) {
    console.log(JSON.stringify(e, null, 2));
  }
};

export const svdxConnectionStateChangeHandler = async (
  ev: ConnectionStateChangedEvent,
  agent: Agent,
  config?: IConfAgent,
) => {
  if (
    ev.payload.connectionRecord.role === DidExchangeRole.Responder &&
    ev.payload.connectionRecord.state !== "completed"
  ) {
    return;
  }

  await agent.connections.addConnectionType(
    ev.payload.connectionRecord.id,
    ev.payload.connectionRecord.theirLabel || "svdx",
  );

  console.log("connection accepted", JSON.stringify(ev, null, 2));

  const connections = await agent.connections.findAllByConnectionTypes([
    ev.payload.connectionRecord.theirLabel || "svdx",
  ]);

  if (connections.length < 2) {
    return;
  }

  connections.sort(
    (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
  );

  while (connections.length > 1) {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const con = connections.pop()!;

    console.log(`deleting ${con.id}`);
    await agent.connections.deleteById(con.id);
  }

  try {
    await agent.proofs.requestProof({
      protocolVersion: "v2",
      connectionId: connections[0].id,
      proofFormats: {
        anoncreds: {
          name: "proof-request",
          version: "1.0",
          requested_attributes: {
            email: {
              name: "email",
              restrictions: [
                {
                  schema_id: config?.agentSVDXSchemaId,
                  cred_def_id: config?.agentSVDXCredDefId,
                },
              ],
            },
          },
        },
      },
    });
  } catch (e) {
    console.log(JSON.stringify(e, null, 2));
    console.log("failed to issue credential");
  }
};

export const isProofStateChangedEvent = (
  e: BaseEvent,
): e is ProofStateChangedEvent => e.type === ProofEventTypes.ProofStateChanged;

export const waitForProofExchangeRecordSubject = (
  subject: ReplaySubject<BaseEvent> | Observable<BaseEvent>,
  {
    threadId,
    parentThreadId,
    state,
    previousState,
    timeoutMs = 10000,
    count = 1,
  }: {
    threadId?: string;
    parentThreadId?: string;
    state?: ProofState;
    previousState?: ProofState | null;
    timeoutMs?: number;
    count?: number;
  },
) => {
  const observable: Observable<BaseEvent> =
    subject instanceof ReplaySubject ? subject.asObservable() : subject;
  return lastValueFrom(
    observable.pipe(
      filter(isProofStateChangedEvent),
      filter(
        (e) =>
          previousState === undefined ||
          e.payload.previousState === previousState,
      ),
      filter(
        (e) =>
          threadId === undefined || e.payload.proofRecord.threadId === threadId,
      ),
      filter(
        (e) =>
          parentThreadId === undefined ||
          e.payload.proofRecord.parentThreadId === parentThreadId,
      ),
      filter(
        (e) => state === undefined || e.payload.proofRecord.state === state,
      ),
      timeout(timeoutMs),
      catchError(() => {
        throw new Error(
          `ProofStateChangedEvent event not emitted within specified timeout: ${timeoutMs}
          previousState: ${previousState},
          threadId: ${threadId},
          parentThreadId: ${parentThreadId},
          state: ${state}
        }`,
        );
      }),
      take(count),
      map((e) => e.payload.proofRecord),
    ),
  );
};

export const attachShortUrlHandler = (server: Express, agent: Agent): void => {
  server.get(
    "/invitations/:invitationId",
    async (req: Request, res: Response) => {
      try {
        const { invitationId } = req.params;

        const outOfBandRecord = await agent.oob.findByCreatedInvitationId(
          invitationId,
        );

        if (
          !outOfBandRecord ||
          outOfBandRecord.state !== OutOfBandState.AwaitResponse
        ) {
          return res.status(404).send("Not found");
        }

        const invitationJson = outOfBandRecord.outOfBandInvitation.toJSON();

        return res
          .header("Content-Type", "application/json")
          .send(invitationJson);
      } catch (error) {
        console.error(error);
        return res.status(500).send("Internal Server Error");
      }
    },
  );
};