import Dexie, { type Table } from "dexie";
import {
  finalizeEvent,
  generateSecretKey,
  getPublicKey,
  nip04,
  nip19,
  SimplePool,
  verifyEvent,
} from "nostr-tools";
import type { Event as NostrEvent } from "nostr-tools";
import { derived, get, type Writable, writable } from "svelte/store";
import { browser } from "$app/environment";
import { getBreezSDK, refreshBalances } from "$lib/breez.svelte";
import type { BindingLiquidSdk } from "@breeztech/breez-sdk-liquid/web";
import {
  bytesToHex,
  hexToBytes,
  type MetaEntry,
  persistentDbWritable,
} from "./utils";
import { cashuDB, cashuState, getNpub } from "./cashu.svelte";
import { send } from "./payments";

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"
    | "lookup_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));
  const breezSDK = await getBreezSDK();

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

async function handleEvent(
  evt: NostrEvent,
  privkey: string,
  ourPubkey: string,
  relays: string[],
  breezSDK: BindingLiquidSdk,
): 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> {
  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" };
      }

      const breezSDK = await getBreezSDK();
      const parsed = await breezSDK.parse(destination);

      if (parsed.type !== "bolt11") {
        throw { code: "INTERNAL", message: "not a bolt11 invoice" };
      }
      const paymentHash = (parsed as any).invoice.paymentHash;

      const existing = await nwcDB.paidInvoices.get(paymentHash);
      if (existing) {
        throw { code: "OTHER", message: "invoice already paid" };
      }

      const sender = send(destination, 0, parsed);
      let res = await sender.next();
      while (!res.done) {
        res = await sender.next();
      }

      if (res.value.error) {
        throw { code: "PAYMENT_FAILED", message: res.value.error };
      }

      if (res.value.paymentHash) {
        await nwcDB.paidInvoices.put({
          paymentHash: res.value.paymentHash,
          timestamp: Math.floor(Date.now() / 1000),
        });
      }

      await refreshBalances();
      return {
        preimage: res.value.preimage,
        paymentHash: res.value.paymentHash,
      };
    }
    case "get_balance": {
      const breezSDK = await getBreezSDK();
      const info = await breezSDK.getInfo();
      return {
        balance: info.walletInfo.balanceSat * 1000 + get(cashuState).balance *
            1000,
      };
    }
    case "list_transactions": {
      const from_timestamp =
        (req.params?.from_timestamp as number | undefined) ?? 0;
      const to_timestamp = (req.params?.to_timestamp as number | undefined) ??
        undefined;
      const limit = (req.params?.limit as number | undefined) ?? 20;
      const offset = (req.params?.offset as number | undefined) ?? undefined;

      const breezSDK = await getBreezSDK();
      const liquidPayments = (await breezSDK.listPayments({
        fromTimestamp: from_timestamp,
        toTimestamp: to_timestamp,
        limit,
        offset,
      })) as any[];

      const cashuTxns = await cashuDB.txns.toArray();

      const allTxns = [
        ...liquidPayments.map((p) => ({
          type: p.paymentType,
          invoice: p.bolt11,
          description: p.description,
          payment_hash: p.paymentHash,
          preimage: p.paymentPreimage,
          amount: p.amountSat * 1000,
          fees_paid: p.feesSat * 1000,
          created_at: Math.floor(p.paymentTime / 1000),
          settled_at: p.status === "complete"
            ? Math.floor(p.paymentTime / 1000)
            : undefined,
        })),
        ...cashuTxns.map((p) => ({
          type: p.paymentType,
          invoice: "",
          description: "Cashu",
          payment_hash: p.txId,
          preimage: "",
          amount: p.amountSat * 1000,
          fees_paid: 0,
          created_at: p.timestamp,
          settled_at: p.timestamp,
        })),
      ].sort((a, b) => b.created_at - a.created_at);

      return {
        transactions: allTxns,
      };
    }
    case "get_info": {
      const breezSDK = await getBreezSDK();
      const info = await breezSDK.getInfo();
      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",
          "lookup_invoice",
        ],
        notifications: [],
      };
    }
    case "make_invoice": {
      const amountMsat = req.params?.amount as number | undefined;

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

      const npub = await getNpub();
      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,
      };
    }
    case "lookup_invoice": {
      const paymentHash = req.params?.payment_hash as string | undefined;

      if (!paymentHash) {
        throw { code: "INTERNAL", message: "missing_payment_hash" };
      }

      const breezSDK = await getBreezSDK();
      const payments = (await breezSDK.listPayments({})) as any[];
      const payment = payments.find((p) => p.paymentHash === paymentHash);

      if (!payment) {
        throw { code: "NOT_FOUND", message: "invoice_not_found" };
      }

      return {
        type: payment.paymentType,
        invoice: payment.bolt11,
        description: payment.description,
        payment_hash: payment.paymentHash,
        preimage: payment.paymentPreimage,
        amount: payment.amountSat * 1000,
        fees_paid: payment.feesSat * 1000,
        created_at: Math.floor(payment.paymentTime / 1000),
        settled_at: payment.status === "complete"
          ? Math.floor(payment.paymentTime / 1000)
          : undefined,
      };
    }
    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);
}
