/**
 * Signed cookie session for org admin web login (ADR-0021).
 *
 * Payload is HMAC-SHA256 signed (HUB_SESSION_SECRET). No server-side session
 * table in v1 — logout clears the cookie; stolen-cookie revoke is deferred.
 */
import { createHmac, timingSafeEqual } from "node:crypto";

export const SESSION_COOKIE_NAME = "cph_session";
export const OAUTH_STATE_COOKIE_NAME = "cph_oauth_state";

/** Default session TTL: 7 days. */
export const DEFAULT_SESSION_TTL_SECONDS = 7 * 24 * 60 * 60;

/** OAuth state cookie TTL: 10 minutes. */
export const OAUTH_STATE_TTL_SECONDS = 10 * 60;

interface SessionTimes {
  readonly iat: number;
  readonly exp: number;
}

export type SessionIdentity =
  | {
      readonly userId: string;
      readonly feishuOpenId: string;
    }
  | {
      readonly userId: string;
      readonly feishuIdentityId: string;
      readonly feishuConnectionId: string;
      readonly feishuOrganizationId: string;
    };

export type SessionPayload = SessionIdentity & SessionTimes;

export interface OAuthStatePayload {
  readonly nonce: string;
  readonly returnTo: string;
  readonly organizationId?: string | undefined;
  readonly connectionId?: string | undefined;
  readonly exp: number;
}

export function signSession(
  payload: SessionIdentity,
  secret: string,
  ttlSeconds: number = DEFAULT_SESSION_TTL_SECONDS,
  nowSeconds: number = Math.floor(Date.now() / 1000),
): string {
  const full: SessionPayload = {
    ...payload,
    iat: nowSeconds,
    exp: nowSeconds + ttlSeconds,
  };
  return signJson(full, secret);
}

export function verifySession(
  token: string,
  secret: string,
  nowSeconds: number = Math.floor(Date.now() / 1000),
): SessionPayload | null {
  const payload = verifyJson<Record<string, unknown>>(token, secret);
  if (payload === null) return null;
  if (typeof payload["userId"] !== "string" || payload["userId"] === "") return null;
  if (typeof payload["iat"] !== "number" || typeof payload["exp"] !== "number") return null;
  if (payload["exp"] <= nowSeconds) return null;
  const legacy = typeof payload["feishuOpenId"] === "string" && payload["feishuOpenId"] !== "";
  const scoped = typeof payload["feishuIdentityId"] === "string" && payload["feishuIdentityId"] !== "" &&
    typeof payload["feishuConnectionId"] === "string" && payload["feishuConnectionId"] !== "" &&
    typeof payload["feishuOrganizationId"] === "string" && payload["feishuOrganizationId"] !== "";
  if (legacy === scoped) return null;
  if (legacy) {
    return {
      userId: payload["userId"],
      feishuOpenId: payload["feishuOpenId"] as string,
      iat: payload["iat"],
      exp: payload["exp"],
    };
  }
  return {
    userId: payload["userId"],
    feishuIdentityId: payload["feishuIdentityId"] as string,
    feishuConnectionId: payload["feishuConnectionId"] as string,
    feishuOrganizationId: payload["feishuOrganizationId"] as string,
    iat: payload["iat"],
    exp: payload["exp"],
  };
}

/*
 * OAuth state may be legacy/unscoped or bind both an Organization and its
 * intended Feishu connection. A half-scoped state is always invalid.
 */
export function signOAuthState(
  payload: Omit<OAuthStatePayload, "exp">,
  secret: string,
  ttlSeconds: number = OAUTH_STATE_TTL_SECONDS,
  nowSeconds: number = Math.floor(Date.now() / 1000),
): string {
  const full: OAuthStatePayload = {
    ...payload,
    exp: nowSeconds + ttlSeconds,
  };
  return signJson(full, secret);
}

export function verifyOAuthState(
  token: string,
  secret: string,
  nowSeconds: number = Math.floor(Date.now() / 1000),
): OAuthStatePayload | null {
  const payload = verifyJson<OAuthStatePayload>(token, secret);
  if (payload === null) return null;
  if (typeof payload.nonce !== "string" || payload.nonce === "") return null;
  if (typeof payload.returnTo !== "string") return null;
  if (typeof payload.exp !== "number" || payload.exp <= nowSeconds) return null;
  const hasOrganization = typeof payload.organizationId === "string" && payload.organizationId !== "";
  const hasConnection = typeof payload.connectionId === "string" && payload.connectionId !== "";
  if (hasOrganization !== hasConnection) return null;
  return {
    nonce: payload.nonce,
    returnTo: payload.returnTo,
    ...(hasOrganization
      ? { organizationId: payload.organizationId!, connectionId: payload.connectionId! }
      : {}),
    exp: payload.exp,
  };
}

function signJson(value: unknown, secret: string): string {
  const body = Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
  const sig = hmac(body, secret);
  return `${body}.${sig}`;
}

function verifyJson<T>(token: string, secret: string): T | null {
  const dot = token.indexOf(".");
  if (dot <= 0 || dot === token.length - 1) return null;
  const body = token.slice(0, dot);
  const sig = token.slice(dot + 1);
  const expected = hmac(body, secret);
  if (!safeEqual(sig, expected)) return null;
  try {
    return JSON.parse(Buffer.from(body, "base64url").toString("utf8")) as T;
  } catch {
    return null;
  }
}

function hmac(body: string, secret: string): string {
  return createHmac("sha256", secret).update(body).digest("base64url");
}

function safeEqual(a: string, b: string): boolean {
  const ba = Buffer.from(a);
  const bb = Buffer.from(b);
  if (ba.length !== bb.length) return false;
  return timingSafeEqual(ba, bb);
}
