/**
 * CharacterSprite
 *
 * 원소별 속성 문양과 티어 프레임을 가진 SVG 스프라이트.
 * 같은 둥근 캐릭터처럼 보이지 않도록 색, 문양, 실루엣, 오라가 함께 달라집니다.
 */
import { css, keyframes } from '@emotion/react';
import { Fragment, useId, useMemo, type ReactElement } from 'react';
import {
  CHARACTER_VISUAL,
  DEFAULT_VISUAL,
  type EyeStyle,
  type ParticleType,
  type PatternType,
} from '../data/characterVisual';
import earthAsset from '../assets/elements/earth.png';
import fireAsset from '../assets/elements/fire.png';
import waterAsset from '../assets/elements/water.png';
import windAsset from '../assets/elements/wind.png';

type RasterMotion = 'fire' | 'water' | 'wind' | 'earth';

const RASTER_SPRITES: Record<string, { src: string; motion: RasterMotion }> = {
  fire: { src: fireAsset, motion: 'fire' },
  water: { src: waterAsset, motion: 'water' },
  wind: { src: windAsset, motion: 'wind' },
  earth: { src: earthAsset, motion: 'earth' },
};

const fireFlicker = keyframes`
  0%, 100% { transform: translateY(0) scale(1) rotate(0deg); filter: brightness(1) saturate(1.04); }
  18% { transform: translateY(-2px) scale(1.035, 0.985) rotate(-1.4deg); filter: brightness(1.12) saturate(1.18); }
  36% { transform: translateY(1px) scale(0.985, 1.035) rotate(1.1deg); filter: brightness(0.96) saturate(1.08); }
  62% { transform: translateY(-3px) scale(1.045, 0.975) rotate(0.8deg); filter: brightness(1.18) saturate(1.22); }
  82% { transform: translateY(-1px) scale(1.01, 1.02) rotate(-0.6deg); filter: brightness(1.04) saturate(1.12); }
`;

const waterRipple = keyframes`
  0%, 100% { transform: translateY(0) scale(1) rotate(0deg); filter: brightness(1); }
  30% { transform: translateY(-2px) scale(1.02, 0.99) rotate(0.8deg); filter: brightness(1.08); }
  58% { transform: translateY(1px) scale(0.99, 1.02) rotate(-0.7deg); filter: brightness(0.98); }
  78% { transform: translateY(-1px) scale(1.015, 1) rotate(0.4deg); filter: brightness(1.04); }
`;

const windFlow = keyframes`
  0%, 100% { transform: translate(0, 0) scale(1) rotate(0deg); opacity: 0.96; }
  25% { transform: translate(2px, -2px) scale(1.02) rotate(1.6deg); opacity: 1; }
  52% { transform: translate(-2px, 1px) scale(0.99, 1.02) rotate(-1.2deg); opacity: 0.92; }
  76% { transform: translate(1px, -1px) scale(1.01) rotate(0.8deg); opacity: 0.98; }
`;

const earthGrow = keyframes`
  0%, 100% { transform: translateY(0) scale(1) rotate(0deg); filter: brightness(1); }
  38% { transform: translateY(-1px) scale(1.01, 1.015) rotate(-0.4deg); filter: brightness(1.05); }
  72% { transform: translateY(1px) scale(0.995, 1.005) rotate(0.5deg); filter: brightness(0.98); }
`;

const rasterFloat = keyframes`
  0%, 100% { transform: translateY(0) scale(1); }
  50% { transform: translateY(-3px) scale(1.02); }
`;

const glowPulse = keyframes`
  0%, 100% { opacity: 0.08; transform: scale(0.94); }
  50% { opacity: 0.18; transform: scale(1.04); }
`;

const rasterWrapStyle = (size: number, color: string, tier: number) => css`
  position: relative;
  width: ${size}px;
  height: ${size}px;
  display: inline-block;
  border-radius: 999px;
  filter: drop-shadow(0 7px 10px rgba(15, 23, 42, 0.16));
  animation: ${rasterFloat} 2.2s ease-in-out infinite;

  &::before {
    content: '';
    position: absolute;
    inset: ${Math.max(1, size * 0.04)}px;
    border-radius: 999px;
    background: ${color};
    opacity: ${tier >= 3 ? 0.16 : 0.08};
    filter: blur(${Math.max(6, size * 0.14)}px);
    animation: ${glowPulse} 2.3s ease-in-out infinite;
  }

  &::after {
    content: '';
    position: absolute;
    inset: ${Math.max(1, size * 0.03)}px;
    border-radius: 999px;
    border: ${Math.max(1, size * 0.025)}px solid rgba(255, 255, 255, 0.6);
    box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.08);
    pointer-events: none;
  }
`;

