import {
  isAndroid,
  isClient,
  isIos,
  isWeb,
  useIsomorphicLayoutEffect,
} from '@tamagui/constants'
import {
  StyleObjectIdentifier,
  StyleObjectProperty,
  StyleObjectPseudo,
  StyleObjectRules,
  nonAnimatableStyleProps,
  stylePropsText,
  stylePropsTransform,
  tokenCategories,
  validPseudoKeys,
  validStyles as validStylesView,
} from '@tamagui/helpers'
import React from 'react'
import { getConfig, getFont, getSetting } from '../config'
import { isDevTools } from '../constants/isDevTools'
import {
  getMediaImportanceIfMoreImportant,
  getMediaKey,
  getMediaKeyImportance,
  mediaKeyMatch,
} from '../hooks/useMedia'
import { mediaState as globalMediaState, mediaQueryConfig } from './mediaState'
import type {
  AllGroupContexts,
  AnimationDriver,
  ClassNamesObject,
  ComponentContextI,
  DebugProp,
  GetStyleResult,
  GetStyleState,
  PseudoStyles,
  RulesToInsert,
  SpaceTokens,
  SplitStyleProps,
  StaticConfig,
  StyleObject,
  TamaguiComponentState,
  TamaguiInternalConfig,
  TextStyle,
  ThemeParsed,
  ViewStyleWithPseudos,
} from '../types'
import { createMediaStyle } from './createMediaStyle'
import { fixStyles } from './expandStyles'
import { getCSSStylesAtomic, getStyleAtomic, styleToCSS } from './getCSSStylesAtomic'
import { getDefaultProps } from './getDefaultProps'
import {
  extractValueFromDynamic,
  getDynamicVal,
  getOppositeScheme,
  isColorStyleKey,
} from './getDynamicVal'
import { getGroupPropParts } from './getGroupPropParts'
import { insertStyleRules, shouldInsertStyleRules, updateRules } from './insertStyleRule'
import { isActivePlatform, getPlatformSpecificityBump } from './isActivePlatform'
import { isActiveTheme } from './isActiveTheme'
import { log } from './log'
import { normalizeValueWithProperty } from './normalizeValueWithProperty'
import { propMapper } from './propMapper'
import {
  type PseudoDescriptorKey,
  pseudoDescriptors,
  pseudoPriorities,
  defaultMediaImportance,
} from './pseudoDescriptors'
import { skipProps } from './skipProps'
import { sortString } from './sortString'
import { transformsToString } from './transformsToString'

export type SplitStyles = ReturnType<typeof getSplitStyles>

export type SplitStyleResult = ReturnType<typeof getSplitStyles>

let conf: TamaguiInternalConfig

// WeakMap to track original token values for style objects
// Used to preserve '$8' style tokens instead of resolved 'var(--t-space-8)'
// for context prop propagation to children (issues #3670, #3676)
export const styleOriginalValues = new WeakMap<object, Record<string, any>>()

type StyleSplitter = (
  props: { [key: string]: any },
  staticConfig: StaticConfig,
  theme: ThemeParsed,
  themeName: string,
  componentState: TamaguiComponentState,
  styleProps: SplitStyleProps,
  parentSplitStyles?: GetStyleResult | null,
  context?: ComponentContextI,
  groupContext?: AllGroupContexts | null,
  // web-only
  elementType?: string,
  startedUnhydrated?: boolean,
  debug?: DebugProp,
  // resolved animation driver (respects animatedBy prop)
  animationDriver?: AnimationDriver | null
) => null | GetStyleResult

export const PROP_SPLIT = '-'

// Normalize group keys like $group-press to $group-true-press when the group name
// doesn't exist in context (defaults to the unnamed 'true' group)
function normalizeGroupKey(
  key: string,
  groupContext: AllGroupContexts | null | undefined
): string {
  const parts = key.split('-')
  const plen = parts.length
  if (
    // check if its actually a simple group selector to avoid breaking selectors
    plen === 2 ||
    (plen === 3 && pseudoPriorities[parts[parts.length - 1]])
  ) {
    const name = parts[1]
    if (name !== 'true' && groupContext && !groupContext[name]) {
      return key.replace('$group-', '$group-true-')
    }
  }
  return key
}

// if you need and easier way to test performance, you can do something like this
// add this early return somewhere in this file and you can see roughly where it slows down:

// return {
//   space,
//   hasMedia,
//   fontFamily: styleState.fontFamily,
//   viewProps: {
//     children: props.children,
//   },
//   style: {
//     borderColor: props.borderColor,
//     borderWidth: props.borderWidth,
//     padding: props.padding,
//   },
//   pseudos,
//   classNames,
//   rulesToInsert,
//   dynamicThemeAccess,
// }

function isValidStyleKey(
  key: string,
  validStyles: Record<string, boolean>,
  accept?: Record<string, any>
) {
  return key in validStyles ? true : accept && key in accept
}

