import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';

import { isVideoHooksPath } from '@/components/hooksPlayer/utils';
import { ControlSliderKey } from '@/state/createV2Store';
import { Project } from '@/state/projectStore';
import { ModelType, SessionStore } from '@/state/sessionStore';
import {
  CREATE_SLIDER_DEFAULT_VALUES,
  TRUSTED_CDN_HOSTNAME,
} from '@/utils/constants';

import {
  ALL_AURA_IMAGE_URLS,
  REDIRECTED_FROM_PARAM,
  REDIRECTED_FROM_VALUES,
  SIGNUP_SOURCE_VALUES,
  UNTITLED_PROJECT_NAME,
} from './constants';
import { isDev, isStaging } from './environment';
import { isSubscriber } from './session';

/**
 * Validates that a string contains only numeric characters (digits, optional decimal point, optional negative sign) or is empty
 * @param value - The string to validate
 * @returns true if the string is empty or a valid number format (including negatives and decimals), false otherwise
 */
export const allowOnlyNumbers = (value: string): boolean => {
  return value === '' || /^-?\d*\.?\d*$/.test(value);
};

export const getProjectName = (project: Project) => {
  return project?.name && project?.name.length > 0
    ? project.name
    : UNTITLED_PROJECT_NAME;
};

// Get Seconds from input such as 01:02.312, returns null if input is invalid.
export const decodeTimeFormat = (input?: string | undefined) => {
  if (!input) {
    return null;
  }

  const regex = /^\d+:\d+(\.\d+)?$/;
  if (!regex.test(input)) {
    return null;
  }

  const tokens = input.split(':').reverse();
  if (tokens.length != 2) {
    return null;
  }

  const [secondsToken, minutesToken] = tokens;
  const [seconds, milliseconds] = secondsToken.split('.');
  const minutes = parseInt(minutesToken);
  const secondsNum = parseInt(seconds);

  if (isNaN(secondsNum) || isNaN(minutes)) {
    return null;
  }

  const millisecondsNum = milliseconds ? parseFloat(`0.${milliseconds}`) : 0;
  return Number((secondsNum + millisecondsNum + minutes * 60).toFixed(2));
};

// From input in seconds such as 62, get MM:SS format representation, so 01:02 in this case.
export const encodeTimeFormat = (
  input?: number | null | undefined,
  toFixed: number = 0,
  zeroPrefix: boolean = true
) => {
  if (!input && input !== 0) {
    return null;
  }

  const totalSeconds = input;
  let seconds = Math.floor(totalSeconds % 60);
  let minutes = Math.floor(totalSeconds / 60);
  const fractionalPart = (totalSeconds % 1).toFixed(toFixed).slice(2);

  if (seconds === 60) {
    minutes++;
    seconds = 0;
  }

  return (
    minutes.toLocaleString('en-US', {
      minimumIntegerDigits: zeroPrefix ? 2 : 1,
    }) +
    ':' +
    seconds.toLocaleString('en-US', { minimumIntegerDigits: 2 }) +
    (toFixed > 0 ? `.${fractionalPart}` : '')
  );
};
export const encodeTimeFormatWithMilliseconds = (
  input?: number | null | undefined
) => {
  if (!input && input !== 0) {
    return null;
  }
  const minutes = Math.floor(input / 60);
  const seconds = Math.floor(input % 60);
  const ms = Math.floor((input % 1) * 1000);
  return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
};

export const formatDate = (date: Date, dateTimeFormatParams: any = {}) => {
  const { timeStyle, ...otherParams } = dateTimeFormatParams;

  const options = {
    dateStyle: 'short',
    timeZone: 'America/New_York',
    ...otherParams,
  };

  if (timeStyle !== null && timeStyle !== undefined) {
    options.timeStyle = timeStyle;
  } else if (!('timeStyle' in dateTimeFormatParams)) {
    options.timeStyle = 'short';
  }

  return new Intl.DateTimeFormat('en-US', options).format(date);
};

