import Dexie, { type Table } from "dexie";
import {
  finalizeEvent,
  generateSecretKey,
  getPublicKey,
  nip04,
  SimplePool,
  verifyEvent,
} from "nostr-tools";
import type { Event as NostrEvent } from "nostr-tools";
import { derived, get } from "svelte/store";
import { browser } from "$app/environment";
import {
  bytesToHex,
  hexToBytes,
  type MetaEntry,
  persistentDbWritable,
} from "$lib";
import { walletState } from "$lib/wallet.svelte";

interface PaidInvoice {
  paymentHash: string;
  timestamp: number;
}

class NwcDB extends Dexie {
  meta!: Table<MetaEntry, string>;
  paidInvoices!: Table<PaidInvoice, string>;

  constructor() {
    super("nwc");
    this.version(2).stores({
      meta: "&key",
      paidInvoices: "&paymentHash",
    });
  }
}

const nwcDB = new NwcDB();

export const nwcPrivkey = persistentDbWritable<string | null, NwcDB>(
  "nwc_privkey",
  null,
  nwcDB,
);

export const nwcSecret = persistentDbWritable<string | null, NwcDB>(
  "nwc_secret",
  null,
  nwcDB,
);

export const nwcRelays = persistentDbWritable<string[], NwcDB>(
  "nwc_relays",
  [
    "wss://relay.arx-ccn.com",
  ],
  nwcDB,
);

export const connectUri = derived(
  [nwcPrivkey, nwcSecret, nwcRelays],
  ([$nwcPrivkey, $nwcSecret, $nwcRelays]) => {
    if (!$nwcPrivkey || !$nwcSecret) return "";
    const privBytes = hexToBytes($nwcPrivkey);
    const relayParam = encodeURIComponent($nwcRelays.join(","));
    return `nostr+walletconnect://${
      getPublicKey(privBytes)
    }?relay=${relayParam}&secret=${$nwcSecret}`;
  },
);

interface JsonRpcReq {
  id: string;
  method:
    | "pay_invoice"
    | "get_balance"
    | "list_transactions"
    | "get_info"
    | "make_invoice";
  params: Record<string, unknown>;
}

interface JsonRpcSuccess<T = unknown> {
  result_type: string;
  result: T;
}

interface JsonRpcError {
  result_type: string;
  error: {
    code:
      | "RATE_LIMITED"
      | "NOT_IMPLEMENTED"
      | "INSUFFICIENT_BALANCE"
      | "QUOTA_EXCEEDED"
      | "RESTRICTED"
      | "UNAUTHORIZED"
      | "INTERNAL"
      | "NOT_FOUND"
      | "PAYMENT_FAILED"
      | "OTHER";
    message: string;
  };
}

type JsonRpcResp = JsonRpcSuccess | JsonRpcError;

let pool: SimplePool | null = null;
let sub: ReturnType<SimplePool["subscribeMany"]> | null = null;
let active = false;

export async function start(): Promise<void> {
  if (!browser || active) return;
  ensurePrivkey();
  ensureSecret();
  await connect();
  active = true;
}

export function stop(): void {
  if (!browser || !active) return;
  sub?.close();
  pool?.close([]);
  pool = null;
  sub = null;
  active = false;
}

nwcRelays.subscribe(() => {
  if (active) {
    stop();
    start();
  }
});

function ensurePrivkey(): string {
  const current = get(nwcPrivkey);
  if (current) return current;
  const pk = bytesToHex(generateSecretKey());
  nwcPrivkey.set(pk);
  return pk;
}

function ensureSecret(): string {
  const current = get(nwcSecret);
  if (current) return current;
  const secret = bytesToHex(generateSecretKey());
  nwcSecret.set(secret);
  return secret;
}

async function connect(): Promise<void> {
  const relays = get(nwcRelays);
  const maybePriv = get(nwcPrivkey);
  if (!maybePriv) throw new Error("NWC privkey missing");
  const privkey = maybePriv;
  const pubkey = getPublicKey(hexToBytes(privkey));

  pool = new SimplePool();
  sub = pool.subscribeMany(relays, [
    {
      kinds: [23194], // NWC request events
      "#p": [pubkey],
    },
  ], {
    onevent: async (evt: NostrEvent) => {
      await handleEvent(evt, privkey, pubkey, relays);
    },
  });
}