export const getSplitStyles: StyleSplitter = (
  props,
  staticConfig,
  theme,
  themeName,
  componentState,
  styleProps,
  parentSplitStyles,
  componentContext,
  groupContext,
  elementType,
  startedUnhydrated,
  debug,
  animationDriver
) => {
  conf = conf || getConfig()
  // use passed animationDriver or fall back to context/config
  const driver =
    animationDriver ||
    componentContext?.animationDriver ||
    (conf.animations as AnimationDriver)

  if (props.passThrough) {
    return null
  }

  // a bit icky, we need no normalize but not fully
  if (
    isWeb &&
    styleProps.isAnimated &&
    driver?.isReactNative &&
    !styleProps.noNormalize
  ) {
    styleProps.noNormalize = 'values'
  }

  const { shorthands } = conf
  const {
    isHOC,
    isText,
    isInput,
    variants,
    isReactNative,
    inlineProps,
    inlineWhenUnflattened,
    parentStaticConfig,
    acceptsClassName,
  } = staticConfig

  const viewProps: GetStyleResult['viewProps'] = {}
  const mediaState = styleProps.mediaState || globalMediaState

  const shouldDoClasses = acceptsClassName && isWeb && !styleProps.noClass

  const rulesToInsert: RulesToInsert =
    process.env.TAMAGUI_TARGET === 'native' ? (undefined as any) : {}
  const classNames: ClassNamesObject = {}

  let space: SpaceTokens | null = props.space
  let pseudos: PseudoStyles | null = null
  let hasMedia: boolean | Set<string> = false
  let dynamicThemeAccess: boolean | undefined
  let pseudoGroups: Set<string> | undefined
  let mediaGroups: Set<string> | undefined
  let className = (props.className as string) || '' // existing classNames
  let mediaStylesSeen = 0

  const validStyles =
    staticConfig.validStyles ||
    (staticConfig.isText || staticConfig.isInput ? stylePropsText : validStylesView)

  if (
    process.env.NODE_ENV === 'development' &&
    (debug === 'profile' || (globalThis as any).time)
  ) {
    // @ts-expect-error
    time`split-styles-setup`
  }

  /**
   * Not the biggest fan of creating an object but it is a nice API
   */
  const styleState: GetStyleState = {
    classNames,
    conf,
    props,
    styleProps,
    componentState,
    staticConfig,
    style: null,
    theme,
    usedKeys: {},
    viewProps,
    context: componentContext,
    debug,
    // resolved animation driver (respects animatedBy prop)
    animationDriver: driver,
  }

  // only used by compiler
  if (process.env.IS_STATIC === 'is_static') {
    const { fallbackProps } = styleProps
    if (fallbackProps) {
      styleState.props = new Proxy(props, {
        get(_, key, val) {
          if (!Reflect.has(props, key)) {
            return Reflect.get(fallbackProps, key)
          }
          return Reflect.get(props, key)
        },
      })
    }
  }

  if (
    process.env.NODE_ENV === 'development' &&
    (debug === 'profile' || (globalThis as any).time)
  ) {
    // @ts-expect-error
    time`style-state`
  }

  if (process.env.NODE_ENV === 'development' && debug === 'verbose' && isClient) {
    if (isDevTools) {
      console.groupCollapsed('🔹 getSplitStyles 👇')
      log({
        props,
        staticConfig,
        shouldDoClasses,
        styleProps,
        rulesToInsert,
        componentState,
        styleState,
        theme: { ...theme },
      })
    }
  }

  const { asChild } = props
  const { accept } = staticConfig
  const { noSkip, disableExpandShorthands, noExpand, styledContext } = styleProps
  const { webContainerType } = conf.settings
  const parentVariants = parentStaticConfig?.variants
  for (const keyOg in props) {
    let keyInit = keyOg
    let valInit = props[keyInit]

    if (keyInit === 'children') {
      viewProps[keyInit] = valInit
      continue
    }

    if (
      process.env.NODE_ENV === 'development' &&
      (debug === 'profile' || (globalThis as any).time)
    ) {
      // @ts-expect-error
      time`before-prop-${keyInit}`
    }

    if (process.env.NODE_ENV === 'test' && keyInit === 'jestAnimatedStyle') {
      continue
    }

    // for custom accept sub-styles
    if (accept) {
      const accepted = accept[keyInit]
      if (
        (accepted === 'style' || accepted === 'textStyle') &&
        valInit &&
        typeof valInit === 'object'
      ) {
        viewProps[keyInit] = getSubStyle(styleState, keyInit, valInit, styleProps.noClass)
        continue
      }
    }

    // normalize shorthands up front
    if (!disableExpandShorthands) {
      if (keyInit in shorthands) {
        keyInit = shorthands[keyInit]
      }
    }

    if (keyInit === 'className') continue // handled above first

    // when asChild, skip default props - they shouldn't be passed down to children
    if (asChild) {
      const defaults = getDefaultProps(staticConfig)
      if (defaults) {
        // check both original key and expanded key (after shorthand expansion)
        const defaultVal = defaults[keyOg] ?? defaults[keyInit]
        if (defaultVal !== undefined && valInit === defaultVal) {
          continue
        }
      }
    }

    // keyInit === 'style' is handled in skipProps
    if (keyInit in skipProps && !noSkip && !isHOC) {
      if (keyInit === 'group') {
        if (process.env.TAMAGUI_TARGET === 'web') {
          // add container style
          const identifier = `t_group_${valInit}`
          const containerType = webContainerType || 'inline-size'
          const containerCSS = [
            'container',
            undefined,
            identifier,
            undefined,
            [
              `.${identifier} { container-name: ${valInit}; container-type: ${containerType}; }`,
            ],
          ] satisfies StyleObject
          addStyleToInsertRules(rulesToInsert, containerCSS)
        }
      }
      // transition prop is skipped when it's a named animation (e.g. 'quick')
      // but raw CSS values (from $platform-web) should pass through as style
      if (
        keyInit === 'transition' &&
        typeof valInit === 'string' &&
        !driver?.animations?.[valInit]
      ) {
        // not a known animation name, treat as raw CSS
      } else {
        continue
      }
    }

    let isValidStyleKeyInit = isValidStyleKey(keyInit, validStyles, accept)

    // this is all for partially optimized (not flattened)... maybe worth removing?
    if (process.env.TAMAGUI_TARGET === 'web') {
      // react-native-web ignores data-* attributes, fixes passing them to animated views
      if (staticConfig.isReactNative && keyInit.startsWith('data-')) {
        keyInit = keyInit.replace('data-', '')
        viewProps['dataSet'] ||= {}
        viewProps['dataSet'][keyInit] = valInit
        continue
      }
    }

    if (process.env.TAMAGUI_TARGET === 'native') {
      if (!isValidStyleKeyInit) {
        if (!isAndroid) {
          // only works in android
          if (keyInit === 'elevationAndroid') continue
        }

        // map userSelect to native prop
        if (keyInit === 'userSelect') {
          keyInit = 'selectable'
          valInit = valInit !== 'none'
        } else if (keyInit.startsWith('data-')) {
          continue
        }
      }
    }

    if (process.env.TAMAGUI_TARGET === 'web') {
      if (!noExpand) {
        /**
         * Copying in the accessibility/prop handling from react-native-web here
         * Keeps it in a single loop, avoids dup de-structuring to avoid bundle size
         */

        if (keyInit === 'disabled' && valInit === true) {
          viewProps['aria-disabled'] = true
          // Enhance with native semantics
          if (
            elementType === 'button' ||
            elementType === 'form' ||
            elementType === 'input' ||
            elementType === 'select' ||
            elementType === 'textarea'
          ) {
            viewProps.disabled = true
          }
          if (!variants?.disabled) {
            continue
          }
        }

        if (keyInit === 'testID') {
          if (isReactNative) {
            viewProps.testID = valInit
          } else {
            viewProps['data-testid'] = valInit
            // also keep testID when using RN animation driver (Animated.View
            // from react-native-web only forwards testID, not data-testid)
            if (styleProps.isAnimated && driver?.isReactNative) {
              viewProps.testID = valInit
            }
          }
          continue
        }

        if (keyInit === 'id') {
          viewProps.id = valInit
          continue
        }
      }
    }

    /**
     * There's (some) reason to this madness: we want to allow returning media/pseudo from variants
     * Say you have a variant hoverable: { true: { hoverStyle: {} } }
     * We run propMapper first to expand variant, then we run the inner loop and look again
     * for if there's a pseudo/media returned from it.
     */

    let isVariant = !isValidStyleKeyInit && variants && keyInit in variants

    const isStyleLikeKey = isValidStyleKeyInit || isVariant

    let isPseudo = keyInit in validPseudoKeys
    let isMedia = !isStyleLikeKey && !isPseudo ? getMediaKey(keyInit) : false
    let isMediaOrPseudo = Boolean(isMedia || isPseudo)

    if (isMediaOrPseudo && isMedia === 'group') {
      keyInit = normalizeGroupKey(keyInit, groupContext)
    }

    const isStyleProp = isValidStyleKeyInit || isMediaOrPseudo || (isVariant && !noExpand)

    if (isStyleProp && (asChild === 'except-style' || asChild === 'except-style-web')) {
      continue
    }

    const shouldPassProp =
      (!isStyleProp && isHOC) ||
      // is in parent variants
      (isHOC && parentVariants && keyInit in parentVariants) ||
      inlineProps?.has(keyInit)

    const parentVariant = parentVariants?.[keyInit]
    const isHOCShouldPassThrough = Boolean(
      isHOC &&
      (isValidStyleKeyInit || isMediaOrPseudo || parentVariant || keyInit in skipProps)
    )

    const shouldPassThrough = shouldPassProp || isHOCShouldPassThrough

    if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
      // console.groupEnd() // react native was not nesting right
      console.groupCollapsed(
        `  🔑 ${keyOg}${
          keyInit !== keyOg ? ` (shorthand for ${keyInit})` : ''
        } ${shouldPassThrough ? '(pass)' : ''}`
      )
      log({ isVariant, valInit, shouldPassProp })
      if (isClient) {
        log({
          variants,
          variant: variants?.[keyInit],
          isVariant,
          isHOCShouldPassThrough,
          usedKeys: { ...styleState.usedKeys },
          parentStaticConfig,
        })
      }
    }

    if (shouldPassThrough) {
      // // TODO bring this back but probably improve it?
      // if (isPseudo) {
      //   // this is a lot... but we need to track sub-keys so we don't override them in future things that aren't passed down
      //   // like our own variants that aren't in parent
      //   const pseudoStyleObject = getSubStyle(
      //     styleState,
      //     keyInit,
      //     valInit,
      //     fontFamily,
      //     true,
      //     state.noClass
      //   )
      //   const descriptor = pseudoDescriptors[keyInit]
      //   for (const key in pseudoStyleObject) {
      //     debugger
      //   }
      // }

      passDownProp(viewProps, keyInit, valInit, isMediaOrPseudo)

      if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
        console.groupEnd()
      }

      // if it's a variant here, we have a two layer variant...
      // aka styled(Input, { unstyled: true, variants: { unstyled: {} } })
      // which now has it's own unstyled + the child unstyled...
      // so *don't* skip applying the styles if its different from the parent one
      if (!isVariant) {
        continue
      }
    }

    // after shouldPassThrough
    if (!noSkip) {
      if (
        keyInit in skipProps &&
        !(
          keyInit === 'transition' &&
          typeof valInit === 'string' &&
          !driver?.animations?.[valInit]
        )
      ) {
        if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
          console.groupEnd()
        }
        continue
      }
    }

    // we sort of have to update fontFamily all the time: before variants run, after each variant
    if (isText || isInput) {
      if (
        valInit &&
        (keyInit === 'fontFamily' || keyInit === shorthands['fontFamily']) &&
        valInit in conf.fontsParsed
      ) {
        styleState.fontFamily = valInit
      }
    }

    const disablePropMap = isMediaOrPseudo || !isStyleLikeKey

    propMapper(keyInit, valInit, styleState, disablePropMap, (key, val, originalVal) => {
      const isStyledContextProp = styledContext && key in styledContext

      if (!isHOC && disablePropMap && !isStyledContextProp && !isMediaOrPseudo) {
        viewProps[key] = val
        return
      }

      if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
        console.groupCollapsed('  💠 expanded', keyInit, '=>', key)
        log(val)
        console.groupEnd()
      }

      if (val == null) return

      if (process.env.TAMAGUI_TARGET === 'native') {
        if (key === 'pointerEvents') {
          viewProps[key] = val
          return
        }
      }

      if (
        (!isHOC && isValidStyleKey(key, validStyles, accept)) ||
        (process.env.TAMAGUI_TARGET === 'native' && isAndroid && key === 'elevation')
      ) {
        mergeStyle(styleState, key, val, 1, false, originalVal)
        return
      }

      // re-run with expanded key
      isPseudo = key in validPseudoKeys
      isMedia = isPseudo ? false : getMediaKey(key)
      isMediaOrPseudo = Boolean(isMedia || isPseudo)
      isVariant = variants && key in variants

      // handle group key transformation for variant-expanded keys (issue #3613)
      if (isMedia === 'group') {
        key = normalizeGroupKey(key, groupContext)
      }

      if (
        inlineProps?.has(key) ||
        (process.env.IS_STATIC === 'is_static' && inlineWhenUnflattened?.has(key))
      ) {
        viewProps[key] = props[key] ?? val
      }

      // have to run this logic again here because expansions may need to be passed down
      // see StyledButtonVariantPseudoMerge test
      const shouldPassThrough =
        (styleProps.noExpand && isPseudo) ||
        (isHOC && (isMediaOrPseudo || parentStaticConfig?.variants?.[keyInit]))

      if (shouldPassThrough) {
        passDownProp(viewProps, key, val, isMediaOrPseudo)
        if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
          console.groupCollapsed(` - passing down prop ${key}`)
          log({ val, after: { ...viewProps[key] } })
          console.groupEnd()
        }
        return
      }

      if (isPseudo) {
        if (!val) return

        // TODO can avoid processing this if !shouldDoClasses + state is off
        // (note: can't because we need to set defaults on enter/exit or else enforce that they should)
        const pseudoStyleObject = getSubStyle(
          styleState,
          key,
          val,
          styleProps.noClass && !(process.env.IS_STATIC === 'is_static')
        )

        if (!shouldDoClasses || process.env.IS_STATIC === 'is_static') {
          pseudos ||= {}
          pseudos[key] ||= {}

          // if compiler we can just set this and continue on our way
          if (process.env.IS_STATIC === 'is_static') {
            Object.assign(pseudos[key], pseudoStyleObject)
            return
          }
        }

        const descriptor = pseudoDescriptors[key as keyof typeof pseudoDescriptors]
        const isEnter = key === 'enterStyle'
        const isExit = key === 'exitStyle'

        // don't continue here on isEnter && !state.unmounted because we need to merge defaults
        if (!descriptor) {
          return
        }

        // on server only generate classes for enterStyle
        if (shouldDoClasses && !isExit) {
          const pseudoStyles = getStyleAtomic(pseudoStyleObject, descriptor)

          if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
            console.info('pseudo:', key, pseudoStyleObject, pseudoStyles)
          }

          for (const psuedoStyle of pseudoStyles) {
            const fullKey = `${psuedoStyle[StyleObjectProperty]}${PROP_SPLIT}${descriptor.name}`
            addStyleToInsertRules(rulesToInsert, psuedoStyle)
            classNames[fullKey] = psuedoStyle[StyleObjectIdentifier]
          }
        }

        if (!shouldDoClasses || isExit || isEnter) {
          // we don't skip this if disabled because we need to animate to default states that aren't even set:
          // so if we have <Stack enterStyle={{ opacity: 0 }} />
          // we need to animate from 0 => 1 once enter is finished
          // see the if (isDisabled) block below which loops through animatableDefaults

          const descriptorKey = descriptor.stateKey || descriptor.name

          let isDisabled = componentState[descriptorKey] === false
          if (isExit) {
            isDisabled = !styleProps.isExiting
          }
          if (isEnter && componentState.unmounted === false) {
            isDisabled = true
          }

          if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
            console.groupCollapsed('pseudo', key, { isDisabled })
            log({ pseudoStyleObject, isDisabled, descriptor, componentState })
            console.groupEnd()
          }

          const importance = descriptor.priority

          const pseudoOriginalValues = styleOriginalValues.get(pseudoStyleObject)
          for (const pkey in pseudoStyleObject) {
            const val = pseudoStyleObject[pkey]
            // when disabled ensure the default value is set for future animations to align

            if (isDisabled) {
              applyDefaultStyle(pkey, styleState)
            } else {
              const curImportance = styleState.usedKeys[pkey] || 0
              const shouldMerge = importance >= curImportance

              if (shouldMerge) {
                if (process.env.IS_STATIC === 'is_static') {
                  pseudos ||= {}
                  pseudos[key] ||= {}
                  pseudos[key][pkey] = val
                }
                mergeStyle(
                  styleState,
                  pkey,
                  val,
                  importance,
                  false,
                  pseudoOriginalValues?.[pkey]
                )
              }

              if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
                log('    subKey', pkey, shouldMerge, {
                  importance,
                  curImportance,
                  pkey,
                  val,
                })
              }
            }
          }

          // set this after the loop over pseudoStyleObject so it applies before setting usedKeys
          if (!isDisabled) {
            // mark usedKeys based on pseudoStyleObject
            for (const key in val) {
              const k = shorthands[key] || key
              styleState.usedKeys[k] = Math.max(importance, styleState.usedKeys[k] || 0)
            }
          }
        }

        return
      }

      // media
      if (isMedia) {
        if (!val) return

        // for some reason 'space' in val upsetting next ssr during prod build
        // technically i guess this also will not apply if 0 space which makes sense?
        const mediaKeyShort = key.slice(isMedia == 'theme' ? 7 : 1)

        hasMedia ||= true
        const hasSpace = val['space']

        if (hasSpace || !shouldDoClasses || styleProps.willBeAnimated) {
          if (!hasMedia || typeof hasMedia === 'boolean') {
            hasMedia = new Set()
          }
          hasMedia.add(mediaKeyShort)
        }

        // can bail early
        if (isMedia === 'platform') {
          if (!isActivePlatform(key)) {
            return
          }
        }

        if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
          log(`  📺 ${key}`, {
            key,
            val,
            props,
            shouldDoClasses,
            acceptsClassName,
            componentState,
            mediaState,
          })
        }

        const priority = mediaStylesSeen
        mediaStylesSeen += 1

        // for theme media ($theme-light, $theme-dark), generate CSS classes for proper SSR
        // when noClass is set (inline animation drivers), de-opt to inline styles so the
        if (shouldDoClasses) {
          const mediaStyle = getSubStyle(styleState, key, val, false)
          const mediaStyles = getCSSStylesAtomic(mediaStyle)

          for (const style of mediaStyles) {
            // handle nested media:
            // for now we're doing weird stuff, getCSSStylesAtomic will put the
            // $platform-web into property so we can check it here
            const property = style[StyleObjectProperty]
            const isSubStyle = property[0] === '$'
            if (isSubStyle && !isActivePlatform(property)) {
              continue
            }

            const out = createMediaStyle(
              style,
              mediaKeyShort,
              mediaQueryConfig,
              isMedia,
              false,
              priority
            )

            if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
              log(`📺 media style:`, out)
            }

            // this is imperfect it should be fixed further down, we mess up property when dealing with
            // media-sub-style, like $sm={{ $platform-web: {} }}
            // property is just $platform-web, it should br $platform-web-bg, so we add extra info from style
            // but that info includes the value too
            const subKey = isSubStyle ? style[2] : ''
            const fullKey = `${
              style[StyleObjectProperty]
            }${subKey}${PROP_SPLIT}${mediaKeyShort}${style[StyleObjectPseudo] || ''}`

            addStyleToInsertRules(rulesToInsert, out as any)
            classNames[fullKey] = out[StyleObjectIdentifier]
          }
        } else {
          const isThemeMedia = isMedia === 'theme'
          const isGroupMedia = isMedia === 'group'
          const isPlatformMedia = isMedia === 'platform'

          if (!isThemeMedia && !isPlatformMedia && !isGroupMedia) {
            if (!mediaState[mediaKeyShort]) {
              if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
                log(`  📺 ❌ DISABLED ${mediaKeyShort}`)
              }
              return
            }
            if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
              log(`  📺 ✅ ENABLED ${mediaKeyShort}`)
            }
          }

          const mediaStyle = getSubStyle(styleState, key, val, true)

          let importanceBump = 0

          if (isThemeMedia) {
            if (
              process.env.TAMAGUI_TARGET === 'native' &&
              isIos &&
              getSetting('fastSchemeChange')
            ) {
              // iOS will use https://reactnative.dev/docs/dynamiccolorios
              // so need to predefine the dynamic color before merging the styles
              // for example: <StyledYStack $theme-dark={{borderColor: '$red10'}} $theme-light={{borderColor: '$green10'}}> => {borderColor: {dynamic: {dark: '$red10', light: '$green10'}}}

              styleState.style ||= {}
              const scheme = mediaKeyShort
              const oppositeScheme = getOppositeScheme(mediaKeyShort)
              const themeOriginalValues = styleOriginalValues.get(mediaStyle)
              const isCurrentScheme = themeName === scheme || themeName.startsWith(scheme)

              for (const subKey in mediaStyle) {
                const val = extractValueFromDynamic(mediaStyle[subKey], scheme)
                const existing = styleState.style[subKey]

                // Only color properties support DynamicColorIOS - non-color properties
                // like opacity, dimensions, etc. will crash if wrapped with {dynamic: {...}}
                // See: https://github.com/tamagui/tamagui/issues/3096
                // See: https://github.com/tamagui/tamagui/issues/2980
                if (!isColorStyleKey(subKey)) {
                  // non-color properties require re-render to update
                  dynamicThemeAccess = true
                  // only apply if this is the current theme
                  if (isCurrentScheme) {
                    // update mediaStyle so the later merge loop uses correct value
                    mediaStyle[subKey] = val
                  } else {
                    // remove from mediaStyle so it doesn't get merged with wrong theme's value
                    delete mediaStyle[subKey]
                  }
                  continue
                }

                // if there's already a dynamic object from the other theme pseudo prop,
                // merge directly to avoid importance conflicts between $theme-dark and $theme-light
                if (existing?.dynamic) {
                  existing.dynamic[scheme] = val
                  mediaStyle[subKey] = existing
                } else {
                  const oppositeVal = extractValueFromDynamic(existing, oppositeScheme)
                  mediaStyle[subKey] = getDynamicVal({
                    scheme,
                    val,
                    oppositeVal,
                  })
                  mergeStyle(
                    styleState,
                    subKey,
                    mediaStyle[subKey],
                    priority,
                    false,
                    themeOriginalValues?.[subKey]
                  )
                }
              }
            } else {
              // non-ios or no fastschemechange - need re-renders for theme changes
              dynamicThemeAccess = true
              if (!(themeName === mediaKeyShort || themeName.startsWith(mediaKeyShort))) {
                return
              }
            }
          } else if (isGroupMedia) {
            const groupInfo = getGroupPropParts(mediaKeyShort)
            const groupName = groupInfo.name

            // $group-x
            const groupState = groupContext?.[groupName]?.state
            const groupPseudoKey = groupInfo.pseudo
            const groupMediaKey = groupInfo.media

            if (!groupState) {
              if (process.env.NODE_ENV === 'development' && debug) {
                log(`No parent with group prop, skipping styles: ${groupName}`)
              }
              // we still want to indicate we should listen! this is how subscribeToGroupContext knows to run
              pseudoGroups ||= new Set()
              return
            }

            const componentGroupState = componentState.group?.[groupName]

            if (groupMediaKey) {
              mediaGroups ||= new Set()
              mediaGroups.add(groupMediaKey)
              const mediaState = componentGroupState?.media
              let isActive = mediaState?.[groupMediaKey]

              // use parent styles if width and height hardcoded we can do an inline media match and avoid double render
              if (!mediaState && groupState.layout) {
                isActive = mediaKeyMatch(groupMediaKey, groupState.layout)
              }

              if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
                log(` 🏘️ GROUP media ${groupMediaKey} active? ${isActive}`, {
                  ...mediaState,
                  usedKeys: { ...styleState.usedKeys },
                })
              }
              if (!isActive) {
                // ensure we set the defaults so animations work
                for (const pkey in mediaStyle) {
                  applyDefaultStyle(pkey, styleState)
                }

                return
              }
              importanceBump = 2
            }

            if (groupPseudoKey) {
              pseudoGroups ||= new Set()
              pseudoGroups.add(groupName)
              const componentGroupPseudoState = (
                componentGroupState ||
                // fallback to context initially
                groupContext?.[groupName].state
              )?.pseudo

              const isActive = componentGroupPseudoState?.[groupPseudoKey]
              const priority = pseudoPriorities[groupPseudoKey]

              if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
                log(
                  ` 🏘️ GROUP pseudo ${groupMediaKey} active? ${isActive}, priority ${priority}`,
                  {
                    componentGroupPseudoState: { ...componentGroupPseudoState },
                    usedKeys: { ...styleState.usedKeys },
                  }
                )
              }
              if (!isActive) {
                // ensure we set the defaults so animations work
                for (const pkey in mediaStyle) {
                  applyDefaultStyle(pkey, styleState)
                }

                return
              }
              importanceBump = priority
            }
          } else if (isPlatformMedia) {
            // Platform styles use specificity-based importance bumps so that more
            // specific platform selectors reliably win over broader ones regardless
            // of prop declaration order (e.g. $platform-tv always overrides
            // $platform-native for the same property, even if tv is listed first).
            importanceBump = getPlatformSpecificityBump(mediaKeyShort)
          }

          const mediaOriginalValues = styleOriginalValues.get(mediaStyle)

          // extract transition from group pseudo styles (e.g., $group-scenario4-hover.transition)
          if (isGroupMedia && mediaStyle.transition) {
            styleState.pseudoTransitions ||= {}
            styleState.pseudoTransitions[
              `$${mediaKeyShort}` as keyof typeof styleState.pseudoTransitions
            ] = mediaStyle.transition as any
          }

          function mergeMediaStyle(key: string, val: any, originalVal?: any) {
            // on native, non-style keys from media queries (like numberOfLines)
            // need to go to viewProps, not style
            if (process.env.TAMAGUI_TARGET === 'native') {
              if (!isValidStyleKey(key, validStyles, accept)) {
                viewProps[key] = val
                return
              }
            }
            styleState.style ||= {}
            const didMerge = mergeMediaByImportance(
              styleState,
              mediaKeyShort,
              key,
              val,
              mediaState[mediaKeyShort],
              importanceBump,
              debug,
              originalVal
            )
            if (didMerge && key === 'fontFamily') {
              styleState.fontFamily = mediaStyle.fontFamily as string
            }
          }

          for (const subKey in mediaStyle) {
            if (subKey === 'space') {
              continue
            }
            if (subKey[0] === '$') {
              const subMediaType = getMediaKey(subKey)
              if (subMediaType === 'platform') {
                if (!isActivePlatform(subKey)) continue
              } else if (subMediaType === 'theme') {
                if (!isActiveTheme(subKey, themeName)) continue
              } else if (subMediaType === true) {
                // regular media query nested inside platform/theme/media
                const subKeyShort = subKey.slice(1)
                if (!mediaState[subKeyShort]) continue
              }

              const nestedVal = mediaStyle[subKey] as Record<string, any>
              const subOriginalValues = styleOriginalValues.get(nestedVal)

              // Nested styles are more specific than their outer context because
              // they require both conditions to be true. Calculate an importance
              // that is the sum of both the outer and inner importances so that:
              //   1) nested always beats non-nested
              //   2) $xs={{ $platform-android: ... }} and
              //      $platform-android={{ $xs: ... }} produce identical importance
              //      (last-declared wins for the same property)
              const isSizeMediaKey = !!mediaState[mediaKeyShort]
              const outerBase = isSizeMediaKey
                ? getMediaKeyImportance(mediaKeyShort)
                : defaultMediaImportance

              let innerBase: number
              if (subMediaType === 'platform') {
                innerBase =
                  defaultMediaImportance + getPlatformSpecificityBump(subKey.slice(1))
              } else if (subMediaType === true) {
                innerBase = getMediaKeyImportance(subKey.slice(1))
              } else {
                innerBase = defaultMediaImportance
              }

              const nestedImportance = outerBase + importanceBump + innerBase + 1

              for (const subSubKey in nestedVal) {
                // expand shorthands — getSubStyle doesn't expand keys
                // inside nested $ objects (they pass through propMapper as-is)
                const expandedKey = shorthands[subSubKey] || subSubKey
                const { usedKeys } = styleState
                if (usedKeys[expandedKey] && usedKeys[expandedKey] > nestedImportance) {
                  continue
                }
                styleState.style ||= {}
                mergeStyle(
                  styleState,
                  expandedKey,
                  nestedVal[subSubKey],
                  nestedImportance,
                  false,
                  subOriginalValues?.[subSubKey]
                )
                if (expandedKey === 'fontFamily') {
                  styleState.fontFamily = nestedVal[subSubKey] as string
                }
              }
            } else {
              mergeMediaStyle(subKey, mediaStyle[subKey], mediaOriginalValues?.[subKey])
            }
          }
        }

        return // end media
      }

      // pass to view props
      if (!isVariant) {
        if (isStyledContextProp) {
          return
        }

        viewProps[key] = val
      }
    })

    if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
      try {
        log(` ✔️ expand complete`, keyInit)
        log('style', { ...styleState.style })
        log('viewProps', { ...viewProps })
        log('transforms', { ...styleState.flatTransforms })
      } catch {
        // RN can run into PayloadTooLargeError: request entity too large
      }
      console.groupEnd()
    }
  } // end prop loop

  if (
    process.env.NODE_ENV === 'development' &&
    (debug === 'profile' || (globalThis as any).time)
  ) {
    // @ts-expect-error
    time`split-styles-propsend`
  }

  // style prop after:

  const avoidNormalize = styleProps.noNormalize === false

  if (!avoidNormalize) {
    if (styleState.style) {
      fixStyles(styleState.style)

      if (!styleProps.noExpand && !styleProps.noMergeStyle) {
        // shouldn't this be better? but breaks some tests weirdly, need to check
        if (isWeb && (isReactNative ? driver?.inputStyle !== 'css' : true)) {
          styleToCSS(styleState.style)
        }
      }
    }

    // these are only the flat transforms
    // always do this at the very end to preserve the order strictly (animations, origin)
    // and allow proper merging of all pseudos before applying
    if (styleState.flatTransforms) {
      // we need to match the order for animations to work because it needs consistent order
      // was thinking of having something like `state.prevTransformsOrder = ['y', 'x', ...]
      // but if we just handle it here its not a big cost and avoids having stateful things
      // so the strategy is: always sort by a consistent order, until you run into a "duplicate"
      // because you can have something like:
      //   [{ translateX: 0 }, { scale: 1 }, { translateX: 10 }]
      // so basically we sort until we get to a duplicate... we could sort even smarter but
      // this should work for most (all?) of our cases since the order preservation really only needs to apply
      // to the "flat" transform props
      styleState.style ||= {}
      mergeFlatTransforms(styleState.style, styleState.flatTransforms)
    }

    // add in defaults if not set:
    if (parentSplitStyles) {
      if (process.env.TAMAGUI_TARGET === 'web') {
        if (shouldDoClasses) {
          for (const key in parentSplitStyles.classNames) {
            const val = parentSplitStyles.classNames[key]
            if ((styleState.style && key in styleState.style) || key in classNames)
              continue
            classNames[key] = val
          }
        }
      }
      if (!shouldDoClasses) {
        for (const key in parentSplitStyles.style) {
          if (key in classNames || (styleState.style && key in styleState.style)) continue
          styleState.style ||= {}
          styleState.style[key] = parentSplitStyles.style[key]
        }
      }
    }
  }

  // Button for example uses disableClassName: true but renders to a 'button' element, so needs this
  if (process.env.TAMAGUI_TARGET === 'web') {
    const shouldStringifyTransforms =
      !styleProps.noNormalize &&
      !staticConfig.isReactNative &&
      !staticConfig.isHOC &&
      (!styleProps.isAnimated || driver?.inputStyle === 'css')

    if (shouldStringifyTransforms && Array.isArray(styleState.style?.transform)) {
      styleState.style.transform = transformsToString(styleState.style!.transform) as any
    }
  }

  if (process.env.TAMAGUI_TARGET === 'web') {
    if (!styleProps.noMergeStyle && styleState.style && shouldDoClasses) {
      let retainedStyles: ViewStyleWithPseudos | undefined
      let shouldRetain = false

      if (styleState.style['$$css']) {
        // avoid re-processing for rnw
      } else {
        const atomic = getCSSStylesAtomic(styleState.style)

        for (const atomicStyle of atomic) {
          const [key, value, identifier] = atomicStyle

          const isAnimatedAndTransitionOnly =
            styleProps.isAnimated &&
            styleProps.noClass &&
            props.animateOnly?.includes(key)

          // animateOnly properties should always use className on server and initial
          // client render to avoid hydration mismatch (server has isAnimated=false but
          // client has isAnimated=true for CSS driver, causing different style output)
          const nonAnimatedTransitionOnly =
            !isAnimatedAndTransitionOnly &&
            !styleProps.isAnimated &&
            isClient &&
            driver?.outputStyle === 'css' &&
            props.animateOnly?.includes(key)

          if (isAnimatedAndTransitionOnly) {
            retainedStyles ||= {}
            retainedStyles[key] = styleState.style[key]
          } else if (nonAnimatedTransitionOnly) {
            retainedStyles ||= {}
            retainedStyles[key] = value
            shouldRetain = true
          } else {
            addStyleToInsertRules(rulesToInsert, atomicStyle)
            classNames[key] = identifier
          }
        }

        if (process.env.NODE_ENV === 'development' && props.debug === 'verbose') {
          // console.groupEnd() // ensure group ended from loop above
          console.groupCollapsed(`🔹 getSplitStyles final style object`)
          console.info(styleState.style)
          console.info(`retainedStyles`, retainedStyles)
          console.groupEnd()
        }

        if (shouldRetain || !(process.env.IS_STATIC === 'is_static')) {
          styleState.style = retainedStyles || {}
        }
      }
    }

    // when noClass is true (inline animation driver) extract non-animatable
    // base styles to atomic CSS classNames so the driver doesn't manage them
    // skip for RNW animation drivers since their AnimatedView doesn't forward classNames
    if (
      !styleProps.noMergeStyle &&
      styleState.style &&
      !shouldDoClasses &&
      styleProps.isAnimated &&
      !driver?.isReactNative
    ) {
      if (!styleState.style['$$css']) {
        const toConvert: Record<string, any> = {}
        let hasProps = false
        const animateOnly = props.animateOnly as string[] | undefined
        for (const key in styleState.style) {
          if (key in nonAnimatableStyleProps) {
            toConvert[key] = styleState.style[key]
            delete styleState.style[key]
            hasProps = true
          }
        }
        if (hasProps) {
          const atomic = getCSSStylesAtomic(toConvert)
          for (const atomicStyle of atomic) {
            addStyleToInsertRules(rulesToInsert, atomicStyle)
            classNames[atomicStyle[StyleObjectProperty]] =
              atomicStyle[StyleObjectIdentifier]
          }
        }
      }
    }
  }

  // merge after the prop loop - and always keep it on style dont turn into className except if RN gives us
  const styleProp = props.style

  if (!styleProps.noMergeStyle && styleProp) {
    if (isHOC) {
      viewProps.style = normalizeStyle(styleProp)
    } else {
      const isArray = Array.isArray(styleProp)
      const len = isArray ? styleProp.length : 1
      for (let i = 0; i < len; i++) {
        const style = isArray ? styleProp[i] : styleProp
        if (style) {
          if (style['$$css']) {
            Object.assign(styleState.classNames, style)
          } else {
            styleState.style ||= {}
            Object.assign(styleState.style, normalizeStyle(style))
          }
        }
      }
    }
  }

  // native: swap out the right family based on weight/style
  if (process.env.TAMAGUI_TARGET === 'native') {
    // set accessible when tabIndex is 0 (issue #3350)
    if (viewProps.tabIndex === 0) {
      viewProps.accessible ??= true
    }

    const style = styleState.style
    if (style?.fontFamily) {
      const faceInfo = getFont(style.fontFamily as string)?.face
      if (faceInfo) {
        const overrideFace =
          faceInfo[style.fontWeight as string]?.[style.fontStyle || 'normal']?.val
        if (overrideFace) {
          style.fontFamily = overrideFace
          styleState.fontFamily = overrideFace
          // If we pass both font family (e.g. InterBold) and a font weight (e.g. 900), android gets confused and just shows the default font, so we remove these:
          delete style.fontWeight
          delete style.fontStyle
        }
      }
      if (process.env.NODE_ENV === 'development' && debug && debug !== 'profile') {
        log(`Found fontFamily native: ${style.fontFamily}`, faceInfo)
      }
    }
  }

  if (
    process.env.NODE_ENV === 'development' &&
    (debug === 'profile' || (globalThis as any).time)
  ) {
    // @ts-expect-error
    time`split-styles-pre-result`
  }

  const result: GetStyleResult = {
    hasMedia,
    fontFamily: styleState.fontFamily,
    viewProps,
    style: styleState.style as any,
    pseudos,
    classNames,
    rulesToInsert,
    dynamicThemeAccess,
    pseudoGroups,
    mediaGroups,
    overriddenContextProps: styleState.overriddenContextProps,
    pseudoTransitions: styleState.pseudoTransitions,
  }

  const asChildExceptStyleLike =
    asChild === 'except-style' || asChild === 'except-style-web'

  if (!styleProps.noMergeStyle) {
    if (!asChildExceptStyleLike) {
      const style = styleState.style

      if (process.env.TAMAGUI_TARGET === 'web') {
        // merge className and style back into viewProps:
        // only emit font class if fontFamily was explicitly in props (not from defaults)
        let fontFamily = isText || isInput ? styleState.fontFamily : null
        if (fontFamily && fontFamily[0] === '$') {
          fontFamily = fontFamily.slice(1)
        }
        const fontFamilyClassName = fontFamily ? `font_${fontFamily}` : ''
        const groupClassName = props.group ? `t_group_${props.group}` : ''
        const componentNameFinal = props.componentName || staticConfig.componentName
        const componentNameClassName =
          props.asChild || !componentNameFinal || componentNameFinal === 'Text'
            ? ''
            : `is_${componentNameFinal}`

        let classList: string[] = []
        if (componentNameClassName) classList.push(componentNameClassName)
        // is_View gets base flex styles + font reset, is_Text gets base text styles
        if (!isText) classList.push('is_View')
        else classList.push('is_Text')
        if (fontFamilyClassName) classList.push(fontFamilyClassName)
        if (classNames) classList.push(Object.values(classNames).join(' '))
        if (groupClassName) classList.push(groupClassName)
        if (props.className) classList.push(props.className)
        const finalClassName = classList.join(' ')

        // use $$css for RNW components OR when animated with RNW driver
        // (driver's AnimatedView doesn't forward className)
        const needsCssStyles =
          isReactNative || (styleProps.isAnimated && driver?.isReactNative)

        if (styleProps.isAnimated && driver?.inputStyle === 'css') {
          // CSS animation driver uses className directly
          viewProps.className = finalClassName
          if (style) {
            viewProps.style = style as any
          }
        } else if (needsCssStyles) {
          // RNW or RNW-animated: apply classNames via $$css
          let cnStyles: Record<string, unknown> | undefined
          for (const name of finalClassName.split(' ')) {
            cnStyles ||= { $$css: true }
            cnStyles[name] = name
          }
          viewProps.style = cnStyles
            ? [...(Array.isArray(style) ? style : [style]), cnStyles]
            : [style]
        } else {
          // regular web: use className directly
          if (finalClassName) {
            viewProps.className = finalClassName
          }
          if (style) {
            viewProps.style = style as any
          }
        }
      } else {
        if (style) {
          // native assign styles
          viewProps.style = style as any
        }
      }
    }
  }

  if (process.env.NODE_ENV === 'development' && debug === 'verbose') {
    if (isClient && isDevTools) {
      // end collapsed log above
      console.groupEnd()

      console.groupCollapsed('🔹 getSplitStyles ===>')
      try {
        // prettier-ignore
        const logs = {
          ...result,
          className,
          componentState,
          viewProps,
          rulesToInsert,
          parentSplitStyles,
        }
        for (const key in logs) {
          log(key, logs[key])
        }
      } catch {
        // RN can run into PayloadTooLargeError: request entity too large
      }
      console.groupEnd()
    }
  }

  if (
    process.env.NODE_ENV === 'development' &&
    (debug === 'profile' || (globalThis as any).time)
  ) {
    // @ts-expect-error
    time`split-styles-done`
  }

  return result
}