// May be called in a server environment
export const formatDateString = (
  dateString: string,
  locale?: Intl.LocalesArgument
): string => {
  const options: Intl.DateTimeFormatOptions = {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  };

  return new Date(dateString).toLocaleDateString(locale, options);
};

export const formatDateStringWTime = (
  dateString: string,
  locale?: Intl.LocalesArgument,
  short?: boolean
): string => {
  const date = new Date(dateString);

  if (short) {
    return (
      date.toLocaleDateString('en-US', {
        month: 'numeric',
        day: 'numeric',
        year: '2-digit',
      }) +
      ' at ' +
      date
        .toLocaleTimeString('en-US', {
          hour: 'numeric',
          minute: '2-digit',
          hour12: true,
        })
        .toLowerCase()
    );
  }

  const options: Intl.DateTimeFormatOptions = {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
  };

  return new Date(dateString).toLocaleDateString(locale, options);
};

export const getRelativeTime = (dateString: string) => {
  const now = Date.now();
  const past = new Date(dateString).getTime();

  // Check if date is valid
  if (isNaN(past)) {
    throw new Error('Invalid date string');
  }

  const diffMs = now - past;

  // If date is in the future, return as negative
  if (diffMs < 0) {
    const futureDiff = Math.abs(diffMs);
    return `in ${formatTimeDifference(futureDiff)}`;
  }

  // Less than 30 seconds
  if (diffMs < 30000) {
    return 'just now';
  }

  return formatTimeDifference(diffMs) + ' ago';
};

export const formatTimeDifference = (diffMs: number) => {
  const seconds = Math.floor(diffMs / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);
  const weeks = Math.floor(days / 7);
  const months = Math.floor(days / 30.44);
  const years = Math.floor(days / 365.25);
  const parts = [];
  if (years > 0) {
    parts.push(`${years}y`);
    const remainingMonths = months % 12;
    if (remainingMonths > 0) {
      parts.push(`${remainingMonths}mo`);
    }
  } else if (months > 0) {
    parts.push(`${months}mo`);
    const remainingWeeks = Math.floor((days % 30.44) / 7);
    if (remainingWeeks > 0) {
      parts.push(`${remainingWeeks}w`);
    }
  } else if (weeks > 0) {
    parts.push(`${weeks}w`);
    const remainingDays = days % 7;
    if (remainingDays > 0) {
      parts.push(`${remainingDays}d`);
    }
  } else if (days > 0) {
    parts.push(`${days}d`);
    const remainingHours = hours % 24;
    if (remainingHours > 0) {
      parts.push(`${remainingHours}h`);
    }
  } else if (hours > 0) {
    parts.push(`${hours}h`);
    const remainingMinutes = minutes % 60;
    if (remainingMinutes > 0) {
      parts.push(`${remainingMinutes}m`);
    }
  } else {
    parts.push(`${minutes}m`);
  }

  return parts.slice(0, 2).join(', ');
};

export const DEFAULT_V3_MODEL_NAME = 'chirp-v3-5';
export const DEFAULT_V4_MODEL_NAME = 'chirp-v4';
export const DEFAULT_AUK_MODEL_NAME = 'chirp-auk';
export const DEFAULT_URBO_MODEL_NAME = 'chirp-auk-turbo';
export const DEFAULT_BLUEJAY_MODEL_NAME = 'chirp-bluejay';
export const DEFAULT_CROW_MODEL_NAME = 'chirp-crow';

/**
 * Model feature values - extracted from ModelType schema
 * This gives you autocomplete for the available feature values
 */
export type ModelFeatureValue = NonNullable<ModelType['features']>[number];

/**
 * Helper function to check if a model is version 3.5 or lower
 * @param modelName - The model name to check
 * @returns true if the model is version 3.5 or lower, false otherwise
 */
