import type {
  IOpenAiChatGptAuthCredentials,
  IOpenAiChatGptAuthOptions,
  IOpenAiChatGptCompleteDeviceCodeOptions,
  IOpenAiChatGptDeviceCode,
  IOpenAiChatGptDeviceCodePollOptions,
  IOpenAiChatGptTokenData,
  IOpenAiChatGptTokenInfo,
} from './smartai.interfaces.js';

export const OPENAI_CHATGPT_AUTH_ISSUER = 'https://auth.openai.com';
export const OPENAI_CHATGPT_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
export const OPENAI_CHATGPT_CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex';
export const OPENAI_CHATGPT_DEFAULT_ORIGINATOR = 'smartai';

const DEVICE_CODE_TIMEOUT_MS = 15 * 60 * 1000;

export class OpenAiChatGptAuthError extends Error {
  public status?: number;
  public body?: string;

  constructor(message: string, options: { status?: number; body?: string } = {}) {
    super(message);
    this.name = 'OpenAiChatGptAuthError';
    this.status = options.status;
    this.body = options.body;
  }
}

export interface IOpenAiChatGptAuthorizationCode {
  authorizationCode: string;
  codeChallenge: string;
  codeVerifier: string;
}

interface IOpenAiChatGptTokenResponse {
  id_token?: unknown;
  access_token?: unknown;
  refresh_token?: unknown;
}

function getFetch(options: IOpenAiChatGptAuthOptions): typeof fetch {
  const fetchFunction = options.fetch ?? globalThis.fetch;
  if (!fetchFunction) {
    throw new OpenAiChatGptAuthError('fetch is not available for OpenAI ChatGPT authentication.');
  }
  return fetchFunction;
}

function getIssuer(options: IOpenAiChatGptAuthOptions): string {
  return (options.issuer ?? OPENAI_CHATGPT_AUTH_ISSUER).replace(/\/+$/, '');
}

function getClientId(options: IOpenAiChatGptAuthOptions): string {
  return options.clientId ?? OPENAI_CHATGPT_CLIENT_ID;
}

function asString(value: unknown, name: string): string {
  if (typeof value !== 'string' || value.length === 0) {
    throw new OpenAiChatGptAuthError(`OpenAI ChatGPT auth response is missing ${name}.`);
  }
  return value;
}

function asOptionalString(value: unknown): string | undefined {
  return typeof value === 'string' && value.length > 0 ? value : undefined;
}

function asIntervalSeconds(value: unknown): number {
  const interval = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);
  if (!Number.isFinite(interval) || interval <= 0) {
    throw new OpenAiChatGptAuthError('OpenAI ChatGPT device-code response has an invalid interval.');
  }
  return interval;
}

async function readJson(response: Response, context: string): Promise<unknown> {
  const body = await response.text();
  if (!response.ok) {
    throw new OpenAiChatGptAuthError(`${context} failed with status ${response.status}.`, {
      status: response.status,
      body,
    });
  }

  try {
    return body ? JSON.parse(body) : {};
  } catch (error) {
    throw new OpenAiChatGptAuthError(`${context} returned invalid JSON: ${(error as Error).message}`, {
      status: response.status,
      body,
    });
  }
}