function mergeFlatTransforms(target: TextStyle, flatTransforms: Record<string, any>) {
  Object.entries(flatTransforms)
    .sort(([a], [b]) => sortString(a, b))
    .forEach(([key, val]) => {
      mergeTransform(target, key, val, true)
    })
}

function mergeStyle(
  styleState: GetStyleState,
  key: string,
  val: any,
  importance: number,
  disableNormalize = false,
  originalVal?: any
) {
  const { viewProps, styleProps, staticConfig, usedKeys } = styleState

  const existingImportance = usedKeys[key] || 0
  if (existingImportance > importance) {
    return
  }

  // Track context overrides for pseudo/media styles (issues #3670, #3676)
  // When a style sets a key that's in context props, update overriddenContextProps
  // so it propagates to children. Use the original token value (like '$8')
  // instead of the resolved CSS variable (like 'var(--t-space-8)')
  // so children's functional variants can look up token values.
  const contextProps =
    staticConfig.context?.props || staticConfig.parentStaticConfig?.context?.props
  if (contextProps && key in contextProps) {
    styleState.overriddenContextProps ||= {}
    // Priority: 1) originalVal from propMapper, 2) tracked original from variant resolution, 3) val
    const originalFromState = styleState.originalContextPropValues?.[key]
    styleState.overriddenContextProps[key] = originalVal ?? originalFromState ?? val
  }

  if (key in stylePropsTransform) {
    styleState.flatTransforms ||= {}
    usedKeys[key] = importance
    styleState.flatTransforms[key] = val
  } else {
    const shouldNormalize = isWeb && !disableNormalize && !styleProps.noNormalize
    const out = shouldNormalize ? normalizeValueWithProperty(val, key) : val
    if (
      // accept is for props not styles
      staticConfig.accept &&
      key in staticConfig.accept
    ) {
      viewProps[key] = out
    } else {
      styleState.style ||= {}
      usedKeys[key] = importance
      styleState.style[key] =
        // if you dont do this you'll be passing props.transform arrays directly here and then mutating them
        // if theres any flatTransforms later, causing issues (mutating props is bad, in strict mode styles get borked)
        key === 'transform' && Array.isArray(out) ? [...out] : out
    }
  }
}

