import * as React from 'react';
import {
  Platform,
  type StyleProp,
  StyleSheet,
  type ViewStyle,
} from 'react-native';
import warnOnce from 'warn-once';

import DebugContainer from './DebugContainer';
import {
  ScreenProps,
  ScreenStackHeaderConfigProps,
  StackPresentationTypes,
} from '../types';
import { ScreenStackHeaderConfig } from './ScreenStackHeaderConfig';
import Screen, { type ScreenInstance } from './Screen';
import ScreenStack from './ScreenStack';
import { RNSScreensRefContext } from '../contexts';
import { FooterComponent } from './ScreenFooter';
import { Edge, SafeAreaViewProps } from './safe-area/SafeAreaView.types';
import { SafeAreaView } from './safe-area/SafeAreaView';
import { featureFlags } from '../flags';
import { isIOS26OrHigher } from './helpers/PlatformUtils';
import {
  EdgeInsetApplicationContext,
  useEdgeInsetApplication,
} from './contexts/EdgeInsetApplicationContext';

type Props = Omit<
  ScreenProps,
  'enabled' | 'isNativeStack' | 'hasLargeHeader'
> & {
  screenId: string;
  headerConfig?: ScreenStackHeaderConfigProps | undefined;
  contentStyle?: StyleProp<ViewStyle> | undefined;
  /**
   * Per-edge override of the safe area insets applied to the screen's content.
   *
   * On iOS 26+ the screen is laid out under the navigation bar and its content
   * is wrapped in a `SafeAreaView`. By default, the top edge is applied for
   * non-transparent header. Edges explicitly set to `true` or `false` override
   * the default.
   *
   * When `ScrollView` with `contentInsetAdjustmentBehavior="automatic"` is used
   * as Screen's content, setting `top: false` might mitigate some problems like
   * "jumping" behavior with non-transparent header and large title.
   *
   * Limitation: with `top: false`, a `formSheet` using `fitToContents` with a
   * visible header is sized to the content alone, without room for the header,
   * which overlaps the top of the content.
   *
   * Has no effect below iOS 26 and on other platforms.
   *
   * Please note that this prop is marked as unstable and might be subject of
   * breaking changes, including removal.
   *
   * @platform ios
   */
  unstable_safeAreaEdges?: SafeAreaViewProps['edges'] | undefined;
};

