import { noop } from 'lodash-es';
import { runInAction } from 'mobx';
import { useCallback } from 'react';
import React from 'react';

import { useStores } from '@/app/(root)/AppProviders';
import {
  getPromptMaxLengthForModel,
  getTagsMaxLengthForModel,
} from '@/app/(root)/create/createV2/utils';
import { modelValidForMumbleMode } from '@/app/(root)/create/v2/utils';
import { ClipBrowserRegistryContext } from '@/components/clipBrowser/useClipBrowserRegistry';
import { ModalTypes } from '@/components/modal/constants/ModalTypes';
import { toast } from '@/components/toast/Toast';
import { ToastV2 } from '@/components/toast/ToastV2';
import { useContextSelector } from '@/hooks/useContextSelector';
import { components } from '@/lib/gen';
import { TransactionLogger } from '@/logging/logWebUserEvent';
import { Clip } from '@/state/clipStore';
import { MenusStore } from '@/state/menusStore';
import {
  CaptchaConsumer,
  FeatureKey,
  PlanKey,
  SessionStore,
} from '@/state/sessionStore';
import {
  DEFAULT_AUDIO_WEIGHT_VALUE,
  DEFAULT_CREATE_CONTROL_VALUE,
  DEFAULT_PROJECT_ID,
  MAX_NEGATIVE_STYLE_CHARS,
} from '@/utils/constants';
import { genEndpoint } from '@/utils/dynamicConfigs';
import { isDevOrStaging } from '@/utils/environment';
import { eventLogger } from '@/utils/event-logger';
import { ActionName } from '@/utils/event-names';
import sanitizeNumber from '@/utils/sanitizeNumber';
import { isProjectsFeatureEnabled } from '@/utils/session';
import { modelSupportsFeature } from '@/utils/utils';

// we have ~5m20s available total for infill output + context + cover or artist song (using cover_start_s and artist_start_s)

enum GenerateTask {
  Cover = 'cover',

  Extend = 'extend',
  UploadExtend = 'upload_extend',
  ArtistConsistency = 'artist_consistency',
  Vox = 'vox',

  Infill = 'infill',
  FixedInfill = 'fixed_infill',
  InfillIntro = 'infill_intro',
  InfillOutro = 'infill_outro',

  InfillCover = 'cover_infill',
  InfillPersona = 'artist_infill',

  GenStem = 'gen_stem',
  StemCondition = 'stem_condition',
  StemConditionInfill = 'stem_condition_infill',
  CoverStemCondition = 'cover_stem_condition',

  ArtistCover = 'artist_cover',
  VoxCover = 'vox_cover',
  ArtistExtend = 'artist_extend',

  PlaylistCondition = 'playlist_condition',
  VoxPlaylistCondition = 'vox_playlist_condition',
  Underpainting = 'underpainting',
  Overpainting = 'overpainting',

  SampleCondition = 'sample_condition',
}

export enum PromptType {
  Onebox = 'Onebox',
  Custom = 'Custom',
}

export type CustomPrompt = {
  type: PromptType.Custom;
  title?: string;
  tags?: string;
  negativeTags?: string;
  lyrics?: string;
  lyricsSubject?: string;
  lyricsModel?: string;
  lastLyricsGeneration?: {
    title: string;
    text: string;
    prompt: string;
    lyricsModel: string;
  };
  lastTagsGeneration?: {
    requestId: string;
    generatedTags: string;
  };
};

export type OneboxPrompt = {
  type: PromptType.Onebox;
  prompt?: string;
  lyricsModel?: string;
  instrumental?: boolean;
  overrideLyrics?: string;
  overrideTags?: string;
  overrideTitle?: string;
};

export enum ReferenceType {
  Cover = 'Cover',
  Persona = 'Persona',
  Extend = 'Extend',
  Infill = 'Infill',
  FixedInfill = 'FixedInfill',
  GenStem = 'GenStem',
  StemCondition = 'StemCondition',
  Playlist = 'Playlist',
  Underpaint = 'Underpaint',
  Overpaint = 'Overpaint',
  Sample = 'Sample',
}

export type AlignmentSpec = {
  historyClipId?: string;
  historyEndSeconds?: number;
  futureClipId?: string;
  futureStartSeconds?: number;
};

export type CoverReference = {
  type: ReferenceType.Cover;
  clipId: string;
  startSeconds?: number;
  endSeconds?: number;
};

export type SampleReference = {
  type: ReferenceType.Sample;
  sampleClipIds: string[];
};

export type PersonaReference = {
  type: ReferenceType.Persona;
  clipId: string;
  personaId: string;
  startSeconds?: number;
  endSeconds?: number;
};

export type ExtendReference = {
  type: ReferenceType.Extend;
  clipId: string;
  isUpload: boolean;
  startSeconds: number;
  contextLyrics: string;
  lyricsUpdated: boolean;
};

export type InfillReference = {
  type: ReferenceType.Infill;
  clipId: string;
  startSeconds: number;
  endSeconds: number;
  durationSeconds?: number;
  contextStartSeconds: number;
  contextEndSeconds: number;
  contextLyrics: string;
  infillLyrics: string;
  generateFullContext?: boolean;
  lyricsUpdated: boolean;
};

