import { CashuMint, CashuWallet, type Proof, type SendOptions } from '@cashu/cashu-ts';
import { bytesToHex } from '@noble/ciphers/utils';
import { secp256k1 } from '@noble/curves/secp256k1';
import { NDKEvent, type NDKEventId, NDKKind, type NDKTag, type NDKUser } from '@nostr-dev-kit/ndk';
import { getSigner, ndk } from './ndk';

import { State, property } from '@lit-app/state';

export type WalletHistory = {
  id: string;
  amount: number;
  created_at: number;
  direction: 'in' | 'out';
  tokenEventId: NDKEventId;
  nutZapEventId?: NDKEventId;
  senderPubkey?: string;
  recipientPubkey: string;
};

class Wallet extends State {
  static mintUrl = 'https://testnut.cashu.space'; // TODO: this should be user controlled

  private walletEvent: NDKEvent | null = null;
  private cashuWallet: CashuWallet = new CashuWallet(new CashuMint(Wallet.mintUrl));

  private proofs: Proof[] = [];
  private eventIdToProofIdMap: Map<NDKEventId, [string, string][]> = new Map();

  private isHandlingZapEvent = false;
  private zapEventsQueue: NDKEvent[] = [];
  private latestHistoryTimestamp = 0;

  @property() public balance = 0;
  @property() public history: WalletHistory[] = [];
  @property() public sortedHistory: WalletHistory[] = [];

  async loadWallet() {
    if (this.walletEvent) return;
    await getSigner();
    const user = await ndk.signer!.user()!;
    const privateWalletEvent = await ndk.fetchEvent({
      kinds: [17375 as NDKKind],
      authors: [user.pubkey],
    });
    if (!privateWalletEvent) {
      const walletPrivateKey = secp256k1.utils.randomPrivateKey();
      const walletPublicKey = secp256k1.getPublicKey(walletPrivateKey);
      const newPrivateWalletEvent = new NDKEvent(ndk);
      newPrivateWalletEvent.kind = NDKKind.CashuWallet;
      newPrivateWalletEvent.content = await ndk.signer!.encrypt(
        user,
        JSON.stringify([
          ['privkey', bytesToHex(walletPrivateKey)],
          ['mint', Wallet.mintUrl],
        ]),
        'nip44',
      );
      const publicWalletEvent = new NDKEvent(ndk);
      publicWalletEvent.kind = NDKKind.CashuMintList;
      publicWalletEvent.content = '';
      publicWalletEvent.tags = [
        ['mint', Wallet.mintUrl, 'sat'],
        ['pubkey', bytesToHex(walletPublicKey)],
      ];
      await newPrivateWalletEvent.sign();
      await newPrivateWalletEvent.publish();
      await publicWalletEvent.sign();
      await publicWalletEvent.publish();
      this.walletEvent = newPrivateWalletEvent;
    } else this.walletEvent = privateWalletEvent;

    const walletHistory = await ndk.fetchEvents({
      kinds: [NDKKind.CashuWalletTx],
      authors: [user.pubkey],
      since: this.walletEvent?.created_at,
    });

    for (const event of walletHistory) {
      await this.processHistoryEvent(event);
    }

    const proofsEvents = await ndk.fetchEvents({
      kinds: [NDKKind.CashuToken],
      authors: [user.pubkey],
    });

    for (const proofsEvent of proofsEvents) {
      const {
        mint,
        proofs = [],
        del = [],
      } = JSON.parse(await ndk.signer!.decrypt(user, proofsEvent.content, 'nip44')) as {
        mint: string;
        proofs: Proof[];
        del?: NDKEventId[];
      };

      if (mint !== this.cashuWallet.mint.mintUrl) continue;

      for (const eventId of del) {
        const toDeletes = this.eventIdToProofIdMap.get(eventId);
        if (toDeletes)
          for (const proof of toDeletes)
            this.proofs = this.proofs.filter((p) => p.secret !== proof[0] || p.C !== proof[1]);
        this.eventIdToProofIdMap.delete(eventId);
      }

      for (const proof of proofs) this.proofs.push(proof);

      this.eventIdToProofIdMap.set(
        proofsEvent.id,
        proofs.map((p) => [p.secret, p.C]),
      );
    }

    ndk
      .subscribe({
        kinds: [NDKKind.Nutzap],
        '#p': [user.pubkey],
        since: this.latestHistoryTimestamp,
      })
      .on('event', async (event) => {
        this.zapEventsQueue.push(event);
        this.handleZapEvent();
      });

    ndk
      .subscribe({
        kinds: [NDKKind.CashuWalletTx],
        authors: [user.pubkey],
        since: this.latestHistoryTimestamp,
      })
      .on('event', async (event) => {
        await this.processHistoryEvent(event);
      });

    this.recalculateBalance();
  }

  private async processHistoryEvent(event: NDKEvent) {
    const user = await ndk.signer!.user()!;
    const parsedContent = JSON.parse(await ndk.signer!.decrypt(user, event.content, 'nip44'));
    const direction = parsedContent.find((p: [string, string]) => p[0] === 'direction')?.[1];
    const amount = parsedContent.find((p: [string, string]) => p[0] === 'amount')?.[1];
    const tokenEventId = parsedContent.find((p: [string, string]) => p[0] === 'e')?.[1];
    const nutZapEventId = event.tags.find((t: NDKTag) => t[0] === 'e')?.[1];
    const senderPubkey = event.tags.find((t: NDKTag) => t[0] === 'p')?.[1];
    if (!nutZapEventId || !senderPubkey) return;
    this.history.push({
      id: event.id,
      amount: Number.parseInt(amount),
      created_at: event.created_at ?? 0,
      direction: direction as 'in' | 'out',
      tokenEventId,
      nutZapEventId: nutZapEventId as NDKEventId,
      senderPubkey,
      recipientPubkey: event.pubkey,
    });
    this.latestHistoryTimestamp = Math.max(this.latestHistoryTimestamp, event.created_at ?? 0);
    this.sortedHistory = [...this.history.sort((a, b) => b.created_at - a.created_at)];
  }

