'use client';

import * as Sentry from '@sentry/nextjs';
import { StatsigClient } from '@statsig/react-bindings';
import { reaction, runInAction } from 'mobx';

import { components } from '@/lib/gen';
import logWebUserEvent from '@/logging/logWebUserEvent';
import { CROW_WEB_UI, SIDEBAR_WIDTH } from '@/utils/constants';
import {
  SubscriptionLevel,
  SubscriptionPeriod,
  isFeatureEnabledForPlan,
} from '@/utils/session';
import {
  DEFAULT_BLUEJAY_MODEL_NAME,
  DEFAULT_CROW_MODEL_NAME,
  canUseModel,
  getDefaultModel,
  valueOrDefault,
} from '@/utils/utils';

import { RootStore, Substore } from './rootStore';
import { makeAutoObservableSubstore } from './utils';

interface FeatureFlagContext {
  enabled: boolean;
  loading: boolean;
}

export interface FeatureConfig {
  title: string;
  subtitle: string;
  icon: string;
  background_image: string;
  required_plans: string[];
  enabled: boolean;
}

export enum FeatureKey {
  PERSONAS = 'personas',
  REMASTER = 'remaster',
  SONG_EDITOR = 'song_editor',
  CROP_FADE = 'crop_fade',
  REPLACE_SECTION = 'replace_section',
  STEMS = 'stems',
  LONGER_AUDIO_UPLOAD = 'longer_audio_upload',
  MAGIC_DESCRIPTIONS = 'magic_descriptions',
  EXCLUDE_STYLES = 'exclude_styles',
  ADVANCED_SLIDERS = 'advanced_sliders',
  OUT_OF_CREDITS = 'out_of_credits',
  DOWNLOAD_WAV = 'download_wav',
  SIMULTANEOUS_GENS = 'simultaneous_gens',
  VOCALS = 'vocals',
  PLAYLIST_CONDITION = 'playlist_condition',
  STUDIO = 'studio',
  UPGRADE_LATEST_MODEL = 'upgrade_latest_model',
  GENERATE_VIDEO = 'generate_video',
}

export type FeatureUpsellConfig = {
  [key in FeatureKey]?: FeatureConfig;
};

// Copied from spreadshet for bots_feature
// https://docs.google.com/spreadsheets/d/1MO_ktsujN2RmN54M717MQs_BdObARzBFWXHRWI5rcmU/edit?usp=sharing
export enum PlanFeature {
  V4 = 'v4',
  Cover = 'cover',
  Persona = 'persona',
  EditMode = 'edit_mode',
  CanBuyCreditTopUps = 'can_buy_credit_top_ups',
  CommercialRights = 'commercial_rights',
  GenerateSongImage = 'generate_song_image',
  GenerateSongVideo = 'generate_song_video',
  GetStems = 'get_stems',
  NegativeTags = 'negative_tags',
  LongUploads = 'long_uploads',
  V4Remaster = 'v4_remaster',
  ConvertAudio = 'convert_audio',
  Remaster = 'remaster',
  Auk = 'auk',
  Studio = 'studio',
  VariationRemaster = 'variation_remaster',
  PLAYLIST_CONDITION = 'playlist_condition',
  ControlSliders = 'create_control_sliders',
  TagUpsample = 'tag_upsample',
}

export enum PlanKey {
  Free = 'free',
  Pro = 'pro',
  Basic = 'basic',
  Premier = 'premier',
  Student = 'student',
  Pro_20250501 = 'pro_20250501', // new pro plan created on 2025-05-01
}

export const FREE_PLAN_FALLBACK_NAME = 'Free Plan';
export const PRO_PLAN_FALLBACK_NAME = 'Paid Plan';

export type SubscriptionInfo =
  components['schemas']['SubscriptionInfoResponse'];

export type UsagePlanFeature = components['schemas']['FeatureSchema'];
export type ModelType = components['schemas']['ExternalModelTypeSchema'];
export type RemasterModelType =
  components['schemas']['ExternalRemasterModelTypeSchema'];
