/**
 * Generic OAuth2 token decrypt and refresh for integrations.
 *
 * Provider-agnostic: uses oauth2Config.tokenUrl with standard grant_type=refresh_token.
 * Returns decrypted accessToken, refreshing first if the token is expired or expiring soon.
 */

import { fetchJson } from '../../lib/utils/type-cast-helpers';
import { isRecord } from '../../lib/utils/type-guards';
import { internal } from '../_generated/api';
import type { Id } from '../_generated/dataModel';
import type { ActionCtx } from '../_generated/server';
import { encryptString } from '../lib/crypto/encrypt_string';
import { createDebugLog } from '../lib/debug_log';
import type { IntegrationWithCredentials } from './shared_types';

const debugLog = createDebugLog('DEBUG_INTEGRATIONS', '[Integrations OAuth2]');

interface TokenRefreshResponse {
  access_token: string;
  refresh_token?: string;
  token_type: string;
  expires_in?: number;
  scope?: string;
}

// Parses a token-endpoint OAuth2 error body into its standard `error` /
// `error_description` fields (RFC 6749 §5.2). Returns null for non-JSON or
// non-conforming bodies so callers fall back to a generic message.
function parseOAuthError(
  errorText: string,
): { code: string; description?: string } | null {
  try {
    const parsed: unknown = JSON.parse(errorText);
    if (isRecord(parsed) && typeof parsed.error === 'string') {
      return {
        code: parsed.error,
        description:
          typeof parsed.error_description === 'string'
            ? parsed.error_description
            : undefined,
      };
    }
    return null;
  } catch (err) {
    console.warn(
      '[Integration OAuth2] Token endpoint returned non-JSON error body:',
      err,
    );
    return null;
  }
}

