import React from 'react'
import { AccessibilityInfo, Platform, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import { viewports } from '@telus-uds/system-constants'

import { applyTextStyles } from '../ThemeProvider/utils/styles'
import { useTheme } from '../ThemeProvider/useTheme'
import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
import { useResponsiveThemeTokens } from '../ThemeProvider/useResponsiveThemeTokens'
import { a11yProps } from '../utils/props/a11yProps'
import { getTokensPropType, selectTokens } from '../utils/props/tokens'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { variantProp } from '../utils/props/variantProp'
import { viewProps } from '../utils/props/viewProps'
import { wrapStringsInText } from '../utils/children'
import {
  createMediaQueryStyles,
  StyleSheet as StyleSheetUtils
} from '../utils/ssr-media-query/ssr-media-query'
import { useResponsiveProp } from '../utils/useResponsiveProp'
import { IconButton } from '../IconButton/IconButton'
import { useCopy } from '../utils/useCopy'
import { dictionary } from './dictionary'
import { useViewport } from '../ViewportProvider/useViewport'

const CONTENT_MAX_WIDTH = 'max'

const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])

const staticStyles = StyleSheet.create({
  contentRow: { flexDirection: 'row', flexShrink: 1 }
})

/**
 * Wraps media query stylesheet ids in the `dataSet` shape components use to expose them as
 * `data-media` on web. Returns nothing when there are no ids, so no attribute is rendered.
 *
 * @param {string} [mediaIds] - space-separated `rnmq-*` ids from `StyleSheet.create`
 * @returns {{ dataSet: { media: string } } | undefined}
 *
 * @example
 * <View {...mediaDataSet(ids.container)} />
 */
const mediaDataSet = (mediaIds) => (mediaIds ? { dataSet: { media: mediaIds } } : undefined)

/**
 * Narrows the `useResponsiveThemeTokens` viewport map to the current viewport's tokens. Under
 * `enableMediaQueryStyleSheet` every token read must go through this — a token read straight off
 * the map is a per-viewport object rather than a value, which renders as a blank style or takes the
 * render down (#7220). The `xs` fallback covers a render before the viewport resolves and is
 * removable via #7241.
 */
const selectCurrentTokens = (resolvedTokens, viewport, enableMediaQueryStyleSheet) =>
  enableMediaQueryStyleSheet
    ? resolvedTokens[viewport] ?? resolvedTokens[viewports.keys[0]]
    : resolvedTokens

/**
 * Selects the container tokens that serialise to real CSS declarations. Both paths use it, so
 * widening it widens both at once.
 *
 * Do not widen it back to a token spread. `icon` and `dismissIcon` are React components: under the
 * media query stylesheet a spread stringifies them into the declaration block, whose braces make
 * `insertRule` throw `Unexpected }` and take the render down with it, and on the default path they
 * land in the container's inline style, where the browser drops them while parsing the server HTML
 * and leaves the hydrating client holding a style the document never had. `iconGap`, `iconSize`,
 * `containerPaddingLeft` and `fontName` are dropped for the milder version of the same problem —
 * they hyphenate into declarations no browser understands (#7220).
 *
 * `lineHeight` is left out too: it is a ratio that `applyTextStyles` multiplies by the font size, so
 * emitting it here would set a `1.5px` line height on everything inside.
 */
const selectContainerStyles = ({
  backgroundColor,
  borderBottomWidth,
  borderColor,
  borderLeftWidth,
  borderRadius,
  borderRightWidth,
  borderTopWidth,
  color,
  fontSize,
  fontWeight,
  justifyContent,
  paddingBottom,
  paddingLeft,
  paddingRight,
  paddingTop
}) => ({
  backgroundColor,
  borderBottomWidth,
  borderColor,
  borderLeftWidth,
  borderRadius,
  borderRightWidth,
  borderTopWidth,
  color,
  fontSize,
  fontWeight,
  justifyContent,
  paddingBottom,
  paddingLeft,
  paddingRight,
  paddingTop
})

const selectTextStyles = (tokens, themeOptions, isDismissible) => {
  const textTokens = selectTokens('Typography', tokens)
  const styles = {
    ...textTokens,
    themeOptions,
    overflow: 'hidden'
  }

  if (!isDismissible) {
    styles.flexShrink = 1
  }

  return applyTextStyles(styles)
}

const selectIconProps = ({ iconSize, iconColor }) => ({
  size: iconSize,
  color: iconColor
})

const selectIconContainerStyles = ({ iconGap }) => ({
  paddingRight: iconGap,
  placeContent: 'start'
})

