import { useAuth } from '@clerk/nextjs';
import createClient from 'openapi-fetch';
import type { FetchResponse } from 'openapi-fetch';
import type {
  HttpMethod,
  MediaType,
  PathsWithMethod,
} from 'openapi-typescript-helpers';

import { toast } from '@/components/toast/Toast';
import { getDeviceId } from '@/utils/device';
import { isDevOrStaging } from '@/utils/environment';

import { paths } from './gen';

let lowestObservedDeltaFromServerTimeMs = Infinity;

export const getLowestObservedDeltaFromServerTimeMs = () => {
  return lowestObservedDeltaFromServerTimeMs;
};

// Hey! This is important!
// Users can change their system clock. This breaks things like client-side isClipTimedOut checks unless we know how far off the client clock is from the server clock.
// For example, clips might look like they're never timed out if the client clock is behind the server clock, or ALWAYS timed out if the client clock is ahead of the server clock.
// (This happened to a VIP, which is why I'm in here doing this right now.)
// This has proven itself to be phenomenally annoying to debug, so please don't delete this unless you, like, really really want to.
export const getMsSinceServerReportedTime = (time: number | string | Date) => {
  if (!isFinite(lowestObservedDeltaFromServerTimeMs)) {
    return Date.now() - new Date(time).getTime();
  }

  const adjustedNow = new Date(
    Date.now() - lowestObservedDeltaFromServerTimeMs
  );

  const serverTime = new Date(time);
  return adjustedNow.getTime() - serverTime.getTime();
};

export type ApiClient = ReturnType<typeof createClient<paths>>;

/**
 * Helper type to get the shape of a successful API response
 */
export type ApiResponse<
  M extends HttpMethod | Uppercase<HttpMethod>,
  P extends PathsWithMethod<paths, Lowercase<M>>,
> =
  Lowercase<M> extends HttpMethod
    ? Lowercase<M> extends keyof paths[P]
      ? paths[P][Lowercase<M>] extends Record<string, unknown>
        ? NonNullable<
            FetchResponse<
              paths[P][Lowercase<M>],
              Lowercase<M>,
              MediaType
            >['data']
          >
        : never
      : never
    : never;

let sunoSessionId: string | null = null;
let sunoSessionIdTTL: number = new Date().getTime();
async function getBrowserToken() {
  try {
    const browserData = { timestamp: new Date().getTime() };
    // Making this semi-structured so we can pass some sealed client intel
    // in a future PR.
    return JSON.stringify({ token: btoa(JSON.stringify(browserData)) });
  } catch (e) {
    return JSON.stringify({ token: 'error' });
  }
}

export function getSunoSessionId() {
  const currentTimestamp = new Date().getTime();
  if (currentTimestamp >= sunoSessionIdTTL!) return null;
  return sunoSessionId;
}

function updateSunoSessionId(res: Response) {
  const currentTimestamp = new Date().getTime();
  const newSessionId = res.headers.get('Session-Id');
  if (newSessionId) {
    if (newSessionId != sunoSessionId) {
      sunoSessionId = newSessionId;
      sunoSessionIdTTL = currentTimestamp + 15 * 60 * 1000;
    } else if (currentTimestamp >= sunoSessionIdTTL!) {
      sunoSessionId = newSessionId;
      sunoSessionIdTTL = currentTimestamp + 15 * 60 * 1000;
    }
  }
}

export function clearSunoSessionId() {
  sunoSessionId = null;
  sunoSessionIdTTL = new Date().getTime();
}

/**
 * We rely on the Clerk `useAuth` hook to get the token, so we do this to pull
 * it out for use in non-React land.
 */
let getAuthToken: (() => Promise<string | null>) | null = null;
export function setGetAuthToken(getToken: () => Promise<string | null>) {
  getAuthToken = getToken;
}

let apiClient: ApiClient | null = null;
export function getInstance() {
  if (!apiClient) {
    const apiBase = process.env.NEXT_PUBLIC_API_BASE_ECS;
    const deviceId = getDeviceId();
    apiClient = createClient<paths>({
      baseUrl: apiBase || '/',
      fetch: async (url: RequestInfo | URL, options?: RequestInit) => {
        // Not that we should be fetching with the API client in SSR, but
        // `useAuth()` does not expose `getToken` in SSR
        const token = getAuthToken ? await getAuthToken() : null;
        const browserToken = await getBrowserToken();
        return fetch(url, {
          ...options,
          headers: {
            ...options?.headers,
            Authorization: `Bearer ${token}`,
            ...(deviceId ? { 'Device-Id': deviceId } : {}),
            ...(browserToken ? { 'Browser-Token': browserToken } : {}),
          },
        }).then((res) => {
          const dateHeader = res.headers.get('date');
          if (dateHeader) {
            const currentTimeServer = new Date(dateHeader);
            const currentTimeClient = new Date();

            const deltaMs =
              currentTimeClient.getTime() - currentTimeServer.getTime();

            if (deltaMs < lowestObservedDeltaFromServerTimeMs) {
              lowestObservedDeltaFromServerTimeMs = Math.min(
                lowestObservedDeltaFromServerTimeMs,
                currentTimeClient.getTime() - currentTimeServer.getTime()
              );

              if (isDevOrStaging) {
                console.log(
                  'Server timestamp received. Client clock is ahead by at most',
                  lowestObservedDeltaFromServerTimeMs,
                  'ms'
                );
              }
            }
          }

          if (
            res.status === 429 &&
            res.url.endsWith('/update_reaction_type/')
          ) {
            toast({
              title:
                'Rate limit exceeded. Please wait before retrying actions.',
              status: 'error',
              duration: 2000,
              isClosable: true,
            });
          }
          updateSunoSessionId(res);
          return res;
        });
      },
    });
  }
  return apiClient;
}

export function useApiClient() {
  const { getToken } = useAuth();
  setGetAuthToken(getToken);
  return getInstance();
}
