// cli/api.js
// Thin client that calls the auth-vault Edge Function

import { createRequire } from "module";
const require = createRequire(import.meta.url);

import Conf from "conf";
import { getConfOptions } from "./conf-path.js";

const config = new Conf(getConfOptions());

// ============================================================
// Get Edge Function base URL from local config
// ============================================================
export function getBaseUrl() {
  const url = config.get("supabase_url") || process.env.CLAUTH_SUPABASE_URL;
  if (!url) throw new Error("Supabase URL not configured. Run: clauth setup");
  return `${url}/functions/v1/auth-vault`;
}

export function getAnonKey() {
  const key = config.get("supabase_anon_key") || process.env.CLAUTH_SUPABASE_ANON_KEY;
  if (!key) throw new Error("Supabase anon key not configured. Run: clauth setup");
  return key;
}

// ============================================================
// Core POST helper
// ============================================================
// Bound every vault round-trip with a wall-clock timeout. Without this, a TCP
// stall (DNS black-hole, half-open socket — NOT a clean 4xx) hangs the caller
// forever. Callers that treat persistence as best-effort (e.g. the call_agent
// scratchpad mirror) rely on this resolving to an error rather than blocking.
// Override via CLAUTH_VAULT_TIMEOUT_MS; default 8s.
const VAULT_FETCH_TIMEOUT_MS = (() => {
  const n = Number(process.env.CLAUTH_VAULT_TIMEOUT_MS);
  return Number.isFinite(n) && n > 0 ? n : 8000;
})();

// ============================================================
// Backend / external-resource error model
// ============================================================
// A VaultBackendError means the vault backend (the auth-vault Edge Function or
// the Postgres database behind it) never rendered an auth verdict — the request
// timed out, the network failed, the function 5xx'd, the DB was unreachable, or
// we were rate-limited. These are TRANSIENT and must NEVER be counted as an
// authentication failure (a wrong password). Treating a database timeout as a
// bad password is the bug that turned a DB blip into a permanent vault lockout.
export class VaultBackendError extends Error {
  constructor(kind, message, { status = null, detail = null, cause = null } = {}) {
    super(message);
    this.name = "VaultBackendError";
    this.kind = kind;              // timeout | network | server_error | rate_limited | db_error | unknown
    this.isBackend = true;         // marker for callers: do not strike
    this.retriable = true;         // the verdict was never rendered — safe to retry
    this.status = status;          // HTTP status if the server responded
    this.detail = detail;          // human-readable backend detail
    if (cause) this.cause = cause;
  }
}

// Server `reason` strings that are genuine AUTH VERDICTS (the backend evaluated
// the credential and answered). These DO count — they are not backend errors.
const AUTH_VERDICT_REASONS = [
  "invalid_token", "wrong_password", "invalid_password",
  "machine_locked", "machine_disabled", "machine_not_found",
];

// Classify a server-supplied `reason` string. Returns a VaultBackendError kind
// when the reason describes a backend/transient condition, or null when the
// reason is a real auth verdict (or unknown — caller decides).
export function classifyServerReason(reason) {
  if (!reason || typeof reason !== "string") return null;
  const r = reason.toLowerCase();
  if (AUTH_VERDICT_REASONS.some((v) => r.includes(v))) return null; // genuine verdict
  if (/rate[_\s-]?limit/.test(r)) return "rate_limited";
  if (/(database|\bdb\b|postgres|connection|pool|statement timeout|unavailable|internal|timeout|5\d\d)/.test(r)) return "db_error";
  return null;
}

// Classify a thrown error (transport/timeout/network/typed). Returns a kind
// string when it is a backend/transient failure, or null when it is not.
export function classifyBackendError(err) {
  if (!err) return null;
  if (err instanceof VaultBackendError) return err.kind;
  const name = err.name || "";
  const msg = (err.message || "").toString();
  if (name === "TimeoutError" || name === "AbortError" || /\babort(ed)?\b|\btim(e|ed)?\s*out\b|timeout/i.test(msg)) return "timeout";
  if (/fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ECONNRESET|EPIPE|network|socket hang/i.test(msg)) return "network";
  return null;
}

