// clauth — auth-vault Edge Function v2
// Added: IP whitelist, rate limiting, machine lockout (fail_count + locked)

import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

const SUPABASE_URL          = Deno.env.get("SUPABASE_URL")!;
const SERVICE_ROLE_KEY      = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const CLAUTH_HMAC_SALT      = Deno.env.get("CLAUTH_HMAC_SALT")!;
const ADMIN_BOOTSTRAP_TOKEN = Deno.env.get("CLAUTH_ADMIN_BOOTSTRAP_TOKEN")!;

const ALLOWED_IPS: string[] = (Deno.env.get("CLAUTH_ALLOWED_IPS") || "")
  .split(",").map(s => s.trim()).filter(Boolean);

const RATE_LIMIT_MAX    = 30;
const RATE_LIMIT_WINDOW = 60;
const REPLAY_WINDOW_MS  = 5 * 60 * 1000;
const MAX_FAIL_COUNT    = 5;
const DEFAULT_INSTALL_ID = "default";

async function hmacSha256(key: string, message: string): Promise<string> {
  const cryptoKey = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(key),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
  );
  const sig = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
  return Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, "0")).join("");
}

async function sha256Hex(value: string): Promise<string> {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
  return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, "0")).join("");
}

function randomToken(bytes = 24): string {
  const data = new Uint8Array(bytes);
  crypto.getRandomValues(data);
  return Array.from(data).map(b => b.toString(16).padStart(2, "0")).join("");
}

function normalizeInstallId(value: unknown): string {
  const text = String(value || "").trim().toLowerCase();
  const normalized = text.replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
  return normalized || DEFAULT_INSTALL_ID;
}

function getClientIP(req: Request): string {
  return req.headers.get("cf-connecting-ip") ||
    req.headers.get("x-real-ip") ||
    req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
}

function checkIP(ip: string): { allowed: boolean; reason?: string } {
  if (ALLOWED_IPS.length === 0) return { allowed: true };
  if (ALLOWED_IPS.includes(ip)) return { allowed: true };
  return { allowed: false, reason: `IP not whitelisted: ${ip}` };
}

async function checkRateLimit(sb: any, machine_hash: string): Promise<{ allowed: boolean; reason?: string }> {
  const windowStart = new Date(Date.now() - RATE_LIMIT_WINDOW * 1000).toISOString();
  const { count } = await sb.from("clauth_audit")
    .select("id", { count: "exact", head: true })
    .eq("machine_hash", machine_hash)
    .gte("created_at", windowStart);
  if ((count || 0) >= RATE_LIMIT_MAX) {
    return { allowed: false, reason: `Rate limit: ${count}/${RATE_LIMIT_MAX} per ${RATE_LIMIT_WINDOW}s` };
  }
  return { allowed: true };
}

async function validateHMAC(sb: any, body: any): Promise<{ valid: boolean; reason?: string }> {
  const now = Date.now();
  if (Math.abs(now - body.timestamp) > REPLAY_WINDOW_MS) return { valid: false, reason: "timestamp_expired" };

  const { data: machine, error } = await sb.from("clauth_machines")
    .select("hmac_seed_hash, enabled, fail_count, locked")
    .eq("machine_hash", body.machine_hash).single();

  if (error || !machine) return { valid: false, reason: "machine_not_found" };
  if (!machine.enabled)   return { valid: false, reason: "machine_disabled" };
  if (machine.locked)     return { valid: false, reason: "machine_locked" };

  const window   = Math.floor(body.timestamp / REPLAY_WINDOW_MS);
  const message  = `${body.machine_hash}:${window}`;
  const expected = await hmacSha256(body.password, message);
  const seedHash = await sha256Hex(`seed:${body.machine_hash}:${body.password}`);

  if (seedHash !== machine.hmac_seed_hash || expected !== body.token) {
    const newCount   = (machine.fail_count || 0) + 1;
    const shouldLock = newCount >= MAX_FAIL_COUNT;
    await sb.from("clauth_machines")
      .update({ fail_count: newCount, locked: shouldLock })
      .eq("machine_hash", body.machine_hash);
    return { valid: false, reason: shouldLock ? `machine_locked after ${newCount} failures` : `invalid_token (${newCount}/${MAX_FAIL_COUNT})` };
  }

  await sb.from("clauth_machines")
    .update({ last_seen: new Date().toISOString(), fail_count: 0 })
    .eq("machine_hash", body.machine_hash);
  return { valid: true };
}