export type FixedInfillReference = Omit<InfillReference, 'type'> & {
  type: ReferenceType.FixedInfill;
};

export type GenStemReference = {
  type: ReferenceType.GenStem;
  clipId: string;
  stemType?: number;
  stemTypeGroup: string | undefined;
  stemTask?: string;
};

export type StemConditionReference = {
  type: ReferenceType.StemCondition;
  clipId: string;
  stemControlTags: string;
  startSeconds?: number;
  endSeconds?: number;
};

export type PlaylistReference = {
  type: ReferenceType.Playlist;
  playlistId: string;
  playlistClipIds: string[];
};

export type UnderpaintReference = {
  type: ReferenceType.Underpaint;
  clipId: string;
};

export type OverpaintReference = {
  type: ReferenceType.Overpaint;
  clipId: string;
};

const basePayload: Partial<components['schemas']['GenParamsSpec']> = {
  project_id: undefined,
  token: undefined,
  task: undefined,
  generation_type: 'TEXT' as const,

  title: undefined,
  tags: undefined,
  negative_tags: undefined,
  mv: undefined,
  prompt: undefined,
  gpt_description_prompt: undefined,
  make_instrumental: false,
  user_uploaded_images_b64: null,

  metadata: {},
  override_fields: [],

  cover_clip_id: null,
  cover_start_s: null,
  cover_end_s: null,

  persona_id: null,
  artist_clip_id: null,
  artist_start_s: null,
  artist_end_s: null,

  continue_clip_id: null,
  continued_aligned_prompt: null,
  continue_at: null,

  infill_context_start_s: undefined,
  infill_context_end_s: undefined,
  infill_start_s: undefined,
  infill_end_s: undefined,
  infill_dur_s: undefined,

  stem_type_id: undefined,
  stem_type_group_name: undefined,
  stem_task: undefined,
};

export type GenerateParamsReference =
  | SampleReference
  | CoverReference
  | PersonaReference
  | ExtendReference
  | InfillReference
  | FixedInfillReference
  | GenStemReference
  | StemConditionReference
  | PlaylistReference
  | UnderpaintReference
  | OverpaintReference;

export type GenerateParams = {
  studioProjectId?: string;
  projectId?: string;
  editSessionId?: string;
  prompt: CustomPrompt | OneboxPrompt;
  references?: GenerateParamsReference[];
  modelTier: ModelTier;
  modelOverride?: string;
  mumbleMode?: boolean;
  controlSliders?: {
    weirdnessConstraint?: number;
    styleWeight?: number;
    audioWeight?: number;
  };
  alignmentOverrides?: AlignmentSpec;
  enablePersonalization?: boolean;
  personalizationTargetUserId?: string;
  doPersonalizeLyrics?: boolean;
  voxPersonaConfig?: {
    enabled: boolean;
    gptCfg: number;
    diffusionCfg: number;
  };
  maxMode?: boolean;
  vocalGender?: string;
  // Optional desired duration in seconds (120–480). If omitted, backend defaults apply
  duration?: number;
  isRemix?: boolean;
  disableVolumeNormalization?: boolean;
  batchOffset?: number;
};

export enum ModelTier {
  V3 = 'V3',
  V3_5 = 'V3.5',
  V4 = 'V4',
  V4_5 = 'V4.5',
  V4_5_PLUS = 'V4.5+',
  V5 = 'V5',
}

export const MINIMUM_MODEL_TIER = ModelTier.V3;

export const getClipModelTier = (
  clip: Clip,
  allowV3 = true,
  allowProModels = true
) => {
  let result: ModelTier;

  if (clip.major_model_version === 'v3') {
    result = ModelTier.V3;
  } else if (clip.major_model_version === 'v3.5') {
    result = ModelTier.V3_5;
  } else if (clip.major_model_version === 'v4') {
    result = ModelTier.V4;
  } else if (
    clip.major_model_version === 'v4.5' ||
    clip.major_model_version.includes('auk')
  ) {
    result = ModelTier.V4_5;
  } else if (
    clip.major_model_version === 'v4.5+' ||
    clip.major_model_version.includes('bluejay')
  ) {
    result = ModelTier.V4_5_PLUS;
  } else if (
    clip.major_model_version === 'v5' ||
    clip.major_model_version.includes('crow')
  ) {
    result = ModelTier.V5;
  } else if (allowProModels) {
    result = ModelTier.V5;
  } else {
    result = ModelTier.V3_5;
  }

  if (!allowV3 && result === ModelTier.V3) {
    return ModelTier.V3_5;
  }
  return result;
};

export const STANDARD_MODELS = [
  'chirp-v3-0',
  'chirp-v3-5',
  'chirp-v4',
  'chirp-v4-5',
  'chirp-v4-5-plus',
  'chirp-auk',
  'chirp-bluejay',
  'chirp-crow',
];

export enum ModelVersion {
  // Standard models, used for normal generation
  V3_BASE = 'chirp-v3-0',
  V3_5_BASE = 'chirp-v3-5',
  V4_BASE = 'chirp-v4',