export const isModelV35OrLower = (modelName?: string | null): boolean => {
  return (
    (modelName?.includes('chirp-v3-5') ||
      modelName?.includes('chirp-v3') ||
      modelName?.includes('chirp-v2')) ??
    false
  );
};

export const getDefaultModel = (models: ModelType[]) => {
  const defaultModelName = models.find(
    (model: ModelType) => model.can_use && model.is_default_model
  )?.external_key;
  return defaultModelName || DEFAULT_V3_MODEL_NAME;
};

/**
 * Check if a user can use a model by its external key
 * @param billingModels - Array of billing models from session
 * @param externalKey - The external key to search for (e.g., 'chirp-bluejay', 'chirp-v4')
 * @returns True if the user can use the model, false otherwise
 */
export const canUseModel = (
  billingModels: ModelType[] | undefined,
  externalKey: string
): boolean => {
  const model = billingModels?.find(
    (model) => model.external_key === externalKey
  );
  return model?.can_use ?? false;
};

export const PLAYLIST_CLIPS_PAGE_SIZE = 50;
export const PLAYLISTS_PAGE_SIZE = 12;

export const FLAGGING_REASON_MIN_LENGTH = 10;
export const FLAGGING_REASON_MAX_LENGTH = 100;

/* Style related utils */
export const DEFAULT_FOCUS_PARAMS = {
  borderColor: 'gray',
  boxShadow: '0 0 0 0.5px black',
};

export const truncateText = (text: string, length = 20) =>
  text.length > length ? `${text.substring(0, length - 3)}...` : text;

export function toShuffled<T>(elementArray: T[]): T[] {
  return (elementArray || [])
    .map((el) => ({ value: el, sort: Math.random() }))
    .sort((a, b) => a.sort - b.sort)
    .map((item) => item.value);
}

/**
 * Picks a randomized selection of `n` elements from the given array
 */
export function getRandomSubset<T>(elements: T[], size = elements.length) {
  const targetSize = Math.min(size, elements.length);

  // Do not modify original array
  const shuffled = elements.slice();

  // Fisher-Yates shuffle, but only up to size elements
  for (let i = 0; i < targetSize; i++) {
    const j = i + Math.floor(Math.random() * (elements.length - i));
    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
  }

  // Truncate the unnecessary elements
  shuffled.length = targetSize;

  return shuffled;
}

// quantize: round an input value to the nearest quantizeValue
// Example: to quantize an input number to the nearest multiple of 0.04:
// ```quantize(input, 0.04)```
export const quantize = (input: number, quantizeValue: number) => {
  return Math.round(input / quantizeValue) * quantizeValue;
};

export const isWavDownloadAvailable = (session: SessionStore) => {
  // TODO: Migrate to PlanFeature.WavDownload once this flag is in prod
  return isSubscriber(session);
};

export const capitalizeFirstLetter = (word: string) => {
  if (!word) {
    return '';
  }
  return word.charAt(0).toUpperCase() + word.slice(1);
};

export const getCountString = (count: number, hideZero = false) => {
  if (!count) {
    return hideZero ? '' : '0';
  }
  if (count >= 1000 && count < 10000 - 50) {
    return (count / 1000).toFixed(1) + 'K';
  } else if (count >= 10000 - 50 && count < 1000000 - 500) {
    return (count / 1000).toFixed(0) + 'K';
  } else if (count >= 1000000 - 500) {
    return (count / 1000000).toFixed(1) + 'M';
  }
  return count.toFixed(0);
};

export const valueOrDefault = (value: any, defaultValue: any) => {
  return value !== undefined ? value : defaultValue;
};

export const sleep = (ms: number) =>
  new Promise((resolve) => setTimeout(resolve, ms));

/**
 * Returns a CSS color variable
 *
 * Input is parsed for words to support camelCase or other formats
 */
