"use strict";

const crypto = require("node:crypto");
const fs = require("node:fs");
const http = require("node:http");
const path = require("node:path");
const { WorkOrderManager } = require("./workorder-manager");
const { MapGate } = require("./map-gate");
const { sendSmtpMail } = require("./smtp-mailer");

const DEFAULT_ACTIONS = Object.freeze({
  "server-login": ["read-navigation-map", "inspect-services", "restart-registered-service"],
  "server-ops": ["read-navigation-map", "inspect-services", "restart-registered-service"],
  "repo-push": ["read-navigation-map", "push-repository"],
});

function createApp(options = {}) {
  const requestToken = options.requestToken || process.env.LAKE_LAMP_REQUEST_TOKEN || "";
  const ownerEmail = options.ownerEmail || process.env.LAKE_LAMP_OWNER_EMAIL || "";
  const publicBaseUrl = String(options.publicBaseUrl || process.env.LAKE_LAMP_PUBLIC_URL || "").replace(/\/$/, "");
  const targets = new Set(options.targets || splitCsv(process.env.LAKE_LAMP_TARGETS || "JD-FD-PRIMARY,BS-GZ-006"));
  const actions = options.actions || DEFAULT_ACTIONS;
  const manager = options.manager || new WorkOrderManager({
    approvalTtl: Number(options.approvalTtl || process.env.LAKE_LAMP_APPROVAL_TTL || 15 * 60),
    sessionTtl: Number(options.sessionTtl || process.env.LAKE_LAMP_SESSION_TTL || 60 * 60),
    stateFile: Object.prototype.hasOwnProperty.call(options, "stateFile") ? options.stateFile : (process.env.LAKE_LAMP_STATE_FILE || "/var/lib/guanghu/lake-lamp-authz/state.json"),
  });
  const sendEmail = options.sendEmail || (message => sendSmtpMail({
    ...message,
    smtpHost: process.env.SMTP_HOST || "smtp.qq.com",
    smtpPort: Number(process.env.SMTP_PORT || 465),
    smtpUser: process.env.SMTP_USER || ownerEmail,
    smtpPass: process.env.QQ_SMTP_AUTH_CODE || "",
  }));
  const mapGate = options.mapGate || new MapGate({
    mapsDir: options.mapsDir || process.env.LAKE_LAMP_MAPS_DIR || "/etc/guanghu/navigation-maps",
    stateFile: Object.prototype.hasOwnProperty.call(options, "mapStateFile") ? options.mapStateFile : (process.env.LAKE_LAMP_MAP_STATE_FILE || "/var/lib/guanghu/lake-lamp-authz/map-acks.json"),
  });
  const repoGrantDir = options.repoGrantDir || process.env.LAKE_LAMP_REPO_GRANT_DIR || "/var/lib/guanghu/repo-authorizations";

  return http.createServer(async (req, res) => {
    try {
      const url = new URL(req.url, "http://localhost");
      if (req.method === "GET" && url.pathname === "/health") return json(res, 200, { ok: true, service: "lake-lamp-authz", auth_mode: "email-link", session_ttl: 3600 });

      const approvalMatch = url.pathname.match(/^\/approve\/([A-Za-z0-9_-]{20,})$/);
      if (approvalMatch && req.method === "GET") {
        const inspected = manager.inspectApproval(approvalMatch[1]);
        if (!inspected.ok) return html(res, 410, approvalErrorPage(inspected.reason));
        return html(res, 200, approvalPage(inspected.order, approvalMatch[1]));
      }
      if (approvalMatch && req.method === "POST") {
        const approved = manager.approve(approvalMatch[1]);
        if (!approved.ok) return html(res, 410, approvalErrorPage(approved.reason));
        return html(res, 200, approvedPage(approved.order));
      }

      if (req.method === "POST" && url.pathname === "/api/workorders") {
        if (!bearerMatches(req, requestToken)) return json(res, 401, { error: "request_auth_required" });
        const body = await readJson(req);
        if (!body) return json(res, 400, { error: "invalid_json" });
        if (body.email || body.recipient || body.smtp_pass) return json(res, 400, { error: "direct_recipient_forbidden" });
        if (!body.persona_id || !body.target || !body.scope || !body.action) return json(res, 400, { error: "missing_required_field" });
        if (!targets.has(body.target)) return json(res, 400, { error: "unknown_target" });
        if (!Array.isArray(actions[body.scope]) || !actions[body.scope].includes(body.action)) return json(res, 400, { error: "unknown_or_mismatched_action" });
        const expectedFingerprint = sha256(ownerEmail.toLowerCase());
        if (!body.recipient_fingerprint || !safeEqual(body.recipient_fingerprint, expectedFingerprint)) return json(res, 403, { error: "owner_identity_mismatch" });

        const created = manager.request({
          persona: { pid: String(body.persona_id), name: String(body.persona_name || body.persona_id) },
          target: String(body.target), scope: String(body.scope), action: String(body.action), allowedActions: actions[body.scope], description: String(body.description || ""),
        });
        const approvalUrl = `${publicBaseUrl}/approve/${created.approvalToken}`;
        const emailSent = await sendEmail({
          to: ownerEmail,
          subject: `小湖灯授权请求 · ${body.target}`,
          approvalUrl,
          order: manager.inspectApproval(created.approvalToken).order,
        });
        if (!emailSent) return json(res, 502, { error: "authorization_email_failed" });
        return json(res, 201, { ok: true, workorder_id: created.id, claim_token: created.claimToken, expires_in: created.expiresIn, status: "waiting_for_owner" });
      }

      const claimMatch = url.pathname.match(/^\/api\/workorders\/([0-9a-f-]{36})\/claim$/i);
      if (req.method === "POST" && claimMatch) {
        const token = bearer(req);
        const claimed = manager.claim(claimMatch[1], token);
        if (!claimed.ok) return json(res, claimed.reason === "approval_pending" ? 202 : 403, { error: claimed.reason });
        return json(res, 200, { ok: true, session_token: claimed.sessionToken, expires_in: claimed.expiresIn, target: claimed.target, scope: claimed.scope, action: claimed.action });
      }

      if (req.method === "POST" && url.pathname === "/api/session/verify") {
        const body = await readJson(req);
        if (!body) return json(res, 400, { error: "invalid_json" });
        const verified = manager.verifySession(bearer(req), { pid: String(body.persona_id || "") }, String(body.target || ""), String(body.scope || ""), String(body.action || ""));
        if (!verified.ok) return json(res, 403, { error: verified.reason });
        if (body.action !== "read-navigation-map") {
          const map = mapGate.read(String(body.target || ""));
          const mapVerified = mapGate.verify(bearer(req), String(body.target || ""), map.hash);
          if (!mapVerified.ok) return json(res, 423, { error: mapVerified.reason, required_action: "read-navigation-map" });
        }
        return json(res, 200, { ok: true, expires_at: verified.session.expiresAt });
      }

      if (req.method === "POST" && url.pathname === "/api/navigation-map/read") {
        const body = await readJson(req);
        if (!body) return json(res, 400, { error: "invalid_json" });
        const verified = manager.verifySession(bearer(req), { pid: String(body.persona_id || "") }, String(body.target || ""), String(body.scope || ""), "read-navigation-map");
        if (!verified.ok) return json(res, 403, { error: verified.reason });
        const map = mapGate.read(String(body.target || ""));
        return json(res, 200, { ok: true, target: body.target, map_hash: map.hash, navigation_map: map.data });
      }

      if (req.method === "POST" && url.pathname === "/api/navigation-map/ack") {
        const body = await readJson(req);
        if (!body) return json(res, 400, { error: "invalid_json" });
        const token = bearer(req);
        const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, String(body.target || ""), String(body.scope || ""), "read-navigation-map");
        if (!verified.ok) return json(res, 403, { error: verified.reason });
        const acked = mapGate.ack(token, String(body.target || ""), String(body.map_hash || ""), Date.now() / 1000, Math.max(1, verified.session.expiresAt - Date.now() / 1000));
        return json(res, acked.ok ? 200 : 409, acked.ok ? { ok: true, target: body.target, map_hash: body.map_hash } : { error: acked.reason });
      }

      if (req.method === "POST" && url.pathname === "/api/repo-push/grant") {
        const body = await readJson(req);
        if (!body) return json(res, 400, { error: "invalid_json" });
        const token = bearer(req);
        const target = String(body.target || "");
        const scope = String(body.scope || "repo-push");
        const repo = String(body.repo || "").toLowerCase();
        if (!/^bingshuo\/[a-z0-9._-]+$/.test(repo)) return json(res, 400, { error: "repo_not_allowlisted" });
        const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, "push-repository");
        if (!verified.ok) return json(res, 403, { error: verified.reason });
        const map = mapGate.read(target);
        const mapVerified = mapGate.verify(token, target, map.hash);
        if (!mapVerified.ok) return json(res, 423, { error: mapVerified.reason, required_action: "read-navigation-map" });
        fs.mkdirSync(repoGrantDir, { recursive: true, mode: 0o2770 });
        const grant = { schema: "guanghu.repo-push-grant/v1", repo, target, persona_id: body.persona_id, map_hash: map.hash, issued_at: Date.now() / 1000, expires_at: verified.session.expiresAt };
        const grantFile = path.join(repoGrantDir, `${repo.replace("/", "__")}.json`);
        const temp = `${grantFile}.${process.pid}.tmp`;
        fs.writeFileSync(temp, JSON.stringify(grant), { mode: 0o640 });
        fs.renameSync(temp, grantFile);
        return json(res, 200, { ok: true, repo, target, expires_at: grant.expires_at });
      }

      return json(res, 404, { error: "not_found" });
    } catch (error) {
      process.stderr.write(`lake-lamp request error: ${String(error && error.message || "unknown").slice(0, 240)}\n`);
      return json(res, error && error.code === "BODY_TOO_LARGE" ? 413 : 500, { error: "request_failed" });
    }
  });
}