export const getSubStyle = (
  styleState: GetStyleState,
  subKey: string,
  styleIn: object,
  avoidMergeTransform?: boolean
): TextStyle => {
  const { staticConfig, conf, styleProps } = styleState
  const styleOut: TextStyle = {}
  let originalValues: Record<string, any> | undefined

  for (let key in styleIn) {
    const val = styleIn[key]
    key = conf.shorthands[key] || key

    // extract transition from pseudo-style props (e.g., hoverStyle.transition)
    // store it separately for animation drivers to use for enter/exit timing
    if (key === 'transition') {
      styleState.pseudoTransitions ||= {}
      styleState.pseudoTransitions[subKey as keyof typeof styleState.pseudoTransitions] =
        val
      // for CSS driver, also add transition to CSS output so native CSS transitions work
      // group styles ($group-*) need !important to override inline base transition
      const driver = styleState.animationDriver
      if (driver?.outputStyle === 'css') {
        const animationConfig = driver.animations?.[val as string]
        if (animationConfig) {
          const important = subKey[0] === '$' ? ' !important' : ''
          styleOut['transition'] = `all ${animationConfig}${important}`
        }
      }
      // not a known animation name, pass through as raw CSS
      if (
        !styleOut['transition'] &&
        typeof val === 'string' &&
        !driver?.animations?.[val]
      ) {
        styleOut['transition'] = val
      }
      continue
    }

    const shouldSkip = !staticConfig.isHOC && key in skipProps && !styleProps.noSkip
    if (shouldSkip) {
      continue
    }

    propMapper(key, val, styleState, false, (skey, sval, originalVal) => {
      // Track original values for context prop propagation
      if (originalVal !== undefined) {
        originalValues ||= {}
        originalValues[skey] = originalVal
      }
      // pseudo inside media
      if (skey in validPseudoKeys) {
        sval = getSubStyle(styleState, skey, sval, avoidMergeTransform)
      }
      if (!avoidMergeTransform && skey in stylePropsTransform) {
        mergeTransform(styleOut, skey, sval)
      } else {
        styleOut[skey] = styleProps.noNormalize
          ? sval
          : normalizeValueWithProperty(sval, key)
      }
    })
  }

  if (!avoidMergeTransform) {
    const parentTransform = styleState.style?.transform
    const flatTransforms = styleState.flatTransforms
    const styleOutTransform = styleOut.transform

    if (Array.isArray(styleOutTransform) && styleOutTransform.length) {
      // Inline conflict check - faster than building lookup object for small arrays
      const len = styleOutTransform.length

      if (Array.isArray(parentTransform)) {
        const merged: any[] = []
        outer: for (let i = 0; i < parentTransform.length; i++) {
          const pt = parentTransform[i]
          for (const pk in pt) {
            for (let j = 0; j < len; j++) {
              for (const sk in styleOutTransform[j]) {
                if (pk === sk) continue outer
                break
              }
            }
            merged.push(pt)
            break
          }
        }
        for (let i = 0; i < len; i++) merged.push(styleOutTransform[i])
        styleOut.transform = merged
      }

      if (flatTransforms) {
        outer: for (const fk in flatTransforms) {
          const ck = fk === 'x' ? 'translateX' : fk === 'y' ? 'translateY' : fk
          for (let j = 0; j < len; j++) {
            for (const sk in styleOutTransform[j]) {
              if (ck === sk) continue outer
              break
            }
          }
          mergeTransform(styleOut, fk, flatTransforms[fk])
        }
      }
    } else if (flatTransforms) {
      mergeFlatTransforms(styleOut, flatTransforms)
    }
  }

  if (!styleProps.noNormalize) {
    fixStyles(styleOut)
  }

  // Store original values in WeakMap instead of on the object itself
  if (originalValues) {
    styleOriginalValues.set(styleOut, originalValues)
  }

  return styleOut
}

