// credentials.mjs — パスワード認証 + TOTP 二要素認証 + アカウントロック
//
// デジタル庁標準仕様 機能要件:
//   非機能 #6: ID+PW (8文字以上, 90日更新)
//   非機能 #7 / 0390050: 認証失敗5回でロック、二要素認証(IC/生体/WebAuthn)対応
//   0390048-0390051: 個人単位ID・パスワード管理、組織・職務・職位での権限設定
//
// 外部依存なし（node:crypto のみ）で PBKDF2 パスワードハッシュと TOTP(RFC 6238) を実装。
// 資格情報は credentials.json に保存（DATABASE_URL 設定時は別途 DB 化を推奨）。

import crypto from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";

// ── ポリシー設定（環境変数で上書き可） ──────────────────────────────────────
export const PW_MIN_LENGTH = Number(process.env.PW_MIN_LENGTH ?? 8);
export const PW_MAX_AGE_DAYS = Number(process.env.PW_MAX_AGE_DAYS ?? 90);
export const MAX_FAILED_ATTEMPTS = Number(process.env.MAX_FAILED_ATTEMPTS ?? 5);
export const LOCK_DURATION_MIN = Number(process.env.LOCK_DURATION_MIN ?? 30);
const PBKDF2_ITERATIONS = 120_000;
const SESSION_TTL_MIN = Number(process.env.SESSION_TTL_MIN ?? 480); // 8 時間

// ── パスワードハッシュ（PBKDF2-SHA512） ────────────────────────────────────
export function hashPassword(password, salt = crypto.randomBytes(16).toString("hex")) {
  const hash = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 64, "sha512").toString("hex");
  return { salt, hash, iterations: PBKDF2_ITERATIONS };
}

export function verifyPassword(password, salt, expectedHash) {
  const actual = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 64, "sha512").toString("hex");
  const a = Buffer.from(actual);
  const b = Buffer.from(expectedHash);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

export function validatePasswordPolicy(password) {
  const errors = [];
  if (typeof password !== "string" || password.length < PW_MIN_LENGTH) {
    errors.push(`パスワードは${PW_MIN_LENGTH}文字以上が必要です`);
  }
  if (!/[A-Za-z]/.test(password) || !/[0-9]/.test(password)) {
    errors.push("パスワードは英字と数字を含む必要があります");
  }
  return errors;
}

// ── TOTP（RFC 6238, HMAC-SHA1, 30 秒ステップ, 6 桁） ───────────────────────
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

export function generateTotpSecret(length = 20) {
  const bytes = crypto.randomBytes(length);
  let bits = "";
  for (const byte of bytes) bits += byte.toString(2).padStart(8, "0");
  let secret = "";
  for (let i = 0; i + 5 <= bits.length; i += 5) {
    secret += BASE32_ALPHABET[parseInt(bits.slice(i, i + 5), 2)];
  }
  return secret;
}

function base32Decode(secret) {
  const clean = secret.replace(/=+$/, "").toUpperCase().replace(/\s/g, "");
  let bits = "";
  for (const char of clean) {
    const idx = BASE32_ALPHABET.indexOf(char);
    if (idx === -1) continue;
    bits += idx.toString(2).padStart(5, "0");
  }
  const bytes = [];
  for (let i = 0; i + 8 <= bits.length; i += 8) {
    bytes.push(parseInt(bits.slice(i, i + 8), 2));
  }
  return Buffer.from(bytes);
}

export function generateTotp(secret, atMs = Date.now(), step = 30, digits = 6) {
  const counter = Math.floor(atMs / 1000 / step);
  const buf = Buffer.alloc(8);
  buf.writeBigInt64BE(BigInt(counter));
  const hmac = crypto.createHmac("sha1", base32Decode(secret)).update(buf).digest();
  const offset = hmac[hmac.length - 1] & 0x0f;
  const code = (hmac.readUInt32BE(offset) & 0x7fffffff) % 10 ** digits;
  return String(code).padStart(digits, "0");
}

export function verifyTotp(secret, token, window = 1) {
  if (!secret || !token) return false;
  const normalized = String(token).trim();
  for (let i = -window; i <= window; i++) {
    if (generateTotp(secret, Date.now() + i * 30_000) === normalized) return true;
  }
  return false;
}

// ── セッショントークン（HMAC 署名, ステートレス） ──────────────────────────
function sessionSecret() {
  return process.env.SESSION_SECRET ?? "dev-insecure-session-secret-change-in-production";
}

export function issueSession(user) {
  const payload = {
    sub: user.id,
    name: user.name,
    department: user.department ?? "",
    roles: user.roles ?? [],
    exp: Date.now() + SESSION_TTL_MIN * 60_000,
  };
  const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
  const sig = crypto.createHmac("sha256", sessionSecret()).update(body).digest("base64url");
  return `${body}.${sig}`;
}

export function verifySession(token) {
  if (!token || typeof token !== "string" || !token.includes(".")) return null;
  const [body, sig] = token.split(".");
  const expected = crypto.createHmac("sha256", sessionSecret()).update(body).digest("base64url");
  const a = Buffer.from(sig);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;
  let payload;
  try { payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8")); } catch { return null; }
  if (!payload.exp || payload.exp < Date.now()) return null;
  return { id: payload.sub, name: payload.name, department: payload.department, roles: payload.roles, authMode: "password" };
}

// ── 資格情報ストア ─────────────────────────────────────────────────────────
// GOV_STORAGE=memory ではファイルを読み書きせずメモリのみ（デモ等向け、server-core.mjsと同じ方針）
const MEMORY_ONLY = process.env.GOV_STORAGE === "memory";

export function makeCredentialStore(credentialsFile) {
  let memUsers = null;
  async function readAll() {
    if (MEMORY_ONLY) return structuredClone(memUsers ?? []);
    try {
      return JSON.parse(await readFile(credentialsFile, "utf8"));
    } catch (err) {
      if (err.code === "ENOENT") return [];
      if (["EPERM", "EACCES"].includes(err.code)) return [];
      throw err;
    }
  }
  async function writeAll(users) {
    if (MEMORY_ONLY) { memUsers = structuredClone(users); return; }
    try {
      await writeFile(credentialsFile, JSON.stringify(users, null, 2) + "\n", "utf8");
    } catch (err) {
      if (!["EPERM", "EACCES"].includes(err.code)) throw err;
    }
  }

  return {
    readAll,
    async findByUserId(userId) {
      return (await readAll()).find((u) => u.userId === userId) ?? null;
    },
    async upsert(user) {
      const users = await readAll();
      const idx = users.findIndex((u) => u.userId === user.userId);
      if (idx === -1) users.push(user);
      else users[idx] = user;
      await writeAll(users);
      return user;
    },
    async update(userId, mutate) {
      const users = await readAll();
      const idx = users.findIndex((u) => u.userId === userId);
      if (idx === -1) return null;
      users[idx] = mutate(users[idx]);
      await writeAll(users);
      return users[idx];
    },
  };
}

// ── ログイン判定（ロック・ポリシー込み） ──────────────────────────────────
export function isLocked(user) {
  return Boolean(user.lockedUntil && new Date(user.lockedUntil).getTime() > Date.now());
}

export function isPasswordExpired(user) {
  if (!user.passwordChangedAt) return false;
  const ageMs = Date.now() - new Date(user.passwordChangedAt).getTime();
  return ageMs > PW_MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
}