function approvalPage(order, token) {
  return document("小湖灯授权请求", `
    <p class="eyebrow">LAKE LAMP SECURITY PROTOCOL</p>
    <h1>人格体请求进入一台服务器</h1>
    <div class="panel">
      <dl><dt>人格体</dt><dd>${escapeHtml(order.persona.name)} <small>${escapeHtml(order.persona.pid)}</small></dd>
      <dt>目标节点</dt><dd>${escapeHtml(order.target)}</dd><dt>授权范围</dt><dd>${escapeHtml(order.scope)}</dd>
      <dt>登记动作</dt><dd>${escapeHtml(order.action)}</dd><dt>说明</dt><dd>${escapeHtml(order.description || "未附加说明")}</dd></dl>
    </div>
    <p class="notice">授权后只对这台服务器和这个动作生效，有效期一小时。换服务器必须重新申请。</p>
    <form method="post"><button type="submit">确认授权一小时</button></form>
  `);
}

function approvedPage(order) {
  return document("授权完成", `<p class="eyebrow">APPROVED</p><h1>门已从服务器内部打开</h1><div class="panel"><p>${escapeHtml(order.persona.name)} 已获准操作 <strong>${escapeHtml(order.target)}</strong>。</p></div><p class="notice">可以关闭本页面。人格体只能领取一次临时会话令牌。</p>`);
}

