/**
 * EpfoService — EPFO / UAN assisted-lookup proxy (mirrors GstService).
 *
 * The EPFO member portal does not offer an anonymous lookup like the GST portal:
 * the "Know your UAN" / member flow is gated by an **OTP to the member's
 * registered mobile**. So the handshake is two steps:
 *   POST /otp     { uan }                 → opens a session, requests the OTP
 *   POST /verify  { sessionId, uan, otp } → submits the OTP, returns the member
 *                                            record (name, DOB, status, …)
 *
 * The real portal navigation is gated behind EPFO_LIVE=true. Until the live
 * selectors/OTP are validated against a real session, the service runs in STUB
 * mode (deterministic responses) so the app integration is exercisable in dev.
 *
 * Aadhaar verification is intentionally OUT OF SCOPE here — UIDAI restricts it to
 * licensed AUA/KUA via consented e-KYC; it cannot be portal-scraped. Aadhaar
 * stays assisted-manual in PPMS.
 */
import express from "express";
import type { Browser, BrowserContext, Page } from "playwright";
import { isUan, mobileHint, stubOtp, stubVerify } from "./stub";

const PORT = Number(process.env.PORT ?? 3004);
const SESSION_TTL_MS = Number(process.env.SESSION_TTL_MS ?? 5 * 60 * 1000); // 5 min
const LIVE = process.env.EPFO_LIVE === "true";
const PORTAL_URL = process.env.EPFO_PORTAL_URL ?? "https://unifiedportal-mem.epfindia.gov.in/memberinterface/";

function log(level: string, msg: string, ctx?: Record<string, unknown>) {
  const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...ctx });
  (level === "ERROR" || level === "WARN" ? process.stderr : process.stdout).write(line + "\n");
}

// ── Sessions ───────────────────────────────────────────────────────────────────

interface Session {
  uan: string;
  createdAt: number;
  context?: BrowserContext;
  page?: Page;
}
const sessions = new Map<string, Session>();
let seq = 0;
const newSessionId = () => `epfo_${Date.now().toString(36)}_${(seq++).toString(36)}`;

setInterval(() => {
  const now = Date.now();
  let pruned = 0;
  for (const [id, s] of sessions) {
    if (now - s.createdAt > SESSION_TTL_MS) {
      s.context?.close().catch(() => {});
      sessions.delete(id);
      pruned++;
    }
  }
  if (pruned) log("INFO", "Pruned expired sessions", { pruned, remaining: sessions.size });
}, 60_000).unref();

// ── Browser (only launched in LIVE mode) ───────────────────────────────────────

let _browser: Browser | null = null;
async function getBrowser(): Promise<Browser> {
  if (_browser?.isConnected()) return _browser;
  const { chromium } = await import("playwright");
  _browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"] });
  _browser.on("disconnected", () => { _browser = null; });
  return _browser;
}

// ── App ────────────────────────────────────────────────────────────────────────

const app = express();
app.use(express.json());

app.get("/health", (_req, res) => {
  res.json({ status: "ok", mode: LIVE ? "live" : "stub", sessionCount: sessions.size });
});

/** POST /otp { uan } → { sessionId, mobileHint } — request an OTP to the member's mobile. */
app.post("/otp", async (req, res) => {
  const { uan } = req.body ?? {};
  const sessionId = newSessionId();

  if (!LIVE) {
    const r = stubOtp(uan, sessionId);
    if (r.ok) {
      sessions.set(sessionId, { uan, createdAt: Date.now() });
      log("INFO", "OTP requested (stub)", { sessionId });
    }
    return res.status(r.status).json(r.body);
  }

  if (!isUan(uan)) return res.status(400).json({ error: "A 12-digit UAN is required" });

  try {
    const browser = await getBrowser();
    const context = await browser.newContext();
    const page = await context.newPage();
    await page.goto(PORTAL_URL, { waitUntil: "domcontentloaded", timeout: 30_000 });
    // TODO(live): drive the member portal's "Know your UAN" OTP request:
    //   fill UAN, solve the on-page captcha, click "Get OTP", read the masked mobile.
    // Selectors must be validated against a real session before enabling EPFO_LIVE.
    sessions.set(sessionId, { uan, createdAt: Date.now(), context, page });
    return res.json({ sessionId, mobileHint: mobileHint() });
  } catch (e) {
    log("ERROR", "POST /otp failed", { err: String(e) });
    return res.status(502).json({ error: `EPFO portal error: ${String(e)}` });
  }
});

/** POST /verify { sessionId, uan, otp } → { matched, name, status } — submit the OTP. */
app.post("/verify", async (req, res) => {
  const { sessionId, uan, otp } = req.body ?? {};
  const s = (sessionId && sessions.get(sessionId)) || undefined;

  if (!LIVE) {
    const r = stubVerify(s, uan, otp);
    // A valid handshake consumes the session (one OTP per request).
    if (r.ok && sessionId) sessions.delete(sessionId);
    log("INFO", "Verify (stub)", { sessionId, matched: r.body.matched });
    return res.status(r.status).json(r.body);
  }

  if (!s) return res.status(410).json({ error: "Session expired — request a new OTP" });
  if (!isUan(uan) || s.uan !== uan) return res.status(400).json({ error: "UAN mismatch" });
  if (typeof otp !== "string" || !/^\d{4,8}$/.test(otp)) return res.status(400).json({ error: "A valid OTP is required" });

  try {
    // TODO(live): submit the OTP and scrape the member record (name/DOB/status).
    const result = { matched: false, name: null as string | null, status: null as string | null };
    s.context?.close().catch(() => {});
    sessions.delete(sessionId);
    return res.json(result);
  } catch (e) {
    log("ERROR", "POST /verify failed", { err: String(e) });
    return res.status(502).json({ error: `EPFO portal error: ${String(e)}` });
  }
});

app.listen(PORT, () => log("INFO", `EpfoService listening`, { port: PORT, mode: LIVE ? "live" : "stub" }));
