import {
  CashuMint,
  CashuWallet,
  MintQuoteState,
  type Proof,
} from "@cashu/cashu-ts";
import { get, writable } from "svelte/store";
import { getMnemonic } from "$lib/wallet.svelte";
import { privateKeyFromSeedWords as nostrPrivateKeyFromSeedWords } from "nostr-tools/nip06";
import { getBreezSDK } from "$lib/breez.svelte";
import { finalizeEvent, getPublicKey, nip19, nip98 } from "nostr-tools";
import Dexie, { type Table } from "dexie";
import { BASE_DOMAIN } from "$lib/config";

interface MetaEntry {
  key: string;
  value: string;
}

interface CashuTxn {
  txId: string;
  paymentType: "receive" | "send";
  amountSat: number;
  timestamp: number;
  status: "complete";
}

class CashuDB extends Dexie {
  proofs!: Table<Proof, string>;
  meta!: Table<MetaEntry, string>;
  txns!: Table<CashuTxn, string>;

  constructor() {
    super("cashu");
    this.version(1).stores({
      proofs: "&secret",
      meta: "&key",
    });
    this.version(2).stores({
      txns: "&txId",
    });
  }
}

export const cashuDB = new CashuDB();

export type { CashuTxn };

const MINT_URL = "https://mint.minibits.cash/Bitcoin";

interface CashuState {
  balance: number;
  meltThreshold: number;
  lastUpdated: number;
}

export const cashuState = writable<CashuState>({
  balance: 0,
  meltThreshold: 2000,
  lastUpdated: 0,
});

let proofs: Proof[] = [];

async function loadProofs() {
  proofs = await cashuDB.proofs.toArray();
  updateBalance();
}

async function persistProofs() {
  await cashuDB.proofs.clear();
  if (proofs.length) await cashuDB.proofs.bulkPut(proofs);
}

async function fetchWithNip98<T>(
  { url, method, body }: {
    url: string;
    method: string;
    body?: BodyInit | null;
  },
): Promise<T> {
  const mnemonic = await getMnemonic();
  const nostrPrivateKey = nostrPrivateKeyFromSeedWords(mnemonic);
  const urlWithoutQueryParams = url.split("?")[0];
  const npubCashNip98 = await nip98.getToken(
    urlWithoutQueryParams,
    method,
    (event) => finalizeEvent(event, nostrPrivateKey),
  );
  return fetch(url, {
    method,
    headers: {
      "Authorization": `Nostr ${npubCashNip98}`,
    },
    body,
  }).then((data) => data.json());
}

export async function getNpub() {
  const mnemonic = await getMnemonic();
  const nostrPrivateKey = nostrPrivateKeyFromSeedWords(mnemonic);
  const nostrPubkey = getPublicKey(nostrPrivateKey);
  return nip19.npubEncode(nostrPubkey);
}

export async function getCashuAddress() {
  return `${await getNpub()}@${BASE_DOMAIN}`;
}

interface NpubCashQuote {
  createdAt: number;
  paidAt?: number;
  expiresAt: number;
  mintUrl: string;
  quoteId: string;
  request: string;
  amount: number;
  state: "PAID" | "ISSUED" | "INFLIGHT";
  locked: boolean;
}

interface PaginationMetadata {
  total: number;
  limit: number;
}

async function tryRedeemUnredeemedCashuQuotes() {
  const lastRedeemedCashuQuoteTimestamp =
    localStorage.getItem("lastRedeemedCashuQuoteTimestamp") || 0;
  let quotes: NpubCashQuote[] = [];
  while (true) {
    const currentQuotes = await fetchWithNip98<
      {
        error: false;
        data: { quotes: NpubCashQuote[] };
        metadata: PaginationMetadata;
      } | {
        error: true;
        message: string;
      }
    >({
      url:
        `https://npubx.cash/api/v2/wallet/quotes?since=${lastRedeemedCashuQuoteTimestamp}&limit=50&offset=${quotes.length}`,
      method: "GET",
    });
    if (currentQuotes.error === false) {
      quotes.push(...currentQuotes.data.quotes);
      if (quotes.length >= currentQuotes.metadata.total) {
        break;
      }
    } else {
      throw new Error(currentQuotes.message);
    }
  }
  quotes = quotes.sort((a, b) => a.createdAt - b.createdAt);
  for (const quote of quotes) {
    if (quote.state === "PAID") {
      const mint = new CashuMint(quote.mintUrl);
      const wallet = new CashuWallet(mint);
      const req = await mint.checkMintQuote(quote.quoteId);
      if (req.state === MintQuoteState.PAID && quote.paidAt) {
        const newProofs = await wallet.mintProofs(quote.amount, quote.quoteId);
        proofs.push(...newProofs);
        await persistProofs();
        const amountReceived = newProofs.reduce((sum, p) => sum + p.amount, 0);
        cashuTxns.update((txs) => {
          txs.push({
            txId: `cashu-quote-${quote.quoteId}`,
            paymentType: "receive",
            amountSat: amountReceived,
            timestamp: quote.paidAt
              ? Math.floor(quote.paidAt)
              : Math.floor(Date.now() / 1000),
            status: "complete",
          });
          return txs;
        });
        persistCashuTxns();
        updateBalance();
        localStorage.setItem(
          "lastRedeemedCashuQuoteTimestamp",
          quote.paidAt.toString(),
        );
      }
    }
  }
  return quotes;
}