  // Big models, used for cover/extension/persona/infill
  V3_5_TAU = 'chirp-v3-5-tau',
  V4_TAU = 'chirp-v4-tau',
  V4_5_AUK = 'chirp-auk',
  V4_5_AUK_INFILL = 'chirp-auk-infill',
  V4_5_BLUEJAY = 'chirp-bluejay',

  V5_CROW = 'chirp-crow',
}

const TAU_MODELS_BY_CATEGORY = {
  [ModelTier.V3]: ModelVersion.V3_5_TAU,
  [ModelTier.V3_5]: ModelVersion.V3_5_TAU,
  [ModelTier.V4]: ModelVersion.V4_TAU,
  [ModelTier.V4_5]: ModelVersion.V4_5_AUK,
  [ModelTier.V4_5_PLUS]: ModelVersion.V4_5_BLUEJAY,
  [ModelTier.V5]: ModelVersion.V5_CROW,
};

const BASE_MODELS_BY_CATEGORY = {
  [ModelTier.V3]: ModelVersion.V3_BASE,
  [ModelTier.V3_5]: ModelVersion.V3_5_BASE,
  [ModelTier.V4]: ModelVersion.V4_BASE,
  [ModelTier.V4_5]: ModelVersion.V4_5_AUK,
  [ModelTier.V4_5_PLUS]: ModelVersion.V4_5_BLUEJAY,
  [ModelTier.V5]: ModelVersion.V5_CROW,
};

const BIG_MODEL_REFERENCE_TYPES = {
  [ReferenceType.Extend]: false,

  [ReferenceType.Cover]: true,
  [ReferenceType.Persona]: true,
  [ReferenceType.Infill]: true,
  [ReferenceType.FixedInfill]: true,

  [ReferenceType.GenStem]: true,
  [ReferenceType.StemCondition]: true,
  [ReferenceType.Playlist]: true,
  [ReferenceType.Underpaint]: true,
  [ReferenceType.Overpaint]: true,

  [ReferenceType.Sample]: true,
};

const TASK_SPECIFIC_MODELS = {
  [GenerateTask.FixedInfill]: 'chirp-carp',
};

const resolveModel = (params: GenerateParams) => {
  if (params.modelOverride) return params.modelOverride;

  if (
    params.references?.some(
      (reference) => reference.type === ReferenceType.Infill
    ) &&
    params.modelTier === ModelTier.V4_5
  ) {
    // v4.5 classic infill uses a special model
    return ModelVersion.V4_5_AUK_INFILL;
  }

  if (
    params.references?.some(
      (reference) => reference.type === ReferenceType.GenStem
    )
  ) {
    return ModelVersion.V3_BASE; // minimum model tier, overwritten on backend anyway
  }

  if (
    params.references?.some(
      (reference) => reference.type === ReferenceType.StemCondition
    )
  ) {
    return ModelVersion.V5_CROW;
  }

  // base models for extend & referenceless generations. big models for the rest.
  const modelsByCategory = params.references?.some(
    (reference) => BIG_MODEL_REFERENCE_TYPES[reference.type]
  )
    ? TAU_MODELS_BY_CATEGORY
    : BASE_MODELS_BY_CATEGORY;

  return modelsByCategory[params.modelTier];
};

const truncateAndLog = (field: string, text: string, maxLength: number) => {
  if (text.length > maxLength) {
    console.error(
      `"${field}" is too long. It has been truncated to ${maxLength} characters from ${text.length} characters.`
    );
  }
  return text.slice(0, maxLength);
};