const selectDismissIconProps = ({ dismissIconSize, dismissIconColor }) => ({
  size: dismissIconSize,
  color: dismissIconColor
})

const selectDismissButtonContainerStyles = ({ dismissButtonGap }) => ({
  paddingLeft: dismissButtonGap,
  placeContent: 'start'
})

const selectContentContainerStyle = (
  themeTokens,
  maxWidth,
  system,
  viewport,
  useContentMaxWidth
) => ({
  maxWidth: system && (useContentMaxWidth || viewport === 'xl') ? maxWidth : '100%',
  width: '100%',
  paddingRight: themeTokens?.containerPaddingRight,
  paddingLeft: themeTokens?.containerPaddingLeft
})

/**
 * Builds the styles, media query ids and icon props for a render with `enableMediaQueryStyleSheet`
 * on. Every style set that varies with the viewport is compiled from the full token map into `@media`
 * rules, so the values a server render resolved at `xs` are corrected by CSS rather than by a
 * re-layout on hydration.
 *
 * Each style set gets its own `StyleSheet.create` call. This was required while
 * `create-stylesheet-mobile` re-cloned its accumulator inside the per-key loop, so a call carrying
 * more than one style set kept only the last one's media styles on iOS and Android. That is fixed;
 * the calls are left split because merging them would change nothing.
 *
 * The media rules carry `!important`, so a set that keeps its resolved values as the base style
 * leaves those values inert on web while native, which injects no rules, still reads them.
 *
 * Deliberately not memoized. Its inputs are the viewport token map, which `useResponsiveThemeTokens`
 * rebuilds on every render, so a `useMemo` would recompute every time while adding a dependency
 * array to maintain. Re-running it is cheap where it counts: the media query stylesheet
 * content-hashes each rule and `addCss` skips one it already holds, so repeat renders insert no CSS.
 *
 * @param {object} args
 * @param {object} args.tokensByViewport - the `xs`-to-`xl` token map from `useResponsiveThemeTokens`
 * @param {object} args.currentTokens - `tokensByViewport` narrowed to the current viewport
 * @returns {{ styles: object, mediaIds: object, iconProps: object, dismissIconProps: object }}
 */
const buildMediaQueryStyles = ({
  tokensByViewport,
  currentTokens,
  themeOptions,
  maxWidth,
  dismissible,
  system,
  useContentMaxWidth
}) => {
  const byViewport = (select) =>
    Object.entries(tokensByViewport).reduce((acc, [vp, viewportTokens]) => {
      acc[vp] = select(viewportTokens, vp)
      return acc
    }, {})

  const container = StyleSheetUtils.create({
    container: createMediaQueryStyles(
      byViewport((viewportTokens) => ({
        ...selectContainerStyles(viewportTokens),
        flexDirection: 'column'
      }))
    )
  })

  const contentContainer = StyleSheetUtils.create({
    contentContainer: createMediaQueryStyles(
      byViewport((viewportTokens, vp) => ({
        ...selectContentContainerStyle(viewportTokens, maxWidth, system, vp, useContentMaxWidth),
        flexDirection: 'row',
        flexShrink: 1,
        justifyContent: 'space-between',
        ...(system && { alignSelf: 'center' })
      }))
    )
  })

  const iconContainer = StyleSheetUtils.create({
    iconContainer: {
      ...selectIconContainerStyles(currentTokens),
      ...createMediaQueryStyles(byViewport(selectIconContainerStyles))
    }
  })

  const dismissButtonContainer = StyleSheetUtils.create({
    dismissButtonContainer: {
      ...selectDismissButtonContainerStyles(currentTokens),
      ...createMediaQueryStyles(byViewport(selectDismissButtonContainerStyles))
    }
  })

  const text = StyleSheetUtils.create({
    text: {
      ...selectTextStyles(currentTokens, themeOptions, dismissible),
      ...createMediaQueryStyles(
        byViewport((viewportTokens) => selectTextStyles(viewportTokens, themeOptions, dismissible))
      )
    }
  })

  return {
    styles: {
      container: container.styles.container,
      contentContainer: contentContainer.styles.contentContainer,
      iconContainer: iconContainer.styles.iconContainer,
      dismissButtonContainer: dismissButtonContainer.styles.dismissButtonContainer,
      text: text.styles.text
    },
    mediaIds: {
      container: container.ids.container,
      contentContainer: contentContainer.ids.contentContainer,
      iconContainer: iconContainer.ids.iconContainer,
      dismissButtonContainer: dismissButtonContainer.ids.dismissButtonContainer,
      text: text.ids.text
    },
    iconProps: selectIconProps(currentTokens),
    dismissIconProps: selectDismissIconProps(currentTokens)
  }
}

