import { Router, type Request, type Response } from "express";
import { randomBytes, randomUUID } from "crypto";
import { db, sessions, sessionRequests, type Session } from "@workspace/db";
import { eq, and } from "drizzle-orm";
import { lnbitsService } from "../lib/lnbits.js";
import { agentService } from "../lib/agent.js";
import { pricingService } from "../lib/pricing.js";
import { getBtcPriceUsd, usdToSats } from "../lib/btc-oracle.js";

const router = Router();

// ── Env-var config ─────────────────────────────────────────────────────────────

function envInt(name: string, fallback: number): number {
  const raw = parseInt(process.env[name] ?? "", 10);
  return Number.isFinite(raw) && raw > 0 ? raw : fallback;
}

const MIN_DEPOSIT_SATS = envInt("SESSION_MIN_DEPOSIT_SATS", 100);
const MAX_DEPOSIT_SATS = envInt("SESSION_MAX_DEPOSIT_SATS", 10_000);
const MIN_BALANCE_SATS = envInt("SESSION_MIN_BALANCE_SATS", 50);
const EXPIRY_HOURS = envInt("SESSION_EXPIRY_HOURS", 24);
const EXPIRY_MS = EXPIRY_HOURS * 60 * 60 * 1000;

// ── Helpers ────────────────────────────────────────────────────────────────────

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

function checkExpired(session: Session): boolean {
  return session.expiresAt !== null && new Date() > session.expiresAt;
}

function extractMacaroon(req: Request): string | null {
  const auth = req.headers.authorization ?? "";
  if (auth.startsWith("Bearer ")) return auth.slice(7).trim();
  return null;
}

function sessionView(session: Session, includeInvoice = false) {
  const base = {
    sessionId: session.id,
    state: session.state,
    balanceSats: session.balanceSats,
    expiresAt: session.expiresAt?.toISOString() ?? null,
    minimumBalanceSats: MIN_BALANCE_SATS,
    ...(session.macaroon && (session.state === "active" || session.state === "paused")
      ? { macaroon: session.macaroon }
      : {}),
  };

  if (includeInvoice && session.state === "awaiting_payment") {
    return {
      ...base,
      invoice: {
        paymentRequest: session.depositPaymentRequest,
        amountSats: session.depositAmountSats,
        ...(lnbitsService.stubMode ? { paymentHash: session.depositPaymentHash } : {}),
      },
    };
  }

  if (session.topupPaymentHash && !session.topupPaid) {
    return {
      ...base,
      pendingTopup: {
        paymentRequest: session.topupPaymentRequest,
        amountSats: session.topupAmountSats,
        ...(lnbitsService.stubMode ? { paymentHash: session.topupPaymentHash } : {}),
      },
    };
  }

  return base;
}

// ── Auto-advance: awaiting_payment → active ────────────────────────────────────

async function advanceSessionPayment(session: Session): Promise<Session> {
  if (session.state !== "awaiting_payment" || session.depositPaid) return session;

  const paid = await lnbitsService.checkInvoicePaid(session.depositPaymentHash);
  if (!paid) return session;

  const macaroon = randomBytes(32).toString("hex");
  const expiresAt = new Date(Date.now() + EXPIRY_MS);

  const updated = await db
    .update(sessions)
    .set({
      state: "active",
      depositPaid: true,
      balanceSats: session.depositAmountSats,
      macaroon,
      expiresAt,
      updatedAt: new Date(),
    })
    .where(and(eq(sessions.id, session.id), eq(sessions.state, "awaiting_payment")))
    .returning();

  return updated[0] ?? session;
}

// ── Auto-advance: pending topup paid → credit balance ─────────────────────────

async function advanceTopup(session: Session): Promise<Session> {
  if (!session.topupPaymentHash || session.topupPaid) return session;

  const paid = await lnbitsService.checkInvoicePaid(session.topupPaymentHash);
  if (!paid) return session;

  const newBalance = session.balanceSats + (session.topupAmountSats ?? 0);
  const newState =
    session.state === "paused" && newBalance >= MIN_BALANCE_SATS ? "active" : session.state;
  const expiresAt = new Date(Date.now() + EXPIRY_MS);

  const updated = await db
    .update(sessions)
    .set({
      state: newState,
      topupPaid: true,
      balanceSats: newBalance,
      expiresAt,
      updatedAt: new Date(),
    })
    .where(eq(sessions.id, session.id))
    .returning();

  return updated[0] ?? session;
}

// ── POST /sessions ─────────────────────────────────────────────────────────────