const validateReferenceCombinations = (
  references: GenerateParamsReference[] | undefined
) => {
  if (!references || references.length === 0) {
    return;
  }

  const referenceTypes = references.map((ref) => ref.type);
  const nonPersonaReferenceTypes = referenceTypes.filter(
    (t) => t !== ReferenceType.Persona
  );

  // Check for duplicate reference types
  const uniqueTypes = new Set(nonPersonaReferenceTypes);
  if (uniqueTypes.size !== nonPersonaReferenceTypes.length) {
    throw new Error('Duplicate non-persona reference types are not allowed');
  }

  // Check for valid combinations
  const hasInfill =
    referenceTypes.includes(ReferenceType.Infill) ||
    referenceTypes.includes(ReferenceType.FixedInfill);
  const hasSample = referenceTypes.includes(ReferenceType.Sample);
  const hasCover = referenceTypes.includes(ReferenceType.Cover);
  const hasPersona = referenceTypes.includes(ReferenceType.Persona);
  const hasExtend = referenceTypes.includes(ReferenceType.Extend);
  const hasGenStem = referenceTypes.includes(ReferenceType.GenStem);
  const hasStemCondition = referenceTypes.includes(ReferenceType.StemCondition);
  const hasPlaylist = referenceTypes.includes(ReferenceType.Playlist);
  const hasUnderpaint = referenceTypes.includes(ReferenceType.Underpaint);
  const hasOverpaint = referenceTypes.includes(ReferenceType.Overpaint);

  // Infill can be combined with cover or persona, but not both
  if (hasInfill && hasCover && hasPersona) {
    throw new Error('Infill cannot be combined with both cover and persona');
  }

  if (
    hasSample &&
    (hasCover ||
      hasPersona ||
      hasInfill ||
      hasExtend ||
      hasGenStem ||
      hasStemCondition ||
      hasPlaylist ||
      hasUnderpaint ||
      hasOverpaint)
  ) {
    throw new Error('Sample cannot be combined with other operations');
  }

  // Extend cannot be combined with other operations
  if (
    hasExtend &&
    (hasCover ||
      hasInfill ||
      hasGenStem ||
      hasStemCondition ||
      hasPlaylist ||
      hasUnderpaint ||
      hasOverpaint)
  ) {
    throw new Error('Extend cannot be combined with other operations');
  }

  // GenStem cannot be combined with other operations
  if (
    hasGenStem &&
    (hasCover ||
      hasPersona ||
      hasInfill ||
      hasExtend ||
      hasStemCondition ||
      hasPlaylist ||
      hasUnderpaint ||
      hasOverpaint)
  ) {
    throw new Error('GenStem cannot be combined with other operations');
  }

  // StemCondition can be combined with Cover, but not with other untimed operations
  if (
    hasStemCondition &&
    (hasExtend ||
      hasGenStem ||
      hasPlaylist ||
      hasUnderpaint ||
      hasOverpaint ||
      hasPersona)
  ) {
    throw new Error(
      'StemCondition cannot be combined with extend, genStem, playlist, underpaint, persona, or overpaint'
    );
  }

  // Playlist operations can only be combined with persona (for VOX_PLAYLIST_CONDITION)
  if (
    hasPlaylist &&
    (hasCover ||
      hasInfill ||
      hasExtend ||
      hasGenStem ||
      hasStemCondition ||
      hasUnderpaint ||
      hasOverpaint)
  ) {
    throw new Error(
      'Playlist cannot be combined with other operations except persona'
    );
  }

  // Underpaint and Overpaint cannot be combined with other operations
  if (
    (hasUnderpaint || hasOverpaint) &&
    (hasCover ||
      hasPersona ||
      hasInfill ||
      hasExtend ||
      hasGenStem ||
      hasStemCondition ||
      hasPlaylist)
  ) {
    throw new Error(
      'Underpaint/Overpaint cannot be combined with other operations'
    );
  }

  // Underpaint and Overpaint cannot be combined with each other
  if (hasUnderpaint && hasOverpaint) {
    throw new Error('Underpaint and Overpaint cannot be combined');
  }
};

const mapAndFilterSliders = (
  sliders:
    | {
        weirdnessConstraint?: number;
        styleWeight?: number;
        audioWeight?: number;
      }
    | undefined,
  canControlSliders: string[]
):
  | {
      weirdness_constraint?: number;
      style_weight?: number;
      audio_weight?: number;
    }
  | undefined => {
  if (!sliders) return undefined;

  const nonDefault = {
    weirdness_constraint:
      sliders.weirdnessConstraint === undefined ||
      sliders.weirdnessConstraint === DEFAULT_CREATE_CONTROL_VALUE
        ? undefined
        : sliders.weirdnessConstraint / 100,
    style_weight:
      sliders.styleWeight === undefined ||
      sliders.styleWeight === DEFAULT_CREATE_CONTROL_VALUE
        ? undefined
        : sliders.styleWeight / 100,
    audio_weight:
      sliders.audioWeight === undefined ||
      sliders.audioWeight === DEFAULT_AUDIO_WEIGHT_VALUE
        ? undefined
        : sliders.audioWeight / 100,
  };
  const result = Object.fromEntries(
    Object.entries(nonDefault).filter(
      ([key, value]) => value !== undefined && canControlSliders.includes(key)
    )
  );

  if (Object.keys(result).length === 0) {
    return undefined;
  }

  return result;
};