function ScreenStackItem(
  {
    children,
    headerConfig,
    activityState,
    shouldFreeze,
    stackPresentation,
    sheetAllowedDetents,
    contentStyle,
    style,
    screenId,
    onHeaderHeightChange,
    scrollEdgeEffects,
    // eslint-disable-next-line camelcase
    unstable_sheetFooter,
    // eslint-disable-next-line camelcase
    unstable_safeAreaEdges,
    ...rest
  }: Props,
  ref: React.ForwardedRef<ScreenInstance>,
) {
  const headerVisible = !headerConfig?.hidden;
  const { nextContextValue: nextEdgeContextValue } = useEdgeInsetApplication(
    headerVisible,
    headerConfig?.disableTopInsetApplication ?? false,
    headerConfig?.disableLeftInsetApplication ?? false,
    headerConfig?.disableRightInsetApplication ?? false,
    headerConfig?.disableBottomInsetApplication ?? false,
  );

  const currentScreenRef = React.useRef<ScreenInstance | null>(null);
  const screenRefs = React.useContext(RNSScreensRefContext);

  React.useImperativeHandle(ref, () => currentScreenRef.current!);

  const stackPresentationWithDefault = stackPresentation ?? 'push';
  const headerConfigHiddenWithDefault = headerConfig?.hidden ?? false;

  const isHeaderInModal =
    Platform.OS === 'android'
      ? false
      : stackPresentationWithDefault !== 'push' &&
        headerConfigHiddenWithDefault === false;

  const headerHiddenPreviousRef = React.useRef(headerConfigHiddenWithDefault);

  React.useEffect(() => {
    warnOnce(
      Platform.OS !== 'android' &&
        stackPresentationWithDefault !== 'push' &&
        headerHiddenPreviousRef.current !== headerConfigHiddenWithDefault,
      `Dynamically changing header's visibility in modals will result in remounting the screen and losing all local state.`,
    );

    headerHiddenPreviousRef.current = headerConfigHiddenWithDefault;
  }, [headerConfigHiddenWithDefault, stackPresentationWithDefault]);

  const hasEdgeEffects =
    scrollEdgeEffects === undefined ||
    Object.values(scrollEdgeEffects).some(propValue => propValue !== 'hidden');
  const hasBlurEffect =
    headerConfig?.blurEffect !== undefined &&
    headerConfig.blurEffect !== 'none';

  warnOnce(
    hasEdgeEffects && hasBlurEffect && isIOS26OrHigher,
    '[RNScreens] Using both `blurEffect` and `scrollEdgeEffects` simultaneously may cause overlapping effects.',
  );

  const debugContainerStyle = getPositioningStyle(
    sheetAllowedDetents,
    stackPresentationWithDefault,
  );

  // For iOS, we need to extract background color and apply it to Screen
  // due to the safe area inset at the bottom of ScreenContentWrapper
  let internalScreenStyle;

  if (
    stackPresentationWithDefault === 'formSheet' &&
    Platform.OS === 'ios' &&
    contentStyle
  ) {
    const { screenStyles, contentWrapperStyles } =
      extractScreenStyles(contentStyle);
    internalScreenStyle = screenStyles;
    contentStyle = contentWrapperStyles;
  }

  // On iOS 26+ the screen is laid out under the navigation bar also when the header is opaque
  // (see `+[RNSScreenStackHeaderConfig screensExtendUnderOpaqueNavigationBar]`), so the content
  // is inset by our `SafeAreaView`. See `getSafeAreaEdges` for the applied edges. The
  // `SafeAreaView` is rendered regardless of `unstable_safeAreaEdges` so that changing it does not
  // remount the content.
  const shouldUseSafeAreaView = isIOS26OrHigher;

  const content = (
    <>
      <EdgeInsetApplicationContext.Provider value={nextEdgeContextValue}>
        <DebugContainer
          contentStyle={contentStyle}
          style={debugContainerStyle}
          stackPresentation={stackPresentationWithDefault}>
          {shouldUseSafeAreaView ? (
            <SafeAreaView
              edges={getSafeAreaEdges(headerConfig, unstable_safeAreaEdges)}>
              {children}
            </SafeAreaView>
          ) : (
            children
          )}
        </DebugContainer>
      </EdgeInsetApplicationContext.Provider>
      {/**
       * `HeaderConfig` needs to be the direct child of `Screen` without any intermediate `View`
       * We don't render it conditionally based on visibility to make it possible to dynamically render a custom `header`
       * Otherwise dynamically rendering a custom `header` leaves the native header visible
       *
       * https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md#screenstackheaderconfig
       *
       * HeaderConfig must not be first child of a Screen.
       * See https://github.com/software-mansion/react-native-screens/pull/1825
       * for detailed explanation.
       */}
      <ScreenStackHeaderConfig {...headerConfig} />
      {/* eslint-disable-next-line camelcase */}
      {stackPresentationWithDefault === 'formSheet' && unstable_sheetFooter && (
        <FooterComponent>{unstable_sheetFooter()}</FooterComponent>
      )}
    </>
  );

  return (
    <Screen
      ref={node => {
        currentScreenRef.current = node;

        if (screenRefs === null) {
          console.warn(
            'Looks like RNSScreensRefContext is missing. Make sure the ScreenStack component is wrapped in it',
          );
          return;
        }

        const currentRefs = screenRefs.current;

        if (node === null) {
          // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
          delete currentRefs[screenId];
        } else {
          currentRefs[screenId] = { current: node };
        }
      }}
      enabled
      isNativeStack
      activityState={activityState}
      shouldFreeze={shouldFreeze}
      screenId={screenId}
      stackPresentation={stackPresentationWithDefault}
      hasLargeHeader={headerConfig?.largeTitle ?? false}
      sheetAllowedDetents={sheetAllowedDetents}
      style={[style, internalScreenStyle]}
      scrollEdgeEffects={isHeaderInModal ? undefined : scrollEdgeEffects}
      onHeaderHeightChange={isHeaderInModal ? undefined : onHeaderHeightChange}
      {...rest}>
      {isHeaderInModal ? (
        <ScreenStack style={styles.container}>
          <Screen
            enabled
            isNativeStack
            activityState={activityState}
            shouldFreeze={shouldFreeze}
            hasLargeHeader={headerConfig?.largeTitle ?? false}
            scrollEdgeEffects={scrollEdgeEffects}
            style={StyleSheet.absoluteFill}
            onHeaderHeightChange={onHeaderHeightChange}>
            {content}
          </Screen>
        </ScreenStack>
      ) : (
        content
      )}
    </Screen>
  );
}