export async function decryptAndRefreshIntegrationOAuth2(
  ctx: ActionCtx,
  integration: IntegrationWithCredentials,
  credentialId?: Id<'integrationCredentials'>,
  options?: { forceRefresh?: boolean },
): Promise<string> {
  const { oauth2Auth, oauth2Config } = integration;

  if (!oauth2Auth?.accessTokenEncrypted) {
    throw new Error(
      'Integration OAuth2 not authorized yet. Please complete the OAuth2 authorization flow.',
    );
  }

  const currentTime = Math.floor(Date.now() / 1000);
  const needsRefresh =
    options?.forceRefresh === true ||
    (oauth2Auth.tokenExpiry != null &&
      currentTime >= oauth2Auth.tokenExpiry - 300);

  if (!needsRefresh) {
    debugLog(
      `OAuth2 token still valid for integration ${integration._id} ` +
        `(expires in ${(oauth2Auth.tokenExpiry ?? 0) - currentTime}s)`,
    );
    return await ctx.runAction(
      internal.lib.crypto.internal_actions.decryptString,
      { jwe: oauth2Auth.accessTokenEncrypted },
    );
  }

  // Token needs refresh — check prerequisites
  const missingPrerequisites: string[] = [];
  if (!oauth2Auth.refreshTokenEncrypted) {
    missingPrerequisites.push('refresh token');
  }
  if (!oauth2Config?.clientId) {
    missingPrerequisites.push('client ID');
  }
  if (!oauth2Config?.clientSecretEncrypted) {
    missingPrerequisites.push('client secret');
  }

  if (
    missingPrerequisites.length > 0 ||
    !oauth2Auth.refreshTokenEncrypted ||
    !oauth2Config?.clientId ||
    !oauth2Config.clientSecretEncrypted
  ) {
    console.error(
      `[Integration OAuth2] Token expired for ${integration._id} but cannot refresh: ` +
        `missing ${missingPrerequisites.join(', ')}`,
    );
    if (credentialId) {
      await ctx.runMutation(
        internal.integrations.credential_mutations.updateCredentialsInternal,
        {
          credentialId,
          status: 'error',
          errorMessage: `OAuth2 refresh prerequisites missing: ${missingPrerequisites.join(', ')}`,
          lastErrorAt: Date.now(),
        },
      );
    }
    throw new Error(
      `OAuth2 access token has expired but cannot refresh: missing ${missingPrerequisites.join(', ')}. ` +
        'Please re-authorize the integration.',
    );
  }

  const expiresIn = (oauth2Auth.tokenExpiry ?? 0) - currentTime;
  console.log(
    `[Integration OAuth2] Refreshing token for integration ${integration._id} ` +
      `(${expiresIn > 0 ? `expires in ${expiresIn}s` : `expired ${-expiresIn}s ago`})`,
  );

  const [refreshToken, clientSecret] = await Promise.all([
    ctx.runAction(internal.lib.crypto.internal_actions.decryptString, {
      jwe: oauth2Auth.refreshTokenEncrypted,
    }),
    ctx.runAction(internal.lib.crypto.internal_actions.decryptString, {
      jwe: oauth2Config.clientSecretEncrypted,
    }),
  ]);

  const body = new URLSearchParams({
    client_id: oauth2Config.clientId,
    client_secret: clientSecret,
    refresh_token: refreshToken,
    grant_type: 'refresh_token',
  });

  const refreshTime = Math.floor(Date.now() / 1000);

  const response = await fetch(oauth2Config.tokenUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
    signal: AbortSignal.timeout(15_000),
  });

  if (!response.ok) {
    const errorText = await response.text();
    const parsed = parseOAuthError(errorText);
    const detail = parsed
      ? ` (${parsed.code}${parsed.description ? `: ${parsed.description}` : ''})`
      : '';
    console.error(
      `[Integration OAuth2] Token refresh failed for ${integration._id} ` +
        `(status ${response.status}):`,
      errorText,
    );
    if (credentialId) {
      await ctx.runMutation(
        internal.integrations.credential_mutations.updateCredentialsInternal,
        {
          credentialId,
          status: 'error',
          errorMessage: `OAuth2 refresh failed${detail}`,
          lastErrorAt: Date.now(),
        },
      );
    }
    throw new Error(
      `Failed to refresh OAuth2 token${detail}. Please re-authorize the integration.`,
    );
  }

  const tokens = await fetchJson<TokenRefreshResponse>(response);

  if (!tokens.access_token) {
    console.error(
      `[Integration OAuth2] Token refresh response missing access_token for ${integration._id}`,
    );
    if (credentialId) {
      await ctx.runMutation(
        internal.integrations.credential_mutations.updateCredentialsInternal,
        {
          credentialId,
          status: 'error',
          errorMessage:
            'OAuth2 token refresh returned an invalid response (no access_token)',
          lastErrorAt: Date.now(),
        },
      );
    }
    throw new Error(
      'OAuth2 token refresh returned an invalid response. Please re-authorize the integration.',
    );
  }

  const accessTokenEncrypted = await encryptString(tokens.access_token);
  const refreshTokenEncrypted = tokens.refresh_token
    ? await encryptString(tokens.refresh_token)
    : oauth2Auth.refreshTokenEncrypted;

  const tokenExpiry = tokens.expires_in
    ? refreshTime + tokens.expires_in
    : undefined;

  const updatedOAuth2Auth = {
    accessTokenEncrypted,
    refreshTokenEncrypted,
    tokenExpiry,
    scopes: tokens.scope ? tokens.scope.split(' ') : oauth2Auth.scopes,
  };

  if (credentialId) {
    await ctx.runMutation(
      internal.integrations.credential_mutations.updateCredentialsInternal,
      {
        credentialId,
        oauth2Auth: updatedOAuth2Auth,
        status: 'active',
        lastSuccessAt: Date.now(),
      },
    );
  }

  console.log(
    `[Integration OAuth2] Token refreshed successfully for integration ${integration._id} ` +
      `(new expiry in ${tokens.expires_in ?? 'unknown'}s)`,
  );

  return tokens.access_token;
}
