import { createHash } from 'node:crypto';

import type {
  IAuthenticateGeneric,
  ICredentialDataDecryptedObject,
  ICredentialTestRequest,
  ICredentialType,
  IHttpRequestHelper,
  IHttpRequestOptions,
  INodeProperties,
  Icon,
} from 'n8n-workflow';

const GELIOS_API_BASE_URL = 'https://api.geliospro.com';
const GELIOS_REFRESH_URL = `${GELIOS_API_BASE_URL}/api/v1/auth/refresh`;
const RECENT_ROTATION_TTL_MS = 30_000;
const MAX_RECENT_ROTATIONS = 64;

interface RotatedTokens extends ICredentialDataDecryptedObject {
  accessToken: string;
  refreshToken: string;
  accessExpiresAt: string;
}

interface RecentRotation {
  expiresAt: number;
  tokens: RotatedTokens;
}

const refreshInFlight = new Map<string, Promise<RotatedTokens>>();
const recentRotations = new Map<string, RecentRotation>();

export class NdcProviderRotatingAccessApi implements ICredentialType {
  name = 'ndcProviderRotatingAccessApi';
  displayName = 'NDC Provider Rotating Access API';
  icon: Icon = {
    light: 'file:../icons/ndc.svg',
    dark: 'file:../icons/ndc.dark.svg',
  };
  documentationUrl = '';
  properties: INodeProperties[] = [
    {
      displayName: 'Refresh Token',
      name: 'refreshToken',
      type: 'string',
      typeOptions: { password: true },
      default: '',
      required: true,
      description: 'Rotating refresh token issued by the approved provider profile',
    },
    {
      displayName: 'Access Token',
      name: 'accessToken',
      type: 'hidden',
      typeOptions: {
        expirable: true,
        password: true,
      },
      default: '',
    },
    {
      displayName: 'Access Token Expires At',
      name: 'accessExpiresAt',
      type: 'hidden',
      default: '',
    },
  ];

  async preAuthentication(
    this: IHttpRequestHelper,
    credentials: ICredentialDataDecryptedObject,
  ): Promise<ICredentialDataDecryptedObject> {
    const refreshToken = credentials.refreshToken;
    if (typeof refreshToken !== 'string' || refreshToken.trim() === '') {
      throw new Error('provider_refresh_token_required');
    }

    const key = createHash('sha256').update(refreshToken, 'utf8').digest('hex');
    const now = Date.now();
    pruneRecentRotations(now);
    const recent = recentRotations.get(key);
    if (recent && recent.expiresAt > now) return { ...recent.tokens };

    let pending = refreshInFlight.get(key);
    if (!pending) {
      pending = rotateTokens(this, refreshToken)
        .then((tokens) => {
          rememberRotation(key, tokens);
          return tokens;
        })
        .finally(() => refreshInFlight.delete(key));
      refreshInFlight.set(key, pending);
    }

    return { ...(await pending) };
  }

  authenticate: IAuthenticateGeneric = {
    type: 'generic',
    properties: {
      headers: {
        Authorization: '=Bearer {{$credentials.accessToken}}',
      },
    },
  };

  get test(): ICredentialTestRequest {
    return {
      request: {
        method: 'GET',
        url: `${GELIOS_API_BASE_URL}/api/v1/auth`,
      },
    };
  }
}

async function rotateTokens(
  helper: IHttpRequestHelper,
  refreshToken: string,
): Promise<RotatedTokens> {
  let response: unknown;
  try {
    response = await helper.helpers.httpRequest({
      method: 'POST',
      url: GELIOS_REFRESH_URL,
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      body: { refresh_token: refreshToken },
      json: true,
    } satisfies IHttpRequestOptions);
  } catch {
    throw new Error('provider_access_refresh_failed');
  }

  if (!isRecord(response)) throw new Error('provider_access_refresh_failed');
  const accessToken = response.access_token;
  const nextRefreshToken = response.refresh_token;
  const tokenType = response.token_type;
  const expiresIn = response.expires_in;
  if (
    typeof accessToken !== 'string'
    || accessToken.trim() === ''
    || typeof nextRefreshToken !== 'string'
    || nextRefreshToken.trim() === ''
    || typeof tokenType !== 'string'
    || tokenType.toLowerCase() !== 'bearer'
    || !Number.isSafeInteger(expiresIn)
    || Number(expiresIn) <= 0
  ) {
    throw new Error('provider_access_refresh_failed');
  }

  return {
    accessToken,
    refreshToken: nextRefreshToken,
    accessExpiresAt: new Date(Date.now() + Number(expiresIn) * 1000).toISOString(),
  };
}

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

function pruneRecentRotations(now: number): void {
  for (const [key, value] of recentRotations) {
    if (value.expiresAt <= now) recentRotations.delete(key);
  }
}

function rememberRotation(key: string, tokens: RotatedTokens): void {
  while (recentRotations.size >= MAX_RECENT_ROTATIONS) {
    const oldest = recentRotations.keys().next().value as string | undefined;
    if (!oldest) break;
    recentRotations.delete(oldest);
  }
  recentRotations.set(key, {
    expiresAt: Date.now() + RECENT_ROTATION_TTL_MS,
    tokens: { ...tokens },
  });
}