export function colorVar(srcName: string, alpha?: number) {
  // lowercase the input if there are no lowercase charactersto support SCREAMING_SNAKE_CASE
  const name = srcName.match(/a-z/) ? srcName : srcName.toLowerCase();
  // split into words: "lowercase", "Title", "[numbers]"
  const words = name.match(/([A-Z]?[a-z]+|[A-Z]|[0-9]+)/g) || [name];
  // kebab
  const key = words.map((word) => word.toLowerCase()).join('-');
  // var()-ify
  return typeof alpha === 'number'
    ? `rgb(var(--rgb-${key}) / ${alpha})`
    : `var(--color-${key})`;
}

export const isUSAViaVercel = (country: string | undefined | null) => {
  return country?.toLowerCase() === 'us';
};

/**
 * Formats a ratio of two numbers as a percentage string
 *
 * e.g. formatting the size of an element relative to its parent as a percentage
 */
export function formatRatioAsPercent(
  size: number | undefined,
  parentSize: number | undefined,
  percision = 2
) {
  const decimals = 10 ** percision;
  if (size === undefined || !parentSize) {
    return undefined;
  }
  return `${Math.round((size / parentSize) * 100 * decimals) / decimals}%`;
}

export const responsiveGrid = (hasPreviewClip: boolean) => {
  return hasPreviewClip
    ? 'grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5'
    : 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6';
};

export const getRandomAuraURL = () => {
  const randomIndex = Math.floor(Math.random() * ALL_AURA_IMAGE_URLS.length);
  return ALL_AURA_IMAGE_URLS[randomIndex];
};

/**
 * Very legit verification system
 */
const VERIFIED_HANDLES = new Set(
  isDev || isStaging
    ? ['timbaland', 'flosstradamus', 'kmck', 'sob', 'reet']
    : [
        'timbaland',
        'flosstradamus',
        'ianndior',
        'coffeyanderson',
        'imoliver',
        'black_party',
        'buddahblessthisbeat',
      ]
);
export function isVerifiedProfile(
  profile: { handle?: string },
  handles?: string[]
) {
  return (
    VERIFIED_HANDLES.has(profile.handle || '') ||
    handles?.includes(profile.handle || '')
  );
}
const SECRET_STATS_HANDLES = new Set(
  isDev || isStaging ? ['timbaland', 'kmck', 'sob'] : ['timbaland']
);
export function isSecretStatsProfile(profile: { handle?: string }) {
  return SECRET_STATS_HANDLES.has(profile.handle || '');
}

/**
 * Creates redirect URLs for Clerk authentication with tracking parameters,
 * specifically for the Clerk sign up modal.
 * `clerk.openSignUp` uses:
 *    - `forceRedirectUrl` to determine the sign up redirect URL
 *    - `signInForceRedirectUrl` to determine the sign in redirect URL
 *
 * @param redirectUrl - Base URL to redirect to
 * @param options - Optional query parameters to append to the URL
 * @returns Object with forceRedirectUrl and signInForceRedirectUrl
 */
export function getClerkSignUpRedirectProps(
  redirectUrl: string,
  options?: Record<string, string | undefined>
) {
  const [basePath, queryParams] = redirectUrl.split('?');

  // Parse existing query parameters
  const existingParams = new URLSearchParams(queryParams);
  const optionsParams = new URLSearchParams(
    // @ts-expect-error - Ignoring type error as we're filtering out undefined values
    Object.fromEntries(
      Object.entries(options || {}).filter(([, value]) => value !== undefined)
    )
  );

  const forceRedirectUrl = `${basePath}?${new URLSearchParams({
    ...Object.fromEntries(existingParams),
    ...Object.fromEntries(optionsParams),
    [REDIRECTED_FROM_PARAM]: REDIRECTED_FROM_VALUES.SIGNUP,
  })}`;
  const signInForceRedirectUrl = `${basePath}?${new URLSearchParams({
    ...Object.fromEntries(existingParams),
    ...Object.fromEntries(optionsParams),
    [REDIRECTED_FROM_PARAM]: REDIRECTED_FROM_VALUES.SIGNIN,
  })}`;

  return {
    forceRedirectUrl,
    signInForceRedirectUrl,
  };
}