async function auditLog(sb: any, machine_hash: string, service_name: string, action: string, result: string, detail?: string) {
  await sb.from("clauth_audit").insert({ machine_hash, service_name, action, result, detail });
}

async function handleRetrieve(sb: any, body: any, mh: string) {
  const { service } = body;
  if (!service) return { error: "service required" };
  const { data: svc } = await sb.from("clauth_services").select("*").eq("name", service).single();
  if (!svc)           { await auditLog(sb, mh, service, "retrieve", "fail", "service_not_found"); return { error: "service_not_found" }; }
  if (!svc.enabled)   { await auditLog(sb, mh, service, "retrieve", "denied", "service_disabled"); return { error: "service_disabled" }; }
  if (!svc.vault_key) { await auditLog(sb, mh, service, "retrieve", "fail", "no_key_stored"); return { error: "no_key_stored" }; }
  const { data: secret } = await sb.rpc("vault_decrypt_secret", { secret_name: svc.vault_key });
  await sb.from("clauth_services").update({ last_retrieved: new Date().toISOString() }).eq("name", service);
  await auditLog(sb, mh, service, "retrieve", "success");
  return { service, key_type: svc.key_type, value: secret };
}

async function handleWrite(sb: any, body: any, mh: string) {
  const { service, value } = body;
  if (!service || !value) return { error: "service and value required" };
  const vaultKey = `clauth.${service}`;
  const { error } = await sb.rpc("vault_upsert_secret", { secret_name: vaultKey, secret_value: typeof value === "string" ? value : JSON.stringify(value) });
  if (error) { await auditLog(sb, mh, service, "write", "fail", error.message); return { error: error.message }; }
  await sb.from("clauth_services").update({ vault_key: vaultKey, last_rotated: new Date().toISOString() }).eq("name", service);
  await auditLog(sb, mh, service, "write", "success");
  return { success: true, service, vault_key: vaultKey };
}

async function handleEnable(sb: any, body: any, mh: string) {
  const { service, enabled } = body;
  let q = sb.from("clauth_services").update({ enabled });
  q = service !== "all" ? q.eq("name", service) : q.not("vault_key", "is", null);
  const { error } = await q;
  if (error) return { error: error.message };
  await auditLog(sb, mh, service, enabled ? "enable" : "disable", "success");
  return { success: true, service, enabled };
}

async function handleAdd(sb: any, body: any, mh: string) {
  const { name, label, key_type, description, project } = body;
  if (!name || !label || !key_type) return { error: "name, label, key_type required" };
  const row: any = { name, label, key_type, description: description || null };
  if (project) row.project = project;
  const { error } = await sb.from("clauth_services").insert(row);
  if (error) return { error: error.message };
  await auditLog(sb, mh, name, "add", "success");
  return { success: true, name, label, key_type, project: project || null };
}

async function handleUpdate(sb: any, body: any, mh: string) {
  const { service, project, label, description } = body;
  if (!service) return { error: "service required" };
  const updates: any = { updated_at: new Date().toISOString() };
  if (project !== undefined) updates.project = project || null; // empty string clears project
  if (label !== undefined) updates.label = label;
  if (description !== undefined) updates.description = description || null;
  const { error } = await sb.from("clauth_services").update(updates).eq("name", service);
  if (error) return { error: error.message };
  await auditLog(sb, mh, service, "update", "success", `fields: ${Object.keys(updates).join(", ")}`);
  return { success: true, service, ...updates };
}

async function handleRemove(sb: any, body: any, mh: string) {
  const { service, confirm } = body;
  if (confirm !== `CONFIRM REMOVE ${service.toUpperCase()}`) return { error: "confirm phrase mismatch" };
  await sb.rpc("vault_delete_secret", { secret_name: `clauth.${service}` });
  await sb.from("clauth_services").delete().eq("name", service);
  await auditLog(sb, mh, service, "remove", "success");
  return { success: true, service };
}

