/* =====================================================================
   auth.js — Login, Sessions, Rate-Limiting/Lockout

   Sicherheitsmodell:
   - Owner-Code kommt aus dem POSTFACH_CODE-Secret (Wrangler), Vergleich
     per Konstant-Zeit-String-Vergleich.
   - Team-Codes: SHA-256-Hash-Lookup (users.code_hash).
   - Nach 5 Fehlversuchen pro Client (IP-basiert) 15 Minuten Sperre —
     angelehnt an Cloudflares "Protect your login"-Standardregel.
   - Sessions: Token wird nur gehasht gespeichert. Inaktivitäts-Timeout und
     absolute Höchstdauer beide 24 Stunden (Nutzer-Wunsch 04.08.2026: "den
     Zugangscode auf der Verwaltungsseite nur einmal am Tag eingeben
     müssen") — vorher 30 Min/12 Std, was mitten am Tag zum erneuten
     Login-Zwang führte.
   ===================================================================== */
import { sha256Hex, generateSessionToken, nowIso } from "./crypto.js";
import { logAction } from "./audit.js";

const MAX_ATTEMPTS = 5;
const LOCKOUT_MINUTES = 15;
const INACTIVITY_MS = 24 * 60 * 60 * 1000;
const SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000;

export function clientKey(request) {
  return request.headers.get("CF-Connecting-IP") || "unknown";
}

function timingSafeEqual(a, b) {
  if (a.length !== b.length) return false;
  let mismatch = 0;
  for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return mismatch === 0;
}

export async function isLockedOut(env, key) {
  const row = await env.DB.prepare(`SELECT locked_until FROM login_attempts WHERE key = ?`)
    .bind(key)
    .first();
  if (!row || !row.locked_until) return false;
  return new Date(row.locked_until) > new Date();
}

export async function recordFailedAttempt(env, key) {
  const row = await env.DB.prepare(`SELECT count FROM login_attempts WHERE key = ?`).bind(key).first();
  const count = (row?.count || 0) + 1;
  let lockedUntil = null;
  if (count >= MAX_ATTEMPTS) {
    lockedUntil = new Date(Date.now() + LOCKOUT_MINUTES * 60 * 1000).toISOString();
  }
  await env.DB.prepare(
    `INSERT INTO login_attempts (key, count, locked_until, updated_at) VALUES (?, ?, ?, ?)
     ON CONFLICT(key) DO UPDATE SET count = ?, locked_until = ?, updated_at = ?`
  )
    .bind(key, count, lockedUntil, nowIso(), count, lockedUntil, nowIso())
    .run();
  return { count, lockedUntil };
}

export async function clearAttempts(env, key) {
  await env.DB.prepare(`DELETE FROM login_attempts WHERE key = ?`).bind(key).run();
}

/** Prüft einen eingegebenen Code gegen Owner-Secret und Team-Datenbank. */
export async function verifyCode(env, code) {
  const hash = await sha256Hex(code);

  // 1) Owner-Code aus der Datenbank — den kann Dogi selbst ändern.
  //    Sobald hier einer hinterlegt ist, gilt NUR noch dieser (kein Zweitschlüssel).
  const ownerRow = await env.DB.prepare(
    `SELECT value FROM app_settings WHERE key = 'owner_code_hash'`
  )
    .first()
    .catch(() => null);

  if (ownerRow && ownerRow.value) {
    if (timingSafeEqual(hash, ownerRow.value)) return { ok: true, isOwner: true };
  } else if (env.POSTFACH_CODE && timingSafeEqual(code, env.POSTFACH_CODE)) {
    // 2) Erstzugang: solange noch kein eigener Code gesetzt wurde, gilt das Worker-Secret.
    return { ok: true, isOwner: true };
  }

  // 3) Normale Teammitglieder
  const user = await env.DB.prepare(
    `SELECT id, name, username, role_id, status, expires_at FROM users WHERE code_hash = ?`
  )
    .bind(hash)
    .first();
  if (!user) return { ok: false };
  if (user.status !== "active") return { ok: false, reason: "locked" };
  if (user.expires_at && new Date(user.expires_at) < new Date()) return { ok: false, reason: "expired" };
  return { ok: true, isOwner: false, user };
}

export async function createSession(env, { isOwner, userId }) {
  const token = generateSessionToken();
  const tokenHash = await sha256Hex(token);
  const now = new Date();
  const expiresAt = new Date(now.getTime() + SESSION_MAX_AGE_MS).toISOString();
  await env.DB.prepare(
    `INSERT INTO sessions (token_hash, user_id, is_owner, created_at, last_active_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)`
  )
    .bind(tokenHash, isOwner ? "owner" : userId, isOwner ? 1 : 0, now.toISOString(), now.toISOString(), expiresAt)
    .run();
  return token;
}

/** Löst ein Session-Token auf, prüft Ablauf/Inaktivität, aktualisiert last_active_at. */
export async function resolveSession(env, token) {
  if (!token) return null;
  const tokenHash = await sha256Hex(token);
  const session = await env.DB.prepare(
    `SELECT * FROM sessions WHERE token_hash = ?`
  )
    .bind(tokenHash)
    .first();
  if (!session) return null;

  const now = Date.now();
  if (new Date(session.expires_at).getTime() < now) {
    await env.DB.prepare(`DELETE FROM sessions WHERE token_hash = ?`).bind(tokenHash).run();
    return null;
  }
  if (now - new Date(session.last_active_at).getTime() > INACTIVITY_MS) {
    await env.DB.prepare(`DELETE FROM sessions WHERE token_hash = ?`).bind(tokenHash).run();
    return null;
  }

  await env.DB.prepare(`UPDATE sessions SET last_active_at = ? WHERE token_hash = ?`)
    .bind(new Date().toISOString(), tokenHash)
    .run();

  if (session.is_owner) {
    return { isOwner: true, actor: "owner" };
  }

  const user = await env.DB.prepare(
    `SELECT u.id, u.name, u.username, u.role_id, u.status, r.name as role_name, r.permissions
     FROM users u LEFT JOIN roles r ON r.id = u.role_id WHERE u.id = ?`
  )
    .bind(session.user_id)
    .first();
  if (!user || user.status !== "active") return null;

  const overridesRows = await env.DB.prepare(
    `SELECT permission, allowed FROM user_permission_overrides WHERE user_id = ?`
  )
    .bind(user.id)
    .all();
  const overrides = {};
  for (const r of overridesRows.results || []) overrides[r.permission] = !!r.allowed;

  return {
    isOwner: false,
    actor: user.username,
    userId: user.id,
    name: user.name,
    roleId: user.role_id,
    roleName: user.role_name,
    permissions: user.permissions ? JSON.parse(user.permissions) : [],
    overrides,
  };
}

export async function endSession(env, token) {
  if (!token) return;
  const tokenHash = await sha256Hex(token);
  await env.DB.prepare(`DELETE FROM sessions WHERE token_hash = ?`).bind(tokenHash).run();
}

export async function endAllSessionsForUser(env, userId) {
  await env.DB.prepare(`DELETE FROM sessions WHERE user_id = ?`).bind(userId).run();
}

export async function requireSession(request, env) {
  const auth = request.headers.get("Authorization") || "";
  const token = auth.startsWith("Bearer ") ? auth.slice(7) : null;
  return resolveSession(env, token);
}
