import { Router, type Request, type Response } from "express";
import { randomUUID } from "crypto";
import { db, jobs, invoices, type Job } from "@workspace/db";
import { eq, and } from "drizzle-orm";
import { CreateJobBody, GetJobParams } from "@workspace/api-zod";
import { lnbitsService } from "../lib/lnbits.js";
import { agentService } from "../lib/agent.js";
import { pricingService } from "../lib/pricing.js";

const router = Router();

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

async function getInvoiceById(id: string) {
  const rows = await db.select().from(invoices).where(eq(invoices.id, id)).limit(1);
  return rows[0] ?? null;
}

/**
 * Checks whether the active invoice for a job has been paid and, if so,
 * advances the state machine. Returns the refreshed job after any transitions.
 */
async function advanceJob(job: Job): Promise<Job | null> {
  if (job.state === "awaiting_eval_payment" && job.evalInvoiceId) {
    const evalInvoice = await getInvoiceById(job.evalInvoiceId);
    if (!evalInvoice || evalInvoice.paid) return getJobById(job.id);

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

    const advanced = await db.transaction(async (tx) => {
      await tx
        .update(invoices)
        .set({ paid: true, paidAt: new Date() })
        .where(eq(invoices.id, evalInvoice.id));
      const updated = await tx
        .update(jobs)
        .set({ state: "evaluating", updatedAt: new Date() })
        .where(and(eq(jobs.id, job.id), eq(jobs.state, "awaiting_eval_payment")))
        .returning();
      return updated.length > 0;
    });

    if (!advanced) return getJobById(job.id);

    try {
      const evalResult = await agentService.evaluateRequest(job.request);

      if (evalResult.accepted) {
        const inputEst = pricingService.estimateInputTokens(job.request);
        const outputEst = pricingService.estimateOutputTokens(job.request);
        const breakdown = await pricingService.calculateWorkFeeSats(
          inputEst,
          outputEst,
          agentService.workModel,
        );

        const workInvoiceData = await lnbitsService.createInvoice(
          breakdown.amountSats,
          `Work fee for job ${job.id}`,
        );
        const workInvoiceId = randomUUID();

        await db.transaction(async (tx) => {
          await tx.insert(invoices).values({
            id: workInvoiceId,
            jobId: job.id,
            paymentHash: workInvoiceData.paymentHash,
            paymentRequest: workInvoiceData.paymentRequest,
            amountSats: breakdown.amountSats,
            type: "work",
            paid: false,
          });
          await tx
            .update(jobs)
            .set({
              state: "awaiting_work_payment",
              workInvoiceId,
              workAmountSats: breakdown.amountSats,
              estimatedCostUsd: breakdown.estimatedCostUsd,
              marginPct: breakdown.marginPct,
              btcPriceUsd: breakdown.btcPriceUsd,
              updatedAt: new Date(),
            })
            .where(eq(jobs.id, job.id));
        });
      } else {
        await db
          .update(jobs)
          .set({ state: "rejected", rejectionReason: evalResult.reason, updatedAt: new Date() })
          .where(eq(jobs.id, job.id));
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : "Evaluation error";
      await db
        .update(jobs)
        .set({ state: "failed", errorMessage: message, updatedAt: new Date() })
        .where(eq(jobs.id, job.id));
    }

    return getJobById(job.id);
  }

  if (job.state === "awaiting_work_payment" && job.workInvoiceId) {
    const workInvoice = await getInvoiceById(job.workInvoiceId);
    if (!workInvoice || workInvoice.paid) return getJobById(job.id);

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

    const advanced = await db.transaction(async (tx) => {
      await tx
        .update(invoices)
        .set({ paid: true, paidAt: new Date() })
        .where(eq(invoices.id, workInvoice.id));
      const updated = await tx
        .update(jobs)
        .set({ state: "executing", updatedAt: new Date() })
        .where(and(eq(jobs.id, job.id), eq(jobs.state, "awaiting_work_payment")))
        .returning();
      return updated.length > 0;
    });

    if (!advanced) return getJobById(job.id);

    try {
      const workResult = await agentService.executeWork(job.request);

      // ── Honest post-work accounting ───────────────────────────────────────
      const actualCostUsd = pricingService.calculateActualCostUsd(
        workResult.inputTokens,
        workResult.outputTokens,
        agentService.workModel,
      );

      // Re-use the BTC price locked at invoice time so sats arithmetic is
      // consistent. Fall back to the cached oracle price only if the column
      // is somehow null (should not happen in normal flow).
      const lockedBtcPrice = job.btcPriceUsd ?? 100_000;
      const actualAmountSats = pricingService.calculateActualChargeSats(
        actualCostUsd,
        lockedBtcPrice,
      );
      const refundAmountSats = pricingService.calculateRefundSats(
        job.workAmountSats ?? 0,
        actualAmountSats,
      );
      const refundState = refundAmountSats > 0 ? "pending" : "not_applicable";

      await db
        .update(jobs)
        .set({
          state: "complete",
          result: workResult.result,
          actualInputTokens: workResult.inputTokens,
          actualOutputTokens: workResult.outputTokens,
          actualCostUsd,
          actualAmountSats,
          refundAmountSats,
          refundState,
          updatedAt: new Date(),
        })
        .where(eq(jobs.id, job.id));
    } catch (err) {
      const message = err instanceof Error ? err.message : "Execution error";
      await db
        .update(jobs)
        .set({ state: "failed", errorMessage: message, updatedAt: new Date() })
        .where(eq(jobs.id, job.id));
    }

    return getJobById(job.id);
  }

  return job;
}

// ── POST /jobs ────────────────────────────────────────────────────────────────

router.post("/jobs", async (req: Request, res: Response) => {
  const parseResult = CreateJobBody.safeParse(req.body);
  if (!parseResult.success) {
    const issue = parseResult.error.issues[0];
    const error = issue?.code === "too_big"
      ? "Invalid request: 'request' must be 500 characters or fewer"
      : "Invalid request: 'request' string is required";
    res.status(400).json({ error });
    return;
  }
  const { request } = parseResult.data;

  try {
    const evalFee = pricingService.calculateEvalFeeSats();
    const jobId = randomUUID();
    const invoiceId = randomUUID();

    const lnbitsInvoice = await lnbitsService.createInvoice(evalFee, `Eval fee for job ${jobId}`);

    await db.transaction(async (tx) => {
      await tx.insert(jobs).values({ id: jobId, request, state: "awaiting_eval_payment", evalAmountSats: evalFee });
      await tx.insert(invoices).values({
        id: invoiceId,
        jobId,
        paymentHash: lnbitsInvoice.paymentHash,
        paymentRequest: lnbitsInvoice.paymentRequest,
        amountSats: evalFee,
        type: "eval",
        paid: false,
      });
      await tx.update(jobs).set({ evalInvoiceId: invoiceId, updatedAt: new Date() }).where(eq(jobs.id, jobId));
    });

    res.status(201).json({
      jobId,
      evalInvoice: {
        paymentRequest: lnbitsInvoice.paymentRequest,
        amountSats: evalFee,
        ...(lnbitsService.stubMode ? { paymentHash: lnbitsInvoice.paymentHash } : {}),
      },
    });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Failed to create job";
    res.status(500).json({ error: message });
  }
});

// ── GET /jobs/:id ─────────────────────────────────────────────────────────────

router.get("/jobs/:id", async (req: Request, res: Response) => {
  const paramResult = GetJobParams.safeParse(req.params);
  if (!paramResult.success) { res.status(400).json({ error: "Invalid job id" }); return; }
  const { id } = paramResult.data;

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

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

    const base = { jobId: job.id, state: job.state };

    switch (job.state) {
      case "awaiting_eval_payment": {
        const inv = job.evalInvoiceId ? await getInvoiceById(job.evalInvoiceId) : null;
        res.json({
          ...base,
          ...(inv ? {
            evalInvoice: {
              paymentRequest: inv.paymentRequest,
              amountSats: inv.amountSats,
              ...(lnbitsService.stubMode ? { paymentHash: inv.paymentHash } : {}),
            },
          } : {}),
        });
        break;
      }

      case "awaiting_work_payment": {
        const inv = job.workInvoiceId ? await getInvoiceById(job.workInvoiceId) : null;
        res.json({
          ...base,
          ...(inv ? {
            workInvoice: {
              paymentRequest: inv.paymentRequest,
              amountSats: inv.amountSats,
              ...(lnbitsService.stubMode ? { paymentHash: inv.paymentHash } : {}),
            },
          } : {}),
          ...(job.estimatedCostUsd != null ? {
            pricingBreakdown: {
              estimatedCostUsd: job.estimatedCostUsd,
              marginPct: job.marginPct,
              btcPriceUsd: job.btcPriceUsd,
            },
          } : {}),
        });
        break;
      }

      case "rejected":
        res.json({ ...base, reason: job.rejectionReason ?? undefined });
        break;

      case "complete":
        res.json({
          ...base,
          result: job.result ?? undefined,
          ...(job.actualCostUsd != null ? {
            costLedger: {
              // Token usage
              actualInputTokens: job.actualInputTokens,
              actualOutputTokens: job.actualOutputTokens,
              totalTokens: (job.actualInputTokens ?? 0) + (job.actualOutputTokens ?? 0),
              // USD costs
              actualCostUsd: job.actualCostUsd,
              actualChargeUsd: job.actualAmountSats != null && job.btcPriceUsd
                ? (job.actualAmountSats / 1e8) * job.btcPriceUsd
                : undefined,
              estimatedCostUsd: job.estimatedCostUsd,
              // Sat amounts
              actualAmountSats: job.actualAmountSats,
              workAmountSats: job.workAmountSats,
              // Refund
              refundAmountSats: job.refundAmountSats,
              refundState: job.refundState,
              // Rate context
              marginPct: job.marginPct,
              btcPriceUsd: job.btcPriceUsd,
            },
          } : {}),
        });
        break;

      case "failed":
        res.json({ ...base, errorMessage: job.errorMessage ?? undefined });
        break;

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

// ── POST /jobs/:id/refund ─────────────────────────────────────────────────────

router.post("/jobs/:id/refund", async (req: Request, res: Response) => {
  const paramResult = GetJobParams.safeParse(req.params);
  if (!paramResult.success) { res.status(400).json({ error: "Invalid job id" }); return; }
  const { id } = paramResult.data;

  const { invoice } = req.body as { invoice?: string };
  if (!invoice || typeof invoice !== "string" || invoice.trim().length === 0) {
    res.status(400).json({ error: "Body must include 'invoice' (BOLT11 string)" });
    return;
  }
  const bolt11 = invoice.trim();

  try {
    const job = await getJobById(id);
    if (!job) { res.status(404).json({ error: "Job not found" }); return; }
    if (job.state !== "complete") {
      res.status(409).json({ error: `Job is in state '${job.state}', not 'complete'` });
      return;
    }
    if (job.refundState !== "pending") {
      if (job.refundState === "not_applicable") {
        res.status(409).json({ error: "No refund is owed for this job (actual cost matched or exceeded the invoice amount)" });
      } else if (job.refundState === "paid") {
        res.status(409).json({ error: "Refund has already been sent", refundPaymentHash: job.refundPaymentHash });
      } else {
        res.status(409).json({ error: "Refund is not available for this job" });
      }
      return;
    }

    const refundSats = job.refundAmountSats ?? 0;
    if (refundSats <= 0) {
      res.status(409).json({ error: "Refund amount is zero — nothing to return" });
      return;
    }

    // ── Validate invoice amount ───────────────────────────────────────────
    const decoded = await lnbitsService.decodeInvoice(bolt11);
    if (decoded !== null && decoded.amountSats !== refundSats) {
      res.status(400).json({
        error: `Invoice amount (${decoded.amountSats} sats) does not match refund owed (${refundSats} sats)`,
        refundAmountSats: refundSats,
      });
      return;
    }

    // ── Send refund ───────────────────────────────────────────────────────
    const paymentHash = await lnbitsService.payInvoice(bolt11);

    await db
      .update(jobs)
      .set({ refundState: "paid", refundPaymentHash: paymentHash, updatedAt: new Date() })
      .where(and(eq(jobs.id, id), eq(jobs.refundState, "pending")));

    res.json({
      ok: true,
      refundAmountSats: refundSats,
      paymentHash,
      message: `Refund of ${refundSats} sats sent successfully`,
    });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Failed to process refund";
    res.status(500).json({ error: message });
  }
});

export default router;