/**
 * Creates redirect URLs for Clerk authentication with tracking parameters,
 * specifically for the Clerk sign in modal.
 * `clerk.openSignIn` uses:
 *    - `forceRedirectUrl` to determine the sign in redirect URL
 *    - `signUpForceRedirectUrl` to determine the sign up redirect URL
 *
 * @param redirectUrl - Base URL to redirect to
 * @param options - Optional query parameters to append to the URL
 * @returns Object with forceRedirectUrl and signUpForceRedirectUrl
 */
export function getClerkSignInRedirectProps(
  redirectUrl: string,
  options?: Record<string, string | undefined>
) {
  const [basePath, queryParams] = redirectUrl.split('?');

  // Parse existing query parameters
  const existingParams = new URLSearchParams(queryParams);
  const optionsParams = new URLSearchParams(
    // @ts-expect-error - Ignoring type error as we're filtering out undefined values
    Object.fromEntries(
      Object.entries(options || {}).filter(([, value]) => value !== undefined)
    )
  );

  const forceRedirectUrl = `${basePath}?${new URLSearchParams({
    ...Object.fromEntries(existingParams),
    ...Object.fromEntries(optionsParams),
    [REDIRECTED_FROM_PARAM]: REDIRECTED_FROM_VALUES.SIGNIN,
  })}`;
  const signUpForceRedirectUrl = `${basePath}?${new URLSearchParams({
    ...Object.fromEntries(existingParams),
    ...Object.fromEntries(optionsParams),
    [REDIRECTED_FROM_PARAM]: REDIRECTED_FROM_VALUES.SIGNUP,
  })}`;

  return {
    forceRedirectUrl,
    signUpForceRedirectUrl,
  };
}

export function getSignUpSource(pathname: string) {
  switch (true) {
    case pathname.startsWith('/song/'):
      return SIGNUP_SOURCE_VALUES.SONG_PAGE;
    case pathname === '/':
      return SIGNUP_SOURCE_VALUES.HOME_PAGE;
    case pathname === '/create':
      return SIGNUP_SOURCE_VALUES.CREATE_PAGE;
    case pathname === '/me':
      return SIGNUP_SOURCE_VALUES.LIBRARY_PAGE;
    case pathname === '/search':
      return SIGNUP_SOURCE_VALUES.SEARCH_PAGE;
    case pathname === '/home':
      return SIGNUP_SOURCE_VALUES.SPLASH_PAGE;
    case pathname.startsWith('/profile') || pathname.startsWith('/@'):
      return SIGNUP_SOURCE_VALUES.PROFILE_PAGE;
    case pathname === '/notifications':
      return SIGNUP_SOURCE_VALUES.NOTIFICATIONS_PAGE;
    default:
      return SIGNUP_SOURCE_VALUES.MOBILE_SONG_PAGE;
  }
}

export function isActiveNavTab(contextName: string, pathname: string) {
  switch (contextName) {
    case 'home':
      return pathname === '/';
    case 'create':
      return pathname === '/create' || pathname === '/create/v2';
    case 'search':
      return pathname === '/search';
    case 'library':
      return (
        pathname === '/me' ||
        pathname.startsWith('/me/') ||
        pathname.startsWith('/playlist/') ||
        pathname.startsWith('/song/')
      );
    case 'invites':
      return pathname === '/invites';
    case 'notifications':
      return pathname === '/notifications';
    case 'hooks':
      return isVideoHooksPath(pathname);
    case 'live-radio':
      return pathname === '/live-radio';
    case 'sunoverse':
      return pathname === '/sunoverse';
    default:
      return false;
  }
}

