export interface OAuthTokenResponse {
  token_type?: string | null;
  access_token?: string | null;
  refresh_token?: string | null;
  expires_in?: string | number | null;
  exp?: string | number | null;
  refresh_expires_in?: string | number | null;
}

export interface StoredAuthTokens {
  accessToken: string;
  refreshToken: string | null;
  expiresAt: number | null;
}

interface StoreAuthTokenOptions {
  nowMs?: number;
  requireRefreshToken?: boolean;
}

interface CookieOptions {
  expiresAt?: number;
  maxAgeSeconds?: number;
}

const ACCESS_TOKEN_COOKIE = "access_token";
const REFRESH_TOKEN_COOKIE = "refresh_token";
const ACCESS_TOKEN_EXPIRES_AT_COOKIE = "access_token_expires_at";
const REFRESH_TOKEN_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
const DEFAULT_EXPIRY_SKEW_MS = 60_000;
const LEGACY_LOCAL_STORAGE_KEYS = ["accessToken", "token"];

function canUseDocument(): boolean {
  return typeof document !== "undefined";
}

function canUseLocalStorage(): boolean {
  return typeof localStorage !== "undefined";
}

function shouldUseSecureCookie(): boolean {
  return typeof window !== "undefined" && window.location?.protocol === "https:";
}

function encodeCookiePart(value: string): string {
  return encodeURIComponent(value);
}

function decodeCookiePart(value: string): string {
  try {
    return decodeURIComponent(value);
  } catch {
    return value;
  }
}

function getCookie(name: string): string | null {
  if (!canUseDocument()) {
    return null;
  }

  const encodedName = `${encodeCookiePart(name)}=`;
  const cookies = document.cookie ? document.cookie.split(";") : [];

  for (const cookie of cookies) {
    const item = cookie.trim();
    if (item.startsWith(encodedName)) {
      return decodeCookiePart(item.slice(encodedName.length));
    }
  }

  return null;
}

function setCookie(name: string, value: string, options: CookieOptions = {}): void {
  if (!canUseDocument()) {
    return;
  }

  const parts = [
    `${encodeCookiePart(name)}=${encodeCookiePart(value)}`,
    "Path=/",
    "SameSite=Lax",
  ];

  if (options.maxAgeSeconds !== undefined) {
    parts.push(`Max-Age=${Math.max(0, Math.floor(options.maxAgeSeconds))}`);
  }

  if (options.expiresAt !== undefined) {
    parts.push(`Expires=${new Date(options.expiresAt).toUTCString()}`);
  }

  if (shouldUseSecureCookie()) {
    parts.push("Secure");
  }

  document.cookie = parts.join("; ");
}

function deleteCookie(name: string): void {
  if (!canUseDocument()) {
    return;
  }

  const parts = [
    `${encodeCookiePart(name)}=`,
    "Path=/",
    "Max-Age=0",
    "Expires=Thu, 01 Jan 1970 00:00:00 GMT",
    "SameSite=Lax",
  ];

  if (shouldUseSecureCookie()) {
    parts.push("Secure");
  }

  document.cookie = parts.join("; ");
}

function parsePositiveSeconds(value: string | number | null | undefined): number | null {
  if (value === null || value === undefined || value === "") {
    return null;
  }

  const seconds = typeof value === "number" ? value : Number.parseInt(value, 10);
  if (!Number.isFinite(seconds) || seconds <= 0) {
    return null;
  }

  return seconds;
}

function normalizeIsoFraction(value: string): string {
  return value.replace(/\.(\d{3})\d+(Z|[+-]\d{2}:?\d{2})$/, ".$1$2");
}

function parseAbsoluteTime(value: string | number | null | undefined): number | null {
  if (value === null || value === undefined || value === "") {
    return null;
  }

  if (typeof value === "number") {
    if (!Number.isFinite(value) || value <= 0) {
      return null;
    }
    return value < 1_000_000_000_000 ? value * 1000 : value;
  }

  const numericValue = Number(value);
  if (Number.isFinite(numericValue) && numericValue > 0) {
    return numericValue < 1_000_000_000_000 ? numericValue * 1000 : numericValue;
  }

  const parsed = Date.parse(normalizeIsoFraction(value));
  return Number.isFinite(parsed) ? parsed : null;
}

