/**
 * 接続元IPの取り出しと許可判定（純粋関数。DBもファイルも `process.env` も触らない）。
 *
 * ★段数・許可リスト・直接アクセス可否はすべて引数で受け取る★
 *   `lib/config/allowed-ips.ts` から呼び出し側が渡す。ここに値を書かないこと
 *   （CLAUDE.md §7 のハードコード禁止。かつ、引数化しないと期待値表をテストできない）。
 */

/**
 * `X-Forwarded-For` から接続元IPを取り出す。
 *
 * ★末尾から `hops` 番目の要素を返す★（`hops = 1` なら末尾）。
 * nginx は `$proxy_add_x_forwarded_for` 相当で「自分が見た接続元」を★末尾に追記★する
 * （D-077 項5）ため、信頼できるのは末尾側だけ。
 * ★先頭はクライアントが自由に詐称できるので絶対に使わないこと★ —
 * 先頭を採ると、`X-Forwarded-For: <許可IP>` を自分で付けるだけでIP制限を素通りできる。
 *
 * 要素数が `hops` に満たない場合は `null`（取れない扱い）。
 * ★足りないぶんを先頭で埋めないこと★（自称IPを掴んでしまう）。
 *
 * ★表記の正規化は行わない★（P1 の割り切り）:
 *   IPv6 短縮形の展開・IPv4射影（`::ffff:`）の展開・ポートの除去はしない。
 *   `isAllowedIp()` が完全一致比較なので、`ALLOWED_IPS` には
 *   nginx が実際に付ける表記をそのまま書く必要がある。
 *   ここでやってよいのは `,` での分割と前後の空白除去まで。
 *
 * ★要素数で「直接アクセスかどうか」を判断しないこと★:
 *   クライアントが XFF を送らない普通のリクエストは、nginx が1個足して「要素1個」になる。
 *   つまり要素数だけでは nginx 経由の正常系と直接アクセスを区別できない。
 *
 * @param headerValue `Headers.get("x-forwarded-for")` の戻り値。
 *   ★同名ヘッダが複数行来ても `Headers.get()` が `", "` で連結した1文字列を返す★ので、
 *   分割して末尾を取れば正しく動く（`getSetCookie` のような個別取得は不要）。
 * @param hops 信頼済みプロキシの段数（`TRUSTED_PROXY_HOPS`）
 */
export function clientIp(
  headerValue: string | null | undefined,
  hops: number
): string | null {
  if (!headerValue) return null;
  if (hops < 1) return null;

  const parts = headerValue.split(",");
  if (parts.length < hops) return null;

  const candidate = parts[parts.length - hops]?.trim();
  if (!candidate) return null;
  return candidate;
}

/**
 * 接続元IPが許可されているかを判定する。
 *
 * ★判定の順序が重要★: 「ip が null かどうか」が先、「allowlist が空かどうか」は後。
 *   `ip === null` は `allowAllWhenEmpty` の影響を★一切受けない★。
 *   この2つを1つの設定値でまとめて扱うと、allowlist が空の初期状態のまま本番へ出た瞬間に
 *   nginx を経由しない直接アクセスが素通りする（「ヘッダが無ければ拒否」と矛盾する）。
 *
 * @param ip `clientIp()` の戻り値。`null` は「XFF から接続元が取れなかった」＝直接アクセス
 * @param allowedIps 許可リスト（★完全一致比較のみ。CIDR 非対応★）
 * @param allowAllWhenEmpty 許可リストが空のとき制限をかけないか（`ip` が取れているときだけ効く）
 * @param allowDirectAccess `ip === null` を通すか。
 *   ★呼び出し側が `ALLOW_DIRECT_ACCESS`（＝ `process.env.ALLOW_DIRECT_ACCESS === "true"`）を渡す。
 *     この関数自身は `process.env` を読まない★
 */
export function isAllowedIp(
  ip: string | null,
  allowedIps: readonly string[],
  allowAllWhenEmpty: boolean,
  allowDirectAccess: boolean
): boolean {
  // 先に null を捌く。allowAllWhenEmpty はここに一切関与しない
  if (ip === null) return allowDirectAccess;

  if (allowedIps.length === 0) return allowAllWhenEmpty;

  return allowedIps.includes(ip);
}