export type UsagePlanDescription =
  components['schemas']['UsagePlanDescription'];
export type FeatureDescription = components['schemas']['FeatureDescription'];

export type TableFeature = components['schemas']['TableFeature'];

export type TableSection = components['schemas']['TableSection'];
export type CreditPack = components['schemas']['CreditPackSchema'];

export type UsagePlanTableComparison =
  components['schemas']['UsagePlanWebTableComparison'];

export type FaqItem = components['schemas']['FaqItem'];

export type CtaButtons = components['schemas']['CtaButtons'];

export type PlanChangePreview =
  components['schemas']['PlanChangePreviewSchemaResponse'];
export type PlanChange = components['schemas']['PlanChangeSchemaResponse'];

export type SubscriptionCancel =
  components['schemas']['SubscriptionCancelSchemaResponse'];
export type CreditPurchase =
  components['schemas']['CreditPurchaseSchemaResponse'];
export type PurchaseStatus =
  components['schemas']['PurchaseStatusSchemaResponse'];
export type CheckoutSession =
  components['schemas']['CheckoutSessionSchemaResponse'];
export type PlanChangeSpec = components['schemas']['PlanChangeSpec'];
export type PurchaseSpec = components['schemas']['PurchaseSpec'];

export type DiscountOfferWithRedemption =
  components['schemas']['DiscountOfferWithRedemptionSchema'];

export type CreateOnboardingContext = {
  step: CreateOnboardingStep;
  playedClipId: string | null;
  likedClipId: string | null;
  loggedSteps: string[];
  isDismissed: boolean;
};
export type CreateOnboardingStep =
  | 'dice_tooltip'
  | 'play_tooltip_1'
  | 'play_tooltip_2'
  | 'like_tooltip'
  | 'create_tooltip'
  | 'share_tooltip'
  | 'credits_tooltip';

export enum CaptchaConsumer {
  Generation = 'generation',
}

export const FORCE_ENABLE_CAPTCHA = false;

const STATUS_URL =
  process.env.NEXT_PUBLIC_STATUS_URL || 'https://statusz.suno.ai/z';

const SESSION_STATE_STORAGE_KEY = 'session-state';

export type UsagePlanSchema = components['schemas']['UsagePlanSchema'];
export type ArtistProfileInfo =
  components['schemas']['ArtistProfileInfoSchema'];

export class SessionStore implements Substore {
  statsigClient: StatsigClient | undefined;

  user: any = null;
  userId: string | null = null;
  clerkId: string | null = null;
  userEmail: string | null = null;
  username: string | null = null;
  phoneNumber: string | null = null;
  roles: any = {};
  flags: Record<string, boolean> | null = null;
  configs: Record<string, any> | null = null;
  models: ModelType[] = [];
  billingModels: ModelType[] = [];
  remasterModelTypes: RemasterModelType[] = [];
  audioUploadLimits?: { min: number; max: number };
  previewInitialWidth: number = SIDEBAR_WIDTH;
  country: string | null = null;
  subscriptionAnchor?: string | null = null;
  subscriptionLevel?: SubscriptionLevel | null = null;
  subscriptionPeriod?: SubscriptionPeriod | null = null;
  isPreviousSubscriber?: boolean;
  isSubscriber?: boolean;

  sub: any = {};
  isSubLoaded = false;
  usagePlanFeatures: UsagePlanFeature[] = [];
  usagePlanTableComparison: UsagePlanTableComparison | null = null;
  userPlanKey: string | null = null;
  credits: number = 50;
  freeCoverClipsLeft: number = 0;
  freePersonaClipsLeft: number = 0;
  freeRemastersLeft: number = 0;
  freeV4GensLeft: number = 0;
  paidCreditsRemaining: number = 0;
  daysLeftInSubscription: number | false = false;

  notifyOutOfCredits: boolean = false;
  notifyOutOfConcurrency: boolean = false;