export function fnv1a(input: string) {
  let hash = 0x811c9dc5; // 32-bit
  for (let i = 0; i < input.length; i++) {
    hash ^= input.charCodeAt(i);
    hash +=
      (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
  }
  return hash >>> 0;
}

export function normalizeToScale(
  value: number,
  srcMin: number,
  srcMax: number,
  destMin = 0,
  destMax = 1
) {
  const srcRange = srcMax - srcMin;
  const destRange = destMax - destMin;
  return srcRange
    ? ((value - srcMin) / srcRange) * destRange + destMin
    : 0.5 * destRange + destMin;
}

/**
 * Checks if a model supports a particular feature (e.g. control sliders, tag upsample)
 *
 * @param modelName The model name/version to check
 * @param allModels The list of all models (from session.billingModels)
 * @param featureName The feature name to check
 * @returns True if the model supports feature
 */
export const modelSupportsFeature = (
  modelName: string,
  allModels: ModelType[],
  featureName: ModelFeatureValue
): boolean => {
  if (!modelName) {
    return false;
  }

  return allModels.some(
    (model: ModelType) =>
      model.external_key === modelName && model.features?.includes(featureName)
  );
};

export const redirectToCreatePage = (
  session?: SessionStore,
  pathname?: string,
  router?: AppRouterInstance
) => {
  if (!router || !pathname || !session) {
    return;
  }
  const createPaths = ['/create', '/create/v2'];
  const preferredCreatePath = session.flags?.['create-v2-page']
    ? '/create/v2'
    : '/create';
  if (!createPaths.includes(pathname)) {
    router.push(preferredCreatePath);
  }
};

/**
 * Checks if any control sliders have been modified from their default values
 *
 * @param controlSliders The current control slider values
 * @param defaultValues Optional custom default values to check against
 * @returns True if any slider has been modified from its default value
 */
export const hasChangedControlSliders = (
  controlSliders: Partial<{ [key in ControlSliderKey]: number | undefined }>,
  defaultValues: {
    [key in ControlSliderKey]: number;
  } = CREATE_SLIDER_DEFAULT_VALUES
): boolean => {
  return Object.entries(defaultValues).some(([key, defaultValue]) => {
    const currentValue = controlSliders[key as ControlSliderKey];
    return currentValue !== undefined && currentValue !== defaultValue;
  });
};

/**
 * Gets all root clip IDs from a list of personas
 * @param personas Array of personas or single persona
 * @returns Array of root clip IDs
 */
export const getRootClipIdsFromPersonas = (personas: any[] | any): string[] => {
  if (!personas) return [];

  // Handle single persona case
  if (!Array.isArray(personas)) {
    return personas.clip?.id ? [personas.clip.id] : [];
  }

  // Handle array of personas
  return personas
    .filter((persona) => persona?.clip?.id)
    .map((persona) => persona.clip.id);
};

export const getFirstNSongsFromPlaylist = (
  playlist: any,
  n: number = 4
): string[] => {
  if (!playlist || !playlist.playlist_clips) return [];

  return playlist.playlist_clips
    .slice(0, n)
    .map((playlistClip: any) => playlistClip.clip.id);
};

/**
 * Helper function to validate resource URL is from trusted domain
 */
export const isValidResourceUrl = (url: string | null | undefined): boolean => {
  if (!url) return false;
  try {
    const urlObj = new URL(url);
    return urlObj.hostname === TRUSTED_CDN_HOSTNAME;
  } catch {
    return false;
  }
};

/**
 * Helper function to strip asterisks from description for copying
 * @param description - The description to strip asterisks from
 * @returns The description with asterisks removed
 */
export const stripAsterisks = (description: string): string => {
  return description
    .replace(/\*\*([^*]+)\*\*/g, '$1')
    .replace(/\*([^*]+)\*/g, '$1');
};

export function formatCredits(credits: number): string {
  if (credits >= 1000000) {
    const millions = credits / 1000000;
    return credits % 1000000 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
  } else if (credits >= 1000) {
    const thousands = credits / 1000;
    return credits % 1000 === 0 ? `${thousands}k` : `${thousands.toFixed(1)}k`;
  }
  return credits.toString();
}