// on native no need to insert any css
const useInsertEffectCompat = isWeb
  ? React.useInsertionEffect || useIsomorphicLayoutEffect
  : () => {}

// perf: ...args a bit expensive on native
export const useSplitStyles: StyleSplitter = (a, b, c, d, e, f, g, h, i, j, k, l, m) => {
  'use no memo'

  const res = getSplitStyles(a, b, c, d, e, f, g, h, i, j, k, l, m)

  if (process.env.TAMAGUI_TARGET !== 'native') {
    useInsertEffectCompat(() => {
      if (res) {
        insertStyleRules(res.rulesToInsert)
      }
    }, [res?.rulesToInsert])
  }

  return res
}

function addStyleToInsertRules(rulesToInsert: RulesToInsert, styleObject: StyleObject) {
  if (process.env.TAMAGUI_TARGET === 'web') {
    const identifier = styleObject[StyleObjectIdentifier]
    if (shouldInsertStyleRules(identifier)) {
      updateRules(identifier, styleObject[StyleObjectRules])
      rulesToInsert[identifier] = styleObject
    }
  }
}

const defaultColor = process.env.TAMAGUI_DEFAULT_COLOR || 'rgba(0,0,0,0)'
const animatableDefaults = {
  ...Object.fromEntries(
    Object.entries(tokenCategories.color).map(([k, v]) => [k, defaultColor])
  ),
  opacity: 1,
  scale: 1,
  scaleX: 1,
  scaleY: 1,
  rotate: '0deg',
  rotateX: '0deg',
  rotateY: '0deg',
  rotateZ: '0deg',
  skewX: '0deg',
  skewY: '0deg',
  x: 0,
  y: 0,
  borderRadius: 0,
}