function getRasterMotionStyle(motion: RasterMotion) {
  switch (motion) {
    case 'fire':
      return css`${fireFlicker} 0.82s ease-in-out infinite`;
    case 'water':
      return css`${waterRipple} 1.7s ease-in-out infinite`;
    case 'wind':
      return css`${windFlow} 1.35s ease-in-out infinite`;
    case 'earth':
      return css`${earthGrow} 2.2s ease-in-out infinite`;
  }
}

const rasterImageStyle = (size: number, motion: RasterMotion) => css`
  position: absolute;
  inset: 0;
  width: ${size}px;
  height: ${size}px;
  object-fit: contain;
  transform-origin: 50% 62%;
  animation: ${getRasterMotionStyle(motion)};
  will-change: transform, filter, opacity;
`;

// ─── 색상 유틸 ───────────────────────────────────────────────

function hexToRgb(hex: string): [number, number, number] {
  const h = hex.replace('#', '');
  return [
    parseInt(h.slice(0, 2), 16),
    parseInt(h.slice(2, 4), 16),
    parseInt(h.slice(4, 6), 16),
  ];
}

function rgbToHex(r: number, g: number, b: number): string {
  return (
    '#' +
    [r, g, b]
      .map((x) => Math.min(255, Math.max(0, Math.round(x))).toString(16).padStart(2, '0'))
      .join('')
  );
}

function darken(hex: string, amt: number): string {
  const [r, g, b] = hexToRgb(hex);
  return rgbToHex(r - amt, g - amt, b - amt);
}

function mix(hexA: string, hexB: string, ratio: number): string {
  const [ar, ag, ab] = hexToRgb(hexA);
  const [br, bg, bb] = hexToRgb(hexB);
  return rgbToHex(
    ar + (br - ar) * ratio,
    ag + (bg - ag) * ratio,
    ab + (bb - ab) * ratio
  );
}