async function handleRevoke(sb: any, body: any, mh: string) {
  const { service, confirm } = body;
  const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
  if (confirm !== phrase) return { error: "confirm phrase mismatch", required: phrase };
  if (service === "all") {
    const { data: svcs } = await sb.from("clauth_services").select("name").not("vault_key", "is", null);
    for (const s of svcs || []) await sb.rpc("vault_delete_secret", { secret_name: `clauth.${s.name}` });
    await sb.from("clauth_services").update({ vault_key: null, enabled: false }).neq("id", "00000000-0000-0000-0000-000000000000");
  } else {
    await sb.rpc("vault_delete_secret", { secret_name: `clauth.${service}` });
    await sb.from("clauth_services").update({ vault_key: null, enabled: false }).eq("name", service);
  }
  await auditLog(sb, mh, service, "revoke", "success");
  return { success: true, service };
}

async function handleStatus(sb: any, body: any, mh: string) {
  let q = sb.from("clauth_services")
    .select("name, label, key_type, enabled, vault_key, last_retrieved, last_rotated, created_at, project, description")
    .order("project", { ascending: true, nullsFirst: true })
    .order("name");
  if (body.project) q = q.eq("project", body.project);
  const { data: services } = await q;
  await auditLog(sb, mh, "all", "status", "success");
  return { services: services || [] };
}

async function handleChangePassword(sb: any, body: any, mh: string) {
  const { new_hmac_seed_hash } = body;
  if (!new_hmac_seed_hash) return { error: "new_hmac_seed_hash required" };
  const { error } = await sb.from("clauth_machines")
    .update({ hmac_seed_hash: new_hmac_seed_hash, fail_count: 0, locked: false })
    .eq("machine_hash", mh);
  if (error) return { error: error.message };
  await auditLog(sb, mh, "system", "change-password", "success");
  return { success: true };
}

async function getMachineInstallId(sb: any, machine_hash: string): Promise<string> {
  const { data: machine } = await sb.from("clauth_machines")
    .select("install_id")
    .eq("machine_hash", machine_hash)
    .single();
  return normalizeInstallId(machine?.install_id);
}

async function handleCreateEnrollment(sb: any, body: any, mh: string) {
  const install_id = normalizeInstallId(body.install_id || await getMachineInstallId(sb, mh));
  const ttl_minutes = Math.max(5, Math.min(Number(body.ttl_minutes || 60), 24 * 60));
  const code = `ce_${randomToken(24)}`;
  const token_hash = await sha256Hex(code);
  const expires_at = new Date(Date.now() + ttl_minutes * 60 * 1000).toISOString();
  const label = body.label ? String(body.label).slice(0, 120) : null;

  const { error } = await sb.from("clauth_machine_enrollments").insert({
    install_id,
    token_hash,
    label,
    created_by_machine_hash: mh,
    expires_at,
  });
  if (error) {
    await auditLog(sb, mh, "system", "create-enrollment", "fail", error.message);
    return { error: error.message };
  }

  await auditLog(sb, mh, "system", "create-enrollment", "success", `install_id=${install_id}`);
  return { success: true, enrollment_code: code, install_id, expires_at, label };
}

async function handleRedeemEnrollment(sb: any, body: any) {
  const { machine_hash, hmac_seed_hash, enrollment_code } = body;
  if (!machine_hash || !hmac_seed_hash || !enrollment_code) {
    return { error: "machine_hash, hmac_seed_hash, enrollment_code required" };
  }

  const token_hash = await sha256Hex(String(enrollment_code).trim());
  const nowIso = new Date().toISOString();
  const { data: enrollment, error: lookupError } = await sb.from("clauth_machine_enrollments")
    .select("id, install_id, label, expires_at, consumed_at")
    .eq("token_hash", token_hash)
    .single();

  if (lookupError || !enrollment) return { error: "enrollment_not_found" };
  if (enrollment.consumed_at) return { error: "enrollment_already_used" };
  if (new Date(enrollment.expires_at).getTime() < Date.now()) return { error: "enrollment_expired" };

  const label = body.label || enrollment.label || null;
  const install_id = normalizeInstallId(enrollment.install_id);

  const { error: machineError } = await sb.from("clauth_machines").upsert(
    { machine_hash, hmac_seed_hash, label, install_id, enabled: true, fail_count: 0, locked: false },
    { onConflict: "machine_hash" }
  );
  if (machineError) return { error: machineError.message };

  const { data: consumedRows, error: consumeError } = await sb.from("clauth_machine_enrollments")
    .update({ consumed_at: nowIso, redeemed_by_machine_hash: machine_hash })
    .eq("id", enrollment.id)
    .is("consumed_at", null)
    .select("id");
  if (consumeError) return { error: consumeError.message };
  if (!consumedRows || consumedRows.length !== 1) return { error: "enrollment_already_used" };

  await auditLog(sb, machine_hash, "system", "redeem-enrollment", "success", `install_id=${install_id}`);
  return { success: true, machine_hash, install_id };
}