  isCreditsLoaded: boolean = false;
  callback: any = null;

  isMaintenance = false;
  isScheduledMaintenance = false;
  message = '';

  captchaVerification: null | (() => Promise<string | null>) = null;
  shownCreationTour: boolean = true;
  hasAcceptedTimbalandTerms: boolean = false;
  hasSetRemixPerm: boolean = false;
  preferredTags: string[] = [];
  shownClaimUsername: boolean = true;
  sessionIsLoaded: boolean = false;
  sessionIsLoading: boolean = false;
  userConfigIsLoaded: boolean = false;
  experiments: { [experiment: string]: boolean } = {};
  shownCreateOnboarding: boolean = false;
  dismissedBanners: string[] = [];
  createOnboardingContext: CreateOnboardingContext = {
    step: 'dice_tooltip',
    playedClipId: null,
    likedClipId: null,
    loggedSteps: [],
    isDismissed: false,
  };

  isWelcomeModalOpen: boolean = false;

  usagePlanFaqs: FaqItem[] = [];

  artistProfileInfo: ArtistProfileInfo | null = null;
  artistProfileInfoLoaded: boolean = false;

  readonly root: RootStore;
  get apiClient() {
    return this.root.apiClient;
  }
  // DO NOT ADD INIT LOGIC IN constructor. this will run on every page! considering adding any it in the component init logic instead
  constructor(root: RootStore) {
    this.root = root;
    makeAutoObservableSubstore(this);
  }

  // this will run on every page! considering adding any it in the component init logic instead
  initialize() {
    // todo: move load status out of initialize
    this.loadStatus();
    this.loadFromLocalStorage();
    this.initLocalStorageUpdate();
  }

  loadStatus = async () => {
    try {
      const response = await fetch(STATUS_URL);
      const data = await response.json();
      runInAction(() => {
        if (data?.status === 'maintenance') {
          this.isMaintenance = true;
          this.message = data?.message;
        } else if (data?.status === 'scheduled_maintenance') {
          this.isScheduledMaintenance = true;
          this.message = data?.message;
        }
      });
    } catch (e) {
      return;
    }
  };

  setStatsigClient = (statsigClient: StatsigClient) => {
    this.statsigClient = statsigClient;
  };

  storageObject = () => {
    return {
      previewInitialWidth: this.previewInitialWidth,
      createOnboardingContext: this.createOnboardingContext,
      shownCreateOnboarding: this.shownCreateOnboarding,
      dismissedBanners: this.dismissedBanners,
      // Add shallow copy to ensure MobX detects array changes
      _dismissedBannersSlice: this.dismissedBanners.slice(),
    };
  };

  initLocalStorageUpdate = () => {
    reaction(
      () => this.storageObject(),
      (data) => {
        if (this.root.isLocalStorageAvailable) {
          // Remove the internal slice property before saving
          const { _dismissedBannersSlice, ...storageData } = data;
          const storageStr = JSON.stringify(storageData);
          localStorage.setItem(SESSION_STATE_STORAGE_KEY, storageStr);
        }
      }
    );
  };

  loadFromLocalStorage = () => {
    if (this.root.isLocalStorageAvailable) {
      const storageStr = localStorage.getItem(SESSION_STATE_STORAGE_KEY);
      if (storageStr) {
        try {
          const stateJson = JSON.parse(storageStr);
          if (stateJson) {
            runInAction(() => {
              this.previewInitialWidth = valueOrDefault(
                stateJson.previewInitialWidth,
                232
              );
              this.createOnboardingContext = stateJson.createOnboardingContext;
              this.shownCreateOnboarding = stateJson.shownCreateOnboarding;
              this.dismissedBanners = stateJson.dismissedBanners || [];
            });
          }
        } catch (e) {
          console.error('Error loading from localStorage:', e);
          // Clear invalid localStorage data
          localStorage.removeItem(SESSION_STATE_STORAGE_KEY);
        }
      }
    }
  };