export default React.forwardRef(ScreenStackItem);

function getPositioningStyle(
  allowedDetents: ScreenProps['sheetAllowedDetents'],
  presentation: StackPresentationTypes,
) {
  const isIOS = Platform.OS === 'ios';

  if (presentation !== 'formSheet') {
    return styles.container;
  }

  if (isIOS) {
    if (
      allowedDetents !== 'fitToContents' &&
      featureFlags.experiment.synchronousScreenUpdatesEnabled
    ) {
      return styles.container;
    } else {
      return styles.absoluteWithNoBottom;
    }
  }

  /**
   * Note: `bottom: 0` is intentionally excluded from these styles for two reasons:
   *
   * 1. Omitting the bottom constraint ensures the Yoga layout engine does not dynamically
   * recalculate the Screen and content size during animations.
   *
   * 2. Including `bottom: 0` with 'position: absolute' would force
   * the component to anchor itself to an ancestor's bottom edge. This creates
   * a dependency on the ancestor's size, whereas 'fitToContents' requires the
   * FormSheet's dimensions to be derived strictly from its children.
   *
   * It was tested reliably only on Android.
   */
  if (allowedDetents === 'fitToContents') {
    return styles.absoluteWithNoBottom;
  }

  return styles.container;
}

type SplitStyleResult = {
  screenStyles: {
    backgroundColor?: ViewStyle['backgroundColor'] | undefined;
  };
  contentWrapperStyles: StyleProp<ViewStyle>;
};

// TODO: figure out whether other styles, like borders, filters, etc.
// shouldn't be applied on the Screen level on iOS due to the inset.
function extractScreenStyles(style: StyleProp<ViewStyle>): SplitStyleResult {
  const flatStyle = StyleSheet.flatten(style);

  const { backgroundColor, ...contentWrapperStyles } = flatStyle as ViewStyle;

  const screenStyles = {
    backgroundColor,
  };

  return {
    screenStyles,
    contentWrapperStyles,
  };
}

function getSafeAreaEdges(
  headerConfig: ScreenStackHeaderConfigProps | undefined,
  edgesOverride: SafeAreaViewProps['edges'],
): SafeAreaViewProps['edges'] {
  if (!isIOS26OrHigher) {
    return {};
  }

  // On iOS 26+ the screen extends under the navigation bar regardless of the header configuration
  // (see `+[RNSScreenStackHeaderConfig screensExtendUnderOpaqueNavigationBar]`), so by default the
  // content is inset from the top whenever the header is visible and opaque. Translucent and hidden
  // headers keep the content under the bar / status bar, as on other iOS versions.
  const edges: Partial<Record<Edge, boolean>> =
    headerConfig?.translucent || headerConfig?.hidden ? {} : { top: true };

  // Only edges explicitly set by the user override the defaults. `undefined` must not be copied,
  // as `SafeAreaView` treats a present-but-undefined edge as disabled.
  if (edgesOverride) {
    for (const edge of SAFE_AREA_EDGES) {
      const value = edgesOverride[edge];
      if (value !== undefined) {
        edges[edge] = value;
      }
    }
  }

  return edges;
}

const SAFE_AREA_EDGES: readonly Edge[] = ['top', 'right', 'bottom', 'left'];

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  absoluteWithNoBottom: {
    position: 'absolute',
    top: 0,
    start: 0,
    end: 0,
  },
});