async function postJson(url: string, body: unknown, options: IOpenAiChatGptAuthOptions): Promise<unknown> {
  const response = await getFetch(options)(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  return readJson(response, `POST ${url}`);
}

async function postForm(url: string, body: URLSearchParams, options: IOpenAiChatGptAuthOptions): Promise<unknown> {
  const response = await getFetch(options)(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
  });
  return readJson(response, `POST ${url}`);
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function parseJwtPayload(jwt: string): Record<string, unknown> {
  const parts = jwt.split('.');
  if (parts.length !== 3 || !parts[1]) {
    throw new OpenAiChatGptAuthError('OpenAI ChatGPT auth returned an invalid token.');
  }

  try {
    return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')) as Record<string, unknown>;
  } catch (error) {
    throw new OpenAiChatGptAuthError(`OpenAI ChatGPT token could not be parsed: ${(error as Error).message}`);
  }
}

export function parseOpenAiChatGptTokenInfo(token: string): IOpenAiChatGptTokenInfo {
  const claims = parseJwtPayload(token);
  const profile = claims['https://api.openai.com/profile'] as Record<string, unknown> | undefined;
  const auth = claims['https://api.openai.com/auth'] as Record<string, unknown> | undefined;
  const expiresAtSeconds = typeof claims.exp === 'number' ? claims.exp : undefined;

  return {
    email: asOptionalString(claims.email) ?? asOptionalString(profile?.email),
    chatgptPlanType: asOptionalString(auth?.chatgpt_plan_type),
    chatgptUserId: asOptionalString(auth?.chatgpt_user_id) ?? asOptionalString(auth?.user_id),
    chatgptAccountId: asOptionalString(auth?.chatgpt_account_id),
    chatgptAccountIsFedramp: auth?.chatgpt_account_is_fedramp === true,
    expiresAt: expiresAtSeconds ? new Date(expiresAtSeconds * 1000).toISOString() : undefined,
    rawJwt: token,
  };
}

function createTokenData(
  response: IOpenAiChatGptTokenResponse,
  existingTokenData?: IOpenAiChatGptTokenData,
): IOpenAiChatGptTokenData {
  const accessToken = asOptionalString(response.access_token) ?? existingTokenData?.accessToken;
  const refreshToken = asOptionalString(response.refresh_token) ?? existingTokenData?.refreshToken;
  const idToken = asOptionalString(response.id_token) ?? existingTokenData?.idToken;
  if (!accessToken) {
    throw new OpenAiChatGptAuthError('OpenAI ChatGPT auth response is missing access_token.');
  }
  if (!refreshToken) {
    throw new OpenAiChatGptAuthError('OpenAI ChatGPT auth response is missing refresh_token.');
  }
  const tokenInfo = parseOpenAiChatGptTokenInfo(idToken ?? accessToken);

  return {
    accessToken,
    refreshToken,
    idToken,
    accountId: tokenInfo.chatgptAccountId,
    tokenInfo,
  };
}

export async function requestOpenAiChatGptDeviceCode(
  options: IOpenAiChatGptAuthOptions = {},
): Promise<IOpenAiChatGptDeviceCode> {
  const issuer = getIssuer(options);
  const response = await postJson(`${issuer}/api/accounts/deviceauth/usercode`, {
    client_id: getClientId(options),
  }, options) as Record<string, unknown>;

  return {
    verificationUrl: `${issuer}/codex/device`,
    userCode: asString(response.user_code ?? response.usercode, 'user_code'),
    deviceAuthId: asString(response.device_auth_id, 'device_auth_id'),
    intervalSeconds: asIntervalSeconds(response.interval),
  };
}

export async function pollOpenAiChatGptDeviceCode(
  deviceCode: IOpenAiChatGptDeviceCode,
  options: IOpenAiChatGptDeviceCodePollOptions = {},
): Promise<IOpenAiChatGptAuthorizationCode> {
  const issuer = getIssuer(options);
  const pollUrl = `${issuer}/api/accounts/deviceauth/token`;
  const timeoutMs = options.timeoutMs ?? DEVICE_CODE_TIMEOUT_MS;
  const sleepFunction = options.sleep ?? sleep;
  const startedAt = Date.now();

  while (Date.now() - startedAt < timeoutMs) {
    const response = await getFetch(options)(pollUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        device_auth_id: deviceCode.deviceAuthId,
        user_code: deviceCode.userCode,
      }),
    });

    if (response.ok) {
      const body = await readJson(response, `POST ${pollUrl}`) as Record<string, unknown>;
      return {
        authorizationCode: asString(body.authorization_code, 'authorization_code'),
        codeChallenge: asString(body.code_challenge, 'code_challenge'),
        codeVerifier: asString(body.code_verifier, 'code_verifier'),
      };
    }

    if (response.status !== 403 && response.status !== 404) {
      const body = await response.text();
      throw new OpenAiChatGptAuthError(`OpenAI ChatGPT device-code polling failed with status ${response.status}.`, {
        status: response.status,
        body,
      });
    }

    await response.arrayBuffer().catch(() => undefined);
    const remaining = timeoutMs - (Date.now() - startedAt);
    await sleepFunction(Math.min(deviceCode.intervalSeconds * 1000, Math.max(remaining, 0)));
  }

  throw new OpenAiChatGptAuthError('OpenAI ChatGPT device-code login timed out.');
}