  setPreviewInitialWidth = (width: number) => {
    if (width > 0) {
      this.previewInitialWidth = width;
    }
  };

  updateCatchaCallback(callback: any) {
    this.callback = callback;
  }

  loadSession = async ({
    retries = 1,
    forceLoad = false,
  }: {
    retries?: number;
    forceLoad?: boolean;
  }) => {
    this.sessionIsLoading = true;
    // this is force the session api to called only once when the page is loaded
    // if you need to change this, double check with the core team
    if (!this.sessionIsLoaded || forceLoad) {
      const { data } = (await this.apiClient.GET('/api/session/', {
        headers: {
          credentials: 'include',
        },
      })) as any;

      if (data) {
        this.user = data.user;
        this.userId = data.user?.id || null;
        this.clerkId = data.user?.clerk_id || null;
        this.userEmail = data.user?.email || null;
        this.username = data.user?.username || null;
        this.phoneNumber = data.user?.phone_number || null;
        /**
         * Historically we sent session username to Statsig as the email field.
         * This is confusing, and there are cases where older staff accounts
         * weren't flagged into features properly.
         *
         * We are now sending the actual email, but will log the discrepancy
         * in case we see the bug going the other way and need to investigate.
         */
        if (data.user?.email !== data.user?.username) {
          logWebUserEvent({
            actionName: 'StatsigDebug',
            context: {
              type: 'emailUsernameMismatch',
              userId: this.userId,
              email: this.user?.email,
              username: this.user?.username,
            },
          });
        }
        this.roles = data.roles;
        this.flags = data.flags;
        this.configs = data.configs;
        this.models = data.models;
        this.remasterModelTypes = data.remaster_model_types || [];
      } else {
        if (retries > 0) {
          setTimeout(() => {
            this.loadSession({ retries: retries - 1 });
          }, 1000);
        }
      }
    }
    this.sessionIsLoading = false;
    this.sessionIsLoaded = true;
  };

  getViewableModels = () => {
    return this.billingModels;
  };

  getRemasterModelTypes = () => {
    return this.remasterModelTypes;
  };

  loadTableComparisonInfo = async () => {
    const { data } = await this.apiClient.GET(
      '/api/billing/usage-plan-web-table-comparison/',
      {
        headers: {
          credentials: 'include',
        },
      }
    );
    this.usagePlanTableComparison =
      data?.usage_plan_web_table_comparison || null;
    return { data };
  };

  loadFaqInfo = async () => {
    const { data } = await this.apiClient.GET('/api/billing/usage-plan-faq/', {
      headers: {
        credentials: 'include',
      },
    });
    this.usagePlanFaqs = data?.faq || [];
    return { data };
  };