/**
 * Builds the same shape as `buildMediaQueryStyles` from a single resolved token set, for the default
 * path where `enableMediaQueryStyleSheet` is off. `mediaIds` is empty, so no `dataSet` is rendered.
 *
 * @param {object} args
 * @param {object} args.themeTokens - the resolved token set from `useThemeTokens`
 * @returns {{ styles: object, mediaIds: object, iconProps: object, dismissIconProps: object }}
 */
const buildDefaultStyles = ({
  themeTokens,
  themeOptions,
  maxWidth,
  dismissible,
  viewport,
  system,
  useContentMaxWidth
}) => ({
  styles: {
    container: {
      flexDirection: 'column',
      ...selectContainerStyles(themeTokens)
    },
    contentContainer: {
      flexDirection: 'row',
      flexShrink: 1,
      justifyContent: 'space-between',
      ...selectContentContainerStyle(themeTokens, maxWidth, system, viewport, useContentMaxWidth),
      ...(system && { alignSelf: 'center' })
    },
    iconContainer: selectIconContainerStyles(themeTokens),
    dismissButtonContainer: selectDismissButtonContainerStyles(themeTokens),
    text: selectTextStyles(themeTokens, themeOptions, dismissible)
  },
  mediaIds: {},
  iconProps: selectIconProps(themeTokens),
  dismissIconProps: selectDismissIconProps(themeTokens)
})

/**
 * A banner that highlights important messages:
 * - Status message to show there is an error or outage of services
 * - Confirmation message in response to user action
 * - Account information letting the user know they have almost used all their data
 *
 * ## Usage criteria
 * - Use simple text content only, such as bold test or links. Don’t use other complex components or paragraphs
 * - Show notifications before the content that the message is related to, otherwise show at the top of the main content
 * - Show system notifications at the top of the page, below the navigation, and expands the full-width of the viewport
 * - When showing multiple notifications, show them in order of importance from top to bottom
 * - Use variants to visually show the type of message contained within the Notification.
 * - The icon and color will indicate meaning and importance
 *
 * ### Variants
 * - Use `variant.style` to set the visual style of the notification
 * - Use `dismissible` prop to enable dismissible functionality
 *
 * ### When to use the system style variant?
 * - Use `variant.style ‘system’` to show system-based messages coming from the application
 * - Don’t use the `system` style variant when the message is in response to user action
 *
 * ## Variants
 *
 * #### Success
 * Use the success variant to provide feedback of a successful operation. The message will include an icon to indicate meaning and importance.
 *
 * #### Warning
 * Use the warning variant to provide feedback of a warning; users would still be able to proceed forward.
 * The message will include an icon to indicate meaning and importance.
 *
 * #### Error
 * Use the error variant to provide feedback of a failed operation. The message will include an icon to indicate meaning and importance.
 *
 * #### Dismissible
 * Dismissible notifications should only be used for default or success notifications where there is no action needed from the user.
 * If the information is passive then it should be dismissible as indicated by an interactive close button on the far right.
 * If the notification requires an action then make the option(s) clear with a `ChevronLink` or `Link`.
 *
 * Use the `dismissible` prop to allow users to dismiss the Notification at any time. Once dismissed, the Notification is removed from the DOM.
 *
 * Please note:
 *
 * - Default and Success variants are dismissible
 * - Error and Warning variants are not dismissible
 *
 * #### System message
 * Use full-width `Notifications` to show system-based messages coming from the application, not in response to user action.
 * Show system notifications at the top of the page, below the navigation, and expands the full-width of the viewport
 */