async function handleRegisterMachine(sb: any, body: any) {
  const { machine_hash, hmac_seed_hash, label, admin_token } = body;
  if (body.enrollment_code || body.invite_code) {
    return handleRedeemEnrollment(sb, { ...body, enrollment_code: body.enrollment_code || body.invite_code });
  }
  if (admin_token !== ADMIN_BOOTSTRAP_TOKEN) return { error: "invalid_admin_token" };
  const install_id = normalizeInstallId(body.install_id);
  const { error } = await sb.from("clauth_machines").upsert(
    { machine_hash, hmac_seed_hash, label, install_id, enabled: true, fail_count: 0, locked: false },
    { onConflict: "machine_hash" }
  );
  if (error) return { error: error.message };
  return { success: true, machine_hash, install_id };
}

Deno.serve(async (req: Request) => {
  if (req.method === "OPTIONS") {
    return new Response(null, { headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "POST, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, Authorization"
    }});
  }
  if (req.method !== "POST") return Response.json({ error: "POST only" }, { status: 405 });

  const url   = new URL(req.url);
  const route = url.pathname.replace(/^\/auth-vault\/?/, "").replace(/^\//, "");
  const body  = await req.json().catch(() => ({}));
  const sb    = createClient(SUPABASE_URL, SERVICE_ROLE_KEY);
  const ip    = getClientIP(req);

  if (route === "register-machine") return Response.json(await handleRegisterMachine(sb, body));
  if (route === "redeem-enrollment") return Response.json(await handleRedeemEnrollment(sb, body));

  const ipCheck = checkIP(ip);
  if (!ipCheck.allowed) {
    await auditLog(sb, body.machine_hash || "unknown", "system", route, "blocked", ipCheck.reason);
    return Response.json({ error: "ip_blocked", reason: ipCheck.reason }, { status: 403 });
  }

  if (body.machine_hash) {
    const rateCheck = await checkRateLimit(sb, body.machine_hash);
    if (!rateCheck.allowed) {
      await auditLog(sb, body.machine_hash, "system", route, "rate_limited", rateCheck.reason);
      return Response.json({ error: "rate_limited", reason: rateCheck.reason }, { status: 429 });
    }
  }

  const authResult = await validateHMAC(sb, { machine_hash: body.machine_hash, token: body.token, timestamp: body.timestamp, password: body.password });
  if (!authResult.valid) {
    await auditLog(sb, body.machine_hash || "unknown", body.service || "unknown", route, "denied", authResult.reason);
    return Response.json({ error: "auth_failed", reason: authResult.reason }, { status: 401 });
  }

  const mh = body.machine_hash;
  switch (route) {
    case "retrieve": return Response.json(await handleRetrieve(sb, body, mh));
    case "write":    return Response.json(await handleWrite(sb, body, mh));
    case "enable":   return Response.json(await handleEnable(sb, body, mh));
    case "add":      return Response.json(await handleAdd(sb, body, mh));
    case "update":   return Response.json(await handleUpdate(sb, body, mh));
    case "remove":   return Response.json(await handleRemove(sb, body, mh));
    case "revoke":   return Response.json(await handleRevoke(sb, body, mh));
    case "status":          return Response.json(await handleStatus(sb, body, mh));
    case "change-password": return Response.json(await handleChangePassword(sb, body, mh));
    case "create-enrollment": return Response.json(await handleCreateEnrollment(sb, body, mh));
    case "test":            return Response.json({ valid: true, machine_hash: mh, timestamp: body.timestamp, ip });
    default:         return Response.json({ error: "unknown_route", route }, { status: 404 });
  }
});
