import {
  beginHeavyAnimation,
  memo, useEffect, useMemo, useRef, useState,
} from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';

import type {
  ApiStarGiftAttributeBackdrop,
  ApiStarGiftAttributeModel,
  ApiStarGiftAttributePattern,
  ApiStarGiftUnique,
  ApiSticker,
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';

import { requestMeasure, requestMutation } from '../../../../lib/fasterdom/fasterdom';
import { VTT_CRAFT_ATTRIBUTES } from '../../../../util/animations/viewTransitionTypes';
import buildClassName from '../../../../util/buildClassName';
import { NEXT_ARROW_REPLACEMENT } from '../../../../util/localization/format';
import { formatPercent } from '../../../../util/textFormat';
import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
import { getGiftAttributes } from '../../../common/helpers/gifts';
import { REM } from '../../../common/helpers/mediaDimensions';

import { useViewTransition } from '../../../../hooks/animations/useViewTransition';
import { useVtn } from '../../../../hooks/animations/useVtn';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useFlag from '../../../../hooks/useFlag';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';

import AnimatedCounter from '../../../common/AnimatedCounter';
import AnimatedIcon from '../../../common/AnimatedIcon';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import AnimatedIconWithPreview from '../../../common/AnimatedIconWithPreview';
import CustomEmoji from '../../../common/CustomEmoji';
import GiftRibbon from '../../../common/gift/GiftRibbon';
import Icon from '../../../common/icons/Icon';
import RadialPatternBackground from '../../../common/profile/RadialPatternBackground';
import StickerView from '../../../common/StickerView';
import Button from '../../../ui/Button';
import Modal from '../../../ui/Modal';
import Transition from '../../../ui/Transition';
import RadialProgress from './RadialProgress';

import styles from './GiftCraftModal.module.scss';

import craftPatternUrl from '../../../../assets/font-icons/craft.svg';

export type OwnProps = {
  modal: TabState['giftCraftModal'];
};

type StateProps = {
  craftAttributePermilles?: number[];
};

// ===================
// Types
// ===================
type FaceName = 'front' | 'back' | 'left' | 'right' | 'top' | 'bottom';

interface Vector3D {
  x: number;
  y: number;
  z: number;
}

interface FaceData {
  name: FaceName;
  normal: Vector3D;
  translate: [number, number, number];
  rotate: [number, number, number];
}

// ===================
// Configuration
// ===================
const STICKER_SIZE = 3.25 * REM;
const CUBE_SIZE = 7.5 * REM;
const SLOT_SIZE = 4.5 * REM;
const CUBE_HALF = CUBE_SIZE / 2;

const CONFIG = {
  cubeSize: CUBE_SIZE,
  cubeHalf: CUBE_HALF,
  slotSize: SLOT_SIZE,
  perspective: 600,

  // Physics
  maxSpeed: 15,
  friction: 0.992,
  minVelocity: 0.1,
  idleSpeed: 0.4,

  // Animation
  flightSpeed: 0.75,
  minFlightDuration: 250,
  maxFlightDuration: 500,
  flightEasing: 'cubic-bezier(0.7, 0, 1, 1)',
  faceSelectionTime: 1,

  // Square kick
  squareKickStrength: 20,
  kickMomentumDampen: 0.15,

  // Last slot special kick
  lastKickStrength: 40,
  lastKickFriction: 0.998,

  // Result braking
  brakingFriction: 0.985,
  brakingDuration: 700,

  // Craft result
  craftInitialDelay: 350,
  slotFlightInterval: 650,
  craftActionDelay: 200,
  resultFadeDelay: 300,
  resultRotationDuration: 650,
  resultDisplayDuration: 800,

  // Slot removal
  slotRemoveDuration: 150,
};

const ATTRIBUTE_DIAL_SIZE = 4 * REM;

const GRADIENT_COLORS_DEFAULT: [string, string] = ['#425068', '#232E3F'];
const GRADIENT_COLORS_HIGH_CHANCE: [string, string] = ['#365C61', '#1A2F38'];
const GRADIENT_COLORS_FAILED: [string, string] = ['#5C362C', '#351B17'];

const HIGH_CHANCE_THRESHOLD = 95;

const FACES_DATA: FaceData[] = [
  { name: 'front', normal: { x: 0, y: 0, z: 1 }, translate: [0, 0, CUBE_HALF], rotate: [0, 0, 0] },
  { name: 'back', normal: { x: 0, y: 0, z: -1 }, translate: [0, 0, -CUBE_HALF], rotate: [0, 180, 0] },
  { name: 'left', normal: { x: -1, y: 0, z: 0 }, translate: [-CUBE_HALF, 0, 0], rotate: [0, -90, 0] },
  { name: 'right', normal: { x: 1, y: 0, z: 0 }, translate: [CUBE_HALF, 0, 0], rotate: [0, 90, 0] },
  { name: 'top', normal: { x: 0, y: -1, z: 0 }, translate: [0, -CUBE_HALF, 0], rotate: [90, 0, 0] },
  { name: 'bottom', normal: { x: 0, y: 1, z: 0 }, translate: [0, CUBE_HALF, 0], rotate: [-90, 0, 0] },
];

const FACES_BY_NAME = Object.fromEntries(FACES_DATA.map((f) => [f.name, f])) as Record<FaceName, FaceData>;

const PATTERN_STICKER_SIZE = 1.875 * REM;
const PATTERN_STICKER_COLOR = '#FFFFFF';

const PatternAttributePreview = memo(({ sticker }: { sticker: ApiSticker }) => {
  const ref = useRef<HTMLDivElement>();
  return (
    <div
      ref={ref}
      className={styles.patternAttribute}
      style={`width: ${PATTERN_STICKER_SIZE}px; height: ${PATTERN_STICKER_SIZE}px`}
    >
      <StickerView
        containerRef={ref}
        sticker={sticker}
        size={PATTERN_STICKER_SIZE}
        customColor={PATTERN_STICKER_COLOR}
        shouldPreloadPreview
        thumbClassName={styles.patternAttributeThumb}
      />
    </div>
  );
});

type CraftSlotProps = {
  index: number;
  gift?: ApiStarGiftUnique;
  isUsed: boolean;
  isAnimating: boolean;
  isActivated: boolean;
  isRemoving: boolean;
  slotRef: (el: HTMLDivElement | undefined) => void;
  slotInnerRef: (el: HTMLDivElement | undefined) => void;
  onSlotClick: (index: number) => void;
  onRemoveGift: (index: number) => void;
};

const CraftSlot = memo(({
  index,
  gift,
  isUsed,
  isAnimating,
  isActivated,
  isRemoving,
  slotRef,
  slotInnerRef,
  onSlotClick,
  onRemoveGift,
}: CraftSlotProps) => {
  const giftAttributes = useMemo(() => (gift ? getGiftAttributes(gift) : undefined), [gift]);
  const chancePercent = (gift?.craftChancePermille || 0) / 10;

  const handleClick = useLastCallback(() => {
    if (!isActivated) {
      onSlotClick(index);
    }
  });

  const handleRemoveClick = useLastCallback((e: React.MouseEvent) => {
    e.stopPropagation();
    onRemoveGift(index);
  });

  return (
    <div
      className={buildClassName(
        styles.slotWrapper,
        isUsed && styles.used,
        index >= 2 && styles.bottomRow,
      )}
    >
      <div
        ref={slotRef}
        className={buildClassName(
          styles.craftSlot,
          gift && styles.craftSlotFilled,
          isAnimating && styles.animating,
          isRemoving && styles.removing,
        )}
        onClick={handleClick}
      >
        {gift && giftAttributes ? (
          <>
            <div
              ref={slotInnerRef}
              className={styles.slotInner}
            >
              <RadialPatternBackground
                className={styles.slotBackdrop}
                backgroundColors={[giftAttributes.backdrop!.centerColor, giftAttributes.backdrop!.edgeColor]}
                ringsCount={1}
                ovalFactor={1}
                patternSize={12}
                patternIcon={giftAttributes.pattern?.sticker}
              />
              <AnimatedIconFromSticker
                className={styles.slotSticker}
                sticker={giftAttributes.model?.sticker}
                size={STICKER_SIZE}
              />
            </div>
            {chancePercent > 0 && (
              <span className={styles.slotChance}>
                {formatPercent(chancePercent, 0)}
              </span>
            )}
            {!isActivated && (
              <button
                type="button"
                className={styles.slotClear}
                onClick={handleRemoveClick}
              >
                <Icon name="close" />
              </button>
            )}
          </>
        ) : (
          <Icon name="add-filled" className={styles.slotIcon} />
        )}
      </div>
    </div>
  );
});

const GiftCraftModal = ({ modal, craftAttributePermilles }: OwnProps & StateProps) => {
  const {
    closeGiftCraftModal, openGiftCraftSelectModal, selectGiftForCraft,
    craftStarGift, requestConfetti, openGiftInfoModal, openGiftCraftInfoModal,
    resetGiftCraftResult, openGiftPreviewModal,
  } = getActions();

  const lang = useLang();
  const { startViewTransition } = useViewTransition();
  const { createVtnStyle } = useVtn();

  const isOpen = Boolean(modal);
  const renderingModal = useCurrentOrPrev(modal);

  const savedGift1 = renderingModal?.gift1;
  const savedGift2 = renderingModal?.gift2;
  const savedGift3 = renderingModal?.gift3;
  const savedGift4 = renderingModal?.gift4;
  const craftResult = renderingModal?.craftResult;
  const previewAttributes = renderingModal?.previewAttributes;

  // Extract unique gifts from saved gifts
  const gift1 = savedGift1?.gift.type === 'starGiftUnique' ? savedGift1.gift : undefined;
  const gift2 = savedGift2?.gift.type === 'starGiftUnique' ? savedGift2.gift : undefined;
  const gift3 = savedGift3?.gift.type === 'starGiftUnique' ? savedGift3.gift : undefined;
  const gift4 = savedGift4?.gift.type === 'starGiftUnique' ? savedGift4.gift : undefined;

  const mainGift = gift1 || gift2 || gift3 || gift4;
  const lastMainGift = useCurrentOrPrev(mainGift, true);
  const giftTitle = mainGift?.title || renderingModal?.regularGiftTitle || '';

  const gifts = useMemo(
    () => [gift1, gift2, gift3, gift4],
    [gift1, gift2, gift3, gift4],
  );

  const totalChancePermille = useMemo(() => {
    const getChance = (g?: ApiStarGiftUnique) => g?.craftChancePermille || 0;
    return getChance(gift1) + getChance(gift2) + getChance(gift3) + getChance(gift4);
  }, [gift1, gift2, gift3, gift4]);

  const progressPercent = Math.min(100, totalChancePermille / 10);

  const fullGiftTitle = useMemo(() => {
    const giftNumber = mainGift?.number;
    if (!giftTitle) return '';
    return giftNumber ? `${giftTitle} #${giftNumber}` : giftTitle;
  }, [giftTitle, mainGift?.number]);

  const titleGiftSticker = useMemo(() => {
    if (!mainGift) return undefined;
    return getGiftAttributes(mainGift)?.model?.sticker;
  }, [mainGift]);

  const previewModelStickers = useMemo(() => {
    if (!previewAttributes?.length) return undefined;
    return previewAttributes
      .filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model')
      .slice(0, 3)
      .map((attr) => attr.sticker);
  }, [previewAttributes]);

  // Calculate attribute stats for probability circles
  const attributeStats = useMemo(() => {
    const selectedGifts = gifts.filter((g): g is ApiStarGiftUnique => Boolean(g));
    if (selectedGifts.length === 0) return { backdrops: [], patterns: [] };

    const backdropCounts = new Map<number, { count: number; attr: ApiStarGiftAttributeBackdrop }>();
    const patternCounts = new Map<string, { count: number; attr: ApiStarGiftAttributePattern }>();

    for (const gift of selectedGifts) {
      for (const attr of gift.attributes) {
        if (attr.type === 'backdrop') {
          const existing = backdropCounts.get(attr.backdropId);
          if (existing) {
            existing.count++;
          } else {
            backdropCounts.set(attr.backdropId, { count: 1, attr });
          }
        } else if (attr.type === 'pattern') {
          const key = attr.sticker.id;
          const existing = patternCounts.get(key);
          if (existing) {
            existing.count++;
          } else {
            patternCounts.set(key, { count: 1, attr });
          }
        }
      }
    }

    const totalGifts = selectedGifts.length;
    const getPermille = (count: number) => {
      if (!craftAttributePermilles?.length || totalGifts === 0) return 0;
      const giftsIndex = Math.min(totalGifts, craftAttributePermilles.length) - 1;
      const perGiftsArray = craftAttributePermilles[giftsIndex];
      if (!Array.isArray(perGiftsArray)) return 0;
      const countIndex = Math.min(count, perGiftsArray.length) - 1;
      return perGiftsArray[countIndex] ?? 0;
    };

    const backdrops = Array.from(backdropCounts.values())
      .map(({ count, attr }) => ({ attr, count, permille: getPermille(count) }))
      .sort((a, b) => b.count - a.count);

    const patterns = Array.from(patternCounts.values())
      .map(({ count, attr }) => ({ attr, count, permille: getPermille(count) }))
      .sort((a, b) => b.count - a.count);

    return { backdrops, patterns };
  }, [gifts, craftAttributePermilles]);

  // DOM refs
  const cubeRef = useRef<HTMLDivElement>();
  const facesRef = useRef<Record<FaceName, HTMLDivElement | undefined>>({
    front: undefined,
    back: undefined,
    left: undefined,
    right: undefined,
    top: undefined,
    bottom: undefined,
  });
  const slotsRef = useRef<(HTMLDivElement | undefined)[]>([]);
  const slotsInnerRef = useRef<(HTMLDivElement | undefined)[]>([]);
  const backfaceRemovedRef = useRef(false);

  // Physics state refs (mutable, not triggering re-renders)
  const rotationMatrixRef = useRef<DOMMatrix>(new DOMMatrix());
  const velocityRef = useRef({ x: 0, y: 0 });
  const rafIdRef = useRef<number | undefined>(undefined);
  const tempDeltaMatrixRef = useRef<DOMMatrix>(new DOMMatrix());
  const tempNormalRef = useRef<Vector3D>({ x: 0, y: 0, z: 0 });
  const faceTransformsRef = useRef<Record<FaceName, DOMMatrix> | undefined>(undefined);
  const kickIntervalRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
  const lastKickedFaceRef = useRef<FaceName | undefined>(undefined);
  const lastSlotIndexRef = useRef<number>(-1);
  const currentFrictionRef = useRef(CONFIG.friction);

  // Animation state
  const [isActivated, activate, deactivate] = useFlag();
  const [slotsKey, setSlotsKey] = useState(0);

  const coloredFacesRef = useRef<Set<FaceName>>(new Set());
  const [usedSlots, setUsedSlots] = useState<Set<number>>(() => new Set());
  const [animatingSlots, setAnimatingSlots] = useState<Set<number>>(() => new Set());
  const [removingSlots, setRemovingSlots] = useState<Set<number>>(() => new Set());
  const [craftedGiftFace, setCraftedGiftFace] = useState<{
    face: FaceName;
    gift: ApiStarGiftUnique;
  } | undefined>(undefined);
  const [failedFace, setFailedFace] = useState<{
    face: FaceName;
    burnedCount: number;
    isError?: true;
  } | undefined>(undefined);
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [isResultRotationComplete, markResultRotationComplete, resetResultRotation] = useFlag();
  const [isRotationStarted, startRotation, resetRotationStarted] = useFlag();

  // Initialize face transforms
  useEffect(() => {
    if (!faceTransformsRef.current) {
      const transforms: Record<string, DOMMatrix> = {};
      for (const face of FACES_DATA) {
        transforms[face.name] = new DOMMatrix()
          .translateSelf(...face.translate)
          .rotateSelf(...face.rotate);
      }
      faceTransformsRef.current = transforms as Record<FaceName, DOMMatrix>;
    }
  }, []);

  const resetAnimationState = useLastCallback(() => {
    rotationMatrixRef.current = new DOMMatrix();
    velocityRef.current = { x: 0, y: 0 };
    backfaceRemovedRef.current = false;
    lastKickedFaceRef.current = undefined;
    lastSlotIndexRef.current = -1;
    currentFrictionRef.current = CONFIG.friction;

    if (rafIdRef.current) {
      cancelAnimationFrame(rafIdRef.current);
      rafIdRef.current = undefined;
    }
    if (kickIntervalRef.current) {
      clearInterval(kickIntervalRef.current);
      kickIntervalRef.current = undefined;
    }

    coloredFacesRef.current = new Set();
    setUsedSlots(new Set());
    setAnimatingSlots(new Set());
    setRemovingSlots(new Set());
    setRenderingAttributes([]);
    deactivate();
    setCraftedGiftFace(undefined);
    setFailedFace(undefined);
    resetResultRotation();
    resetRotationStarted();

    requestMutation(() => {
      // Reset cube transform
      if (cubeRef.current) {
        cubeRef.current.style.transform = '';
      }

      // Reset face backgrounds and backface-visibility
      Object.values(facesRef.current).forEach((face) => {
        if (face) {
          face.style.background = '';
          face.style.backfaceVisibility = '';
        }
      });

      // Reset slot transforms
      slotsRef.current.forEach((slot) => {
        if (slot) {
          slot.style.transform = '';
          slot.style.transition = '';
          slot.style.position = '';
          slot.style.top = '';
          slot.style.left = '';
          slot.style.marginTop = '';
          slot.style.marginLeft = '';
          slot.classList.remove(styles.craftSlotHidden);
        }
      });
    });
  });

  // Reset state when modal opens
  useEffect(() => {
    if (isOpen) {
      resetAnimationState();
    }
  }, [isOpen, resetAnimationState]);

  // Cleanup animation on unmount
  useEffect(() => {
    return () => {
      if (rafIdRef.current) {
        cancelAnimationFrame(rafIdRef.current);
        rafIdRef.current = undefined;
      }
      if (kickIntervalRef.current) {
        clearInterval(kickIntervalRef.current);
        kickIntervalRef.current = undefined;
      }
    };
  }, []);

  // Fade out slots when result rotation is complete
  useEffect(() => {
    if (isRotationStarted) {
      slotsRef.current.forEach((slot) => {
        if (slot) {
          requestMutation(() => {
            slot.classList.add(styles.craftSlotHidden);
          });
        }
      });
    }
  }, [isRotationStarted]);

  // ===================
  // Physics Functions
  // ===================
  const resetMatrix = (matrix: DOMMatrix) => {
    matrix.a = 1;
    matrix.b = 0;
    matrix.c = 0;
    matrix.d = 1;
    matrix.e = 0;
    matrix.f = 0;
    matrix.m11 = 1;
    matrix.m12 = 0;
    matrix.m13 = 0;
    matrix.m14 = 0;
    matrix.m21 = 0;
    matrix.m22 = 1;
    matrix.m23 = 0;
    matrix.m24 = 0;
    matrix.m31 = 0;
    matrix.m32 = 0;
    matrix.m33 = 1;
    matrix.m34 = 0;
    matrix.m41 = 0;
    matrix.m42 = 0;
    matrix.m43 = 0;
    matrix.m44 = 1;
  };

  const transformNormalZ = (normal: Vector3D, matrix: DOMMatrix): number => {
    return matrix.m13 * normal.x + matrix.m23 * normal.y + matrix.m33 * normal.z;
  };

  const transformNormal = (normal: Vector3D, matrix: DOMMatrix, out: Vector3D): Vector3D => {
    out.x = matrix.m11 * normal.x + matrix.m21 * normal.y + matrix.m31 * normal.z;
    out.y = matrix.m12 * normal.x + matrix.m22 * normal.y + matrix.m32 * normal.z;
    out.z = matrix.m13 * normal.x + matrix.m23 * normal.y + matrix.m33 * normal.z;
    return out;
  };

  const predictRotationMatrix = (ms: number): DOMMatrix => {
    const frames = Math.round(ms / 16.67);
    const { friction, minVelocity } = CONFIG;

    let predVelX = velocityRef.current.x;
    let predVelY = velocityRef.current.y;
    let predMatrix = DOMMatrix.fromMatrix(rotationMatrixRef.current);
    const tempMatrix = new DOMMatrix();

    for (let i = 0; i < frames; i++) {
      if (Math.abs(predVelX) > minVelocity || Math.abs(predVelY) > minVelocity) {
        resetMatrix(tempMatrix);
        tempMatrix.rotateSelf(predVelX, predVelY, 0);
        predMatrix = tempMatrix.multiply(predMatrix);
        predVelX *= friction;
        predVelY *= friction;
      }
    }

    return predMatrix;
  };

  const getLargestVisibleFace = (
    matrix: DOMMatrix,
    excludeFaces: Set<FaceName>,
  ): FaceName | undefined => {
    let bestFace: FaceName | undefined;
    let bestZ = -Infinity;

    for (const face of FACES_DATA) {
      if (excludeFaces.has(face.name)) continue;

      const z = transformNormalZ(face.normal, matrix);
      if (z <= 0) continue;

      if (z > bestZ) {
        bestZ = z;
        bestFace = face.name;
      }
    }

    return bestFace;
  };

  const capVelocity = () => {
    const { maxSpeed } = CONFIG;
    const currentSpeed = Math.sqrt(
      velocityRef.current.x * velocityRef.current.x + velocityRef.current.y * velocityRef.current.y,
    );
    if (currentSpeed > maxSpeed) {
      const scale = maxSpeed / currentSpeed;
      velocityRef.current.x *= scale;
      velocityRef.current.y *= scale;
    }
  };

  const applyKickWithNormal = (flightDirX: number, flightDirY: number, faceNormal: Vector3D, strength: number) => {
    const flightLen = Math.sqrt(flightDirX * flightDirX + flightDirY * flightDirY);
    const fdx = flightDirX / flightLen;
    const fdy = flightDirY / flightLen;

    const fLen = Math.sqrt(fdx * fdx + fdy * fdy + 1);
    const fx = fdx / fLen;
    const fy = fdy / fLen;
    const fz = -1 / fLen;

    const torqueX = faceNormal.y * fz - faceNormal.z * fy;
    const torqueY = faceNormal.z * fx - faceNormal.x * fz;

    velocityRef.current.x *= CONFIG.kickMomentumDampen;
    velocityRef.current.y *= CONFIG.kickMomentumDampen;

    velocityRef.current.x += torqueX * strength;
    velocityRef.current.y += torqueY * strength;
    capVelocity();
  };

  // ===================
  // Decelerate to Face
  // ===================
  const decelerateToFace = useLastCallback((targetFace: FaceName, onComplete: NoneToVoidFunction) => {
    const duration = CONFIG.resultRotationDuration;
    const startTime = performance.now();

    // Keep the actual start matrix (don't extract/reconstruct angles)
    const startMatrix = DOMMatrix.fromMatrix(rotationMatrixRef.current);

    // Get current rotation direction from velocity
    const yRotationDirection = Math.sign(velocityRef.current.y) || 1;

    // Calculate target matrix based on face
    let targetMatrix: DOMMatrix;
    switch (targetFace) {
      case 'front':
        targetMatrix = new DOMMatrix(); // identity
        break;
      case 'back':
        targetMatrix = new DOMMatrix().rotateSelf(0, 180, 0);
        break;
      case 'left':
        targetMatrix = new DOMMatrix().rotateSelf(0, 90, 0);
        break;
      case 'right':
        targetMatrix = new DOMMatrix().rotateSelf(0, -90, 0);
        break;
      case 'top':
        targetMatrix = new DOMMatrix().rotateSelf(-90, 0, 0);
        break;
      case 'bottom':
        targetMatrix = new DOMMatrix().rotateSelf(90, 0, 0);
        break;
      default:
        targetMatrix = new DOMMatrix();
    }

    // Calculate how much extra rotation to add based on current direction
    // This makes the cube "spin through" to the target following inertia
    const extraSpins = yRotationDirection > 0 ? 1 : -1; // Add one full spin in current direction
    const spinMatrix = new DOMMatrix().rotateSelf(0, extraSpins * 360, 0);
    targetMatrix = spinMatrix.multiply(targetMatrix);

    // Stop the regular animation loop
    if (rafIdRef.current) {
      cancelAnimationFrame(rafIdRef.current);
      rafIdRef.current = undefined;
    }

    // Use longer duration for the extra spin
    const scaledDuration = duration * 2;

    const animate = (now: number) => {
      const elapsed = now - startTime;
      const progress = Math.min(elapsed / scaledDuration, 1);
      // Ease out cubic for smooth deceleration
      const eased = 1 - (1 - progress) ** 3;

      // Interpolate matrix elements and orthonormalize
      const m = new DOMMatrix();
      m.m11 = startMatrix.m11 + (targetMatrix.m11 - startMatrix.m11) * eased;
      m.m12 = startMatrix.m12 + (targetMatrix.m12 - startMatrix.m12) * eased;
      m.m13 = startMatrix.m13 + (targetMatrix.m13 - startMatrix.m13) * eased;
      m.m21 = startMatrix.m21 + (targetMatrix.m21 - startMatrix.m21) * eased;
      m.m22 = startMatrix.m22 + (targetMatrix.m22 - startMatrix.m22) * eased;
      m.m23 = startMatrix.m23 + (targetMatrix.m23 - startMatrix.m23) * eased;
      m.m31 = startMatrix.m31 + (targetMatrix.m31 - startMatrix.m31) * eased;
      m.m32 = startMatrix.m32 + (targetMatrix.m32 - startMatrix.m32) * eased;
      m.m33 = startMatrix.m33 + (targetMatrix.m33 - startMatrix.m33) * eased;

      // Gram-Schmidt orthonormalization to prevent distortion
      // Column 1 - normalize
      let len = Math.sqrt(m.m11 * m.m11 + m.m21 * m.m21 + m.m31 * m.m31);
      m.m11 /= len;
      m.m21 /= len;
      m.m31 /= len;
      // Column 2 - subtract projection onto column 1, then normalize
      const dot = m.m11 * m.m12 + m.m21 * m.m22 + m.m31 * m.m32;
      m.m12 -= dot * m.m11;
      m.m22 -= dot * m.m21;
      m.m32 -= dot * m.m31;
      len = Math.sqrt(m.m12 * m.m12 + m.m22 * m.m22 + m.m32 * m.m32);
      m.m12 /= len;
      m.m22 /= len;
      m.m32 /= len;
      // Column 3 - cross product of columns 1 and 2
      m.m13 = m.m21 * m.m32 - m.m31 * m.m22;
      m.m23 = m.m31 * m.m12 - m.m11 * m.m32;
      m.m33 = m.m11 * m.m22 - m.m21 * m.m12;

      rotationMatrixRef.current = m;

      const transformValue = m.toString();
      requestMutation(() => {
        if (cubeRef.current) {
          cubeRef.current.style.transform = transformValue;
        }
      });

      if (progress < 1) {
        requestAnimationFrame(animate);
      } else {
        onComplete();
      }
    };

    requestAnimationFrame(animate);
  });

  // ===================
  // Animation Loop
  // ===================
  const lastTimeRef = useRef<number>(0);

  const startAnimationLoop = useLastCallback(() => {
    if (rafIdRef.current !== undefined) return;

    const { minVelocity, idleSpeed } = CONFIG;
    const tempMatrix = tempDeltaMatrixRef.current;

    lastTimeRef.current = performance.now();

    const update = (now: number) => {
      const dt = (now - lastTimeRef.current) / 16.67;
      lastTimeRef.current = now;

      const vel = velocityRef.current;

      // Apply friction (using current friction ref for dynamic control)
      const frictionFactor = currentFrictionRef.current ** dt;
      vel.x *= frictionFactor;
      vel.y *= frictionFactor;

      // Maintain minimum idle rotation
      const currentSpeed = Math.sqrt(vel.x * vel.x + vel.y * vel.y);
      if (currentSpeed < idleSpeed) {
        if (currentSpeed > 0.001) {
          const scale = idleSpeed / currentSpeed;
          vel.x *= scale;
          vel.y *= scale;
        } else {
          vel.x = idleSpeed * 0.7;
          vel.y = idleSpeed * 0.7;
        }
      }

      // Always rotate if we have any velocity
      if (currentSpeed >= minVelocity) {
        resetMatrix(tempMatrix);
        tempMatrix.rotateSelf(vel.x * dt, vel.y * dt, 0);
        rotationMatrixRef.current = tempMatrix.multiply(rotationMatrixRef.current);

        const transformValue = rotationMatrixRef.current.toString();
        requestMutation(() => {
          if (cubeRef.current) {
            cubeRef.current.style.transform = transformValue;
          }
        });
      }

      rafIdRef.current = requestAnimationFrame(update);
    };

    rafIdRef.current = requestAnimationFrame(update);
  });

  // ===================
  // Slot Flight Animation
  // ===================
  const flySlotToCube = useLastCallback((index: number) => {
    const slot = slotsRef.current[index];
    const cube = cubeRef.current;
    if (!slot || !cube || !faceTransformsRef.current) return;

    if (animatingSlots.has(index) || usedSlots.has(index)) return;

    const {
      perspective, cubeSize, slotSize, flightSpeed,
      minFlightDuration, maxFlightDuration, faceSelectionTime, flightEasing,
    } = CONFIG;

    // Measure DOM positions in a single read phase
    requestMeasure(() => {
      // Get cube's actual center position
      const cubeRect = cube.getBoundingClientRect();
      const cubeCenter = {
        x: cubeRect.left + cubeRect.width / 2,
        y: cubeRect.top + cubeRect.height / 2,
      };

      const slotRect = slot.getBoundingClientRect();
      const slotCenter = {
        x: slotRect.left + slotRect.width / 2,
        y: slotRect.top + slotRect.height / 2,
      };

      performSlotFlight(
        index, slot, cube, cubeCenter, slotCenter,
        perspective, cubeSize, slotSize, flightSpeed,
        minFlightDuration, maxFlightDuration, faceSelectionTime, flightEasing,
      );
    });
  });

  const performSlotFlight = useLastCallback((
    index: number,
    slot: HTMLDivElement,
    cube: HTMLDivElement,
    cubeCenter: { x: number; y: number },
    slotCenter: { x: number; y: number },
    perspective: number,
    cubeSize: number,
    slotSize: number,
    flightSpeed: number,
    minFlightDuration: number,
    maxFlightDuration: number,
    faceSelectionTime: number,
    flightEasing: string,
  ) => {
    if (!faceTransformsRef.current) return;

    const estimatedDistance = Math.sqrt(
      (cubeCenter.x - slotCenter.x) ** 2 + (cubeCenter.y - slotCenter.y) ** 2,
    );
    const animationDuration = Math.max(
      minFlightDuration,
      Math.min(maxFlightDuration, estimatedDistance / flightSpeed),
    );

    // Predict where cube will be at selection point for face selection
    const predictionTime = animationDuration * faceSelectionTime;
    const predictedRotation = predictRotationMatrix(predictionTime);

    const targetFace = getLargestVisibleFace(predictedRotation, coloredFacesRef.current);
    if (!targetFace) return;

    // Predict where face will be at end of flight
    const futureRotation = predictRotationMatrix(animationDuration);
    const faceWorldTransform = futureRotation.multiply(faceTransformsRef.current[targetFace]);

    const face3dZ = faceWorldTransform.m43;
    const perspectiveScale = perspective / (perspective - face3dZ);
    const faceScreenX = faceWorldTransform.m41 * perspectiveScale;
    const faceScreenY = faceWorldTransform.m42 * perspectiveScale;

    const rotationOnly = DOMMatrix.fromMatrix(faceWorldTransform);
    rotationOnly.m41 = 0;
    rotationOnly.m42 = 0;
    rotationOnly.m43 = 0;

    const targetX = cubeCenter.x + faceScreenX - slotCenter.x;
    const targetY = cubeCenter.y + faceScreenY - slotCenter.y;
    const scaleFactor = (cubeSize / slotSize) * perspectiveScale;

    const finalTransform = new DOMMatrix()
      .translateSelf(targetX, targetY, 0)
      .scaleSelf(scaleFactor)
      .multiplySelf(rotationOnly);

    const zRotation = (Math.atan2(rotationOnly.m12, rotationOnly.m11) * 180) / Math.PI;
    const snappedZ = Math.round(zRotation / 90) * 90;

    // Mark as animating
    setAnimatingSlots((prev) => new Set(prev).add(index));

    // Pre-rotate slot and counter-rotate inner content to avoid visible rotation jump
    const slotInner = slotsInnerRef.current[index];
    const finalTransformStr = finalTransform.toString();

    requestMutation(() => {
      slot.style.transition = 'none';
      slot.style.transform = `rotateZ(${snappedZ}deg)`;
      if (slotInner) {
        slotInner.style.transition = 'none';
        slotInner.style.transform = `rotateZ(${-snappedZ}deg)`;
      }
    });

    // Use rAF to trigger animation
    requestAnimationFrame(() => {
      requestMutation(() => {
        slot.style.transition = `transform ${animationDuration}ms ${flightEasing}`;
        slot.style.transform = finalTransformStr;
        if (slotInner) {
          slotInner.style.transition = `transform ${animationDuration}ms ${flightEasing}`;
          slotInner.style.transform = '';
        }
      });
    });

    // Get face normal for kick
    const faceNormal = transformNormal(FACES_BY_NAME[targetFace].normal, futureRotation, tempNormalRef.current);

    const onTransitionEnd = () => {
      slot.removeEventListener('transitionend', onTransitionEnd);

      requestMutation(() => {
        // Remove backface-visibility from all faces on first slot stick
        if (!backfaceRemovedRef.current) {
          backfaceRemovedRef.current = true;
          Object.values(facesRef.current).forEach((face) => {
            if (face) {
              face.style.backfaceVisibility = 'visible';
            }
          });
        }

        // Move slot into the cube so it rotates with it
        if (cube && slot) {
          cube.appendChild(slot);
          const faceData = FACES_BY_NAME[targetFace];
          slot.style.transition = 'none';
          slot.style.position = 'absolute';
          slot.style.top = '50%';
          slot.style.left = '50%';
          slot.style.marginTop = `${-slotSize / 2}px`;
          slot.style.marginLeft = `${-slotSize / 2}px`;
          const faceScale = cubeSize / slotSize;
          const [tx, ty, tz] = faceData.translate;
          const [rx, ry, rz] = faceData.rotate;
          slot.style.transform = `translate3d(${tx}px, ${ty}px, ${tz}px) `
            + `rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg) scale(${faceScale})`;
        }
      });

      coloredFacesRef.current.add(targetFace);

      // Check if this is the last slot - apply stronger kick and reduce friction
      const isLastSlot = index === lastSlotIndexRef.current;
      const kickStrength = isLastSlot ? CONFIG.lastKickStrength : CONFIG.squareKickStrength;

      if (isLastSlot) {
        // Reduce friction for faster spinning
        currentFrictionRef.current = CONFIG.lastKickFriction;
      }

      applyKickWithNormal(targetX, targetY, { ...faceNormal }, kickStrength);
      startAnimationLoop();

      setAnimatingSlots((prev) => {
        const next = new Set(prev);
        next.delete(index);
        return next;
      });
      setUsedSlots((prev) => new Set(prev).add(index));
    };

    slot.addEventListener('transitionend', onTransitionEnd);
  });

  const handleClose = useLastCallback(() => {
    if (isActivated && !craftResult) return;

    if (rafIdRef.current) {
      cancelAnimationFrame(rafIdRef.current);
      rafIdRef.current = undefined;
    }
    closeGiftCraftModal();
  });

  const handleSlotClick = useLastCallback((index: number) => {
    const slotGift = gifts[index];
    if (slotGift) {
      selectGiftForCraft({ slotIndex: index });
    } else {
      openGiftCraftSelectModal({ slotIndex: index });
    }
  });

  const handleCraftClick = useLastCallback(() => {
    // Find all slots with gifts that haven't been used yet
    const giftedIndices = gifts
      .map((g, i) => (g && !usedSlots.has(i) && !animatingSlots.has(i) ? i : -1))
      .filter((i) => i !== -1);

    if (giftedIndices.length === 0) return;

    // Track the last slot index for special handling
    lastSlotIndexRef.current = giftedIndices[giftedIndices.length - 1];

    // Calculate total animation time
    const totalAnimationTime = CONFIG.craftInitialDelay
      + giftedIndices.length * CONFIG.slotFlightInterval + CONFIG.craftActionDelay;

    const totalResultTime = CONFIG.brakingDuration + CONFIG.resultRotationDuration + CONFIG.resultDisplayDuration;
    const endHeavyAnimation = beginHeavyAnimation(totalAnimationTime + totalResultTime);

    // Activate the animation
    activate();

    // Trigger each slot to fly with interval (after initial delay for progress animation)
    giftedIndices.forEach((index, i) => {
      setTimeout(() => {
        flySlotToCube(index);
      }, CONFIG.craftInitialDelay + (i + 1) * CONFIG.slotFlightInterval);
    });

    // Call craft action after all slots have flown
    setTimeout(() => {
      craftStarGift();
    }, totalAnimationTime);

    // End heavy animation after result is shown
    setTimeout(() => {
      endHeavyAnimation();
    }, totalAnimationTime + totalResultTime);
  });

  // Handle craft result
  useEffect(() => {
    if (!craftResult) return;

    // Start braking - increase friction to slow down
    currentFrictionRef.current = CONFIG.brakingFriction;

    if (craftResult.success) {
      // Find a free face for the crafted gift
      const allFaces: FaceName[] = ['front', 'back', 'left', 'right', 'top', 'bottom'];
      const freeFace = allFaces.find((f) => !coloredFacesRef.current.has(f)) || 'front';

      // Set the crafted gift on that face
      setCraftedGiftFace({ face: freeFace, gift: craftResult.gift });

      // Let it spin with braking for a while, then rotate to result
      setTimeout(() => {
        startRotation();
        decelerateToFace(freeFace, () => {
          markResultRotationComplete();
        });
      }, CONFIG.brakingDuration);

      // After braking + rotation + display duration, close and show result
      setTimeout(() => {
        closeGiftCraftModal();
        requestConfetti({ withStars: true });
        openGiftInfoModal({ gift: craftResult.gift });
      }, CONFIG.brakingDuration + CONFIG.resultRotationDuration + CONFIG.resultDisplayDuration);
    } else {
      // Fail case - show broken gift animation
      const allFaces: FaceName[] = ['front', 'back', 'left', 'right', 'top', 'bottom'];
      const freeFace = allFaces.find((f) => !coloredFacesRef.current.has(f)) || 'front';

      // Count burned gifts
      const burnedCount = gifts.filter(Boolean).length;
      const isError = !craftResult.success && craftResult.isError;

      // Set the failed face
      setFailedFace({ face: freeFace, burnedCount, isError: isError || undefined });

      // Let it spin with braking for a while, then rotate to failed face
      setTimeout(() => {
        startRotation();
        decelerateToFace(freeFace, () => {
          markResultRotationComplete();
        });
      }, CONFIG.brakingDuration);
    }
  }, [craftResult, gifts, closeGiftCraftModal, requestConfetti, openGiftInfoModal, decelerateToFace]);

  const hasSelectedGifts = useMemo(
    () => gifts.some((g, i) => g && !usedSlots.has(i)),
    [gifts, usedSlots],
  );

  const allAttributes = useMemo(
    () => [...attributeStats.backdrops, ...attributeStats.patterns],
    [attributeStats.backdrops, attributeStats.patterns],
  );

  const [renderingAttributes, setRenderingAttributes] = useState(allAttributes);

  useEffect(() => {
    if (allAttributes === renderingAttributes) return;

    const getAttrKey = (a: { attr: ApiStarGiftAttributeBackdrop | ApiStarGiftAttributePattern }) => (
      a.attr.type === 'backdrop' ? `b-${a.attr.backdropId}` : `p-${a.attr.sticker.id}`
    );

    const prevKeys = renderingAttributes.map(getAttrKey);
    const newKeys = allAttributes.map(getAttrKey);
    const hasOrderChanged = prevKeys.length !== newKeys.length
      || prevKeys.some((key, i) => key !== newKeys[i]);

    if (hasOrderChanged) {
      startViewTransition(VTT_CRAFT_ATTRIBUTES, () => {
        setRenderingAttributes(allAttributes);
      });
    } else {
      setRenderingAttributes(allAttributes);
    }
  }, [allAttributes, renderingAttributes, startViewTransition]);

  const burnedGifts = useMemo(
    () => gifts.filter((g): g is ApiStarGiftUnique => Boolean(g)),
    [gifts],
  );

  const handleRemoveGift = useLastCallback((index: number) => {
    setRemovingSlots(new Set(removingSlots).add(index));

    setTimeout(() => {
      selectGiftForCraft({ slotIndex: index });
      const newSlots = new Set(removingSlots);
      newSlots.delete(index);
      setRemovingSlots(newSlots);
    }, CONFIG.slotRemoveDuration);
  });

  const slotRefCallbacks = useMemo(() => [0, 1, 2, 3].map((i) => (el: HTMLDivElement | undefined) => {
    slotsRef.current[i] = el;
  }), []);

  const slotInnerRefCallbacks = useMemo(() => [0, 1, 2, 3].map((i) => (el: HTMLDivElement | undefined) => {
    slotsInnerRef.current[i] = el;
  }), []);

  function renderCraftSlot(index: number) {
    return (
      <CraftSlot
        key={`${slotsKey}-${index}`}
        index={index}
        gift={gifts[index]}
        isUsed={usedSlots.has(index)}
        isAnimating={animatingSlots.has(index)}
        isActivated={isActivated}
        isRemoving={removingSlots.has(index)}
        slotRef={slotRefCallbacks[index]}
        slotInnerRef={slotInnerRefCallbacks[index]}
        onSlotClick={handleSlotClick}
        onRemoveGift={handleRemoveGift}
      />
    );
  }

  function renderFaceContent(faceName: FaceName) {
    // Check if this face has the crafted gift (success)
    if (craftedGiftFace?.face === faceName) {
      const giftAttributes = getGiftAttributes(craftedGiftFace.gift);
      if (giftAttributes?.backdrop) {
        return (
          <div className={styles.craftedGiftFace}>
            <RadialPatternBackground
              className={styles.craftedGiftBackdrop}
              backgroundColors={[giftAttributes.backdrop.centerColor, giftAttributes.backdrop.edgeColor]}
              patternIcon={giftAttributes.pattern?.sticker}
              ringsCount={1}
              ovalFactor={1}
              patternSize={12}
            />
            <AnimatedIconFromSticker
              className={styles.craftedGiftSticker}
              sticker={giftAttributes.model?.sticker}
              size={STICKER_SIZE * 1.2}
            />
          </div>
        );
      }
    }

    // Check if this face shows the failed result
    if (failedFace?.face === faceName) {
      return (
        <div className={styles.failedGiftFace}>
          <RadialProgress className={styles.progressRing} progress={0} />
          <AnimatedIconWithPreview
            tgsUrl={LOCAL_TGS_URLS.BrokenGift}
            previewUrl={LOCAL_TGS_PREVIEW_URLS.BrokenGift}
            size={STICKER_SIZE}
            noLoop
          />
          {!failedFace.isError && (
            <span className={styles.burnedCount}>{failedFace.burnedCount}</span>
          )}
        </div>
      );
    }

    // Show progress animation on front face during crafting
    if (faceName === 'front' && isActivated && !craftedGiftFace && !failedFace) {
      return (
        <AnimatedIcon
          tgsUrl={LOCAL_TGS_URLS.CraftProgress}
          play
          noLoop={false}
          size={50}
          forceAlways
        />
      );
    }

    return <Icon name="craft" className={styles.faceIcon} />;
  }

  function render3DCube() {
    return (
      <div ref={cubeRef} className={styles.cube}>
        <div className={styles.corners}>
          <div className={styles.corner} />
          <div className={styles.corner} />
          <div className={styles.corner} />
          <div className={styles.corner} />
          <div className={styles.corner} />
          <div className={styles.corner} />
          <div className={styles.corner} />
          <div className={styles.corner} />
        </div>
        {FACES_DATA.map((face) => {
          const resultFace = craftedGiftFace?.face || failedFace?.face;
          const shouldHide = isRotationStarted && resultFace !== face.name;
          const isFailed = failedFace?.face === face.name;
          return (
            <div
              key={face.name}
              ref={(el) => { facesRef.current[face.name] = el || undefined; }}
              className={buildClassName(
                styles.face,
                shouldHide && styles.faceHidden,
                isFailed && styles.faceFailed,
                progressPercent > HIGH_CHANCE_THRESHOLD && styles.faceHighChance,
              )}
              data-face={face.name}
            >
              {renderFaceContent(face.name)}
            </div>
          );
        })}
      </div>
    );
  }

  function renderCenterAnvil() {
    return (
      <div
        className={buildClassName(styles.centerAnvil,
          progressPercent > HIGH_CHANCE_THRESHOLD
          && styles.centerAnvilHighChance)}
      >
        <RadialProgress className={styles.progressRing} progress={progressPercent} />
        <Icon name="craft" className={styles.anvilIcon} />
        <AnimatedCounter className={styles.percentage} text={formatPercent(progressPercent, 0)} />
      </div>
    );
  }

  function renderHeader() {
    return (
      <div className={styles.header}>
        <div className={buildClassName(styles.slotsGrid, isActivated && styles.activated)}>
          {renderCraftSlot(0)}
          {renderCraftSlot(1)}
          {isActivated ? (
            <div className={styles.cubeWrapper}>
              {render3DCube()}
            </div>
          ) : (
            renderCenterAnvil()
          )}
          {renderCraftSlot(2)}
          {renderCraftSlot(3)}
        </div>
      </div>
    );
  }

  function renderAttributeDial(
    attr: ApiStarGiftAttributeBackdrop | ApiStarGiftAttributePattern,
    permille: number,
  ) {
    const percent = permille / 10;
    const percentText = formatPercent(percent, 0);
    const attrKey = attr.type === 'backdrop' ? `backdrop-${attr.backdropId}` : `pattern-${attr.sticker.id}`;

    return (
      <div
        key={attrKey}
        className={styles.attributeCircle}
        style={createVtnStyle(attrKey, 'craftAttribute')}
      >
        <div className={styles.attributeContent}>
          <RadialProgress className={styles.attributeRing} progress={percent} size={ATTRIBUTE_DIAL_SIZE} />
          {attr.type === 'backdrop' ? (
            <div
              className={styles.colorAttribute}
              style={`background: linear-gradient(135deg, ${attr.centerColor} 0%, ${attr.edgeColor} 100%)`}
            />
          ) : (
            <PatternAttributePreview sticker={attr.sticker} />
          )}
        </div>
        <AnimatedCounter className={styles.attributePercent} text={percentText} />
      </div>
    );
  }

  function renderInfo() {
    return (
      <div className={styles.infoSection}>
        <p className={styles.infoDescription}>
          {lang('GiftCraftDescription', {
            giftLine: (
              <span className={styles.giftLine}>
                {titleGiftSticker && (
                  <AnimatedIconFromSticker
                    className={styles.giftIcon}
                    sticker={titleGiftSticker}
                    size={REM}
                  />
                )}
                <strong>{fullGiftTitle}</strong>
              </span>
            ),
          },
          {
            withNodes: true,
            renderTextFilters: ['br'],
            withMarkdown: true,
          },
          )}
        </p>
        <p className={styles.infoWarning}>
          {lang('GiftCraftWarning', undefined, {
            withNodes: true,
            renderTextFilters: ['br'],
            withMarkdown: true,
          })}
        </p>
        {previewModelStickers && previewModelStickers.length > 0 && (
          <Button
            size="tiny"
            color="transparentBlured"
            pill
            fluid
            className={styles.viewAllButton}
            onClick={handleViewAllVariants}
          >
            {previewModelStickers.map((sticker) => (
              <CustomEmoji
                key={sticker.id}
                sticker={sticker}
                noPlay
              />
            ))}
            <span className={styles.viewAllText}>
              {lang('GiftCraftViewAll', undefined, {
                withNodes: true,
                specialReplacement: NEXT_ARROW_REPLACEMENT,
              })}
            </span>
          </Button>
        )}
        {renderingAttributes.length > 0 && (
          <div className={styles.attributeCircles}>
            {renderingAttributes.map(({ attr, permille }) => renderAttributeDial(attr, permille))}
          </div>
        )}
      </div>
    );
  }

  function renderCraftingInfo() {
    return (
      <div className={styles.craftingSection}>
        <h4 className={styles.craftingTitle}>{lang('GiftCraftingTitle')}</h4>
        <p className={styles.craftingGiftName}>{fullGiftTitle}</p>
        <p className={styles.craftingWarning}>
          {lang('GiftCraftWarning', undefined, {
            withNodes: true,
            renderTextFilters: ['br'],
            withMarkdown: true,
          })}
        </p>
      </div>
    );
  }

  function renderFailedInfo() {
    if (failedFace?.isError) {
      return (
        <div className={styles.failedSection}>
          <p className={styles.failedTitle}>
            {lang('SomethingWentWrong')}
          </p>
        </div>
      );
    }

    const burnedCount = burnedGifts.length;

    return (
      <div className={styles.failedSection}>
        <h4 className={styles.failedTitle}>{lang('GiftCraftFailedTitle')}</h4>
        <p className={styles.failedDescription}>
          {lang('GiftCraftFailedDescription', { count: burnedCount }, {
            pluralValue: burnedCount,
            withNodes: true,
            renderTextFilters: ['br'],
            withMarkdown: true,
          })}
        </p>
        <div className={styles.burnedGifts}>
          {burnedGifts.map((gift) => {
            const giftAttributes = getGiftAttributes(gift);
            if (!giftAttributes) return undefined;
            return (
              <div key={gift.id} className={styles.burnedGift}>
                <div className={styles.burnedGiftInner}>
                  <RadialPatternBackground
                    className={styles.burnedGiftBackdrop}
                    backgroundColors={[
                      giftAttributes.backdrop!.centerColor,
                      giftAttributes.backdrop!.edgeColor,
                    ]}
                    patternIcon={giftAttributes.pattern!.sticker}
                    ringsCount={1}
                    ovalFactor={1}
                    patternSize={8}
                  />
                  <AnimatedIconFromSticker
                    className={styles.burnedGiftSticker}
                    sticker={giftAttributes.model?.sticker}
                    size={STICKER_SIZE * 0.8}
                  />
                </div>
                <GiftRibbon
                  className={styles.burnedGiftBadge}
                  textClassName={styles.burnedGiftBadgeText}
                  color="red"
                  size={40}
                  text={`#${gift.number}`}
                />
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  function renderInfoContent() {
    let activeKey;
    let content;

    if (failedFace) {
      activeKey = 2;
      content = renderFailedInfo();
    } else if (isActivated) {
      activeKey = 1;
      content = renderCraftingInfo();
    } else {
      activeKey = 0;
      content = renderInfo();
    }

    return (
      <Transition
        className={styles.infoTransition}
        name="fade"
        activeKey={activeKey}
      >
        {content}
      </Transition>
    );
  }

  const handleRetryClick = useLastCallback(() => {
    resetGiftCraftResult();
    resetAnimationState();
    setSlotsKey((k) => k + 1);
  });

  function renderCraftButton() {
    const isFailed = Boolean(failedFace);
    const shouldRenderHighChanceButton = hasSelectedGifts && !isActivated && !isFailed
      && progressPercent > HIGH_CHANCE_THRESHOLD;
    const buttonText = isFailed
      ? lang('GiftCraftNewGift')
      : lang('GiftCraftButton', { giftName: fullGiftTitle });

    let activeKey = 0;
    if (isFailed) activeKey = 2;
    else if (isActivated) activeKey = 3;
    else if (hasSelectedGifts) activeKey = 1;

    return (
      <div className={styles.footer}>
        <Button
          className={buildClassName(
            styles.craftButton,
            isActivated && !isFailed && styles.craftButtonCrafting,
            shouldRenderHighChanceButton && styles.craftButtonHighChance,
          )}
          size="smaller"
          color={isFailed ? 'danger' : undefined}
          disabled={!isFailed && (!hasSelectedGifts || isActivated)}
          onClick={isFailed ? handleRetryClick : handleCraftClick}
        >
          <Transition name="fade" activeKey={activeKey}>
            {(isActivated && !isFailed) ? (
              <span className={styles.craftButtonTitle}>
                {lang('GiftCraftSuccessChance', { percent: formatPercent(progressPercent, 0) })}
              </span>
            ) : (
              <div className={styles.craftButtonContent}>
                <span className={styles.craftButtonTitle}>{buttonText}</span>
                {!isFailed && (
                  <span className={styles.craftButtonSubtitle}>
                    {hasSelectedGifts
                      ? lang('GiftCraftSuccessChance', { percent: formatPercent(progressPercent, 0) })
                      : lang(
                        'GiftCraftEmptyHint',
                        { button: <Icon name="add-filled" className={styles.emptyHintIcon} /> },
                        { withNodes: true },
                      )}
                  </span>
                )}
              </div>
            )}
          </Transition>
        </Button>
      </div>
    );
  }

  const handleHelpClick = useLastCallback(() => {
    if (!lastMainGift || (isActivated && !craftResult)) return;
    openGiftCraftInfoModal({ gift: lastMainGift });
  });

  const handleViewAllVariants = useLastCallback(() => {
    if (!lastMainGift) return;
    openGiftPreviewModal({ originGift: lastMainGift, shouldShowCraftableOnStart: true });
  });

  const helpButton = useMemo(() => (
    <Button
      className={styles.helpButton}
      round
      color="translucent-white"
      size="tiny"
      ariaLabel={lang('GiftCraftHelp')}
      onClick={handleHelpClick}
    >
      <Icon name="help" />
    </Button>
  ), [lang, handleHelpClick]);

  return (
    <Modal
      isOpen={isOpen}
      onClose={handleClose}
      className={styles.modal}
      contentClassName={styles.content}
      hasAbsoluteCloseButton
      headerRightToolBar={helpButton}
      isSlim
      isLowStackPriority
      absoluteCloseButtonColor="translucent-white"
    >
      <h3 className={styles.title}>{lang('GiftCraftTitle')}</h3>
      <Transition
        className={styles.patternOverlay}
        slideClassName={styles.patternSlide}
        name="fade"
        activeKey={failedFace ? 1 : (progressPercent > HIGH_CHANCE_THRESHOLD ? 2 : 0)}
      >
        <RadialPatternBackground
          className={styles.patternBackground}
          patternUrl={craftPatternUrl}
          backgroundColors={
            failedFace
              ? GRADIENT_COLORS_FAILED
              : (progressPercent > HIGH_CHANCE_THRESHOLD ? GRADIENT_COLORS_HIGH_CHANCE : GRADIENT_COLORS_DEFAULT)
          }
          patternColor={failedFace ? '#311A15' : (progressPercent > HIGH_CHANCE_THRESHOLD ? '#142A2C' : '#242F42')}
          yPosition={9.5 * REM}
          maxRadius={0.3}
          patternSize={22}
          ovalFactor={1.2}
          ringsCount={2}
        />
      </Transition>
      {renderHeader()}
      {renderInfoContent()}
      {renderCraftButton()}
    </Modal>
  );
};

export default memo(withGlobal<OwnProps>(
  (global): StateProps => {
    return {
      craftAttributePermilles: global.appConfig?.stargiftsCraftAttributePermilles,
    };
  },
)(GiftCraftModal));