let wallet: CashuWallet | undefined;

const walletReady = (async () => {
  if (wallet) return wallet;
  await loadProofs();
  try {
    await getMnemonic();
  } catch (_) {}

  const mint = new CashuMint(MINT_URL);
  wallet = new CashuWallet(mint);
  await wallet.loadMint();
  await tryRedeemUnredeemedCashuQuotes();

  try {
    console.log(`cashu addr: ${await getCashuAddress()}`);
  } catch (_) {
  }

  await persistProofs();
  return wallet;
})();

let quoteRedeemInterval: ReturnType<typeof setInterval> | undefined;
if (quoteRedeemInterval) clearInterval(quoteRedeemInterval);
quoteRedeemInterval = setInterval(
  () => tryRedeemUnredeemedCashuQuotes().then(maybeMelt),
  1000 * 5,
);

function updateBalance() {
  const balance = proofs.reduce((sum, p) => sum + p.amount, 0);
  cashuState.update((s) => ({ ...s, balance, lastUpdated: s.lastUpdated + 1 }));
}

export async function redeemToken(token: string): Promise<void> {
  if (!token.trim()) throw new Error("Token is empty");

  const wallet = await walletReady;
  const received = await wallet.receive(token.trim());
  proofs.push(...received);
  const amountReceived = received.reduce((sum, p) => sum + p.amount, 0);
  cashuTxns.update((txs) => {
    txs.push({
      txId: `cashu-receive-${Date.now()}`,
      paymentType: "receive",
      amountSat: amountReceived,
      timestamp: Math.floor(Date.now() / 1000),
      status: "complete",
    });
    return txs;
  });
  persistCashuTxns();
  updateBalance();
  await persistProofs();
  await maybeMelt();
}

type PrepareReceiveRequest = {
  paymentMethod: "bolt11Invoice";
  amountSat: number;
};

async function createMeltInvoice(amountSat: number): Promise<string> {
  const breezSDK = await getBreezSDK();
  const prepare = await breezSDK.prepareReceivePayment({
    paymentMethod: "bolt11Invoice",
    amount: { type: "bitcoin", payerAmountSat: amountSat },
    amountSat: amountSat,
  } as PrepareReceiveRequest);

  const { destination } = await breezSDK.receivePayment({
    prepareResponse: prepare,
  });
  return destination as string;
}

let tryingToMelt = false;
async function maybeMelt() {
  if (tryingToMelt) return;
  tryingToMelt = true;
  const { balance, meltThreshold } = get(cashuState);
  if (balance < meltThreshold) return;

  try {
    const wallet = await walletReady;
    const invoice = await createMeltInvoice(balance - (balance % 2000));
    const meltQuote = await wallet.createMeltQuote(invoice);

    const amountToMelt = meltQuote.amount + meltQuote.fee_reserve;

    const { keep, send } = await wallet.send(amountToMelt, proofs, {
      includeFees: true,
    });

    proofs = keep;
    await persistProofs();

    const { change } = await wallet.meltProofs(meltQuote, send);
    proofs.push(...change);
    await persistProofs();

    cashuTxns.update((txs) => {
      txs.push({
        txId: `cashu-send-${Date.now()}`,
        paymentType: "send",
        amountSat: amountToMelt,
        timestamp: Math.floor(Date.now() / 1000),
        status: "complete",
      });
      return txs;
    });
    persistCashuTxns();

    updateBalance();
  } catch (err) {
    console.error("Failed to melt Cashu balance", err);
  }
  tryingToMelt = false;
}

export const cashuTxns = writable<CashuTxn[]>([]);

async function loadCashuTxns() {
  try {
    const txns = await cashuDB.txns.toArray();
    cashuTxns.set(txns);
  } catch (err) {
    console.error("Failed to load Cashu txns", err);
  }
}

async function persistCashuTxns() {
  try {
    const txns = get(cashuTxns);
    await cashuDB.txns.clear();
    if (txns.length) await cashuDB.txns.bulkPut(txns);
  } catch (err) {
    console.error("Failed to persist Cashu txns", err);
  }
}

loadCashuTxns();

export async function payBolt11Invoice(invoice: string): Promise<void> {
  if (!invoice.trim()) throw new Error("Invoice is empty");

  const wallet = await walletReady;
  const meltQuote = await wallet.createMeltQuote(invoice);

  const amountToMelt = meltQuote.amount + meltQuote.fee_reserve;

  const { balance } = get(cashuState);
  if (amountToMelt > balance) {
    throw new Error("Insufficient Cashu balance");
  }

  const { keep, send } = await wallet.send(amountToMelt, proofs, {
    includeFees: true,
  });

  proofs = keep;
  await persistProofs();

  const { change } = await wallet.meltProofs(meltQuote, send);

  proofs.push(...change);
  await persistProofs();

  cashuTxns.update((txs) => {
    txs.push({
      txId: `cashu-send-${Date.now()}`,
      paymentType: "send",
      amountSat: amountToMelt,
      timestamp: Math.floor(Date.now() / 1000),
      status: "complete",
    });
    return txs;
  });
  persistCashuTxns();

  updateBalance();
}