const mergeTransform = (obj: TextStyle, key: string, val: any, backwards = false) => {
  if (typeof obj.transform === 'string') {
    return
  }
  obj.transform ||= []
  obj.transform[backwards ? 'unshift' : 'push']({
    [mapTransformKeys[key] || key]: val,
  } as any)
}

const mapTransformKeys = {
  x: 'translateX',
  y: 'translateY',
}

function passDownProp(
  viewProps: object,
  key: string,
  val: any,
  shouldMergeObject = false
) {
  if (shouldMergeObject) {
    const next = {
      ...viewProps[key],
      ...val,
    }
    // need to re-insert it at current position
    delete viewProps[key]
    viewProps[key] = next
  } else {
    viewProps[key] = val
  }
}

function mergeMediaByImportance(
  styleState: GetStyleState,
  mediaKey: string,
  key: string,
  value: any,
  isSizeMedia: boolean,
  importanceBump?: number,
  debugProp?: DebugProp,
  originalVal?: any
) {
  const usedKeys = styleState.usedKeys
  let importance = getMediaImportanceIfMoreImportant(
    mediaKey,
    key,
    styleState,
    isSizeMedia
  )
  if (importanceBump) {
    // With a specificity bump, the effective importance is always
    // defaultMediaImportance + bump. This lets higher-specificity styles
    // (e.g. $platform-tv > $platform-native) override lower-specificity ones
    // regardless of prop declaration order, even when getMediaImportanceIfMoreImportant
    // returns null (meaning the same base importance was already applied).
    //
    // We must re-check `usedKeys[key]` here (rather than relying on the null
    // returned by getMediaImportanceIfMoreImportant) because that function only
    // compares against `defaultMediaImportance`, which equals our base before
    // the bump. We need to compare against the *bumped* value to correctly
    // allow a more-specific style to win.
    const bumpedImportance = defaultMediaImportance + importanceBump
    importance =
      !usedKeys[key] || bumpedImportance > usedKeys[key] ? bumpedImportance : null
  }
  if (process.env.NODE_ENV === 'development' && debugProp === 'verbose') {
    log(
      `mergeMediaByImportance ${key} importance usedKey ${usedKeys[key]} next ${importance}`
    )
  }
  if (importance === null) {
    return false
  }
  if (key in pseudoDescriptors) {
    const descriptor = pseudoDescriptors[key as PseudoDescriptorKey]
    const descriptorKey = descriptor.stateKey || descriptor.name
    const isDisabled = styleState.componentState[descriptorKey] === false
    if (isDisabled) {
      return false
    }
    // For pseudo inside media, value is an object with subkeys
    const pseudoOriginalValues = styleOriginalValues.get(value as object)
    for (const subKey in value) {
      mergeStyle(
        styleState,
        subKey,
        value[subKey],
        importance,
        false,
        pseudoOriginalValues?.[subKey]
      )
    }
  } else {
    mergeStyle(styleState, key, value, importance, false, originalVal)
  }

  return true
}

function normalizeStyle(style: any) {
  const out: Record<string, any> = {}
  for (const key in style) {
    const val = style[key]
    if (key in stylePropsTransform) {
      mergeTransform(out, key, val)
    } else {
      out[key] = normalizeValueWithProperty(val, key)
    }
  }
  if (isWeb && Array.isArray(out.transform)) {
    out.transform = transformsToString(out.transform)
  }
  fixStyles(out)
  return out
}

function applyDefaultStyle(pkey: string, styleState: GetStyleState) {
  const defaultValues = animatableDefaults[pkey]
  if (
    defaultValues != null &&
    !(pkey in styleState.usedKeys) &&
    (!styleState.style || !(pkey in styleState.style))
  ) {
    mergeStyle(styleState, pkey, defaultValues, 1)
  }
}