const getGeneratePayload = (
  token: string | null,
  createSessionToken: string | null,
  userTier: string,
  params: GenerateParams,
  transactionUuid: string,
  session: SessionStore
) => {
  // Validate reference combinations
  validateReferenceCombinations(params.references);

  const featureForcedInferConfig = {
    ...(params.voxPersonaConfig?.enabled && params.voxPersonaConfig
      ? {
          cfg_coef: params.voxPersonaConfig.gptCfg,
          source_cfg_coef: params.voxPersonaConfig.diffusionCfg,
        }
      : {}),
  };

  const modelVersion = resolveModel(params);
  const canControlSliders = modelSupportsFeature(
    modelVersion,
    session.billingModels,
    'create_control_sliders'
  )
    ? params.references?.length
      ? ['weirdness_constraint', 'style_weight', 'audio_weight']
      : ['weirdness_constraint', 'style_weight']
    : [];
  const mappedSliders = mapAndFilterSliders(
    params.controlSliders,
    canControlSliders
  );
  const result = {
    ...basePayload,
    transaction_uuid: transactionUuid,
    edit_session_id: params.editSessionId,
    project_id:
      params.projectId === DEFAULT_PROJECT_ID ? undefined : params.projectId,
    token,
    mv: modelVersion,
    // Pass through optional desired duration in seconds (backend maps to metadata.gen_duration)
    duration: params.duration,

    metadata: {
      web_client_pathname: window.location.pathname,
      is_max_mode: params.maxMode,
      is_mumble: modelValidForMumbleMode(modelVersion)
        ? params.mumbleMode
        : undefined,
      create_mode:
        params.prompt.type === PromptType.Custom ? 'custom' : 'simple',
      user_tier: userTier,
      from_studio_project_id: params.studioProjectId,
      create_session_token: createSessionToken,
      disable_volume_normalization: params.disableVolumeNormalization ?? false,
      forced_infer_config: isDevOrStaging
        ? {
            ...(window as any).matrixModeOverrides,
            ...featureForcedInferConfig,
          }
        : Object.keys(featureForcedInferConfig).length > 0
          ? featureForcedInferConfig
          : undefined,
      ...(params.vocalGender ? { vocal_gender: params.vocalGender } : {}),
      ...(params.isRemix ? { is_remix: true } : {}),
      ...(params.references?.[0]?.type !== ReferenceType.FixedInfill
        ? mappedSliders
          ? { control_sliders: mappedSliders }
          : {}
        : {}),
      can_control_sliders: canControlSliders,
      batch_offset: params.batchOffset,
    } as any,
  };

  if (params.prompt.type === PromptType.Custom) {
    result.title = params.prompt.title || '';
    result.tags = params.prompt.tags || '';
    result.negative_tags = params.prompt.negativeTags || '';
    result.prompt = params.prompt.lyrics || '';
    result.make_instrumental = params.references?.some((ref) =>
      [ReferenceType.Infill, ReferenceType.FixedInfill].includes(ref.type)
    )
      ? undefined
      : !params.prompt.lyrics?.trim() &&
        !params.prompt.lyricsSubject?.trim() &&
        !params.mumbleMode;
    if (params.prompt.lastLyricsGeneration) {
      result.metadata.last_lyrics_generation = {
        title: params.prompt.lastLyricsGeneration.title,
        lyrics: params.prompt.lastLyricsGeneration.text,
        prompt: params.prompt.lastLyricsGeneration.prompt,
        lyrics_model: params.prompt.lastLyricsGeneration.lyricsModel,
      };
    }
    if (params.prompt.lastTagsGeneration) {
      result.metadata.last_tags_generation = {
        tags: params.prompt.lastTagsGeneration.generatedTags,
        request_id: params.prompt.lastTagsGeneration.requestId,
      };
    }
    if (params.prompt.lyricsSubject) {
      result.gpt_description_prompt = params.prompt.lyricsSubject;
      result.override_fields = ['tags'];
      if (params.prompt.lyricsModel) {
        result.metadata.lyrics_model = params.prompt.lyricsModel;
      }
    }
  } else if (params.prompt.type === PromptType.Onebox) {
    result.gpt_description_prompt = params.prompt.prompt || '';
    result.prompt = '';
    result.metadata.lyrics_model = params.prompt.lyricsModel || 'default';
    result.make_instrumental = params.prompt.instrumental || false;
    result.metadata.can_control_sliders = [];
    const overrideFields = [];
    if (!!params.prompt.overrideLyrics) {
      overrideFields.push('prompt');
      result.prompt = params.prompt.overrideLyrics;
    }
    if (!!params.prompt.overrideTags) {
      overrideFields.push('tags');
      result.tags = params.prompt.overrideTags;
    }
    if (!!params.prompt.overrideTitle) {
      result.title = params.prompt.overrideTitle;
    }
    if (overrideFields.length) {
      result.override_fields = overrideFields as ('prompt' | 'tags')[];
    }
  }

  // Handle references
  if (params.references && params.references.length > 0) {
    const sampleReferences = params.references.filter(
      (ref) => ref.type === ReferenceType.Sample
    ) as SampleReference[];
    const coverReference = params.references.find(
      (ref) => ref.type === ReferenceType.Cover
    ) as CoverReference | undefined;
    const personaReference = params.references.find(
      (ref) => ref.type === ReferenceType.Persona
    ) as PersonaReference | undefined;
    const extendReference = params.references.find(
      (ref) => ref.type === ReferenceType.Extend
    ) as ExtendReference | undefined;
    const infillReference = params.references.find(
      (ref) =>
        ref.type === ReferenceType.Infill ||
        ref.type === ReferenceType.FixedInfill
    ) as (InfillReference | FixedInfillReference) | undefined;
    const genStemReference = params.references.find(
      (ref) => ref.type === ReferenceType.GenStem
    ) as GenStemReference | undefined;
    const stemConditionReference = params.references.find(
      (ref) => ref.type === ReferenceType.StemCondition
    ) as StemConditionReference | undefined;
    const playlistReference = params.references.find(
      (ref) => ref.type === ReferenceType.Playlist
    ) as PlaylistReference | undefined;
    const underpaintReference = params.references.find(
      (ref) => ref.type === ReferenceType.Underpaint
    ) as UnderpaintReference | undefined;
    const overpaintReference = params.references.find(
      (ref) => ref.type === ReferenceType.Overpaint
    ) as OverpaintReference | undefined;

    if (sampleReferences.length) {
      result.task = GenerateTask.SampleCondition;
      result.sample_clip_ids = sampleReferences.flatMap(
        (ref) => ref.sampleClipIds
      );
    }

    // Handle Cover
    if (coverReference) {
      result.cover_clip_id = coverReference.clipId;
      result.cover_start_s = sanitizeNumber(coverReference.startSeconds);
      result.cover_end_s = sanitizeNumber(coverReference.endSeconds);
    }

    // Handle Persona
    if (personaReference) {
      result.artist_clip_id = personaReference.clipId;
      result.persona_id = personaReference.personaId;
      result.artist_start_s = personaReference.startSeconds ?? null;
      result.artist_end_s = personaReference.endSeconds ?? null;
      result.override_fields = ['prompt', 'tags'];
    }

    // Handle Extend
    if (extendReference) {
      result.task = extendReference.isUpload
        ? GenerateTask.UploadExtend
        : GenerateTask.Extend;
      result.continue_at = extendReference.startSeconds;
      result.continue_clip_id = extendReference.clipId;
      result.continued_aligned_prompt = extendReference.contextLyrics;
      result.metadata.lyrics_updated = extendReference.lyricsUpdated;
    }

    // Handle Underpaint
    if (underpaintReference) {
      result.task = GenerateTask.Underpainting;
      result.underpainting_clip_id = underpaintReference.clipId;
    }

    // Handle Overpaint
    if (overpaintReference) {
      result.task = GenerateTask.Overpainting;
      result.overpainting_clip_id = overpaintReference.clipId;
    }

    // Handle Infill
    if (infillReference) {
      result.continued_aligned_prompt = result.prompt || '';
      result.prompt = infillReference.contextLyrics;
      result.metadata.infill_lyrics = infillReference.infillLyrics;
      result.metadata.is_remix = true;
      result.task = {
        [ReferenceType.Infill]: GenerateTask.Infill,
        [ReferenceType.FixedInfill]: GenerateTask.FixedInfill,
      }[infillReference.type];
      result.continue_clip_id = infillReference.clipId;
      result.infill_start_s = sanitizeNumber(infillReference.startSeconds);
      result.infill_end_s = sanitizeNumber(infillReference.endSeconds);
      result.infill_dur_s = sanitizeNumber(infillReference.durationSeconds);
      result.infill_context_start_s = sanitizeNumber(
        infillReference.contextStartSeconds
      );
      result.infill_context_end_s = sanitizeNumber(
        infillReference.contextEndSeconds
      );
      result.metadata.lyrics_updated = infillReference.lyricsUpdated;
    }

    // Handle GenStem
    if (genStemReference) {
      result.task = GenerateTask.GenStem;
      result.stem_type_id = genStemReference.stemType;
      result.stem_type_group_name = genStemReference.stemTypeGroup;
      result.stem_task = genStemReference.stemTask;
      result.continue_clip_id = genStemReference.clipId;
      result.metadata.is_remix = true;
    }

    // Handle StemCondition
    if (stemConditionReference) {
      result.task = GenerateTask.StemCondition;
      result.stem_condition_clip_id = stemConditionReference.clipId;
      result.stem_control_tags = stemConditionReference.stemControlTags;
      result.stem_condition_start_s = sanitizeNumber(
        stemConditionReference.startSeconds
      );
      result.stem_condition_end_s = sanitizeNumber(
        stemConditionReference.endSeconds
      );
      result.batch_size = 2;
      result.metadata.is_remix = true;
    }

    // Handle Playlist
    if (playlistReference) {
      result.task = GenerateTask.PlaylistCondition;
      result.playlist_id = playlistReference.playlistId;
      result.playlist_clip_ids = playlistReference.playlistClipIds;
    }

    // Handle combined operations
    if (stemConditionReference && infillReference) {
      result.task = GenerateTask.StemConditionInfill;
      result.include_history_s = 0;
      result.include_future_s = 0;
      // For stem_condition_infill, use infill_lyrics instead of context lyrics (delta infill)
      result.prompt = infillReference.infillLyrics;
    } else if (stemConditionReference && coverReference) {
      // StemCondition + Cover combination
      result.task = GenerateTask.CoverStemCondition;
    } else if (infillReference && coverReference) {
      // Infill + Cover combination
      result.task = GenerateTask.InfillCover;
    } else if (infillReference && personaReference) {
      // Infill + Persona combination
      result.task = GenerateTask.InfillPersona;
    } else if (coverReference && personaReference) {
      // Cover + Persona combination - check if vox persona is enabled
      if (params.voxPersonaConfig?.enabled) {
        result.task = GenerateTask.VoxCover;
      } else {
        result.task = GenerateTask.ArtistCover;
      }
    } else if (extendReference && personaReference) {
      // Extend + Persona combination
      result.task = GenerateTask.ArtistExtend;
    } else if (
      coverReference &&
      !infillReference &&
      !extendReference &&
      !genStemReference &&
      !stemConditionReference &&
      !playlistReference
    ) {
      // Standalone Cover
      result.task = GenerateTask.Cover;
    } else if (personaReference && playlistReference) {
      // Persona + Playlist combination - check if vox persona is enabled
      if (params.voxPersonaConfig?.enabled) {
        result.task = GenerateTask.VoxPlaylistCondition;
      } else {
        // This combination is not supported without vox persona
        throw new Error(
          'Persona + Playlist combination requires vox persona to be enabled'
        );
      }
    } else if (
      personaReference &&
      !infillReference &&
      !extendReference &&
      !genStemReference &&
      !stemConditionReference &&
      !playlistReference
    ) {
      // Standalone Persona - check if vox persona is enabled
      if (params.voxPersonaConfig?.enabled) {
        result.task = GenerateTask.Vox;
      } else {
        result.task = GenerateTask.ArtistConsistency;
      }
    }
  }

  // Handle chameleon mode (personalization)
  if (params.enablePersonalization) {
    result.use_personalization = true;
    if (params.personalizationTargetUserId) {
      result.personalization_user_uuid = params.personalizationTargetUserId;
    }
    if (params.doPersonalizeLyrics !== undefined) {
      result.do_personalize_lyrics = params.doPersonalizeLyrics;
    }
  }

  // Handle generateFullContext for infill operations
  const infillRefWithContext = params.references?.find((ref) =>
    [ReferenceType.Infill, ReferenceType.FixedInfill].includes(ref.type)
  ) as (InfillReference | FixedInfillReference) | undefined;

  if (infillRefWithContext && infillRefWithContext.generateFullContext) {
    if (result.infill_start_s === undefined)
      throw new Error('Infill start is undefined');

    if (result.infill_context_start_s === undefined)
      throw new Error('Infill context start is undefined');

    if (result.infill_end_s === undefined)
      throw new Error('Infill end is undefined');

    if (result.infill_context_end_s === undefined)
      throw new Error('Infill context end is undefined');

    result.include_history_s = sanitizeNumber(
      result.infill_start_s! - result.infill_context_start_s!
    );
    result.include_future_s = sanitizeNumber(
      result.infill_context_end_s! - result.infill_end_s!
    );
  }

  if (result.task && (TASK_SPECIFIC_MODELS as any)[result.task]) {
    result.mv = (TASK_SPECIFIC_MODELS as any)[result.task];
  }

  if (params.alignmentOverrides) {
    result.metadata.override_history_clip_id =
      params.alignmentOverrides.historyClipId;
    result.metadata.override_history_end_seconds =
      params.alignmentOverrides.historyEndSeconds;
    result.metadata.override_future_clip_id =
      params.alignmentOverrides.futureClipId;
    result.metadata.override_future_start_seconds =
      params.alignmentOverrides.futureStartSeconds;
  }

  (['tags'] as (keyof typeof result)[]).forEach((field) => {
    if (result[field]) {
      result[field] = truncateAndLog(
        field,
        result[field],
        getTagsMaxLengthForModel(result.mv)
      );
    }
  });

  (['negative_tags'] as (keyof typeof result)[]).forEach((field) => {
    if (result[field]) {
      result[field] = truncateAndLog(
        field,
        result[field],
        MAX_NEGATIVE_STYLE_CHARS
      );
    }
  });

  (['prompt', 'continued_aligned_prompt'] as (keyof typeof result)[]).forEach(
    (field) => {
      if (result[field]) {
        result[field] = truncateAndLog(
          field,
          result[field],
          getPromptMaxLengthForModel(result.mv)
        );
      }
    }
  );

  return result as components['schemas']['GenParamsSpec'];
};