  loadSubscriptionInfo = async (retries: number = 2) => {
    // Load subscription info, table comparison, and FAQ in parallel
    const [billingResponse] = await Promise.all([
      this.apiClient.GET('/api/billing/info/'),
      this.loadTableComparisonInfo(),
      this.loadFaqInfo(),
    ]);

    const { data, error } = billingResponse;

    if (data) {
      runInAction(() => {
        this.credits = data.total_credits_left || 0;
        this.isCreditsLoaded = true;
        this.usagePlanFeatures = data.accessible_features || [];

        this.freePersonaClipsLeft = data.free_persona_clips_remaining || 0;
        this.freeCoverClipsLeft = data.free_cover_clips_remaining || 0;
        this.freeRemastersLeft = data.free_remasters_remaining || 0;
        this.freeV4GensLeft = data.free_web_v4_gens_remaining || 0;

        this.isPreviousSubscriber = data.has_been_subscriber_before || false;

        this.subscriptionAnchor = data.subscription_anchor;
        this.subscriptionLevel = data.plan?.level;
        this.subscriptionPeriod = data.period as any;
        this.userPlanKey = data.plan?.plan_key || null;
        this.isSubscriber = data.is_active;
        this.sub = data;
        this.isSubLoaded = true;
        this.billingModels = data.models;
        this.remasterModelTypes = data.remaster_model_types || [];
        this.audioUploadLimits = data.audio_upload_limits;
        this.paidCreditsRemaining =
          (data.monthly_limit || 0) - (data.monthly_usage || 0);
        this.daysLeftInSubscription = this.checkIfCreditsExpiring(
          data.cancel_on,
          data.period_end,
          data.monthly_limit,
          data.monthly_usage,
          data.is_active
        );

        // Set default model for new users based on available models and whether they've seen them
        if (
          canUseModel(this.billingModels, DEFAULT_CROW_MODEL_NAME) &&
          !this.root.genForm.hasSeenCrow &&
          this.checkGate(CROW_WEB_UI)
        ) {
          // If user has crow access AND crow web UI gate, set to crow
          this.root.genForm.setMvUserPreference(DEFAULT_CROW_MODEL_NAME);
          runInAction(() => {
            this.root.genForm.hasSeenCrow = true;
          });
        } else if (
          canUseModel(this.billingModels, DEFAULT_BLUEJAY_MODEL_NAME) &&
          !this.root.genForm.hasSeenBluejay
        ) {
          // Else if user has bluejay and hasn't seen it yet, set to bluejay
          this.root.genForm.setMvUserPreference(DEFAULT_BLUEJAY_MODEL_NAME);
          runInAction(() => {
            this.root.genForm.hasSeenBluejay = true;
          });
        }
      });
      return data;
    } else if (error) {
      if (retries > 0) {
        setTimeout(() => {
          this.loadSubscriptionInfo(retries - 1);
        }, 1000);
      }
    }
  };

  deductCredits = (amount: number) => {
    this.credits = Math.max(0, this.credits - amount);
    if (this.credits === 0 && amount > 0 && this.freeV4GensLeft <= 0) {
      this.notifyOutOfCredits = true;
    }
  };

  refundCredits = (amount: number) => {
    this.credits = this.credits + amount;
  };

  deductFreeCoverClip = () => {
    this.freeCoverClipsLeft = Math.max(0, this.freeCoverClipsLeft - 1);
  };

  deductFreePersonaClip = () => {
    this.freePersonaClipsLeft = Math.max(0, this.freePersonaClipsLeft - 1);
  };

  deductFreeRemaster = () => {
    this.freeRemastersLeft = Math.max(0, this.freeRemastersLeft - 1);
  };

  deductFreeV4Gen = () => {
    this.freeV4GensLeft = Math.max(0, this.freeV4GensLeft - 1);
    if (this.freeV4GensLeft <= 0) {
      this.models = this.models.filter((m) => m.major_version !== 4);
      this.root.genForm.setMvUserPreference(getDefaultModel(this.models));
    }
  };

  refundFreeRemaster = () => {
    this.freeRemastersLeft = this.freeRemastersLeft + 1;
  };

  refundFreeV4Gen = () => {
    this.freeV4GensLeft = this.freeV4GensLeft + 1;
  };

  get isStaff(): boolean {
    return this.roles?.staff || false;
  }

  get isOutOfCredits(): boolean {
    return this.credits <= 0;
  }

  get isOutOfFreeV4Gens(): boolean {
    return this.freeV4GensLeft <= 0;
  }

  showOutOfCreditsNotification() {
    this.notifyOutOfCredits = true;
  }

  isFeatureAllowed(name: string): FeatureFlagContext {
    return {
      enabled: this.flags?.[name] || false,
      loading: this.flags === null,
    };
  }

  checkGate(gate: string) {
    return this.statsigClient?.checkGate(gate) || false;
  }

  getModelDisplayName = (modelId: string) => {
    const matchingModel = this.models.find(
      (model) => model.external_key === modelId
    );
    return matchingModel?.name || modelId;
  };

  acceptTOS = async () => {
    const { data } = await this.apiClient.POST(
      '/api/user/accept_custom_mode_tos/'
    );

    if (data) {
    }
  };

