import { Router, type Request, type Response } from "express";
import { randomUUID } from "crypto";
import { db, bootstrapJobs, type BootstrapJob } from "@workspace/db";
import { eq, and } from "drizzle-orm";
import { lnbitsService } from "../lib/lnbits.js";
import { pricingService } from "../lib/pricing.js";
import { provisionerService } from "../lib/provisioner.js";
import { makeLogger } from "../lib/logger.js";

const logger = makeLogger("bootstrap");

const router = Router();

async function getBootstrapJobById(id: string): Promise<BootstrapJob | null> {
  const rows = await db
    .select()
    .from(bootstrapJobs)
    .where(eq(bootstrapJobs.id, id))
    .limit(1);
  return rows[0] ?? null;
}

/**
 * Advances the bootstrap job state machine on each poll.
 *
 * awaiting_payment → (payment confirmed) → provisioning
 *   (provisioner runs async and writes ready/failed to DB)
 *
 * Returns the refreshed job, or null if a DB read is needed.
 */
async function advanceBootstrapJob(job: BootstrapJob): Promise<BootstrapJob | null> {
  if (job.state !== "awaiting_payment") return job;

  const isPaid = await lnbitsService.checkInvoicePaid(job.paymentHash);
  if (!isPaid) return job;

  // Guard: only advance if still awaiting_payment — prevents duplicate provisioning
  // on concurrent polls (each poll independently confirms payment).
  const updated = await db
    .update(bootstrapJobs)
    .set({ state: "provisioning", updatedAt: new Date() })
    .where(and(eq(bootstrapJobs.id, job.id), eq(bootstrapJobs.state, "awaiting_payment")))
    .returning();

  if (updated.length === 0) {
    // Another concurrent request already advanced the state — just re-fetch.
    return getBootstrapJobById(job.id);
  }

  logger.info("bootstrap payment confirmed — starting provisioning", { bootstrapJobId: job.id });

  // Fire-and-forget: provisioner updates DB when done
  void provisionerService.provision(job.id);

  return { ...job, state: "provisioning" };
}

/**
 * POST /api/bootstrap
 *
 * Creates a bootstrap job and returns the Lightning invoice.
 */
router.post("/bootstrap", async (req: Request, res: Response) => {
  try {
    const fee = pricingService.calculateBootstrapFeeSats();
    const jobId = randomUUID();

    const invoice = await lnbitsService.createInvoice(
      fee,
      `Node bootstrap fee — job ${jobId}`,
    );

    await db.insert(bootstrapJobs).values({
      id: jobId,
      state: "awaiting_payment",
      amountSats: fee,
      paymentHash: invoice.paymentHash,
      paymentRequest: invoice.paymentRequest,
    });

    res.status(201).json({
      bootstrapJobId: jobId,
      invoice: {
        paymentRequest: invoice.paymentRequest,
        amountSats: fee,
        paymentHash: invoice.paymentHash,
      },
      stubMode: lnbitsService.stubMode || provisionerService.stubMode,
      message: `Simulate payment with POST /api/dev/stub/pay/${invoice.paymentHash} then poll GET /api/bootstrap/:id`,
    });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Failed to create bootstrap job";
    res.status(500).json({ error: message });
  }
});

/**
 * GET /api/bootstrap/:id
 *
 * Polls status. Triggers provisioning once payment is confirmed.
 * Returns credentials (SSH key delivered once, then cleared) when ready.
 */
router.get("/bootstrap/:id", async (req: Request, res: Response) => {
  const { id } = req.params;
  if (!id || typeof id !== "string") {
    res.status(400).json({ error: "Invalid bootstrap job id" });
    return;
  }

  try {
    let job = await getBootstrapJobById(id);
    if (!job) {
      res.status(404).json({ error: "Bootstrap job not found" });
      return;
    }

    const advanced = await advanceBootstrapJob(job);
    if (advanced) job = advanced;

    const base = {
      bootstrapJobId: job.id,
      state: job.state,
      amountSats: job.amountSats,
      createdAt: job.createdAt,
    };

    switch (job.state) {
      case "awaiting_payment":
        res.json({
          ...base,
          invoice: {
            paymentRequest: job.paymentRequest,
            amountSats: job.amountSats,
            paymentHash: job.paymentHash,
          },
          message: "Waiting for Lightning payment",
        });
        break;

      case "provisioning":
        res.json({
          ...base,
          message: "Payment confirmed — provisioning your Bitcoin node. Poll again in ~30 s.",
        });
        break;

      case "ready": {
        // Atomic one-time SSH key delivery: only the request that wins the
        // guarded UPDATE (WHERE ssh_key_delivered = false) delivers the key.
        // Concurrent first-reads both see delivered=false in the pre-fetched
        // job, but only one UPDATE matches — the other gets 0 rows and falls
        // back to the "already delivered" note.
        let sshPrivateKey: string | null = null;
        let keyNote: string | null = null;

        if (!job.sshKeyDelivered && job.sshPrivateKey) {
          const won = await db
            .update(bootstrapJobs)
            .set({ sshKeyDelivered: true, sshPrivateKey: null, updatedAt: new Date() })
            .where(and(eq(bootstrapJobs.id, job.id), eq(bootstrapJobs.sshKeyDelivered, false)))
            .returning({ id: bootstrapJobs.id });

          if (won.length > 0) {
            // This request won the delivery race — return the key we pre-read.
            sshPrivateKey = job.sshPrivateKey;
          } else {
            keyNote = "SSH private key was delivered on a concurrent request — check your records";
          }
        } else {
          keyNote = "SSH private key was delivered on first retrieval — check your records";
        }

        res.json({
          ...base,
          credentials: {
            nodeIp: job.nodeIp,
            tailscaleHostname: job.tailscaleHostname,
            lnbitsUrl: job.lnbitsUrl,
            sshPrivateKey,
            ...(keyNote ? { sshKeyNote: keyNote } : {}),
          },
          nextSteps: [
            `SSH into your node using the private key above: ssh -i <key_file> root@${job.nodeIp ?? "<nodeIp>"}`,
            "Read your node credentials: cat /root/node-credentials.txt",
            "Monitor Bitcoin sync (takes 1-2 weeks to reach 100%): bash /opt/timmy-node/ops.sh sync",
            "Once sync is complete, fund your LND wallet, then open LNbits to create your wallet and get the API key",
            "Set LNBITS_URL and LNBITS_API_KEY in your Timmy deployment to enable payment processing",
          ],
          stubMode: provisionerService.stubMode,
          message: provisionerService.stubMode
            ? "Stub mode — these are fake credentials. Set DO_API_TOKEN for real provisioning."
            : "Your node is being bootstrapped. Bitcoin sync has started.",
        });
        break;
      }

      case "failed":
        res.json({
          ...base,
          errorMessage: job.errorMessage,
          message: "Provisioning failed. Contact the operator for a refund.",
        });
        break;

      default:
        res.json(base);
    }
  } catch (err) {
    const message = err instanceof Error ? err.message : "Failed to fetch bootstrap job";
    res.status(500).json({ error: message });
  }
});

export default router;