router.post("/sessions", async (req: Request, res: Response) => {
  const rawAmount = req.body?.amount_sats;
  const amountSats = parseInt(String(rawAmount ?? ""), 10);

  if (!Number.isFinite(amountSats) || amountSats < MIN_DEPOSIT_SATS || amountSats > MAX_DEPOSIT_SATS) {
    res.status(400).json({
      error: `amount_sats must be an integer between ${MIN_DEPOSIT_SATS} and ${MAX_DEPOSIT_SATS}`,
    });
    return;
  }

  try {
    const sessionId = randomUUID();
    const invoice = await lnbitsService.createInvoice(amountSats, `Session deposit ${sessionId}`);

    await db.insert(sessions).values({
      id: sessionId,
      state: "awaiting_payment",
      balanceSats: 0,
      depositAmountSats: amountSats,
      depositPaymentHash: invoice.paymentHash,
      depositPaymentRequest: invoice.paymentRequest,
      depositPaid: false,
      expiresAt: new Date(Date.now() + EXPIRY_MS),
    });

    res.status(201).json({
      sessionId,
      state: "awaiting_payment",
      invoice: {
        paymentRequest: invoice.paymentRequest,
        amountSats,
        ...(lnbitsService.stubMode ? { paymentHash: invoice.paymentHash } : {}),
      },
    });
  } catch (err) {
    res.status(500).json({ error: err instanceof Error ? err.message : "Failed to create session" });
  }
});

// ── GET /sessions/:id ─────────────────────────────────────────────────────────

router.get("/sessions/:id", async (req: Request, res: Response) => {
  const id = req.params.id as string;

  try {
    let session = await getSessionById(id);
    if (!session) { res.status(404).json({ error: "Session not found" }); return; }

    // Mark expired sessions
    if (checkExpired(session) && session.state !== "expired") {
      await db
        .update(sessions)
        .set({ state: "expired", updatedAt: new Date() })
        .where(eq(sessions.id, id));
      session = (await getSessionById(id))!;
    }

    // Auto-advance deposit payment
    if (session.state === "awaiting_payment") {
      session = await advanceSessionPayment(session);
    }

    // Auto-advance topup payment
    if (session.topupPaymentHash && !session.topupPaid) {
      session = await advanceTopup(session);
    }

    res.json(sessionView(session, true));
  } catch (err) {
    res.status(500).json({ error: err instanceof Error ? err.message : "Failed to fetch session" });
  }
});

// ── POST /sessions/:id/request ────────────────────────────────────────────────

router.post("/sessions/:id/request", async (req: Request, res: Response) => {
  const id = req.params.id as string;
  const macaroon = extractMacaroon(req);
  const requestText = typeof req.body?.request === "string" ? req.body.request.trim() : "";

  if (!requestText) {
    res.status(400).json({ error: "Body must include 'request' string" });
    return;
  }

  try {
    let session = await getSessionById(id);
    if (!session) { res.status(404).json({ error: "Session not found" }); return; }

    // Auth
    if (!macaroon || macaroon !== session.macaroon) {
      res.status(401).json({ error: "Invalid or missing macaroon. Include 'Authorization: Bearer <macaroon>' header." });
      return;
    }

    // State checks
    if (checkExpired(session) || session.state === "expired") {
      res.status(410).json({ error: "Session has expired" });
      return;
    }

    if (session.state === "paused") {
      res.status(402).json({
        error: "Insufficient balance",
        balance: session.balanceSats,
        minimumRequired: MIN_BALANCE_SATS,
      });
      return;
    }

    if (session.state !== "active") {
      res.status(409).json({ error: `Session is in state '${session.state}'` });
      return;
    }

    if (session.balanceSats < MIN_BALANCE_SATS) {
      // Mark as paused before returning
      await db
        .update(sessions)
        .set({ state: "paused", updatedAt: new Date() })
        .where(eq(sessions.id, id));

      res.status(402).json({
        error: "Insufficient balance",
        balance: session.balanceSats,
        minimumRequired: MIN_BALANCE_SATS,
      });
      return;
    }

    // ── Run the request ───────────────────────────────────────────────────────
    const requestId = randomUUID();
    const btcPriceUsd = await getBtcPriceUsd();

    // Eval phase
    const evalResult = await agentService.evaluateRequest(requestText);
    const evalCostUsd = pricingService.calculateActualCostUsd(
      evalResult.inputTokens,
      evalResult.outputTokens,
      agentService.evalModel,
    );

    let workInputTokens = 0;
    let workOutputTokens = 0;
    let workCostUsd = 0;
    let result: string | null = null;
    let finalState: "complete" | "rejected" | "failed" = "rejected";
    let reason: string | null = null;
    let errorMessage: string | null = null;

    if (evalResult.accepted) {
      try {
        const workResult = await agentService.executeWork(requestText);
        workInputTokens = workResult.inputTokens;
        workOutputTokens = workResult.outputTokens;
        workCostUsd = pricingService.calculateActualCostUsd(
          workResult.inputTokens,
          workResult.outputTokens,
          agentService.workModel,
        );
        result = workResult.result;
        finalState = "complete";
      } catch (err) {
        errorMessage = err instanceof Error ? err.message : "Execution error";
        finalState = "failed";
      }
    } else {
      reason = evalResult.reason;
    }

    // ── Honest accounting ────────────────────────────────────────────────────
    const totalTokenCostUsd = evalCostUsd + workCostUsd;
    const chargeUsd = pricingService.calculateActualChargeUsd(totalTokenCostUsd);
    const debitedSats = usdToSats(chargeUsd, btcPriceUsd);

    const newBalance = session.balanceSats - debitedSats;
    const newSessionState = newBalance < MIN_BALANCE_SATS ? "paused" : "active";
    const expiresAt = new Date(Date.now() + EXPIRY_MS);

    // Persist session request + update session balance atomically
    await db.transaction(async (tx) => {
      await tx.insert(sessionRequests).values({
        id: requestId,
        sessionId: id,
        request: requestText,
        state: finalState,
        result,
        reason,
        errorMessage,
        evalInputTokens: evalResult.inputTokens,
        evalOutputTokens: evalResult.outputTokens,
        workInputTokens: workInputTokens || null,
        workOutputTokens: workOutputTokens || null,
        debitedSats,
        balanceAfterSats: newBalance,
        btcPriceUsd,
      });

      await tx
        .update(sessions)
        .set({
          balanceSats: newBalance,
          state: newSessionState,
          expiresAt,
          updatedAt: new Date(),
        })
        .where(eq(sessions.id, id));
    });

    res.json({
      requestId,
      state: finalState,
      ...(result ? { result } : {}),
      ...(reason ? { reason } : {}),
      ...(errorMessage ? { errorMessage } : {}),
      debitedSats,
      balanceRemaining: newBalance,
      cost: {
        evalSats: usdToSats(
          pricingService.calculateActualChargeUsd(evalCostUsd),
          btcPriceUsd,
        ),
        workSats: workCostUsd > 0
          ? usdToSats(pricingService.calculateActualChargeUsd(workCostUsd), btcPriceUsd)
          : 0,
        totalSats: debitedSats,
        btcPriceUsd,
      },
    });
  } catch (err) {
    res.status(500).json({ error: err instanceof Error ? err.message : "Request failed" });
  }
});

