/**
 * IP制限と認証の判定（`proxy.ts` から★相関IDの発行の後に★呼ぶ）。
 *
 * ★ここは「判定するだけ」。Response は組み立てない★
 *   組み立てを `proxy.ts` 側に寄せることで、判定を素の値でテストでき、
 *   `proxy.ts` は薄い順序制御だけで済む。
 *
 * 判定の順序は ★IP制限（403）→ 認証（401 / redirect）★。
 * IPで弾くべき相手にトークンの当たり外れを試させないため、IPを先に見る。
 */
import { clientIp, isAllowedIp } from "@/lib/auth/client-ip";
import { SESSION_COOKIE_NAME } from "@/lib/auth/cookie";
import { readAuthToken, verifyToken } from "@/lib/auth/token";
import {
  ALLOW_ALL_WHEN_EMPTY,
  ALLOW_DIRECT_ACCESS,
  ALLOWED_IPS,
  TRUSTED_PROXY_HOPS,
} from "@/lib/config/allowed-ips";
import { verifyAgentAuthorization } from "@/lib/ds-agent/tokens";

/** 発行する cookie の名前。定義は `lib/auth/cookie.ts`（発行・削除・判定でズレないように1箇所） */
export { SESSION_COOKIE_NAME };

/** 認証をかけないパス。★matcher で除外せず、ここで分岐して通す★（保護漏れを見つけやすくするため） */
const PUBLIC_PATHS = ["/login", "/api/auth/login"] as const;

export type AuthDecision =
  /** 通してよい */
  | { kind: "allow" }
  /** IP制限で拒否（403） */
  | { kind: "deny_ip" }
  /** 未認証（画面は /login へリダイレクト、APIは 401） */
  | { kind: "unauthenticated" };

/** `/login` と `/api/auth/login` は認証不要（ここを保護すると誰もログインできない） */
export function isPublicPath(pathname: string): boolean {
  return PUBLIC_PATHS.some(
    (p) => pathname === p || pathname.startsWith(`${p}/`)
  );
}

/**
 * APIルート向けかどうか。未認証時に 401 を返すか `/login` へ飛ばすかの分岐に使う。
 * ブラウザのページ遷移をJSONの401で返すと画面が真っ白になるため分ける。
 */
export function isApiPath(pathname: string): boolean {
  return pathname === "/api" || pathname.startsWith("/api/");
}

/**
 * 制御盤（ラズパイ）向けのパスかどうか（指示書AY 段階5。D5・D22）。
 * ★このパスは Bearer だけで通し、cookie では通さない★。それ以外のパスは cookie だけで通し、Bearer を見ない（表4）。
 * ★前方一致で `/api/ds-agents` を拾わないこと★（表4 A8）。
 */
export function isAgentPath(pathname: string): boolean {
  return pathname === "/api/ds-agent" || pathname.startsWith("/api/ds-agent/");
}

interface IpDeps {
  allowedIps?: readonly string[];
  allowAllWhenEmpty?: boolean;
  allowDirectAccess?: boolean;
  trustedProxyHops?: number;
}

/** IP制限の判定。★`authenticate()` と `authenticateAgent()` で共有する（複製しない）★ */
function ipAllowed(forwardedFor: string | null, deps: IpDeps): boolean {
  const allowedIps = deps.allowedIps ?? ALLOWED_IPS;
  const allowAllWhenEmpty = deps.allowAllWhenEmpty ?? ALLOW_ALL_WHEN_EMPTY;
  const allowDirectAccess = deps.allowDirectAccess ?? ALLOW_DIRECT_ACCESS;
  const trustedProxyHops = deps.trustedProxyHops ?? TRUSTED_PROXY_HOPS;
  const ip = clientIp(forwardedFor, trustedProxyHops);
  return isAllowedIp(ip, allowedIps, allowAllWhenEmpty, allowDirectAccess);
}

/**
 * IP制限と認証を判定する。
 *
 * @param pathname `request.nextUrl.pathname`
 * @param forwardedFor `X-Forwarded-For` ヘッダの生値
 * @param sessionCookieValue `dwatch_session` cookie の値
 * @param deps 設定値と期待トークン。★テストのために引数化してある★
 *   （省略時は `lib/config/allowed-ips.ts` と `AUTH_TOKEN` を読む）
 */
export function authenticate(
  pathname: string,
  forwardedFor: string | null,
  sessionCookieValue: string | null,
  deps: IpDeps & {
    expectedToken?: string | undefined;
  } = {}
): AuthDecision {
  const expectedToken =
    "expectedToken" in deps ? deps.expectedToken : readAuthToken();

  // 1. IP制限。★/login も含め全パスにかける★
  //    ログイン画面だけ誰でも開けるのでは、IP制限をかけた意味が薄れる
  if (!ipAllowed(forwardedFor, deps)) {
    return { kind: "deny_ip" };
  }

  // 2. 認証。/login と /api/auth/login はここで通す（matcher では除外しない）
  if (isPublicPath(pathname)) return { kind: "allow" };

  // ★制御盤のパスは cookie では通さない★（proxy は `authenticateAgent()` を呼ぶので、ここは保険。表4 A2）
  if (isAgentPath(pathname)) return { kind: "unauthenticated" };

  // ★cookie の値も必ず定数時間比較を通す。ここで === を書かないこと★
  if (!verifyToken(sessionCookieValue, expectedToken)) {
    return { kind: "unauthenticated" };
  }

  return { kind: "allow" };
}

/**
 * 制御盤のパス（`isAgentPath()`）の判定（指示書AY 段階5。D5・D22）。順序は ★IP制限（403）→ Bearer（401）★。
 * ★IP で拒否したらトークン検証を呼ばない★（表4 A7）。★戻り値に serial を入れない★（後段へヘッダで渡さない。
 * ルートは `requireAgent()` で再検証して serial を得る）。
 *
 * @param authorization `Authorization` ヘッダの生値
 * @param deps IP の設定と `verifyAgentToken`（★テストのために引数化★。省略時は `verifyAgentAuthorization`）
 */
export async function authenticateAgent(
  pathname: string,
  forwardedFor: string | null,
  authorization: string | null,
  deps: IpDeps & {
    verifyAgentToken?: (authorization: string | null) => Promise<string | null>;
  } = {}
): Promise<AuthDecision> {
  if (!ipAllowed(forwardedFor, deps)) return { kind: "deny_ip" };
  const verify = deps.verifyAgentToken ?? verifyAgentAuthorization;
  const serial = await verify(authorization);
  return serial === null ? { kind: "unauthenticated" } : { kind: "allow" };
}