  grantInviteCredits = async (inviterHandle: any) => {
    try {
      const response = await this.apiClient.POST('/api/invite/', {
        body: {
          inviter_handle: inviterHandle,
        },
      });

      const data = response.data as any;

      if (data.error_msg) {
        console.error('Failed to send invite:', data.error_msg);
        return null;
      }

      return data;
    } catch (error) {
      console.error('Error sending invite:', error);
      return null;
    }
  };

  getCaptchaTokenIfRequired = async (consumer: CaptchaConsumer) => {
    let required: boolean | undefined = undefined;

    try {
      const response = await this.apiClient.POST('/api/c/check', {
        body: { ctype: consumer },
      });
      required = response.data?.required;
    } catch (e) {
      console.error('Failed to check captcha verification:', e);
      required = true;
    }

    if (required === true && this.captchaVerification) {
      console.log('captcha required, awaiting verification');
      const timeout = setTimeout(() => {
        Sentry.captureMessage(
          `Captcha verification not complete after 30 seconds. window.crossOriginIsolated is ${typeof window !== 'undefined' && window.crossOriginIsolated}.`
        );
      }, 30000);
      const verified = await this.captchaVerification();
      clearTimeout(timeout);
      console.log('captcha verified', verified);
      return verified;
    } else {
      return null;
    }
  };

  getUserConfig = async () => {
    const resp = await this.apiClient.POST('/api/user/user_config/', {
      body: {},
    });
    const data = resp.data as any;
    if (data) {
      if (!this.userConfigIsLoaded) {
        this.userConfigIsLoaded = true;
      }
      this.shownCreationTour = data.shown_creation_tour;
      this.hasAcceptedTimbalandTerms = data.has_accepted_timbaland_terms;
      this.preferredTags = data.preferred_tags;
      this.shownClaimUsername = data.shown_claim_username === true;
      this.hasSetRemixPerm = data.has_set_remix_perm;
      this.dismissedBanners = data.dismissed_banners_web || [];

      // Load create onboarding state from backend
      if (data.shown_create_onboarding === true) {
        this.shownCreateOnboarding = true;
      }
    }
  };

  hasShownTutorial = async () => {
    this.shownCreationTour = true;
    await this.apiClient.POST('/api/user/update_user_config/', {
      body: {
        shown_creation_tour: true,
      },
    });
  };

  setTimbalandTermsAccepted = async () => {
    await this.apiClient.POST('/api/user/accept_timbaland_terms/');
    this.hasAcceptedTimbalandTerms = true;
  };

  setExperiment = (experimentName: string, isEnabled: boolean) => {
    this.experiments[experimentName] = isEnabled;
  };

  updateShownClaimUsername = async () => {
    try {
      await this.apiClient.POST('/api/user/update_user_config/', {
        body: {
          shown_claim_username: true,
        },
      });
      this.shownClaimUsername = true;
    } catch (error) {
      console.error('Failed to update shown_claim_username:', error);
      throw error;
    }
  };

  updateShownCreateOnboarding = async () => {
    this.shownCreateOnboarding = true;
    try {
      await this.apiClient.POST('/api/user/update_user_config/', {
        body: {
          shown_create_onboarding: true,
        } as any, // Use 'as any' until backend adds the property to the schema
      });
    } catch (error) {
      console.error('Failed to update shown_create_onboarding:', error);
      // Continue anyway - the local flag is set
    }
  };

  setCreateOnboardingContext = (context: CreateOnboardingContext) => {
    this.createOnboardingContext = context;
  };

  shouldShowCreateOnboarding = () => {
    return (
      this.checkGate('new-user-welcome-onboarding') &&
      !this.shownCreateOnboarding &&
      this.roles?.['is_day_zero_user']
    );
  };