export function resolveAccessTokenExpiresAt(
  tokenResponse: OAuthTokenResponse,
  nowMs = Date.now(),
): number | null {
  const absoluteExpiresAt = parseAbsoluteTime(tokenResponse.exp);
  if (absoluteExpiresAt !== null) {
    return absoluteExpiresAt;
  }

  const expiresInSeconds = parsePositiveSeconds(tokenResponse.expires_in);
  if (expiresInSeconds !== null) {
    return nowMs + expiresInSeconds * 1000;
  }

  return null;
}

export function getAccessToken(): string | null {
  return getCookie(ACCESS_TOKEN_COOKIE);
}

export function getRefreshToken(): string | null {
  return getCookie(REFRESH_TOKEN_COOKIE);
}

export function getAccessTokenExpiresAt(): number | null {
  const rawValue = getCookie(ACCESS_TOKEN_EXPIRES_AT_COOKIE);
  if (!rawValue) {
    return null;
  }

  const expiresAt = Number.parseInt(rawValue, 10);
  return Number.isFinite(expiresAt) && expiresAt > 0 ? expiresAt : null;
}

export function isAccessTokenExpired(
  nowMs = Date.now(),
  skewMs = DEFAULT_EXPIRY_SKEW_MS,
): boolean {
  const expiresAt = getAccessTokenExpiresAt();
  if (expiresAt === null) {
    return true;
  }

  return nowMs + skewMs >= expiresAt;
}

function clearLegacyLocalStorageTokens(): void {
  if (!canUseLocalStorage()) {
    return;
  }

  for (const key of LEGACY_LOCAL_STORAGE_KEYS) {
    localStorage.removeItem(key);
  }
}

export function clearAuthTokens(): void {
  deleteCookie(ACCESS_TOKEN_COOKIE);
  deleteCookie(REFRESH_TOKEN_COOKIE);
  deleteCookie(ACCESS_TOKEN_EXPIRES_AT_COOKIE);
  clearLegacyLocalStorageTokens();
}

export function storeAuthTokens(
  tokenResponse: OAuthTokenResponse,
  options: StoreAuthTokenOptions = {},
): StoredAuthTokens {
  const accessToken = tokenResponse.access_token?.trim();
  const nextRefreshToken = tokenResponse.refresh_token?.trim() || getRefreshToken();

  if (!accessToken) {
    clearAuthTokens();
    throw new Error("Missing access_token");
  }

  if (options.requireRefreshToken && !nextRefreshToken) {
    clearAuthTokens();
    throw new Error("Missing refresh_token");
  }

  const nowMs = options.nowMs ?? Date.now();
  const expiresAt = resolveAccessTokenExpiresAt(tokenResponse, nowMs);

  if (expiresAt !== null) {
    setCookie(ACCESS_TOKEN_COOKIE, accessToken, {
      expiresAt,
      maxAgeSeconds: Math.max(0, Math.floor((expiresAt - nowMs) / 1000)),
    });
    setCookie(ACCESS_TOKEN_EXPIRES_AT_COOKIE, String(expiresAt), {
      expiresAt,
      maxAgeSeconds: Math.max(0, Math.floor((expiresAt - nowMs) / 1000)),
    });
  } else {
    setCookie(ACCESS_TOKEN_COOKIE, accessToken);
    deleteCookie(ACCESS_TOKEN_EXPIRES_AT_COOKIE);
  }

  if (tokenResponse.refresh_token?.trim()) {
    setCookie(REFRESH_TOKEN_COOKIE, tokenResponse.refresh_token.trim(), {
      maxAgeSeconds:
        parsePositiveSeconds(tokenResponse.refresh_expires_in) ??
        REFRESH_TOKEN_MAX_AGE_SECONDS,
    });
  }

  clearLegacyLocalStorageTokens();

  return {
    accessToken,
    refreshToken: nextRefreshToken,
    expiresAt,
  };
}