async function generate(
  transactionLogger: TransactionLogger,
  session: SessionStore,
  menus: MenusStore,
  token: string | null,
  createSessionToken: string | null,
  params: GenerateParams
) {
  const endpoint = genEndpoint(session);
  const userTier =
    session.sub?.plan?.id ||
    (session.sub?.plans || []).find(
      (plan: any) => plan.plan_key === PlanKey.Free
    )?.id;
  const payload = getGeneratePayload(
    token,
    createSessionToken,
    userTier,
    params,
    transactionLogger.transactionUuid,
    session
  );

  const finalModelVersion = payload.mv;
  const finalModel = session.billingModels.find(
    (m) => m.external_key === finalModelVersion
  );

  if (finalModel && !finalModel?.can_use) {
    menus.setCurrentUpsellFeature(FeatureKey.UPGRADE_LATEST_MODEL);
    menus.openModal(ModalTypes.UPSELL_MODAL);
    return [];
  }

  const { data, error, response } = await session.apiClient.POST(endpoint, {
    body: payload,
  });

  if (data?.clips?.length) {
    transactionLogger.logWebUserEvent({
      actionName: 'GenerationStartSuccess',
    });
  } else if (response?.status === 429) {
    // todo: upsell
    toast({
      title: 'Please wait.',
      description: 'Please wait for your other generations to finish.',
      status: 'error',
      duration: 4000,
      isClosable: true,
    });
    transactionLogger.logWebUserEvent({
      actionName: 'GenerationStartError',
      principalObjectType: 'error_type',
      principalObjectValue: 'Rate Limited',
    });
  } else if (response.status === 402) {
    if (error?.detail === 'Subscription past due.') {
      toast({
        title: 'Your subscription is past due.',
        description: 'Please update your payment method.',
        status: 'error',
        duration: 4000,
        isClosable: true,
        render: () => {
          return React.createElement(ToastV2, {
            title: 'Your subscription is past due.',
            description: 'Please update your payment method.',
            linkText: 'Manage',
            href: '/account',
          });
        },
      });
      transactionLogger.logWebUserEvent({
        actionName: 'GenerationStartError',
        principalObjectType: 'error_type',
        principalObjectValue: 'Subscription past due',
      });
    } else {
      toast({
        title: 'Need at least 10 credits to create.',
        description: 'Upgrade your account to get more credits!',
        status: 'error',
        duration: 4000,
        isClosable: true,
      });
      transactionLogger.logWebUserEvent({
        actionName: 'GenerationStartError',
        principalObjectType: 'error_type',
        principalObjectValue: 'Insufficient Credits',
      });
    }
  } else if (response.status === 423) {
    toast({
      title: 'Your account requires support assistance',
      description: 'Please contact support@suno.com.',
      status: 'error',
      duration: 6000,
      isClosable: true,
    });
    transactionLogger.logWebUserEvent({
      actionName: 'GenerationStartError',
      principalObjectType: 'error_type',
      principalObjectValue: 'Support Assistance Required',
    });
  } else if (
    response.status === 424 &&
    (error as any) === 'copyright_infringment'
  ) {
    // API just responds with a string
    menus.openModal(ModalTypes.COPYRIGHT_WARNING);
    transactionLogger.logWebUserEvent({
      actionName: 'GenerationStartError',
      principalObjectType: 'error_type',
      principalObjectValue: 'Unacknowledged Copyright Infringement',
    });
  } else if (response.status === 451) {
    toast({
      title: 'The lyrics do not meet our content guidelines',
      description: 'Please try again with different lyrics.',
      status: 'error',
      duration: 6000,
      isClosable: true,
    });
    transactionLogger.logWebUserEvent({
      actionName: 'GenerationStartError',
      principalObjectType: 'error_type',
      principalObjectValue: 'Does Not Meet Guidelines',
    });
  } else if (response.status === 503) {
    toast({
      title: 'We are seeing elevated usage.',
      description:
        'Generations are currently disabled. Please try again later.',
      status: 'error',
      duration: 4000,
      isClosable: true,
    });
    transactionLogger.logWebUserEvent({
      actionName: 'GenerationStartError',
      principalObjectType: 'error_type',
      principalObjectValue: 'Elevated Usage',
    });
  } else if (error) {
    toast({
      title: 'An error occurred.',
      description: 'A system error occurred.',
      status: 'error',
      duration: 4000,
      isClosable: true,
    });

    console.error(error);

    transactionLogger.logWebUserEvent({
      actionName: 'GenerationStartError',
      principalObjectType: 'error_type',
      principalObjectValue: 'Unknown Error',
      context: {
        message:
          (error as any)?.message || (error as any)?.error_message || error,
      },
    });
  }

  if (error) {
    throw error;
  }

  return data.clips as Clip[];
}