  shouldShowCreateOnboardingStep = (step: CreateOnboardingStep) => {
    return (
      this.shouldShowCreateOnboarding() &&
      this.createOnboardingContext?.step === step &&
      !this.createOnboardingContext?.isDismissed &&
      !this.isWelcomeModalCurrentlyOpen()
    );
  };

  clearTooltip = () => {
    // Advance to next step instead of just dismissing
    this.advanceOnboardingStep();
  };

  advanceOnboardingStep = () => {
    const onboardingOrder: CreateOnboardingStep[] = [
      'dice_tooltip',
      'create_tooltip',
      'play_tooltip_1',
      'like_tooltip',
      'share_tooltip',
      'play_tooltip_2',
      'credits_tooltip',
    ];

    const currentStep = this.createOnboardingContext?.step || 'create_tooltip';
    const currentIndex = onboardingOrder.indexOf(currentStep);
    let nextIndex = currentIndex + 1;

    // Skip play_tooltip_2 - go from share_tooltip directly to credits_tooltip
    if (currentStep === 'share_tooltip') {
      nextIndex = onboardingOrder.indexOf('credits_tooltip');
    }

    if (nextIndex < onboardingOrder.length) {
      // Advance to next step
      this.setCreateOnboardingContext({
        ...this.createOnboardingContext,
        step: onboardingOrder[nextIndex],
        isDismissed: false,
      });
    } else {
      // End of onboarding
      this.updateShownCreateOnboarding();
    }
  };

  clearTooltipOnPlay = () => {
    if (
      this.shouldShowCreateOnboarding() &&
      (this.createOnboardingContext?.step === 'play_tooltip_1' ||
        this.createOnboardingContext?.step === 'play_tooltip_2') &&
      !this.createOnboardingContext?.isDismissed
    ) {
      this.setCreateOnboardingContext({
        ...this.createOnboardingContext,
        isDismissed: true,
      });
    }
  };

  clearTooltipOnLike = () => {
    if (
      this.shouldShowCreateOnboarding() &&
      this.createOnboardingContext?.step === 'like_tooltip' &&
      !this.createOnboardingContext?.isDismissed
    ) {
      this.setCreateOnboardingContext({
        ...this.createOnboardingContext,
        isDismissed: true,
      });
    }
  };

  clearTooltipOnShare = () => {
    if (
      this.shouldShowCreateOnboarding() &&
      this.createOnboardingContext?.step === 'share_tooltip' &&
      !this.createOnboardingContext?.isDismissed
    ) {
      this.setCreateOnboardingContext({
        ...this.createOnboardingContext,
        isDismissed: true,
      });
    }
  };

  skipCreateTooltip = () => {
    if (
      this.shouldShowCreateOnboarding() &&
      this.createOnboardingContext?.step === 'create_tooltip'
    ) {
      this.setCreateOnboardingContext({
        ...this.createOnboardingContext,
        step: 'credits_tooltip',
        isDismissed: false,
      });
    }
  };

  clearTooltipOnDice = () => {
    if (
      this.shouldShowCreateOnboarding() &&
      this.createOnboardingContext?.step === 'dice_tooltip' &&
      !this.createOnboardingContext?.isDismissed
    ) {
      this.advanceOnboardingStep();
    }
  };

  logCreateOnboardingStepMounted = (step?: CreateOnboardingStep) => {
    if (!step || !this.createOnboardingContext) {
      return;
    }
    const newLoggedSteps = [
      ...(this.createOnboardingContext?.loggedSteps || []),
      step,
    ];
    this.setCreateOnboardingContext({
      ...this.createOnboardingContext,
      loggedSteps: newLoggedSteps,
    });
  };

  isAukEnabled = () => {
    return isFeatureEnabledForPlan(this, PlanFeature.Auk);
  };

  setHasSetRemixPerm = (value: boolean) => {
    this.hasSetRemixPerm = value;
  };

  get isMultiCurrencyStripeEnabled(): boolean {
    return this.flags?.['enable-multi-ccy-stripe'] || false;
  }

