/**
 * Feishu web OAuth (user login) for org admin panel.
 *
 * Organization login resolves the intended encrypted Feishu Application
 * Connection before building the authorize URL or exchanging the code.
 *
 * Flow:
 *  1. GET authorize URL → user consents
 *  2. callback code → user_access_token (authen/v2/oauth/token)
 *  3. user_access_token → user info (authen/v1/user_info) → open_id
*/
import { classifyNetworkFailure, type NetworkFailureCategory } from "../../connections/networkFailure.js";


export interface FeishuOAuthConfig {
  readonly appId: string;
  readonly appSecret: string;
  readonly redirectUri: string;
  /** Space-separated scopes requested at authorize time. */
  readonly scope: string;
  readonly authorizeBaseUrl?: string;
  readonly tokenUrl?: string;
  readonly userInfoUrl?: string;
  readonly fetchImpl?: typeof fetch;
}

export interface FeishuOAuthUser {
  readonly openId: string;
  readonly unionId?: string | undefined;
  readonly displayName: string;
  readonly avatarUrl: string | null;
}

export class FeishuOAuthError extends Error {
  constructor(
    readonly code: "feishu_oauth_unreachable" | "feishu_oauth_rejected" | "feishu_oauth_invalid_response",
    message: string,
    readonly category: NetworkFailureCategory | "http" | "provider_rejection" | "invalid_response",
    readonly upstreamStatus?: number,
  ) {
    super(message);
    this.name = "FeishuOAuthError";
  }
}

const DEFAULT_AUTHORIZE_BASE = "https://accounts.feishu.cn/open-apis/authen/v1/authorize";
const DEFAULT_TOKEN_URL = "https://open.feishu.cn/open-apis/authen/v2/oauth/token";
const DEFAULT_USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info";

/** Minimal scopes for open_id + name/avatar on user_info. */
export const DEFAULT_OAUTH_SCOPE = "contact:user.base:readonly";

export function buildAuthorizeUrl(config: FeishuOAuthConfig, state: string): string {
  const base = config.authorizeBaseUrl ?? DEFAULT_AUTHORIZE_BASE;
  const url = new URL(base);
  url.searchParams.set("client_id", config.appId);
  url.searchParams.set("response_type", "code");
  url.searchParams.set("redirect_uri", config.redirectUri);
  url.searchParams.set("state", state);
  if (config.scope.trim() !== "") {
    url.searchParams.set("scope", config.scope);
  }
  return url.toString();
}

export async function exchangeCodeForUser(
  config: FeishuOAuthConfig,
  code: string,
): Promise<FeishuOAuthUser> {
  const accessToken = await exchangeCodeForAccessToken(config, code);
  return fetchUserInfo(config, accessToken);
}

async function exchangeCodeForAccessToken(config: FeishuOAuthConfig, code: string): Promise<string> {
  const fetchImpl = config.fetchImpl ?? fetch;
  const tokenUrl = config.tokenUrl ?? DEFAULT_TOKEN_URL;
  let res: Response;
  try {
    res = await fetchImpl(tokenUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json; charset=utf-8" },
      body: JSON.stringify({
        grant_type: "authorization_code",
        client_id: config.appId,
        client_secret: config.appSecret,
        code,
        redirect_uri: config.redirectUri,
      }),
      redirect: "manual",
      signal: AbortSignal.timeout(10_000),
    });
  } catch (error) {
    throw new FeishuOAuthError(
      "feishu_oauth_unreachable",
      "Feishu OAuth token exchange could not reach provider",
      classifyNetworkFailure(error),
    );
  }
  const body = await oauthJson(res, "token exchange");
  if (!res.ok || body["code"] !== 0 ||
      typeof body["access_token"] !== "string" || body["access_token"] === "") {
    throw new FeishuOAuthError(
      "feishu_oauth_rejected",
      "Feishu OAuth token exchange was rejected",
      res.ok ? "provider_rejection" : "http",
      res.status,
    );
  }
  return body["access_token"];
}

async function fetchUserInfo(config: FeishuOAuthConfig, userAccessToken: string): Promise<FeishuOAuthUser> {
  const fetchImpl = config.fetchImpl ?? fetch;
  const userInfoUrl = config.userInfoUrl ?? DEFAULT_USER_INFO_URL;
  let res: Response;
  try {
    res = await fetchImpl(userInfoUrl, {
      method: "GET",
      headers: { Authorization: `Bearer ${userAccessToken}` },
      redirect: "manual",
      signal: AbortSignal.timeout(10_000),
    });
  } catch (error) {
    throw new FeishuOAuthError(
      "feishu_oauth_unreachable",
      "Feishu OAuth user info could not reach provider",
      classifyNetworkFailure(error),
    );
  }
  const body = await oauthJson(res, "user info");
  const data = body["data"];
  if (!res.ok || body["code"] !== 0 || !isRecord(data)) {
    throw new FeishuOAuthError(
      "feishu_oauth_rejected",
      "Feishu OAuth user info was rejected",
      res.ok ? "provider_rejection" : "http",
      res.status,
    );
  }
  const openId = data["open_id"];
  if (typeof openId !== "string" || openId === "") {
    throw new FeishuOAuthError(
      "feishu_oauth_invalid_response",
      "Feishu OAuth user info response is missing open_id",
      "invalid_response",
      res.status,
    );
  }
  const displayName =
    (typeof data["name"] === "string" && data["name"] !== "" ? data["name"] : null) ??
    (typeof data["en_name"] === "string" && data["en_name"] !== "" ? data["en_name"] : null) ??
    openId;
  const avatar =
    (typeof data["avatar_url"] === "string" && data["avatar_url"] !== ""
      ? data["avatar_url"]
      : null) ??
    (typeof data["avatar_middle"] === "string" && data["avatar_middle"] !== ""
      ? data["avatar_middle"]
      : null);
  const unionId = typeof data["union_id"] === "string" && data["union_id"] !== ""
    ? data["union_id"]
    : undefined;
  return { openId, ...(unionId !== undefined ? { unionId } : {}), displayName, avatarUrl: avatar };
}

async function oauthJson(response: Response, operation: string): Promise<Record<string, unknown>> {
  let value: unknown;
  try {
    value = await response.json();
  } catch {
    throw new FeishuOAuthError(
      "feishu_oauth_invalid_response",
      `Feishu OAuth ${operation} returned an invalid response`,
      "invalid_response",
      response.status,
    );
  }
  if (!isRecord(value)) {
    throw new FeishuOAuthError(
      "feishu_oauth_invalid_response",
      `Feishu OAuth ${operation} returned an invalid response`,
      "invalid_response",
      response.status,
    );
  }
  return value;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