export const Notification = React.forwardRef(
  (
    { children, dismissible, copy = 'en', tokens, variant, onDismiss, contentMinWidth, ...rest },
    ref
  ) => {
    const [isDismissed, setIsDismissed] = React.useState(false)

    const announcement =
      rest.accessibilityLabel || (typeof children === 'string' ? children : undefined)

    // announceForAccessibility fallback: see #7275
    React.useEffect(() => {
      if (Platform.OS !== 'web' && announcement) {
        AccessibilityInfo.announceForAccessibility(announcement)
      }
    }, [announcement])

    const viewport = useViewport()
    const getCopy = useCopy({ dictionary, copy })

    const isSystemEnabled = variant?.style?.includes('system')

    const { themeOptions } = useTheme()
    const { enableMediaQueryStyleSheet } = themeOptions
    const useTokens = enableMediaQueryStyleSheet ? useResponsiveThemeTokens : useThemeTokens
    const resolvedTokens = useTokens(
      'Notification',
      tokens,
      variant,
      enableMediaQueryStyleSheet
        ? { system: isSystemEnabled }
        : { system: isSystemEnabled, viewport }
    )
    const currentTokens = selectCurrentTokens(resolvedTokens, viewport, enableMediaQueryStyleSheet)

    const useContentMaxWidth = useResponsiveProp(contentMinWidth) === CONTENT_MAX_WIDTH
    const maxWidth = useResponsiveProp(
      themeOptions?.contentMaxWidth,
      viewports.map.get(viewports.xl)
    )

    const { styles, mediaIds, iconProps, dismissIconProps } = enableMediaQueryStyleSheet
      ? buildMediaQueryStyles({
          tokensByViewport: resolvedTokens,
          currentTokens,
          themeOptions,
          maxWidth,
          dismissible,
          system: isSystemEnabled,
          useContentMaxWidth
        })
      : buildDefaultStyles({
          themeTokens: currentTokens,
          themeOptions,
          maxWidth,
          dismissible,
          viewport,
          system: isSystemEnabled,
          useContentMaxWidth
        })

    if (isDismissed) {
      return null
    }

    const content = wrapStringsInText(
      typeof children === 'function' ? children({ textStyles: styles.text, variant }) : children,
      { style: styles.text, ...mediaDataSet(mediaIds.text) }
    )

    const {
      icon: IconComponent,
      dismissIcon: DismissIconComponent,
      dismissIconColor
    } = currentTokens

    const onDismissPress = () => {
      setIsDismissed(true)
      onDismiss?.()
    }

    return (
      <View
        ref={ref}
        style={styles.container}
        {...mediaDataSet(mediaIds.container)}
        {...selectProps(rest)}
      >
        <View style={styles.contentContainer} {...mediaDataSet(mediaIds.contentContainer)}>
          <View style={staticStyles.contentRow}>
            {IconComponent && (
              <View style={styles.iconContainer} {...mediaDataSet(mediaIds.iconContainer)}>
                <IconComponent {...iconProps} />
              </View>
            )}
            {content && typeof content === 'function'
              ? content({ textStyles: styles.text, variant })
              : content}
          </View>
          {dismissible && DismissIconComponent && (
            <View
              style={styles.dismissButtonContainer}
              {...mediaDataSet(mediaIds.dismissButtonContainer)}
            >
              <IconButton
                action="close"
                onPress={onDismissPress}
                icon={DismissIconComponent}
                accessibilityRole="button"
                accessibilityLabel={getCopy('dismiss')}
                variant={{ inverse: dismissIconColor === '#ffffff', size: 'small' }}
              >
                {() => <DismissIconComponent {...dismissIconProps} />}
              </IconButton>
            </View>
          )}
        </View>
      </View>
    )
  }
)
Notification.displayName = 'Notification'

Notification.propTypes = {
  ...selectedSystemPropTypes,
  /**
   * Content of the `Notification`.
   */
  children: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
  /**
   * Use the `dismissible` prop to allow users to dismiss the Notification at any time.
   */
  dismissible: PropTypes.bool,
  /**
   * Select English or French copy for the accessible label of the dismiss button.
   */
  copy: PropTypes.oneOfType([
    PropTypes.oneOf(['en', 'fr']),
    PropTypes.shape({ dismiss: PropTypes.string })
  ]),
  /**
   * Callback function called when the dismiss button is clicked
   */
  onDismiss: PropTypes.func,
  /**
   * The minimum width of the content in the Notification when using the system variant.
   * This prop accepts responsive values for different viewports.
   * - `xs`: 'max' | 'full'
   * - `sm`: 'max' | 'full'
   * - `md`: 'max' | 'full'
   * - `lg`: 'max' | 'full'
   * - `xl`: 'max' | 'full'
   */
  contentMinWidth: PropTypes.shape({
    xl: PropTypes.oneOf(['max', 'full']),
    lg: PropTypes.oneOf(['max', 'full']),
    md: PropTypes.oneOf(['max', 'full']),
    sm: PropTypes.oneOf(['max', 'full']),
    xs: PropTypes.oneOf(['max', 'full'])
  }),
  tokens: getTokensPropType('Notification'),
  variant: variantProp.propType
}