// ── POST /sessions/:id/topup ──────────────────────────────────────────────────

router.post("/sessions/:id/topup", async (req: Request, res: Response) => {
  const id = req.params.id as string;
  const macaroon = extractMacaroon(req);
  const rawAmount = req.body?.amount_sats;
  const amountSats = parseInt(String(rawAmount ?? ""), 10);

  if (!Number.isFinite(amountSats) || amountSats < MIN_DEPOSIT_SATS || amountSats > MAX_DEPOSIT_SATS) {
    res.status(400).json({
      error: `amount_sats must be an integer between ${MIN_DEPOSIT_SATS} and ${MAX_DEPOSIT_SATS}`,
    });
    return;
  }

  try {
    const session = await getSessionById(id);
    if (!session) { res.status(404).json({ error: "Session not found" }); return; }

    if (!macaroon || macaroon !== session.macaroon) {
      res.status(401).json({ error: "Invalid or missing macaroon" });
      return;
    }

    if (session.state !== "active" && session.state !== "paused") {
      res.status(409).json({ error: `Cannot top up a session in state '${session.state}'` });
      return;
    }

    if (session.topupPaymentHash && !session.topupPaid) {
      res.status(409).json({
        error: "A topup invoice is already pending. Pay it first or poll GET /sessions/:id.",
        pendingTopup: {
          paymentRequest: session.topupPaymentRequest,
          amountSats: session.topupAmountSats,
          ...(lnbitsService.stubMode ? { paymentHash: session.topupPaymentHash } : {}),
        },
      });
      return;
    }

    const invoice = await lnbitsService.createInvoice(amountSats, `Session topup ${id}`);

    await db
      .update(sessions)
      .set({
        topupAmountSats: amountSats,
        topupPaymentHash: invoice.paymentHash,
        topupPaymentRequest: invoice.paymentRequest,
        topupPaid: false,
        updatedAt: new Date(),
      })
      .where(eq(sessions.id, id));

    res.json({
      sessionId: id,
      topup: {
        paymentRequest: invoice.paymentRequest,
        amountSats,
        ...(lnbitsService.stubMode ? { paymentHash: invoice.paymentHash } : {}),
      },
    });
  } catch (err) {
    res.status(500).json({ error: err instanceof Error ? err.message : "Topup failed" });
  }
});

export default router;