  private async handleZapEvent() {
    if (this.isHandlingZapEvent) return;
    const event = this.zapEventsQueue.shift();
    if (!event) return;
    this.isHandlingZapEvent = true;
    const proofs = JSON.parse(event.tags.find((t) => t[0] === 'proof')?.[1] ?? '[]') as Proof[];
    this.proofs.push(...proofs);

    const newProofsEvent = await this.createProofsEvent();
    await this.createHistoryEvent(
      newProofsEvent,
      this.proofs.reduce((acc, curr) => acc + curr.amount, 0),
      'in',
    );
    this.recalculateBalance();
    this.isHandlingZapEvent = false;
  }

  private async createProofsEvent(): Promise<NDKEvent> {
    const newProofsEvent = new NDKEvent(ndk);
    newProofsEvent.kind = NDKKind.CashuToken;
    newProofsEvent.content = await ndk.signer?.encrypt(
      await ndk.signer!.user()!,
      JSON.stringify({
        mint: this.cashuWallet.mint.mintUrl,
        proofs: this.proofs,
        del: Array.from(this.eventIdToProofIdMap.keys()),
      }),
      'nip44',
    )!;
    await newProofsEvent.sign();
    await newProofsEvent.publish();
    return newProofsEvent;
  }

  recalculateBalance() {
    this.proofs = this.proofs.filter(
      (p, index, self) => index === self.findIndex((t) => t.secret === p.secret && t.C === p.C),
    );
    this.balance = this.proofs.reduce((acc, curr) => acc + curr.amount, 0);
  }

  private async createHistoryEvent(newProofsEvent: NDKEvent, amount: number, direction: 'in' | 'out') {
    const historyEvent = new NDKEvent(ndk);
    historyEvent.kind = NDKKind.CashuWalletTx;
    historyEvent.content = await ndk.signer?.encrypt(
      await ndk.signer!.user()!,
      JSON.stringify([
        ['direction', direction],
        ['amount', amount.toString()],
        ['e', newProofsEvent.id],
      ]),
      'nip44',
    )!;
    historyEvent.tags = [
      ['e', newProofsEvent.id],
      ['p', newProofsEvent.pubkey],
    ];
    await historyEvent.sign();
    await historyEvent.publish();
  }

  /** @throws Error if token is invalid */
  async receiveToken(token: string) {
    const proofs = await this.cashuWallet.receive(token);
    if (proofs.length === 0) throw new Error('Invalid token');
    for (const proof of proofs) this.proofs.push(proof);
    const newProofsEvent = await this.createProofsEvent();
    const amount = proofs.reduce((acc, curr) => acc + curr.amount, 0);
    await this.createHistoryEvent(newProofsEvent, amount, 'in');
    this.recalculateBalance();
  }

  private async createToken(amount: number, recipient?: NDKUser) {
    if (amount < 10) throw new Error('Amount must be greater than 10');
    const sendOptions: SendOptions = {
      includeFees: true,
    };
    if (recipient) {
      const mintListEvent = await ndk.fetchEvent({
        kinds: [NDKKind.CashuMintList],
        authors: [recipient.pubkey],
      });
      if (!mintListEvent) throw new Error('Recipient has no mint list');
      const pubkey = mintListEvent.tags.find((t) => t[0] === 'pubkey')?.[1];
      if (!pubkey) throw new Error('Recipient has no mint list');
      sendOptions.pubkey = pubkey;
    }
    await this.cashuWallet.getKeySets();
    const sendResult = await this.cashuWallet.send(amount, this.proofs, sendOptions);
    this.proofs = sendResult.keep;
    const proofsEvent = new NDKEvent(ndk);
    proofsEvent.kind = NDKKind.CashuToken;
    proofsEvent.content = await ndk.signer?.encrypt(
      await ndk.signer!.user()!,
      JSON.stringify({
        mint: this.cashuWallet.mint.mintUrl,
        proofs: this.proofs,
        del: Array.from(this.eventIdToProofIdMap.keys()),
      }),
      'nip44',
    )!;
    await proofsEvent.sign();
    await proofsEvent.publish();
    this.recalculateBalance();
    return sendResult.send;
  }

  async nutZap(amount: number, recipient: NDKUser, eventId?: NDKEventId, message?: string) {
    const token = await this.createToken(amount, recipient);
    const zapEvent = new NDKEvent(ndk);
    zapEvent.kind = NDKKind.Nutzap;
    zapEvent.content = message ?? '';
    zapEvent.tags = [
      ['proof', JSON.stringify(token)],
      ['u', Wallet.mintUrl],
      ['p', recipient.pubkey],
    ];
    if (eventId) zapEvent.tags.push(['e', eventId]);
    await zapEvent.sign();
    await zapEvent.publish();

    const spendingHistoryEvent = new NDKEvent(ndk);
    spendingHistoryEvent.kind = NDKKind.CashuWalletTx;
    const historyContent = [
      ['direction', 'out'],
      ['amount', amount.toString()],
    ];
    if (eventId) historyContent.push(['e', eventId]);
    spendingHistoryEvent.content = await ndk.signer?.encrypt(
      await ndk.signer!.user()!,
      JSON.stringify(historyContent),
      'nip44',
    )!;
    spendingHistoryEvent.tags = [
      ['e', zapEvent.id],
      ['p', recipient.pubkey],
    ];
    await spendingHistoryEvent.sign();
    await spendingHistoryEvent.publish();
  }
}

export const wallet = new Wallet();