async function post(route, body) {
  const url = `${getBaseUrl()}/${route}`;
  const anonKey = getAnonKey();

  let res;
  try {
    res = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${anonKey}`
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(VAULT_FETCH_TIMEOUT_MS)
    });
  } catch (err) {
    // Transport-level failure — the backend never answered. Surface a typed,
    // non-strike error so callers report "backend unreachable", not "bad password".
    const kind = classifyBackendError(err) || "network";
    const why = kind === "timeout"
      ? `vault backend did not respond within ${VAULT_FETCH_TIMEOUT_MS}ms`
      : `cannot reach vault backend (${err.message || "network error"})`;
    throw new VaultBackendError(kind, why, { detail: err.message || String(err), cause: err });
  }

  let data;
  try {
    data = await res.json();
  } catch (err) {
    // 2xx/5xx with a non-JSON or empty body — the function errored without a
    // structured verdict. Transient backend condition, not an auth failure.
    throw new VaultBackendError("server_error", `vault backend returned an unreadable response (HTTP ${res.status})`, { status: res.status, detail: err.message });
  }

  // HTTP 5xx (or any non-ok with no structured error) = backend problem, never a verdict.
  if (res.status >= 500 || (!res.ok && !data.error)) {
    const reasonKind = classifyServerReason(data.reason || data.error) || "server_error";
    throw new VaultBackendError(reasonKind, `vault backend error (HTTP ${res.status})`, { status: res.status, detail: data.error || data.reason || `HTTP ${res.status}` });
  }

  // 4xx WITH a structured error: could be a real verdict (invalid_token) or a
  // transient backend signal surfaced as 4xx (rate_limited). Promote the latter
  // to a typed backend error; leave genuine verdicts in `data` for the caller.
  if (data.error) {
    const reasonKind = classifyServerReason(data.reason || data.error);
    if (reasonKind) {
      throw new VaultBackendError(reasonKind, `vault backend ${reasonKind.replace("_", " ")}`, { status: res.status, detail: data.error || data.reason });
    }
  }

  return data;
}

// ============================================================
// Auth-bearing calls (require HMAC token)
// ============================================================
async function authPost(route, password, machineHash, token, timestamp, extra = {}) {
  return post(route, {
    machine_hash: machineHash,
    token,
    timestamp,
    password,
    ...extra
  });
}

// ============================================================
// Exported API surface
// ============================================================

export async function retrieve(password, machineHash, token, timestamp, service) {
  return authPost("retrieve", password, machineHash, token, timestamp, { service });
}

export async function write(password, machineHash, token, timestamp, service, value) {
  return authPost("write", password, machineHash, token, timestamp, { service, value });
}

export async function enable(password, machineHash, token, timestamp, service, enabled) {
  return authPost("enable", password, machineHash, token, timestamp, { service, enabled });
}

export async function addService(password, machineHash, token, timestamp, name, label, key_type, description, project) {
  const extra = { name, label, key_type, description };
  if (project) extra.project = project;
  return authPost("add", password, machineHash, token, timestamp, extra);
}

export async function updateService(password, machineHash, token, timestamp, service, updates) {
  return authPost("update", password, machineHash, token, timestamp, { service, ...updates });
}

export async function removeService(password, machineHash, token, timestamp, service, confirm) {
  return authPost("remove", password, machineHash, token, timestamp, { service, confirm });
}

export async function revoke(password, machineHash, token, timestamp, service, confirm) {
  return authPost("revoke", password, machineHash, token, timestamp, { service, confirm });
}

export async function status(password, machineHash, token, timestamp, project) {
  const extra = {};
  if (project) extra.project = project;
  return authPost("status", password, machineHash, token, timestamp, extra);
}

export async function test(password, machineHash, token, timestamp) {
  return authPost("test", password, machineHash, token, timestamp);
}

export async function changePassword(password, machineHash, token, timestamp, newSeedHash) {
  return authPost("change-password", password, machineHash, token, timestamp, { new_hmac_seed_hash: newSeedHash });
}

export async function createEnrollment(password, machineHash, token, timestamp, label, ttlMinutes, installId) {
  const extra = {};
  if (label) extra.label = label;
  if (ttlMinutes) extra.ttl_minutes = ttlMinutes;
  if (installId) extra.install_id = installId;
  return authPost("create-enrollment", password, machineHash, token, timestamp, extra);
}

export async function registerMachine(machineHash, seedHash, label, adminToken, extras = {}) {
  return post("register-machine", {
    machine_hash: machineHash,
    hmac_seed_hash: seedHash,
    label,
    admin_token: adminToken,
    ...extras
  });
}

export async function redeemEnrollment(machineHash, seedHash, label, enrollmentCode) {
  return post("redeem-enrollment", {
    machine_hash: machineHash,
    hmac_seed_hash: seedHash,
    label,
    enrollment_code: enrollmentCode
  });
}

export default {
  retrieve, write, enable, addService, updateService, removeService, revoke,
  status, test, createEnrollment, registerMachine, redeemEnrollment, getBaseUrl, getAnonKey,
  VaultBackendError, classifyBackendError, classifyServerReason
};