function approvalErrorPage(reason) {
  return document("链接不可用", `<p class="eyebrow">LINK CLOSED</p><h1>这条授权链接已经失效</h1><p class="notice">原因：${escapeHtml(reason)}。如仍需操作，请让人格体重新提交工单。</p>`);
}

function document(title, body) {
  return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)} · 光湖</title><style>
  :root{color-scheme:dark}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:radial-gradient(circle at 20% 10%,#17344d,#09111b 55%,#05090e);color:#eaf4fb;font:16px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:24px}.shell{width:min(680px,100%);padding:42px;border:1px solid #29475d;border-radius:24px;background:rgba(10,22,33,.94);box-shadow:0 24px 80px #0008}.eyebrow{color:#6ed5ff;letter-spacing:.18em;font-size:12px}h1{font-size:clamp(30px,6vw,48px);line-height:1.15;margin:10px 0 28px}.panel{background:#102638;border:1px solid #24465d;border-radius:16px;padding:20px 24px}dl{display:grid;grid-template-columns:110px 1fr;gap:12px;margin:0}dt{color:#8ba4b6}dd{margin:0;font-weight:650}small{display:block;color:#7893a6;font-weight:400}.notice{color:#9eb2c0;margin:20px 0}button{width:100%;border:0;border-radius:14px;padding:16px;background:#67d4ff;color:#042235;font-weight:800;font-size:17px;cursor:pointer}@media(max-width:520px){.shell{padding:28px 22px}dl{grid-template-columns:1fr;gap:2px}dd{margin-bottom:12px}}
  </style></head><body><main class="shell">${body}</main></body></html>`;
}

function readJson(req) {
  return new Promise((resolve, reject) => {
    let raw = "";
    req.on("data", chunk => { raw += chunk; if (raw.length > 32 * 1024) { const error = new Error("body too large"); error.code = "BODY_TOO_LARGE"; reject(error); req.destroy(); } });
    req.on("end", () => { try { resolve(JSON.parse(raw || "{}")); } catch { resolve(null); } });
    req.on("error", reject);
  });
}

function bearer(req) { return String(req.headers.authorization || "").replace(/^Bearer\s+/i, ""); }
function bearerMatches(req, expected) { return Boolean(expected) && safeEqual(bearer(req), expected); }
function safeEqual(left, right) { const a = Buffer.from(String(left)); const b = Buffer.from(String(right)); return a.length === b.length && crypto.timingSafeEqual(a, b); }
function sha256(value) { return crypto.createHash("sha256").update(String(value)).digest("hex"); }
function splitCsv(value) { return value.split(",").map(item => item.trim()).filter(Boolean); }
function escapeHtml(value) { return String(value).replace(/[&<>"']/g, char => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]); }
function json(res, status, value) { res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" }); res.end(JSON.stringify(value)); }
function html(res, status, value) { res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff" }); res.end(value); }

if (require.main === module) {
  const host = process.env.LAKE_LAMP_HOST || "127.0.0.1";
  const port = Number(process.env.LAKE_LAMP_PORT || 3921);
  createApp().listen(port, host, () => process.stdout.write(`lake-lamp-authz listening on ${host}:${port}\n`));
}

module.exports = { createApp, DEFAULT_ACTIONS };