async function handleEvent(
  evt: NostrEvent,
  privkey: string,
  ourPubkey: string,
  relays: string[],
): Promise<void> {
  try {
    if (!verifyEvent(evt)) return;
    const decrypted = await nip04.decrypt(
      hexToBytes(privkey),
      evt.pubkey,
      evt.content,
    );
    const req = JSON.parse(decrypted) as JsonRpcReq;

    const result = await processRpc(req);
    await sendResponse(
      req.id,
      req.method,
      result,
      evt.pubkey,
      privkey,
      ourPubkey,
      relays,
      evt.id,
    );
  } catch (err) {
    console.error("NWC handleEvent error", err);
    const req = JSON.parse(
      await nip04.decrypt(hexToBytes(privkey), evt.pubkey, evt.content),
    ) as JsonRpcReq;
    const errorResp: JsonRpcError = {
      result_type: req.method,
      error: {
        code: "INTERNAL",
        message: (err as Error).message,
      },
    };
    await sendResponse(
      req.id,
      req.method,
      errorResp,
      evt.pubkey,
      privkey,
      ourPubkey,
      relays,
      evt.id,
    );
  }
}

async function processRpc(
  req: JsonRpcReq,
): Promise<unknown> {
  const currentWalletState = get(walletState);
  if (!currentWalletState.open) throw new Error("Wallet not open");
  console.log("processRpc", req);
  switch (req.method) {
    case "pay_invoice": {
      const destination = req.params?.invoice as string | undefined;
      if (!destination) {
        throw { code: "INTERNAL", message: "missing_invoice" };
      }

      if (await nwcDB.paidInvoices.get(destination)) {
        throw { code: "OTHER", message: "invoice_already_paid" };
      }

      const sender = currentWalletState.wallet.pay(destination, 0);
      let res;
      try {
        res = await sender.next();
        while (!res.done) {
          res = await sender.next();
        }
      } catch (err) {
        throw { code: "PAYMENT_FAILED", message: (err as Error).message };
      }

      await nwcDB.paidInvoices.put({
        paymentHash: destination,
        timestamp: Math.floor(Date.now() / 1000),
      }).catch((err) => {
        console.error("Failed to save paid invoice", err);
      });

      return {};
      // TODO: check if it works without the preimage and paymentHash
      // return {
      //   preimage: res.value.preimage,
      //   paymentHash: res.value.paymentHash,
      // };
    }
    case "get_balance": {
      return {
        balance: currentWalletState.balance * 1000,
      };
    }
    case "list_transactions": {
      const limit = (req.params?.limit as number | undefined) ?? 20;
      const offset = (req.params?.offset as number | undefined) ?? 0;

      const transactions = await currentWalletState.wallet.listPayments(
        limit,
        offset,
      );
      return { transactions };
    }
    case "get_info": {
      const privkey = get(nwcPrivkey);
      if (!privkey) throw new Error("missing_privkey");

      return {
        alias: "Portal BTC Wallet",
        color: "#ff6b35",
        pubkey: getPublicKey(hexToBytes(privkey)),
        network: "mainnet",
        methods: [
          "pay_invoice",
          "get_balance",
          "list_transactions",
          "get_info",
          "make_invoice",
        ],
        notifications: [],
      };
    }
    case "make_invoice": {
      const amountMsat = req.params?.amount as number | undefined;

      if (!amountMsat) {
        throw { code: "INTERNAL", message: "missing_amount" };
      }

      const npub = currentWalletState.wallet.npub;
      if (!npub) throw new Error("Wallet not open");

      const res = await fetch(
        `/.well-known/lnurl-pay/callback/${npub}?amount=${amountMsat}`,
      );

      if (!res.ok) {
        const errorText = await res.text();
        console.error("Failed to create invoice:", errorText);
        throw {
          code: "INTERNAL",
          message: `Failed to create invoice: ${errorText}`,
        };
      }

      const { pr: invoice } = await res.json();

      if (!invoice) {
        throw { code: "INTERNAL", message: "failed_to_create_invoice" };
      }

      return {
        invoice,
      };
    }
    default:
      throw { code: "NOT_IMPLEMENTED", message: "unknown_method" };
  }
}

async function sendResponse(
  id: string,
  method: string,
  payload: JsonRpcResp | unknown,
  receiverPubkey: string,
  privkey: string,
  ourPubkey: string,
  relays: string[],
  reqId: string,
): Promise<void> {
  const resp: JsonRpcResp = (payload as JsonRpcError).error
    ? (payload as JsonRpcError)
    : { result_type: method, result: payload };

  const content = JSON.stringify({ id, ...resp });
  const encrypted = await nip04.encrypt(
    hexToBytes(privkey),
    receiverPubkey,
    content,
  );

  const event: NostrEvent = {
    kind: 23195,
    pubkey: ourPubkey,
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ["p", receiverPubkey],
      ["e", reqId],
    ],
    content: encrypted,
  } as NostrEvent;

  const signedEvent = finalizeEvent(
    event,
    hexToBytes(privkey),
  );
  pool?.publish(relays, signedEvent);
}