export default function useGenerate() {
  const { session, genForm, project, clips: clipsStore, menus } = useStores();

  const clipCreated = useContextSelector(ClipBrowserRegistryContext, (c) =>
    c ? c.clipCreated : noop
  );

  return useCallback(
    async (transactionLogger: TransactionLogger, params: GenerateParams) => {
      const result = await generate(
        transactionLogger,
        session,
        menus,
        await session.getCaptchaTokenIfRequired(CaptchaConsumer.Generation),
        genForm.createSessionToken,
        params
      );

      clipsStore.updateClips(result);

      result.forEach(clipCreated);

      const isProjectsEnabled = isProjectsFeatureEnabled(session);

      session.loadSubscriptionInfo();

      // legacy logging.
      eventLogger.logAudioCreationEvent(
        genForm.isMobile,
        ActionName.createSong,
        genForm,
        session,
        {
          newClipIds: result.map((c) => c.id),
          isProjectsEnabled,
          projectId:
            isProjectsEnabled && project.currentProjectId !== DEFAULT_PROJECT_ID
              ? project.currentProjectId
              : null,
        }
      );

      return result;
    },
    [genForm.createSessionToken, session, clipCreated]
  );
}

/**
 * Creates a callback that can be used to set the generation form model
 */
export function useSetGenFormModel(
  defaultTargetModel = ModelVersion.V3_5_BASE
) {
  const { session, genForm } = useStores();
  return useCallback(
    (targetModel = defaultTargetModel) => {
      const validSessionModels = session
        .getViewableModels()
        .map((model: any) => model.external_key as string);
      if (validSessionModels.includes(targetModel)) {
        runInAction(() => {
          genForm.mv = targetModel;
        });
      }
    },
    [session, genForm, defaultTargetModel]
  );
}