export async function exchangeOpenAiChatGptAuthorizationCode(
  authorizationCode: IOpenAiChatGptAuthorizationCode,
  options: IOpenAiChatGptAuthOptions = {},
): Promise<IOpenAiChatGptTokenData> {
  const issuer = getIssuer(options);
  const response = await postForm(`${issuer}/oauth/token`, new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode.authorizationCode,
    redirect_uri: `${issuer}/deviceauth/callback`,
    client_id: getClientId(options),
    code_verifier: authorizationCode.codeVerifier,
  }), options) as IOpenAiChatGptTokenResponse;

  return createTokenData(response);
}

export function ensureOpenAiChatGptWorkspaceAllowed(
  tokenData: IOpenAiChatGptTokenData,
  forcedChatGptWorkspaceId?: string,
): void {
  if (!forcedChatGptWorkspaceId) {
    return;
  }
  if (tokenData.tokenInfo.chatgptAccountId !== forcedChatGptWorkspaceId) {
    throw new OpenAiChatGptAuthError(`OpenAI ChatGPT login is restricted to workspace ${forcedChatGptWorkspaceId}.`);
  }
}

export async function completeOpenAiChatGptDeviceCodeLogin(
  deviceCode: IOpenAiChatGptDeviceCode,
  options: IOpenAiChatGptCompleteDeviceCodeOptions = {},
): Promise<IOpenAiChatGptTokenData> {
  const authorizationCode = await pollOpenAiChatGptDeviceCode(deviceCode, options);
  const tokenData = await exchangeOpenAiChatGptAuthorizationCode(authorizationCode, options);
  ensureOpenAiChatGptWorkspaceAllowed(tokenData, options.forcedChatGptWorkspaceId);
  return tokenData;
}

export async function refreshOpenAiChatGptTokenData(
  tokenData: IOpenAiChatGptTokenData,
  options: IOpenAiChatGptAuthOptions = {},
): Promise<IOpenAiChatGptTokenData> {
  const issuer = getIssuer(options);
  const response = await postJson(`${issuer}/oauth/token`, {
    client_id: getClientId(options),
    grant_type: 'refresh_token',
    refresh_token: tokenData.refreshToken,
  }, options) as IOpenAiChatGptTokenResponse;

  return createTokenData({
    id_token: response.id_token ?? tokenData.idToken,
    access_token: response.access_token ?? tokenData.accessToken,
    refresh_token: response.refresh_token ?? tokenData.refreshToken,
  }, tokenData);
}

export function createOpenAiChatGptProviderSettings(credentials: IOpenAiChatGptAuthCredentials): {
  apiKey: string;
  baseURL: string;
  headers: Record<string, string>;
} {
  const accountId = credentials.accountId ?? credentials.tokenInfo?.chatgptAccountId;
  const isFedrampAccount = credentials.tokenInfo?.chatgptAccountIsFedramp === true;
  const headers: Record<string, string> = {
    originator: credentials.originator ?? OPENAI_CHATGPT_DEFAULT_ORIGINATOR,
  };

  if (accountId) {
    headers['ChatGPT-Account-ID'] = accountId;
  }
  if (isFedrampAccount) {
    headers['X-OpenAI-Fedramp'] = 'true';
  }

  return {
    apiKey: credentials.accessToken,
    baseURL: credentials.baseUrl ?? OPENAI_CHATGPT_CODEX_BASE_URL,
    headers,
  };
}