  checkIfCreditsExpiring = (
    cancelOn: string | null,
    periodEnd: string | null,
    monthlyLimit: number,
    monthlyUsage: number,
    isActive: boolean
  ): number | false => {
    // Helper function to ensure UTC parsing
    const parseAsUTC = (dateString: string): Date => {
      // If already has timezone info (Z or +/-), use as is
      if (dateString.includes('Z') || /[+-]\d{2}:\d{2}$/.test(dateString)) {
        return new Date(dateString);
      }
      // If has time but no timezone, append Z for UTC
      if (dateString.includes('T')) {
        return new Date(dateString + 'Z');
      }
      // If date only, append time and Z for UTC
      return new Date(dateString + 'T00:00:00Z');
    };

    // Check if subscription is active but will cancel soon (within next 7 days)
    if (cancelOn) {
      // Parse dates as UTC to ensure consistent timezone-aware calculations
      const cancelDate = parseAsUTC(cancelOn);
      const now = new Date();
      const daysDifference = Math.floor(
        (cancelDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
      );

      if (daysDifference <= 7 && daysDifference >= 0) {
        return daysDifference;
      }
    }

    // Check if subscription renewal is coming up and user has high credits (within next 7 days and credits > 50% of monthly limit)
    if (periodEnd && isActive) {
      // Parse dates as UTC to ensure consistent timezone-aware calculations
      const periodEndDate = parseAsUTC(periodEnd);
      const now = new Date();
      const daysDifference = Math.floor(
        (periodEndDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
      );

      // Handle null/undefined values to prevent NaN calculations
      const safeMonthlyLimit = monthlyLimit ?? 0;
      const safeMonthlyUsage = monthlyUsage ?? 0;

      // Calculate remaining credits directly from data parameter to avoid state dependency
      const paidCreditsRemaining = safeMonthlyLimit - safeMonthlyUsage;
      const highCredits =
        safeMonthlyLimit > 0 && paidCreditsRemaining > safeMonthlyLimit * 0.5;

      return daysDifference <= 7 && daysDifference >= 0 && highCredits
        ? daysDifference
        : false;
    }

    return false;
  };

  loadArtistProfileInfo = async () => {
    if (!this.user?.handle) {
      console.warn('No user handle available for artist profile fetch');
      return;
    }

    try {
      const { data } = await this.apiClient.GET('/api/profiles/{handle}/info', {
        params: {
          path: {
            handle: this.user.handle,
          },
        },
      });

      if (data) {
        runInAction(() => {
          this.artistProfileInfo = data;
          this.artistProfileInfoLoaded = true;
        });
      }
    } catch (error) {
      console.error('Failed to load artist profile info:', error);
      runInAction(() => {
        this.artistProfileInfoLoaded = true; // Mark as loaded even on error to prevent retries
      });
    }
  };

  updateArtistProfileInfo = (updatedInfo: ArtistProfileInfo) => {
    this.artistProfileInfo = updatedInfo;
  };

  isBannerDismissed = (bannerId: string): boolean => {
    return this.dismissedBanners.includes(bannerId);
  };

  dismissBanner = async (bannerId: string) => {
    if (!this.dismissedBanners.includes(bannerId)) {
      runInAction(() => {
        this.dismissedBanners = [...this.dismissedBanners, bannerId];
      });

      try {
        await this.apiClient.POST('/api/user/update_user_config/', {
          body: {
            dismissed_banners_web: this.dismissedBanners,
          },
        });
      } catch (error) {
        console.error('Failed to update dismissed banners:', error);
        // Remove from local state if API call fails
        runInAction(() => {
          this.dismissedBanners = this.dismissedBanners.filter(
            (id) => id !== bannerId
          );
        });
        throw error;
      }
    }
  };

  setWelcomeModalOpen = (isOpen: boolean) => {
    this.isWelcomeModalOpen = isOpen;
  };

  isWelcomeModalCurrentlyOpen = () => {
    return this.isWelcomeModalOpen;
  };
}