function luminance(hex: string): number {
  const [r, g, b] = hexToRgb(hex).map((v) => v / 255);
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function readableBase(hex: string): string {
  if (luminance(hex) > 0.82) return mix(hex, '#3182F6', 0.28);
  if (luminance(hex) < 0.08) return mix(hex, '#7C4DFF', 0.32);
  return hex;
}

const TIER_FRAME: Record<number, { stroke: string; accent: string; aura: number; dash?: string }> = {
  1: { stroke: '#94A3B8', accent: '#CBD5E1', aura: 0.08 },
  2: { stroke: '#22C55E', accent: '#86EFAC', aura: 0.12, dash: '4 5' },
  3: { stroke: '#3B82F6', accent: '#93C5FD', aura: 0.18, dash: '7 4' },
  4: { stroke: '#A855F7', accent: '#D8B4FE', aura: 0.24 },
  5: { stroke: '#F59E0B', accent: '#FDE68A', aura: 0.32 },
};

function getTierFrame(tier: number) {
  return TIER_FRAME[Math.min(5, Math.max(1, tier))] ?? TIER_FRAME[1];
}

// ─── 파티클 위치 ──────────────────────────────────────────────

interface ParticlePos {
  angle: number; // 도 (degree)
  r: number;     // 궤도 반지름
  dur: number;   // 애니메이션 주기 (초)
  delay: number; // 시작 딜레이 (초)
}

const PARTICLE_POSITIONS: Record<number, ParticlePos[]> = {
  1: [
    { angle: -70, r: 42, dur: 2.0, delay: 0 },
    { angle: 50,  r: 44, dur: 2.3, delay: 0.8 },
    { angle: 170, r: 41, dur: 1.9, delay: 1.5 },
  ],
  2: [
    { angle: -60, r: 43, dur: 2.0, delay: 0 },
    { angle: 0,   r: 44, dur: 2.3, delay: 0.4 },
    { angle: 60,  r: 42, dur: 2.1, delay: 0.8 },
    { angle: 130, r: 44, dur: 2.4, delay: 1.2 },
    { angle: 200, r: 41, dur: 1.9, delay: 1.6 },
    { angle: 270, r: 43, dur: 2.2, delay: 2.0 },
  ],
  3: [
    { angle: -80, r: 44, dur: 2.0, delay: 0 },
    { angle: -35, r: 46, dur: 2.3, delay: 0.3 },
    { angle: 10,  r: 44, dur: 2.1, delay: 0.6 },
    { angle: 55,  r: 46, dur: 2.4, delay: 0.9 },
    { angle: 100, r: 43, dur: 1.9, delay: 1.2 },
    { angle: 150, r: 45, dur: 2.2, delay: 1.5 },
    { angle: 200, r: 44, dur: 2.0, delay: 1.8 },
    { angle: 255, r: 46, dur: 2.3, delay: 2.1 },
  ],
  4: [
    { angle: -80, r: 45, dur: 2.0, delay: 0 },
    { angle: -35, r: 47, dur: 2.2, delay: 0.25 },
    { angle: 10,  r: 45, dur: 2.1, delay: 0.5 },
    { angle: 55,  r: 47, dur: 2.3, delay: 0.75 },
    { angle: 100, r: 44, dur: 1.9, delay: 1.0 },
    { angle: 150, r: 46, dur: 2.1, delay: 1.25 },
    { angle: 200, r: 45, dur: 2.0, delay: 1.5 },
    { angle: 255, r: 47, dur: 2.2, delay: 1.75 },
  ],
  5: [
    { angle: -80,  r: 45, dur: 1.8, delay: 0 },
    { angle: -50,  r: 47, dur: 2.0, delay: 0.2 },
    { angle: -20,  r: 45, dur: 1.9, delay: 0.4 },
    { angle: 10,   r: 47, dur: 2.1, delay: 0.6 },
    { angle: 40,   r: 45, dur: 1.8, delay: 0.8 },
    { angle: 70,   r: 47, dur: 2.0, delay: 1.0 },
    { angle: 100,  r: 44, dur: 1.9, delay: 1.2 },
    { angle: 130,  r: 47, dur: 2.1, delay: 1.4 },
    { angle: 160,  r: 45, dur: 1.8, delay: 1.6 },
    { angle: 195,  r: 47, dur: 2.0, delay: 1.8 },
    { angle: 230,  r: 45, dur: 1.9, delay: 2.0 },
    { angle: 265,  r: 47, dur: 2.1, delay: 2.2 },
  ],
};

function toRad(deg: number) {
  return (deg * Math.PI) / 180;
}

// ─── 파티클 SVG 모양 ──────────────────────────────────────────

function renderParticleShape(type: ParticleType, color: string, size = 1): ReactElement {
  const s = size;
  switch (type) {
    case 'flame':
      return (
        <polygon
          points={`0,${-6 * s} ${-4 * s},${3 * s} ${4 * s},${3 * s}`}
          fill={color}
          opacity="0.85"
        />
      );
    case 'water':
      return <ellipse rx={3 * s} ry={4 * s} fill={color} opacity="0.8" />;
    case 'wind':
      return (
        <path
          d={`M${-5 * s},0 C${-2 * s},${-3 * s} ${2 * s},${-3 * s} ${5 * s},0`}
          fill="none"
          stroke={color}
          strokeWidth={1.5 * s}
          strokeLinecap="round"
          opacity="0.8"
        />
      );
    case 'earth':
      return <circle r={3 * s} fill={color} opacity="0.75" />;
    case 'crystal':
      return (
        <polygon
          points={`0,${-5 * s} ${3.5 * s},0 0,${5 * s} ${-3.5 * s},0`}
          fill={color}
          opacity="0.8"
        />
      );
    case 'spark': {
      const p = 5 * s;
      return (
        <path
          d={`M0,${-p} L${2 * s},${-1 * s} L${1 * s},${-1 * s} L${3 * s},${p} L${-2 * s},0 L${-1 * s},0 Z`}
          fill={color}
          opacity="0.85"
        />
      );
    }
    case 'star':
      return (
        <polygon
          points={`0,${-5 * s} ${1.5 * s},${-1.8 * s} ${5 * s},${-1.6 * s} ${2.2 * s},${1.0 * s} ${3.1 * s},${4.8 * s} 0,${2.8 * s} ${-3.1 * s},${4.8 * s} ${-2.2 * s},${1.0 * s} ${-5 * s},${-1.6 * s} ${-1.5 * s},${-1.8 * s}`}
          fill={color}
          opacity="0.85"
        />
      );
    case 'smoke':
      return <circle r={4 * s} fill={color} opacity="0.4" />;
    case 'rainbow':
      return (
        <circle r={3 * s} fill={color} opacity="0.85" />
      );
    case 'leaf':
      return (
        <ellipse rx={4 * s} ry={2 * s} fill={color} opacity="0.8" transform="rotate(35)" />
      );
    default:
      return <circle r={3 * s} fill={color} opacity="0.6" />;
  }
}

function renderParticles(
  particleType: ParticleType,
  particleColors: string[],
  tier: number,
  cx: number,
  cy: number,
): ReactElement[] {
  const clampedTier = Math.min(tier, 5) as 1 | 2 | 3 | 4 | 5;
  const positions = PARTICLE_POSITIONS[clampedTier] ?? PARTICLE_POSITIONS[1];

  return positions.map((pos, i) => {
    const rad = toRad(pos.angle);
    const px = Number((cx + pos.r * Math.cos(rad)).toFixed(1));
    const py = Number((cy + pos.r * Math.sin(rad)).toFixed(1));
    const color = particleColors[i % particleColors.length];
    const floatAmt = tier >= 4 ? 5 : 4;
    const dx = Number((Math.cos(rad + Math.PI / 2) * floatAmt).toFixed(1));
    const dy = Number((-Math.abs(Math.sin(rad)) * floatAmt - 2).toFixed(1));

    // animateTransform on outer <g>, static position on inner <g>
    // outer: animates translate(0,0) → translate(dx,dy) → translate(0,0)
    // inner: static translate(px,py)
    // net particle position: (px,py) ↔ (px+dx, py+dy)
    return (
      <g key={i}>
        <animateTransform
          attributeName="transform"
          type="translate"
          values={`0,0; ${dx},${dy}; 0,0`}
          dur={`${pos.dur}s`}
          begin={`${pos.delay}s`}
          repeatCount="indefinite"
        />
        <g transform={`translate(${px},${py})`}>
          {renderParticleShape(particleType, color)}
        </g>
      </g>
    );
  });
}

// ─── 몸체 패턴 ───────────────────────────────────────────────

function renderBodyPattern(
  patternType: PatternType,
  patternColor: string,
  cx: number,
  cy: number,
): ReactElement | null {
  const opacity = 0.22;
  switch (patternType) {
    case 'swirl':
      return (
        <path
          d={`M${cx - 2},${cy - 6} C${cx - 2},${cy - 10} ${cx + 5},${cy - 10} ${cx + 6},${cy - 5} C${cx + 7},${cy} ${cx + 3},${cy + 5} ${cx - 1},${cy + 4}`}
          fill="none"
          stroke={patternColor}
          strokeWidth="1.8"
          opacity={opacity}
          strokeLinecap="round"
        />
      );
    case 'wave':
      return (
        <Fragment>
          <path
            d={`M${cx - 10},${cy + 1} C${cx - 6},${cy - 2} ${cx - 2},${cy + 4} ${cx + 2},${cy + 1} C${cx + 6},${cy - 2} ${cx + 10},${cy + 4} ${cx + 14},${cy + 1}`}
            fill="none"
            stroke={patternColor}
            strokeWidth="1.5"
            opacity={opacity}
          />
          <path
            d={`M${cx - 10},${cy + 7} C${cx - 6},${cy + 4} ${cx - 2},${cy + 10} ${cx + 2},${cy + 7} C${cx + 6},${cy + 4} ${cx + 10},${cy + 10} ${cx + 14},${cy + 7}`}
            fill="none"
            stroke={patternColor}
            strokeWidth="1.2"
            opacity={opacity * 0.85}
          />
        </Fragment>
      );
    case 'crack':
      return (
        <Fragment>
          <path
            d={`M${cx - 2},${cy - 8} L${cx},${cy - 1} L${cx - 4},${cy + 6}`}
            fill="none"
            stroke={patternColor}
            strokeWidth="1.2"
            opacity={opacity}
          />
          <path
            d={`M${cx + 4},${cy - 6} L${cx + 3},${cy + 1} L${cx + 6},${cy + 7}`}
            fill="none"
            stroke={patternColor}
            strokeWidth="1.0"
            opacity={opacity * 0.8}
          />
        </Fragment>
      );
    case 'spiral':
      return (
        <path
          d={`M${cx},${cy} C${cx},${cy - 5} ${cx + 5},${cy - 5} ${cx + 5},${cy} C${cx + 5},${cy + 5} ${cx - 5},${cy + 5} ${cx - 5},${cy} C${cx - 5},${cy - 7} ${cx + 2},${cy - 8} ${cx + 6},${cy - 4}`}
          fill="none"
          stroke={patternColor}
          strokeWidth="1.5"
          opacity={opacity}
          strokeLinecap="round"
        />
      );
    case 'crystal':
      return (
        <Fragment>
          <polygon
            points={`${cx},${cy - 7} ${cx + 6},${cy} ${cx},${cy + 7} ${cx - 6},${cy}`}
            fill="none"
            stroke={patternColor}
            strokeWidth="1.5"
            opacity={opacity}
          />
          <polygon
            points={`${cx},${cy - 7} ${cx + 6},${cy} ${cx},${cy + 7} ${cx - 6},${cy}`}
            fill={patternColor}
            opacity={opacity * 0.5}
          />
        </Fragment>
      );
    case 'none':
    default:
      return null;
  }
}

function renderElementCrest(
  particleType: ParticleType,
  color: string,
  accentColor: string,
  cx: number,
  cy: number,
  tier: number
): ReactElement {
  const crestY = cy + 5;
  const opacity = tier >= 4 ? 0.28 : 0.22;

  switch (particleType) {
    case 'flame':
      return (
        <path
          d={`M${cx},${crestY - 18} C${cx - 13},${crestY - 4} ${cx - 9},${crestY + 12} ${cx},${crestY + 16} C${cx + 12},${crestY + 10} ${cx + 12},${crestY - 2} ${cx + 4},${crestY - 9} C${cx + 2},${crestY - 12} ${cx + 2},${crestY - 15} ${cx},${crestY - 18} Z`}
          fill={accentColor}
          opacity={opacity}
        />
      );
    case 'water':
      return (
        <path
          d={`M${cx},${crestY - 18} C${cx - 12},${crestY - 3} ${cx - 13},${crestY + 8} ${cx},${crestY + 15} C${cx + 13},${crestY + 8} ${cx + 12},${crestY - 3} ${cx},${crestY - 18} Z`}
          fill={accentColor}
          opacity={opacity}
        />
      );
    case 'wind':
      return (
        <Fragment>
          <path
            d={`M${cx - 18},${crestY - 5} C${cx - 7},${crestY - 14} ${cx + 14},${crestY - 12} ${cx + 18},${crestY - 2}`}
            fill="none"
            stroke={accentColor}
            strokeWidth="4"
            strokeLinecap="round"
            opacity={opacity + 0.06}
          />
          <path
            d={`M${cx - 14},${crestY + 7} C${cx - 2},${crestY + 14} ${cx + 11},${crestY + 9} ${cx + 15},${crestY + 1}`}
            fill="none"
            stroke={accentColor}
            strokeWidth="3"
            strokeLinecap="round"
            opacity={opacity}
          />
        </Fragment>
      );
    case 'earth':
    case 'leaf':
      return (
        <Fragment>
          <ellipse
            cx={cx - 6}
            cy={crestY + 3}
            rx="9"
            ry="15"
            fill={accentColor}
            opacity={opacity}
            transform={`rotate(-32 ${cx - 6} ${crestY + 3})`}
          />
          <ellipse
            cx={cx + 7}
            cy={crestY + 4}
            rx="8"
            ry="13"
            fill={color}
            opacity={opacity * 0.85}
            transform={`rotate(34 ${cx + 7} ${crestY + 4})`}
          />
        </Fragment>
      );
    case 'crystal':
      return (
        <polygon
          points={`${cx},${crestY - 19} ${cx + 15},${crestY - 2} ${cx + 7},${crestY + 17} ${cx - 9},${crestY + 16} ${cx - 16},${crestY - 2}`}
          fill={accentColor}
          opacity={opacity}
        />
      );
    case 'spark':
      return (
        <path
          d={`M${cx + 1},${crestY - 20} L${cx + 8},${crestY - 2} L${cx + 1},${crestY - 2} L${cx + 10},${crestY + 18} L${cx - 10},${crestY - 6} L${cx - 2},${crestY - 6} Z`}
          fill={accentColor}
          opacity={opacity + 0.08}
        />
      );
    case 'smoke':
      return (
        <Fragment>
          <circle cx={cx - 10} cy={crestY + 1} r="10" fill={accentColor} opacity={opacity} />
          <circle cx={cx + 2} cy={crestY - 7} r="12" fill={accentColor} opacity={opacity * 0.88} />
          <circle cx={cx + 13} cy={crestY + 3} r="8" fill={accentColor} opacity={opacity * 0.74} />
        </Fragment>
      );
    case 'rainbow':
      return (
        <Fragment>
          <path
            d={`M${cx - 18},${crestY + 10} A18,18 0 0 1 ${cx + 18},${crestY + 10}`}
            fill="none"
            stroke="#FF4D6D"
            strokeWidth="4"
            opacity={opacity + 0.1}
          />
          <path
            d={`M${cx - 13},${crestY + 10} A13,13 0 0 1 ${cx + 13},${crestY + 10}`}
            fill="none"
            stroke="#4ADE80"
            strokeWidth="4"
            opacity={opacity + 0.1}
          />
          <path
            d={`M${cx - 8},${crestY + 10} A8,8 0 0 1 ${cx + 8},${crestY + 10}`}
            fill="none"
            stroke="#60A5FA"
            strokeWidth="4"
            opacity={opacity + 0.1}
          />
        </Fragment>
      );
    case 'star':
    default:
      return (
        <polygon
          points={`${cx},${crestY - 19} ${cx + 5},${crestY - 5} ${cx + 19},${crestY - 5} ${cx + 8},${crestY + 4} ${cx + 12},${crestY + 18} ${cx},${crestY + 9} ${cx - 12},${crestY + 18} ${cx - 8},${crestY + 4} ${cx - 19},${crestY - 5} ${cx - 5},${crestY - 5}`}
          fill={accentColor}
          opacity={opacity}
        />
      );
  }
}

// ─── 눈 렌더링 ────────────────────────────────────────────────

function renderEyes(
  eyeStyle: EyeStyle,
  cx: number,
  cy: number,
  bodyRy: number,
  outlineColor: string,
): ReactElement {
  const eyeY = cy - bodyRy * 0.18;
  const lx = cx - 12;
  const rx = cx + 12;

  // 눈 스타일별 동공 오프셋 및 강조
  const offsets: Record<EyeStyle, { lpo: [number, number]; rpo: [number, number]; hl: [number, number] }> = {
    energetic:  { lpo: [1, -1],  rpo: [-1, -1], hl: [2, -1] },
    calm:       { lpo: [0,  1],  rpo: [0,   1], hl: [2, -1] },
    playful:    { lpo: [0, -1],  rpo: [0,   2], hl: [2, -1] },
    steady:     { lpo: [0,  0],  rpo: [0,   0], hl: [2, -2] },
    mysterious: { lpo: [-1, 0],  rpo: [1,   0], hl: [1, -2] },
    radiant:    { lpo: [0, -1],  rpo: [0,  -1], hl: [2, -2] },
  };
  const { lpo, rpo, hl } = offsets[eyeStyle] ?? offsets.steady;

  const eyeR = eyeStyle === 'radiant' ? 7.5 : 7;
  const pupilR = eyeStyle === 'radiant' ? 4.5 : 4;

  return (
    <Fragment>
      {/* 왼쪽 눈 */}
      <circle cx={lx} cy={eyeY} r={eyeR} fill="white" />
      <circle cx={lx + lpo[0]} cy={eyeY + lpo[1]} r={pupilR} fill="#1A1A1A" />
      <circle cx={lx + hl[0]} cy={eyeY + hl[1]} r={2} fill="white" />

      {/* 오른쪽 눈 */}
      <circle cx={rx} cy={eyeY} r={eyeR} fill="white" />
      <circle cx={rx + rpo[0]} cy={eyeY + rpo[1]} r={pupilR} fill="#1A1A1A" />
      <circle cx={rx + hl[0]} cy={eyeY + hl[1]} r={2} fill="white" />

      {/* radiant: 눈 주변 별 반짝임 */}
      {eyeStyle === 'radiant' && (
        <Fragment>
          <path d={`M${lx - 10},${eyeY - 10} L${lx - 8},${eyeY - 8}`} stroke={outlineColor} strokeWidth="1" opacity="0.6" />
          <path d={`M${lx - 11},${eyeY} L${lx - 9},${eyeY}`} stroke={outlineColor} strokeWidth="1" opacity="0.6" />
          <path d={`M${rx + 8},${eyeY - 10} L${rx + 10},${eyeY - 8}`} stroke={outlineColor} strokeWidth="1" opacity="0.6" />
          <path d={`M${rx + 9},${eyeY} L${rx + 11},${eyeY}`} stroke={outlineColor} strokeWidth="1" opacity="0.6" />
        </Fragment>
      )}
    </Fragment>
  );
}

// ─── Props 타입 ──────────────────────────────────────────────

export interface CharacterSpriteProps {
  elementId: string;
  elementColor: string;
  tier: number;
  /** 렌더 크기 (정사각형 px). 기본 120 */
  size?: number;
  /** obtained / locked / undiscovered */
  state?: 'obtained' | 'locked' | 'undiscovered';
}

// ─── 메인 컴포넌트 ────────────────────────────────────────────

export function CharacterSprite({
  elementId,
  elementColor,
  tier,
  size = 120,
  state = 'obtained',
}: CharacterSpriteProps) {
  const instanceId = useId().replace(/:/g, '');
  const config = CHARACTER_VISUAL[elementId] ?? DEFAULT_VISUAL;
  const [bodyRx, bodyRy] = config.bodyShape ?? [33, 31];
  const frame = getTierFrame(tier);

  const colors = useMemo(() => {
    if (state === 'undiscovered') {
      return {
        body1: '#E5E7EB',
        body2: '#B8C0CC',
        body3: '#7C8798',
        outline: '#667085',
        pattern: '#98A2B3',
        glow: '#CBD5E1',
        crest: '#F8FAFC',
        shine: '#FFFFFF',
      };
    }
    const isLocked = state === 'locked';
    const base = isLocked ? '#9E9E9E' : readableBase(elementColor);
    const glowBase = isLocked ? '#CCCCCC' : readableBase(config.glowColor);
    return {
      body1: mix(base, '#FFFFFF', 0.58),
      body2: mix(base, '#FFFFFF', 0.12),
      body3: mix(base, '#111827', 0.22),
      outline: mix(base, '#0F172A', 0.48),
      pattern: mix(base, '#FFFFFF', 0.18),
      glow: glowBase,
      crest: mix(base, '#FFFFFF', 0.66),
      shine: mix(base, '#FFFFFF', 0.82),
    };
  }, [elementColor, state, config.glowColor]);

  // locked/undiscovered는 간소화 렌더
  const showDetails = state === 'obtained';
  const showGlow = showDetails && tier >= 3;
  const showRing = showDetails && tier >= 4;
  const showRainbow = showDetails && tier >= 5;

  const uid = `cs-${elementId}-${instanceId}`;
  const CX = 60;
  const CY = 64;
  const coreScale = tier >= 5 ? 1.06 : tier >= 4 ? 1.03 : 1;
  const rx = bodyRx * coreScale;
  const ry = bodyRy * coreScale;
  const rasterSprite = RASTER_SPRITES[elementId];

  if (state === 'obtained' && rasterSprite) {
    return (
      <span css={rasterWrapStyle(size, elementColor, tier)} aria-hidden="true">
        <img src={rasterSprite.src} css={rasterImageStyle(size, rasterSprite.motion)} alt="" />
      </span>
    );
  }

  return (
    <svg
      viewBox="0 0 120 120"
      width={size}
      height={size}
      xmlns="http://www.w3.org/2000/svg"
      overflow="visible"
    >
      <defs>
        <filter id={`${uid}-shadow`} x="-35%" y="-35%" width="170%" height="180%">
          <feDropShadow dx="0" dy="7" stdDeviation="5" floodColor="#101828" floodOpacity="0.18" />
        </filter>

        {/* 몸체 그라디언트 */}
        <radialGradient id={`${uid}-grad`} cx="38%" cy="32%" r="65%">
          <stop offset="0%" stopColor={colors.body1} />
          <stop offset="65%" stopColor={colors.body2} />
          <stop offset="100%" stopColor={colors.body3} />
        </radialGradient>

        <linearGradient id={`${uid}-frame`} x1="18" y1="20" x2="102" y2="102">
          <stop offset="0%" stopColor={frame.accent} />
          <stop offset="46%" stopColor={frame.stroke} />
          <stop offset="100%" stopColor={colors.outline} />
        </linearGradient>

        {/* 글로우 필터 (T3+) */}
        {showGlow && (
          <filter id={`${uid}-glow`} x="-25%" y="-25%" width="150%" height="150%">
            <feGaussianBlur stdDeviation={tier >= 4 ? 3 : 2} result="blur" />
            <feMerge>
              <feMergeNode in="blur" />
              <feMergeNode in="SourceGraphic" />
            </feMerge>
          </filter>
        )}

        {/* 레인보우 그라디언트 (T5) */}
        {showRainbow && (
          <linearGradient
            id={`${uid}-rainbow`}
            x1="0"
            y1="0"
            x2="120"
            y2="0"
            gradientUnits="userSpaceOnUse"
          >
            <stop offset="0%" stopColor="#FF0000" />
            <stop offset="20%" stopColor="#FF9900" />
            <stop offset="40%" stopColor="#FFFF00" />
            <stop offset="60%" stopColor="#00FF00" />
            <stop offset="80%" stopColor="#0066FF" />
            <stop offset="100%" stopColor="#CC00FF" />
          </linearGradient>
        )}
      </defs>

      <ellipse cx={CX} cy="101" rx={rx * 0.78} ry="8" fill="#101828" opacity="0.12" />

      {/* ① 배경 오라 */}
      {showDetails && (
        <ellipse
          cx={CX}
          cy={CY}
          rx={rx + 16 + tier * 1.5}
          ry={ry + 16 + tier * 1.5}
          fill={colors.glow}
          opacity={frame.aura}
        >
          <animate
            attributeName="opacity"
            values={`${frame.aura * 0.7};${frame.aura};${frame.aura * 0.7}`}
            dur={`${2.8 - Math.min(tier, 5) * 0.16}s`}
            repeatCount="indefinite"
          />
        </ellipse>
      )}

      {/* ② 에너지 링 (T4) */}
      {showRing && !showRainbow && (
        <ellipse
          cx={CX}
          cy={CY}
          rx={rx + 14}
          ry={ry + 14}
          fill="none"
          stroke={`url(#${uid}-frame)`}
          strokeWidth="2.2"
          strokeDasharray={frame.dash}
          opacity="0.55"
        >
          <animate
            attributeName="rx"
            values={`${rx + 12};${rx + 17};${rx + 12}`}
            dur="2.4s"
            repeatCount="indefinite"
          />
          <animate
            attributeName="ry"
            values={`${ry + 12};${ry + 17};${ry + 12}`}
            dur="2.4s"
            repeatCount="indefinite"
          />
          <animate attributeName="opacity" values="0.3;0.55;0.3" dur="2.4s" repeatCount="indefinite" />
        </ellipse>
      )}

      {/* ③ 레인보우 링 (T5) */}
      {showRainbow && (
        <ellipse
          cx={CX}
          cy={CY}
          rx={rx + 17}
          ry={ry + 17}
          fill="none"
          stroke={`url(#${uid}-rainbow)`}
          strokeWidth="3"
          opacity="0.75"
        >
          <animateTransform
            attributeName="transform"
            type="rotate"
            values={`0 ${CX} ${CY}; 360 ${CX} ${CY}`}
            dur="4s"
            repeatCount="indefinite"
          />
        </ellipse>
      )}

      {/* ④ 파티클 (뒤편) */}
      {showDetails &&
        renderParticles(config.particleType, config.particleColors, tier, CX, CY)}

      {/* ⑤ 티어 프레임 */}
      {state !== 'undiscovered' && (
        <ellipse
          cx={CX}
          cy={CY}
          rx={rx + 6}
          ry={ry + 6}
          fill="rgba(255,255,255,0.18)"
          stroke={`url(#${uid}-frame)`}
          strokeWidth={tier >= 4 ? 3 : tier >= 2 ? 2.4 : 1.8}
          strokeDasharray={tier < 4 ? frame.dash : undefined}
          opacity={state === 'locked' ? 0.52 : 0.9}
          filter={`url(#${uid}-shadow)`}
        />
      )}

      {/* ⑥ 몸체 */}
      <ellipse
        cx={CX}
        cy={CY}
        rx={rx}
        ry={ry}
        fill={`url(#${uid}-grad)`}
        stroke={colors.outline}
        strokeWidth={showGlow ? 2.5 : 2}
        filter={showGlow ? `url(#${uid}-glow)` : undefined}
      >
        {/* idle 호버링 애니메이션 */}
        {state === 'obtained' && (
          <animateTransform
            attributeName="transform"
            type="translate"
            values={`0,0; 0,-3; 0,0`}
            dur="2.2s"
            repeatCount="indefinite"
          />
        )}
      </ellipse>

      {/* ⑦ 속성 문양과 몸체 패턴 */}
      {showDetails &&
        renderElementCrest(config.particleType, colors.pattern, colors.crest, CX, CY, tier)}
      {showDetails &&
        renderBodyPattern(config.patternType, colors.pattern, CX, CY)}

      <ellipse
        cx={CX - rx * 0.28}
        cy={CY - ry * 0.42}
        rx={Math.max(7, rx * 0.22)}
        ry={Math.max(4, ry * 0.12)}
        fill={colors.shine}
        opacity={state === 'obtained' ? 0.38 : 0.16}
        transform={`rotate(-22 ${CX - rx * 0.28} ${CY - ry * 0.42})`}
      />

      {/* ⑧ 눈 */}
      {state !== 'undiscovered' &&
        renderEyes(
          showDetails ? config.eyeStyle : 'steady',
          CX,
          CY,
          ry,
          colors.outline,
        )}

      {/* ⑨ 잠금 눈 가리개 */}
      {state === 'locked' && (
        <path
          d={`M${CX - 14},${CY - ry * 0.18 - 2} Q${CX},${CY - ry * 0.18 + 6} ${CX + 14},${CY - ry * 0.18 - 2}`}
          fill={darken(colors.body2, 25)}
          opacity="0.5"
        />
      )}

      {/* ⑩ 미소 */}
      {state !== 'undiscovered' && (
        <path
          d={`M${CX - 8},${CY + ry * 0.22} Q${CX},${CY + ry * 0.22 + 6} ${CX + 8},${CY + ry * 0.22}`}
          fill="none"
          stroke={colors.outline}
          strokeWidth="1.5"
          strokeLinecap="round"
        />
      )}

      {/* ⑪ 고티어 장식 */}
      {showDetails && tier >= 4 && (
        <Fragment>
          <circle cx={CX - rx - 6} cy={CY - ry * 0.5} r={tier >= 5 ? 3.2 : 2.4} fill={frame.accent} opacity="0.9" />
          <circle cx={CX + rx + 6} cy={CY - ry * 0.35} r={tier >= 5 ? 3.2 : 2.4} fill={frame.accent} opacity="0.9" />
          <circle cx={CX} cy={CY - ry - 9} r={tier >= 5 ? 3.5 : 2.6} fill={frame.accent} opacity="0.86" />
        </Fragment>
      )}

      {/* ⑫ 미발견 ? */}
      {state === 'undiscovered' && (
        <text
          x={CX}
          y={CY + 6}
          textAnchor="middle"
          fontSize="22"
          fill="#888888"
          fontWeight="bold"
          fontFamily="sans-serif"
        >
          ?
        </text>
      )}
    </svg>
  );
}
