﻿'use client'

import React, { useState, useRef, useEffect, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ArrowLeft, Play, Pause, ChevronDown, ChevronUp, Info, Award, Mic, BookOpen, MessageCircle, TrendingUp } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useNavigate } from '@/lib/nextRouterAdapter';
import { useParams } from 'next/navigation';
import { getApiPath } from '@/lib/basePath';

const skillTabs = [
  { id: 'pronunciation', label: 'Pronunciation', score: 75, level: 'Upper Intermediate', color: '#A73121', bgColor: '#F5E6E0', icon: Mic },
  { id: 'intonation', label: 'Intonation', score: 77, level: 'Advanced', color: '#D4B038', bgColor: '#FEF3DC', icon: TrendingUp },
  { id: 'fluency', label: 'Fluency', score: 84, level: 'Advanced', color: '#2563EB', bgColor: '#EFF6FF', icon: MessageCircle },
  { id: 'grammar', label: 'Grammar', score: 79, level: 'Advanced', color: '#059669', bgColor: '#ECFDF5', icon: BookOpen },
  { id: 'vocabulary', label: 'Vocabulary', score: 65, level: 'Intermediate', color: '#7C3AED', bgColor: '#F5F3FF', icon: Award },
];

const INTONATION_TUTORIAL_VIDEO_ID = 'zp0TGDymBb0';
const INTONATION_TUTORIAL_THUMBNAIL = `https://img.youtube.com/vi/${INTONATION_TUTORIAL_VIDEO_ID}/hqdefault.jpg`;
const FLUENCY_TUTORIAL_VIDEO_ID = '-oQXdfyA9JI';
const FLUENCY_TUTORIAL_THUMBNAIL = `https://img.youtube.com/vi/${FLUENCY_TUTORIAL_VIDEO_ID}/hqdefault.jpg`;
const CODING_FONT_STACK = '"JetBrains Mono","Fira Code","Cascadia Code","Consolas","Courier New",monospace';

function safeNumber(value, fallback = 0) {
  const num = Number(value);
  return Number.isFinite(num) ? num : fallback;
}

function clampScore(value) {
  const num = safeNumber(value, 0);
  return Math.max(0, Math.min(100, Math.round(num)));
}

function scoreToLevelLabel(score) {
  const value = safeNumber(score, 0);
  if (value >= 85) return 'Proficient';
  if (value >= 75) return 'Advanced';
  if (value >= 60) return 'Upper Intermediate';
  if (value >= 45) return 'Intermediate';
  if (value >= 30) return 'Elementary';
  return 'Beginner';
}

function hasExplicitScore(value) {
  return Number.isFinite(Number(value));
}

function parseDurationToSeconds(value) {
  if (typeof value === 'number' && Number.isFinite(value)) {
    return Math.max(0, Math.round(value));
  }
  const text = String(value || '').trim();
  if (!text) return 0;
  if (/^\d+(\.\d+)?$/.test(text)) {
    return Math.max(0, Math.round(Number(text)));
  }
  const parts = text.split(':').map((part) => Number(part));
  if (parts.some((part) => !Number.isFinite(part))) return 0;
  if (parts.length === 2) return parts[0] * 60 + parts[1];
  if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
  return 0;
}

function formatDuration(secondsValue) {
  const raw = Number(secondsValue);
  const seconds = Number.isFinite(raw) ? Math.max(0, Math.round(raw)) : 0;
  const mins = Math.floor(seconds / 60);
  const secs = seconds % 60;
  return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}

function chunkTranscript(text, totalSeconds) {
  const words = String(text || '').trim().split(/\s+/).filter(Boolean);
  if (!words.length) return [];
  const chunkSize = 24;
  const total = safeNumber(totalSeconds, 0);
  const chunks = [];
  for (let i = 0; i < words.length; i += chunkSize) {
    const ratio = total > 0 ? i / words.length : 0;
    const seconds = total > 0 ? Math.round(total * ratio) : 0;
    chunks.push({
      time: formatDuration(seconds),
      text: words.slice(i, i + chunkSize).join(' '),
    });
  }
  return chunks;
}

function extractSentenceTranscriptRows(resultJson) {
  const segments = Array.isArray(resultJson?.segments) && resultJson.segments.length
    ? resultJson.segments
    : resultJson?.result
      ? [{ result: resultJson.result }]
      : [];

  return segments.flatMap((segment) => {
    const sentences = Array.isArray(segment?.result?.sentences) ? segment.result.sentences : [];
    return sentences
      .map((sentence) => {
        const text = String(sentence?.sentence || '').trim();
        const start = Number(sentence?.start);
        if (!text) return null;
        return {
          time: formatDuration(Number.isFinite(start) && start > 0 ? start / 1000 : 0),
          text,
        };
      })
      .filter(Boolean);
  });
}

function buildTranscriptRows(resultJson, text, totalSeconds) {
  const sentenceRows = extractSentenceTranscriptRows(resultJson);
  if (sentenceRows.length) return sentenceRows;
  return chunkTranscript(text, totalSeconds);
}

const FUNCTION_WORDS = new Set([
  'a', 'an', 'the', 'and', 'or', 'but', 'if', 'so', 'because', 'as', 'at', 'by', 'for', 'from', 'in', 'into',
  'of', 'on', 'to', 'with', 'through', 'my', 'your', 'his', 'her', 'their', 'our', 'its', 'i', 'you', 'he',
  'she', 'it', 'we', 'they', 'me', 'him', 'them', 'is', 'am', 'are', 'was', 'were', 'be', 'been', 'being',
  'do', 'does', 'did', 'have', 'has', 'had', 'who', 'that', 'this', 'these', 'those',
]);

const INTONATION_OVER_TOOLTIP = 'You incorrectly emphasized this word. Say it softly next time!';
const INTONATION_UNDER_TOOLTIP = 'Make sure you say this word with more energy. It was too soft!';

function normalizeTranscriptWord(value) {
  return String(value || '')
    .toLowerCase()
    .replace(/^[^a-z0-9']+|[^a-z0-9']+$/g, '');
}

function averageNumbers(values) {
  const items = Array.isArray(values) ? values.filter((value) => Number.isFinite(value)) : [];
  if (!items.length) return 0;
  return items.reduce((sum, value) => sum + value, 0) / items.length;
}

function limitIntonationStyles(words) {
  const keep = new Set();

  words
    .map((word, index) => ({ index, priority: safeNumber(word.priority, 0), type: word.type }))
    .filter((entry) => entry.type === 'over')
    .sort((a, b) => b.priority - a.priority)
    .slice(0, 2)
    .forEach((entry) => keep.add(entry.index));

  words
    .map((word, index) => ({ index, priority: safeNumber(word.priority, 0), type: word.type }))
    .filter((entry) => entry.type === 'under')
    .sort((a, b) => a.priority - b.priority)
    .slice(0, 2)
    .forEach((entry) => keep.add(entry.index));

  words
    .map((word, index) => ({ index, priority: safeNumber(word.priority, 0), type: word.type }))
    .filter((entry) => entry.type === 'correct')
    .sort((a, b) => b.priority - a.priority)
    .slice(0, 4)
    .forEach((entry) => keep.add(entry.index));

  return words.map((word, index) => (
    keep.has(index)
      ? word
      : { ...word, type: 'normal', tooltip: '' }
  ));
}

function classifyIntonationWord(detail) {
  const text = String(detail?.word || '').trim();
  if (!text) return null;

  const wordScore = safeNumber(detail?.pronunciation, 0);
  const pauseMs = safeNumber(detail?.pause?.duration, 0);
  const normalizedWord = normalizeTranscriptWord(text);
  const isFunctionWord = FUNCTION_WORDS.has(normalizedWord);
  const phonemes = Array.isArray(detail?.phonemes) ? detail.phonemes : [];
  const stressedScores = phonemes
    .filter((phoneme) => Number(phoneme?.stress_mark) === 1)
    .map((phoneme) => safeNumber(phoneme?.pronunciation, NaN))
    .filter((value) => Number.isFinite(value));
  const hasStress = stressedScores.length > 0;
  const stressedScore = hasStress ? averageNumbers(stressedScores) : wordScore;

  let type = 'normal';
  let tooltip = '';
  let priority = wordScore;

  if (hasStress && stressedScore < 45) {
    type = 'under';
    tooltip = INTONATION_UNDER_TOOLTIP;
    priority = stressedScore;
  } else if (!hasStress && !isFunctionWord && wordScore < 40) {
    type = 'under';
    tooltip = INTONATION_UNDER_TOOLTIP;
    priority = wordScore;
  } else if (!isFunctionWord && hasStress && pauseMs >= 80 && stressedScore >= 78) {
    type = 'over';
    tooltip = INTONATION_OVER_TOOLTIP;
    priority = stressedScore + Math.min(40, pauseMs / 10);
  } else if (!isFunctionWord && hasStress && stressedScore >= 85 && wordScore >= 75) {
    type = 'correct';
    priority = stressedScore;
  } else if (!isFunctionWord && wordScore >= 92) {
    type = 'correct';
    priority = wordScore;
  }

  return { text, type, tooltip, priority };
}

function buildFallbackIntonationWords(text) {
  return String(text || '')
    .split(/\s+/)
    .filter(Boolean)
    .map((word) => ({ text: word, type: 'normal', tooltip: '' }));
}

function buildIntonationTranscriptRows(resultJson, fallbackRows = []) {
  const segments = Array.isArray(resultJson?.segments) && resultJson.segments.length
    ? resultJson.segments
    : resultJson?.result
      ? [{ result: resultJson.result }]
      : [];

  const rows = segments.flatMap((segment) => {
    const sentences = Array.isArray(segment?.result?.sentences) ? segment.result.sentences : [];
    return sentences
      .map((sentence) => {
        const text = String(sentence?.sentence || '').trim();
        if (!text) return null;

        const words = Array.isArray(sentence?.details) && sentence.details.length
          ? limitIntonationStyles(
              sentence.details
                .map((detail) => classifyIntonationWord(detail))
                .filter(Boolean)
            )
          : buildFallbackIntonationWords(text);

        return {
          time: formatDuration(Number.isFinite(Number(sentence?.start)) ? Number(sentence.start) / 1000 : 0),
          words: words.length ? words : buildFallbackIntonationWords(text),
        };
      })
      .filter(Boolean);
  });

  if (rows.length) return rows;

  return (Array.isArray(fallbackRows) ? fallbackRows : []).map((row) => ({
    time: row?.time || '00:00',
    words: buildFallbackIntonationWords(row?.text || ''),
  }));
}

function AnimatedNumber({ target, suffix = '' }) {
  const [value, setValue] = React.useState(0);

  React.useEffect(() => {
    setValue(0);
    const duration = 1200;
    const steps = 40;
    const increment = target / steps;
    let current = 0;
    const timer = setInterval(() => {
      current += increment;
      if (current >= target) {
        setValue(target);
        clearInterval(timer);
      } else {
        setValue(Math.floor(current));
      }
    }, duration / steps);
    return () => clearInterval(timer);
  }, [target]);

  return <span>{value}{suffix}</span>;
}

function normalizePitchState(value) {
  const text = String(value || '').trim();
  if (!text) return 'Balanced';
  return text.charAt(0).toUpperCase() + text.slice(1);
}

function buildIntonationRecommendation(pitchState, pitchVariation) {
  const normalized = String(pitchState || '').trim().toLowerCase();
  const variation = Math.round(safeNumber(pitchVariation, 0));

  if (normalized === 'monotonous') {
    return `Your voice stays in a narrow range at around ${variation}Hz. Add more rise and fall on key words to sound more expressive.`;
  }
  if (normalized === 'excessive') {
    return `Your pitch movement is quite wide at around ${variation}Hz. Keep your emphasis strong, but smooth out sudden jumps so your delivery sounds controlled.`;
  }
  return `Your pitch variation is around ${variation}Hz, which is within the natural target band. Keep stressing key words and pausing clearly between ideas.`;
}

function formatChartTimestamp(secondsValue) {
  const totalSeconds = Math.max(0, Math.round(safeNumber(secondsValue, 0)));
  if (totalSeconds >= 60) {
    const minutes = Math.floor(totalSeconds / 60);
    const seconds = totalSeconds % 60;
    return `${minutes}m${seconds}s`;
  }
  return `${totalSeconds}s`;
}

function buildTimeTicks(durationSeconds) {
  const duration = Math.max(1, Math.round(safeNumber(durationSeconds, 0)));
  const candidates = [5, 10, 15, 20, 30, 45, 60, 90, 120];
  const step = candidates.find((value) => Math.ceil(duration / value) <= 6) || candidates[candidates.length - 1];
  const ticks = [];

  for (let tick = 0; tick < duration; tick += step) {
    ticks.push(tick);
  }

  if (!ticks.length || ticks[ticks.length - 1] !== duration) {
    const lastTick = ticks[ticks.length - 1];
    if (Number.isFinite(lastTick) && duration - lastTick < step * 0.4) {
      ticks[ticks.length - 1] = duration;
    } else {
      ticks.push(duration);
    }
  }

  return Array.from(new Set(ticks));
}

function reducePitchPoints(points, limit = 18) {
  if (!Array.isArray(points) || points.length <= limit) return Array.isArray(points) ? points : [];
  const step = (points.length - 1) / (limit - 1);
  const sampled = [];
  for (let index = 0; index < limit; index += 1) {
    sampled.push(points[Math.round(index * step)]);
  }
  return sampled;
}

function buildFallbackPitchChart(intonation) {
  const segments = (Array.isArray(intonation?.segments) ? intonation.segments : [])
    .map((segment) => ({
      duration: safeNumber(segment?.duration, 0),
      mean_pitch: safeNumber(segment?.mean_pitch, NaN),
    }))
    .filter((segment) => segment.duration > 0 && Number.isFinite(segment.mean_pitch));

  if (!segments.length) return null;

  let elapsed = 0;
  const points = segments.flatMap((segment) => {
    const start = elapsed;
    const end = elapsed + segment.duration;
    const midpoint = start + (segment.duration / 2);
    elapsed = end;

    if (segments.length === 1) {
      return [
        { time_sec: 0, pitch_hz: Number(segment.mean_pitch.toFixed(2)) },
        { time_sec: Number(midpoint.toFixed(2)), pitch_hz: Number(segment.mean_pitch.toFixed(2)) },
        { time_sec: Number(end.toFixed(2)), pitch_hz: Number(segment.mean_pitch.toFixed(2)) },
      ];
    }

    return [
      {
        time_sec: Number(midpoint.toFixed(2)),
        pitch_hz: Number(segment.mean_pitch.toFixed(2)),
      },
    ];
  });

  const observed = points.map((point) => point.pitch_hz).filter((value) => Number.isFinite(value));
  const observedMin = observed.length ? Math.min(...observed) : null;
  const observedMax = observed.length ? Math.max(...observed) : null;
  const duration = Math.max(
    safeNumber(intonation?.duration_seconds, 0),
    safeNumber(points[points.length - 1]?.time_sec, 0),
    1
  );
  const upperCandidate = observedMax ? Math.max(190, observedMax * 1.12) : 300;

  return {
    target_min_hz: 50,
    target_max_hz: 150,
    y_min_hz: 0,
    y_max_hz: Math.max(300, Math.ceil(upperCandidate / 50) * 50),
    duration_seconds: Number(duration.toFixed(2)),
    observed_min_hz: observedMin ? Number(observedMin.toFixed(2)) : null,
    observed_max_hz: observedMax ? Number(observedMax.toFixed(2)) : null,
    points,
    derived_from_segments: true,
  };
}

function getResolvedPitchChart(intonation) {
  const chart = intonation?.chart;
  if (Array.isArray(chart?.points) && chart.points.length) {
    return chart;
  }
  return buildFallbackPitchChart(intonation);
}

function buildPitchSparkBars(chart, fallbackValue = 0) {
  const sourcePoints = reducePitchPoints(
    (Array.isArray(chart?.points) ? chart.points : [])
      .map((point) => safeNumber(point?.pitch_hz, NaN))
      .filter((value) => Number.isFinite(value)),
    20
  );

  if (!sourcePoints.length) {
    const seed = Math.max(16, Math.min(54, Math.round(safeNumber(fallbackValue, 40))));
    return [18, 22, 26, 34, 30, 38, 44, 48, 42, 36, 44, 52, 48, 42, 36, 30, 24, 20, 24, 28]
      .map((height, index) => Math.max(14, Math.min(54, height + Math.round(((seed - 40) / 40) * ((index % 4) - 1) * 2))));
  }

  const min = Math.min(...sourcePoints);
  const max = Math.max(...sourcePoints);
  const spread = Math.max(1, max - min);

  return sourcePoints.map((value) => {
    const ratio = (value - min) / spread;
    return Math.round(14 + (ratio * 38));
  });
}

const chartUi = {
  panelBg: '#FFFFFF',
  panelBorder: '#E7DBCF',
  gridMajor: '#EADFD6',
  gridMinor: '#F3ECE5',
  axisText: '#8B7967',
  heading: '#4A1429',
  body: '#6B5D52',
  danger: '#A73121',
  success: '#059669',
  targetBand: '#DCF6E3',
};

function PitchVariationChart({ chart, errorMessage = '' }) {
  const points = reducePitchPoints(
    (Array.isArray(chart?.points) ? chart.points : [])
      .map((point) => ({
        time_sec: safeNumber(point?.time_sec, NaN),
        pitch_hz: safeNumber(point?.pitch_hz, NaN),
      }))
      .filter((point) => Number.isFinite(point.time_sec) && Number.isFinite(point.pitch_hz))
      .sort((a, b) => a.time_sec - b.time_sec),
    18
  );

  const duration = Math.max(
    safeNumber(chart?.duration_seconds, 0),
    points.length ? safeNumber(points[points.length - 1]?.time_sec, 0) : 0,
    1
  );
  const yMin = safeNumber(chart?.y_min_hz, 0);
  const yMax = Math.max(safeNumber(chart?.y_max_hz, 300), 200);
  const targetMin = safeNumber(chart?.target_min_hz, 50);
  const targetMax = safeNumber(chart?.target_max_hz, 150);
  const width = 980;
  const height = 360;
  const margin = { top: 26, right: 110, bottom: 58, left: 64 };
  const plotWidth = width - margin.left - margin.right;
  const plotHeight = height - margin.top - margin.bottom;

  const scaleX = (value) => margin.left + (Math.max(0, value) / duration) * plotWidth;
  const scaleY = (value) => margin.top + plotHeight - ((value - yMin) / Math.max(1, yMax - yMin)) * plotHeight;
  const yTicks = [];
  for (let tick = yMax; tick >= yMin; tick -= 50) {
    yTicks.push(tick);
  }
  const xTicks = buildTimeTicks(duration);
  const targetBandY = scaleY(targetMax);
  const targetBandHeight = Math.max(0, scaleY(targetMin) - scaleY(targetMax));
  const linePath = points.map((point, index) => (
    `${index === 0 ? 'M' : 'L'} ${scaleX(point.time_sec)} ${scaleY(point.pitch_hz)}`
  )).join(' ');

  if (!points.length) {
    return (
      <div className="rounded-2xl border border-dashed border-[#DCCFC3] bg-[#FAF5F0] px-6 py-10 text-center">
        <div className="text-lg font-extrabold text-[#4A1429] mb-2">Pitch contour unavailable</div>
        <p className="text-sm text-[#6B5D52]">
          {errorMessage || 'We could not extract enough voiced frames from this recording to build a detailed pitch chart.'}
        </p>
      </div>
    );
  }

  const rightLabelX = width - 16;

  return (
    <div
      className="rounded-[28px] p-4"
      style={{ backgroundColor: chartUi.panelBg }}
    >
      <div className="overflow-x-auto">
        <svg viewBox={`0 0 ${width} ${height}`} className="min-w-[880px] w-full h-auto">
          <defs>
            <linearGradient id="pitchLineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" stopColor="#D4B038" />
              <stop offset="100%" stopColor="#B78714" />
            </linearGradient>
            <filter id="pitchGlow" x="-20%" y="-20%" width="140%" height="140%">
              <feDropShadow dx="0" dy="8" stdDeviation="10" floodColor="#D4B038" floodOpacity="0.18" />
            </filter>
          </defs>

          <rect
            x={margin.left}
            y={targetBandY}
            width={plotWidth}
            height={targetBandHeight}
            rx="18"
            fill={chartUi.targetBand}
            fillOpacity="0.95"
          />

          {yTicks.map((tick) => (
            <g key={tick}>
              {tick !== 0 ? (
                <line
                  x1={margin.left}
                  y1={scaleY(tick)}
                  x2={margin.left + plotWidth}
                  y2={scaleY(tick)}
                  stroke={chartUi.gridMajor}
                  strokeDasharray="6 8"
                />
              ) : null}
              <text
                x={margin.left - 18}
                y={scaleY(tick) + 5}
                textAnchor="end"
                className="text-[14px] font-semibold"
                fill={chartUi.axisText}
              >
                {tick}Hz
              </text>
            </g>
          ))}

          {xTicks.map((tick) => (
            <g key={tick}>
              <line
                x1={scaleX(tick)}
                y1={margin.top}
                x2={scaleX(tick)}
                y2={margin.top + plotHeight}
                stroke={chartUi.gridMinor}
                strokeDasharray="4 10"
              />
              <text
                x={scaleX(tick)}
                y={height - 14}
                textAnchor={tick === 0 ? 'start' : tick === xTicks[xTicks.length - 1] ? 'end' : 'middle'}
                className="text-[14px] font-semibold"
                fill={chartUi.axisText}
              >
                {formatChartTimestamp(tick)}
              </text>
            </g>
          ))}

          <path
            d={linePath}
            fill="none"
            stroke="url(#pitchLineGradient)"
            strokeWidth="5"
            strokeLinecap="round"
            strokeLinejoin="round"
            filter="url(#pitchGlow)"
          />

          {points.map((point, index) => (
            <g key={`${point.time_sec}-${point.pitch_hz}-${index}`}>
              <circle
                cx={scaleX(point.time_sec)}
                cy={scaleY(point.pitch_hz)}
                r="7.5"
                fill="#4A1429"
                stroke="#FFFFFF"
                strokeWidth="3"
              />
            </g>
          ))}

          <text x={rightLabelX} y={scaleY(yMax * 0.78)} textAnchor="end" className="text-[15px] font-bold" fill={chartUi.danger}>
            Excessive
          </text>
          <text x={rightLabelX} y={scaleY((targetMin + targetMax) / 2)} textAnchor="end" className="text-[15px] font-bold" fill={chartUi.success}>
            Target Range
          </text>
          <text x={rightLabelX} y={scaleY(targetMin / 2)} textAnchor="end" className="text-[15px] font-bold" fill={chartUi.danger}>
            Monotone
          </text>
        </svg>
      </div>
      <div className="mt-3 text-xs font-medium text-[#6B5D52]">
        {chart?.derived_from_segments
          ? 'This pitch line was reconstructed from stored intonation segments because the original audio contour is no longer available.'
          : 'Detailed contour measured from voiced frames with Parselmouth. The green band marks the recommended 50Hz to 150Hz speaking range.'}
      </div>
    </div>
  );
}

function safeJsonParse(value) {
  if (!value) return null;
  if (typeof value !== 'string') return value;
  const text = value.trim();
  if (!text) return null;

  const unwrapped = stripCodeFence(text);

  const candidates = Array.from(new Set([
    text,
    unwrapped,
    unwrapped.match(/[\[{][\s\S]*[\]}]/)?.[0]?.trim() || '',
  ].filter(Boolean)));

  for (const candidate of candidates) {
    try {
      return JSON.parse(candidate);
    } catch {
      continue;
    }
  }

  return null;
}

function stripCodeFence(value) {
  return String(value || '')
    .trim()
    .replace(/^```(?:json)?\s*/i, '')
    .replace(/\s*```$/i, '')
    .trim();
}

function isJsonArtifactToken(value) {
  const text = String(value || '').trim().toLowerCase();
  if (!text) return true;
  if (text.startsWith('```')) return true;
  if (/^[\[\]\{\},:]+$/.test(text)) return true;
  if (/^"(?:json|answer|active_vocabulary|repeated_words|words|feedback|grammatical_range)"/.test(text)) return true;
  if (/[{}\[\]`]/.test(text)) return true;
  return false;
}

function sanitizeListToken(value) {
  const cleaned = String(value || '')
    .replace(/^[-*•\d\).\s]+/, '')
    .replace(/^"+|"+$/g, '')
    .replace(/,+$/g, '')
    .trim();
  if (!cleaned) return '';
  if (isJsonArtifactToken(cleaned)) return '';
  return cleaned;
}

function parseJsonDeep(value, maxDepth = 2) {
  let current = value;
  for (let i = 0; i < maxDepth; i += 1) {
    if (typeof current !== 'string') break;
    const parsed = safeJsonParse(current);
    if (parsed === null) break;
    current = parsed;
  }
  return current;
}

function extractAnswerPayload(value) {
  const parsed = parseJsonDeep(value, 2);
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && 'answer' in parsed) {
    return parseJsonDeep(parsed.answer, 2);
  }
  return parsed;
}

function toStringList(items) {
  if (!Array.isArray(items)) return [];
  return items
    .map((item) => sanitizeListToken(item))
    .filter(Boolean);
}

function toDisplayLabel(value) {
  const text = String(value || '').trim();
  if (!text) return '';
  return text
    .replace(/[_-]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
    .replace(/\b\w/g, (char) => char.toUpperCase());
}

function normalizeList(value) {
  const payload = extractAnswerPayload(value);
  if (!payload) return [];
  if (Array.isArray(payload)) return toStringList(payload);
  if (payload && typeof payload === 'object') {
    return Object.values(payload)
      .flatMap((entry) => (Array.isArray(entry) ? entry : [entry]))
      .map((entry) => sanitizeListToken(entry))
      .filter(Boolean);
  }
  const text = stripCodeFence(payload);
  if (!text || text.startsWith('{') || text.startsWith('[') || text.includes('```')) return [];
  return text
    .split(/[\n,;|]/)
    .map((item) => sanitizeListToken(item))
    .filter(Boolean);
}

function normalizeText(value) {
  const payload = extractAnswerPayload(value);
  if (payload === null || payload === undefined) return '';
  if (typeof payload === 'string') {
    const text = payload.trim();
    if (text.startsWith('{') || text.startsWith('[')) return '';
    return text;
  }
  if (typeof payload === 'number') return String(payload);
  if (payload && typeof payload === 'object') {
    const text = payload.text
      || payload.message
      || payload.value
      || payload.feedback
      || payload.recommendation
      || payload.overall_feedback
      || payload.summary;
    return text ? String(text).trim() : '';
  }
  return String(payload || '').trim();
}

function normalizeRepetitiveWords(value) {
  const payload = extractAnswerPayload(value);
  if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
    const words = payload.repeated_words || payload.repetitive_words || payload.words;
    if (Array.isArray(words)) return toStringList(words);
  }
  if (Array.isArray(payload)) return toStringList(payload);
  return normalizeList(payload);
}

function splitAlternativeText(value) {
  return String(value || '')
    .split(/\s*(?:->|→)\s*/)
    .map((part) => part.trim())
    .filter(Boolean);
}

function toSynonymList(value) {
  if (Array.isArray(value)) {
    return value.map((item) => sanitizeListToken(item)).filter(Boolean);
  }
  if (typeof value === 'string') {
    return value
      .split(/[,;/|]/)
      .map((item) => sanitizeListToken(item))
      .filter(Boolean);
  }
  if (value && typeof value === 'object') {
    return Object.values(value)
      .flatMap((entry) => (Array.isArray(entry) ? entry : [entry]))
      .map((item) => sanitizeListToken(item))
      .filter(Boolean);
  }
  return [];
}

function formatActiveVocabularyFromObject(value) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
  return Object.entries(value).flatMap(([word, synonyms]) => {
    const base = sanitizeListToken(word);
    const list = toSynonymList(synonyms);
    if (!base && !list.length) return [];
    if (base && list.length) return [`${base} → ${list.join(', ')}`];
    return base ? [base] : list;
  });
}

function extractActiveVocabularyFromJsonishText(value) {
  const text = stripCodeFence(value);
  if (!text) return [];

  const entries = [];
  const jsonPairRegex = /"([^"\n]+)"\s*:\s*\[([^\]]*)\]/g;
  let match = jsonPairRegex.exec(text);
  while (match) {
    const key = sanitizeListToken(match[1]);
    const values = String(match[2] || '')
      .split(',')
      .map((item) => sanitizeListToken(item.replace(/^"+|"+$/g, '')))
      .filter(Boolean);
    if (key && values.length) {
      entries.push(`${key} → ${values.join(', ')}`);
    } else if (key) {
      entries.push(key);
    }
    match = jsonPairRegex.exec(text);
  }

  return entries;
}

function normalizeActiveVocabulary(value) {
  const payload = extractAnswerPayload(value);
  if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
    const vocab = payload.active_vocabulary || payload.activeVocabulary || payload.vocabulary;
    if (vocab && typeof vocab === 'object' && !Array.isArray(vocab)) {
      return formatActiveVocabularyFromObject(vocab);
    }
    const inlineObject = formatActiveVocabularyFromObject(payload);
    if (inlineObject.length) return inlineObject;
  }
  if (Array.isArray(payload)) {
    return payload
      .map((item) => sanitizeListToken(item))
      .filter(Boolean);
  }
  if (typeof payload === 'string') {
    const parsedJsonish = extractActiveVocabularyFromJsonishText(payload);
    if (parsedJsonish.length) return parsedJsonish;
    const cleaned = stripCodeFence(payload);
    if (!cleaned || cleaned.startsWith('{') || cleaned.startsWith('[') || /```|active_vocabulary/i.test(cleaned)) {
      return [];
    }
    return normalizeList(cleaned);
  }
  return normalizeList(payload);
}

function normalizeGrammarRange(value) {
  const payload = extractAnswerPayload(value);
  let range = null;
  if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
    if (Array.isArray(payload.grammatical_range)) {
      range = payload.grammatical_range;
    } else if (Array.isArray(payload.range)) {
      range = payload.range;
    }
  } else if (Array.isArray(payload)) {
    range = payload;
  }

  const items = [];
  if (Array.isArray(range)) {
    range.forEach((entry) => {
      if (!entry) return;
      if (typeof entry === 'string') {
        items.push({ type: 'Structure', example: entry });
        return;
      }
      if (typeof entry === 'object') {
        const keys = Object.keys(entry);
        if (keys.length === 1) {
          const key = keys[0];
          items.push({ type: toDisplayLabel(key) || 'Structure', example: String(entry[key] || '').trim() });
          return;
        }
        const type = entry.type
          || entry.label
          || entry.structure
          || entry.structure_name
          || entry['structure name']
          || 'Structure';
        const example = entry.example
          || entry.sample
          || entry.value
          || entry.example_from_transcript
          || entry['example from transcript']
          || '';
        items.push({ type, example });
      }
    });
  }

  if (items.length) {
    return items
      .filter((item) => String(item?.example || '').trim())
      .map((item) => ({
        type: toDisplayLabel(item.type) || 'Structure',
        example: String(item.example || '').trim(),
      }));
  }
  return normalizeList(payload).map((item) => ({ type: 'Structure', example: item }));
}

function normalizeGrammarErrors(value) {
  const payload = extractAnswerPayload(value);
  const toEntryArray = (candidate) => {
    if (Array.isArray(candidate)) return candidate;
    if (candidate && typeof candidate === 'object') {
      return Object.values(candidate).filter((entry) => entry !== null && entry !== undefined);
    }
    return [];
  };

  const findFeedbackEntries = (source) => {
    if (!source) return [];
    if (Array.isArray(source)) return source;
    if (typeof source !== 'object') return [];

    const queue = [source];
    const seen = new Set();
    const feedbackKeys = ['feedback', 'errors', 'grammar_errors', 'corrections', 'issues', 'mistakes', 'items'];

    while (queue.length) {
      const current = queue.shift();
      if (!current || typeof current !== 'object' || seen.has(current)) continue;
      seen.add(current);

      for (const key of feedbackKeys) {
        const entries = toEntryArray(current[key]);
        if (entries.length) return entries;
      }

      Object.values(current).forEach((value) => {
        if (value && typeof value === 'object') queue.push(value);
      });
    }

    return [];
  };

  const normalizeFeedbackEntry = (entry) => {
    if (!entry) return null;
    if (typeof entry === 'string') return { error: entry };
    if (typeof entry !== 'object') return { error: String(entry) };

    const keys = Object.keys(entry);
    const values = keys
      .map((key) => entry[key])
      .filter((item) => item !== null && item !== undefined)
      .map((item) => (typeof item === 'string' ? item.trim() : item))
      .filter(Boolean);

    let error = entry.error
      || entry.issue
      || entry.title
      || entry.problem
      || entry.original
      || entry.before
      || entry.incorrect
      || entry.text
      || entry.source
      || entry.source_text
      || entry.original_text
      || '';
    let suggestion = entry.suggestion
      || entry.fix
      || entry.correction
      || entry.corrected
      || entry.after
      || entry.rewrite
      || entry.revised
      || entry.target
      || entry.corrected_text
      || '';
    let explanation = entry.explanation
      || entry.detail
      || entry.note
      || entry.reason
      || entry.comment
      || entry.description
      || '';

    if (!error && !suggestion && keys.length >= 2) {
      const [firstValue, secondValue, thirdValue] = values;
      error = typeof firstValue === 'string' ? firstValue : '';
      suggestion = typeof secondValue === 'string' ? secondValue : '';
      explanation = typeof thirdValue === 'string' ? thirdValue : explanation;
    } else if (!error && !suggestion && keys.length === 1) {
      const [onlyKey] = keys;
      const onlyValue = entry[onlyKey];
      if (typeof onlyValue === 'string' && onlyValue.trim()) {
        error = toDisplayLabel(onlyKey) || 'Grammar issue';
        explanation = onlyValue.trim();
      }
    }

    error = String(error || '').trim();
    suggestion = String(suggestion || '').trim();
    explanation = String(explanation || '').trim();

    if (!error && !suggestion && !explanation) return null;
    return { error, suggestion, explanation };
  };

  const findComparisonTexts = (source) => {
    if (!source || typeof source !== 'object') return { originalText: '', correctedText: '' };

    const queue = [source];
    const seen = new Set();

    while (queue.length) {
      const current = queue.shift();
      if (!current || typeof current !== 'object' || seen.has(current)) continue;
      seen.add(current);

      const originalText = String(
        current.original
        || current.original_text
        || current.transcript
        || current.before
        || current.incorrect
        || current.source
        || current.source_text
        || ''
      ).trim();
      const correctedText = String(
        current.corrected
        || current.corrected_text
        || current.revised
        || current.rewrite
        || current.after
        || current.fix
        || current.correction
        || current.target
        || ''
      ).trim();

      if (originalText && correctedText) {
        return { originalText, correctedText };
      }

      Object.values(current).forEach((value) => {
        if (value && typeof value === 'object') queue.push(value);
      });
    }

    return { originalText: '', correctedText: '' };
  };

  const feedback = findFeedbackEntries(payload);
  const normalizedFeedback = feedback
    .map((entry) => normalizeFeedbackEntry(entry))
    .filter((item) => item && (item.error || item.suggestion || item.explanation));

  if (normalizedFeedback.length) return normalizedFeedback;

  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return [];

  const { originalText, correctedText } = findComparisonTexts(payload);

  if (!originalText || !correctedText) return [];

  const splitIntoSentenceChunks = (text) => String(text || '')
    .split(/(?:\r?\n)+|(?<=[.!?])\s+/)
    .map((item) => item.trim())
    .filter(Boolean);
  const normalizeCompare = (text) => String(text || '')
    .toLowerCase()
    .replace(/[“”"']/g, '')
    .replace(/\s+/g, ' ')
    .trim();

  const originalChunks = splitIntoSentenceChunks(originalText);
  const correctedChunks = splitIntoSentenceChunks(correctedText);
  const chunkCount = Math.max(originalChunks.length, correctedChunks.length);
  const derived = [];

  for (let index = 0; index < chunkCount; index += 1) {
    const originalChunk = originalChunks[index] || '';
    const correctedChunk = correctedChunks[index] || '';
    if (!originalChunk || !correctedChunk) continue;
    if (normalizeCompare(originalChunk) === normalizeCompare(correctedChunk)) continue;

    derived.push({
      error: originalChunk,
      suggestion: correctedChunk,
      explanation: 'Suggested correction based on your transcript.',
    });

    if (derived.length >= 3) break;
  }

  if (derived.length) return derived;

  if (normalizeCompare(originalText) === normalizeCompare(correctedText)) return [];

  return [{
    error: originalText,
    suggestion: correctedText,
    explanation: 'Suggested correction based on your transcript.',
  }];
}

function normalizeForTranscriptMatch(value) {
  return String(value || '')
    .normalize('NFKC')
    .toLowerCase()
    .replace(/[\u2018\u2019]/g, "'")
    .replace(/[^a-z0-9']+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function transcriptContainsExcerpt(transcript, excerpt) {
  const haystack = normalizeForTranscriptMatch(transcript);
  const needle = normalizeForTranscriptMatch(excerpt);
  return Boolean(haystack && needle && ` ${haystack} `.includes(` ${needle} `));
}

function transcriptOccurrenceCount(transcript, excerpt) {
  const haystack = normalizeForTranscriptMatch(transcript).split(' ').filter(Boolean);
  const needle = normalizeForTranscriptMatch(excerpt).split(' ').filter(Boolean);
  if (!haystack.length || !needle.length || needle.length > haystack.length) return 0;

  let count = 0;
  for (let index = 0; index <= haystack.length - needle.length; index += 1) {
    if (needle.every((word, offset) => haystack[index + offset] === word)) count += 1;
  }
  return count;
}

export default function ScoreDetails({ token }) {
  const navigate = useNavigate();
  const routeParams = useParams();
  const [activeTab, setActiveTab] = useState('pronunciation');
  const [expandedSections, setExpandedSections] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  const routeToken = Array.isArray(routeParams?.token) ? routeParams.token[0] : routeParams?.token;
  const tokenId = String(token || routeToken || '').trim();

  const toggleSection = (section) => {
    setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }));
  };

  useEffect(() => {
    if (!tokenId) {
      setLoading(false);
      setError('Missing result token.');
      return;
    }

    let active = true;
    const controller = new AbortController();

    const fetchResult = async (source = 'tools', allowFallback = true) => {
      try {
        setLoading(true);
        setError('');
        const res = await fetch(getApiPath(`/api/speech/result?token=${encodeURIComponent(tokenId)}&source=${source}`), {
          signal: controller.signal,
        });

        if (!res.ok) {
          if (res.status === 404 && allowFallback) {
            return fetchResult('legacy', false);
          }
          const payload = await res.json().catch(() => ({}));
          throw new Error(payload?.message || 'Failed to load results.');
        }

        const payload = await res.json();
        if (!active) return;
        setData(payload);
        setLoading(false);
      } catch (err) {
        if (!active || err.name === 'AbortError') return;
        setError(err.message || 'Failed to load results.');
        setLoading(false);
      }
    };

    fetchResult();
    return () => {
      active = false;
      controller.abort();
    };
  }, [tokenId]);

  const summary = data?.summary || {};
  const resultRow = data?.result || {};
  const resultJson = data?.result_json || {};
  const transcriptText = String(summary?.transcript || resultJson?.result?.transcription || '').trim();
  const totalSeconds = parseDurationToSeconds(summary?.speakingTime || resultJson?.result?.numeric_duration || resultJson?.result?.duration);
  const transcriptRows = useMemo(
    () => buildTranscriptRows(resultJson, transcriptText, totalSeconds),
    [resultJson, transcriptText, totalSeconds]
  );
  const intonationTranscriptRows = useMemo(
    () => buildIntonationTranscriptRows(resultJson, transcriptRows),
    [resultJson, transcriptRows]
  );

  const repetitiveWords = useMemo(
    () => normalizeRepetitiveWords(resultRow?.repetitive_words)
      .filter((word) => transcriptOccurrenceCount(transcriptText, word) >= 3),
    [resultRow, transcriptText]
  );
  const activeVocabulary = useMemo(
    () => normalizeActiveVocabulary(resultRow?.active_vocabulary)
      .filter((item) => transcriptContainsExcerpt(transcriptText, splitAlternativeText(item)[0] || item)),
    [resultRow, transcriptText]
  );
  const grammarErrors = useMemo(
    () => normalizeGrammarErrors(resultRow?.grammatical_errors)
      .filter((item) => transcriptContainsExcerpt(transcriptText, item?.error || item)),
    [resultRow, transcriptText]
  );
  const grammarRange = useMemo(
    () => normalizeGrammarRange(resultRow?.grammatical_range)
      .filter((item) => transcriptContainsExcerpt(transcriptText, item?.example || item)),
    [resultRow, transcriptText]
  );
  const grammarRecommendation = normalizeText(resultRow?.grammar_recommendation);
  const vocabularyRecommendation = normalizeText(resultRow?.vocabulary_recommendation);
  const pronunciationGuides = Array.isArray(data?.pronunciation_guides) ? data.pronunciation_guides : [];
  const intonationData = resultJson?.result?.intonation || null;
  const fluencyData = resultJson?.result?.fluency_analysis || null;
  const highlightWords = useMemo(() => {
    const combined = [...repetitiveWords, ...activeVocabulary];
    const words = combined.flatMap((item) => {
      const text = String(item || '').trim();
      if (!text) return [];
      const [base] = splitAlternativeText(text);
      return base ? [base] : [];
    });
    return Array.from(new Set(words));
  }, [repetitiveWords, activeVocabulary]);

  const computedSkillTabs = useMemo(() => {
    const scores = summary?.scores || {};
    const intonationUnavailable = String(intonationData?.analysis_status || '').toLowerCase() === 'unavailable';
    return skillTabs.map((skill) => {
      const rawScore = scores?.[skill.id];
      const scoreValue = clampScore(hasExplicitScore(rawScore) ? rawScore : skill.score);
      return {
        ...skill,
        score: scoreValue,
        level: skill.id === 'intonation' && intonationUnavailable
          ? 'Unavailable'
          : hasExplicitScore(rawScore)
            ? scoreToLevelLabel(scoreValue)
            : skill.level,
      };
    });
  }, [summary, intonationData]);

  useEffect(() => {
    if (!computedSkillTabs.length) return;
    const hasActive = computedSkillTabs.some((skill) => skill.id === activeTab);
    if (!hasActive) {
      setActiveTab(computedSkillTabs[0].id);
    }
  }, [computedSkillTabs, activeTab]);

  const activeSkill = computedSkillTabs.find(s => s.id === activeTab) || computedSkillTabs[0];
  let overallScore = clampScore(summary?.overall);
  if (!overallScore && computedSkillTabs.length) {
    const values = computedSkillTabs.map((skill) => safeNumber(skill.score, 0)).filter((value) => value > 0);
    if (values.length) {
      overallScore = clampScore(values.reduce((sum, value) => sum + value, 0) / values.length);
    }
  }

  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-[#FAF5F0]">
        <div className="w-10 h-10 border-4 border-[#E5DDD5] border-t-[#4A1429] rounded-full animate-spin" />
      </div>
    );
  }

  if (error) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-[#FAF5F0] px-4">
        <div className="bg-white rounded-2xl border border-[#E5DDD5] p-6 max-w-md w-full text-center">
          <div className="text-lg font-extrabold text-[#4A1429] mb-2">Result Not Available</div>
          <div className="text-sm text-[#6B5D52] mb-4">{error}</div>
          <Button onClick={() => navigate('/tools/dashboard')} className="bg-[#4A1429] hover:bg-[#3A1020] text-white font-bold px-6 py-2 rounded-lg">
            Back to Dashboard
          </Button>
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-[#FAF5F0]">
      {/* Header */}
      <div className="bg-gradient-to-r from-[#4A1429] to-[#5A1839] text-white sticky top-0 z-20 shadow-xl border-b-4 border-[#D4B038]">
        <div className="max-w-6xl mx-auto px-4 py-4">
          <div className="flex items-center justify-between mb-4">
            <button
              onClick={() => navigate(-1)}
              className="flex items-center gap-2 text-white/80 hover:text-white transition-colors"
            >
              <ArrowLeft className="w-5 h-5" />
              <span className="font-medium text-sm">Back to Results</span>
            </button>
            <div className="text-center">
              <div className="text-2xl font-extrabold text-[#D4B038]">Score Details</div>
              <div className="text-xs text-white/60">AI-Powered Analysis</div>
            </div>
            <div className="flex items-center gap-2 bg-white/10 rounded-full px-4 py-2">
              <div className="text-2xl font-extrabold text-[#D4B038]">{overallScore}%</div>
              <div className="text-xs text-white/70 leading-tight">Overall<br/>Score</div>
            </div>
          </div>

          {/* Skill Tabs */}
          <div className="flex gap-2 overflow-x-auto pb-1 scrollbar-hide">
            {computedSkillTabs.map((skill) => {
              const Icon = skill.icon;
              const isActive = activeTab === skill.id;
              return (
                <button
                  key={skill.id}
                  onClick={() => setActiveTab(skill.id)}
                  className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all whitespace-nowrap flex-shrink-0 ${
                    isActive
                      ? 'bg-[#D4B038] text-[#4A1429] shadow-lg font-bold'
                      : 'bg-white/10 text-white/80 hover:bg-white/20'
                  }`}
                >
                  <Icon className="w-4 h-4" />
                  <span className="text-sm font-semibold">{skill.label}</span>
                  <span className={`text-xs px-2 py-0.5 rounded-full font-bold ${isActive ? 'bg-[#4A1429] text-[#D4B038]' : 'bg-white/20 text-white'}`}>
                    {skill.score}%
                  </span>
                </button>
              );
            })}
          </div>
        </div>
      </div>

      {/* Content */}
      <div className="max-w-6xl mx-auto px-4 py-6">
        <AnimatePresence mode="wait">
          <motion.div
            key={activeTab}
            initial={{ opacity: 0, y: 16 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -16 }}
            transition={{ duration: 0.25 }}
          >
            {activeTab === 'pronunciation' && <PronunciationDetails skill={activeSkill} guides={pronunciationGuides} />}
            {activeTab === 'intonation' && (
              <IntonationDetails
                skill={activeSkill}
                transcriptRows={intonationTranscriptRows}
                intonation={intonationData}
              />
            )}
            {activeTab === 'fluency' && (
              <FluencyDetails
                skill={activeSkill}
                fluency={fluencyData}
                measuredPaceWpm={resultJson?.result?.speed}
              />
            )}
            {activeTab === 'grammar' && (
              <GrammarDetails
                skill={activeSkill}
                toggleSection={toggleSection}
                expandedSections={expandedSections}
                recommendation={grammarRecommendation}
                rangeItems={grammarRange}
                errorItems={grammarErrors}
              />
            )}
            {activeTab === 'vocabulary' && (
              <VocabularyDetails
                skill={activeSkill}
                toggleSection={toggleSection}
                expandedSections={expandedSections}
                recommendation={vocabularyRecommendation}
                repetitiveWords={repetitiveWords}
                activeVocabulary={activeVocabulary}
                vocabularyStats={resultJson?.result?.vocabulary_stats || null}
              />
            )}
          </motion.div>
        </AnimatePresence>

        {/* Transcript */}
        <TranscriptSection
          transcript={transcriptRows}
          highlightWords={highlightWords}
          audioUrl={data?.audio?.url || ''}
          totalSeconds={totalSeconds}
        />
      </div>
    </div>
  );
}

function ScoreCircle({ score, color, bgColor, label, size = 'lg' }) {
  const r = size === 'lg' ? 70 : 50;
  const cx = size === 'lg' ? 80 : 60;
  const circumference = 2 * Math.PI * r;
  const offset = circumference - (circumference * score) / 100;
  const dim = size === 'lg' ? 160 : 120;

  return (
    <div className="flex flex-col items-center">
      <div className="relative" style={{ width: dim, height: dim }}>
        <svg className="w-full h-full transform -rotate-90" viewBox={`0 0 ${dim} ${dim}`}>
          <circle cx={cx} cy={cx} r={r} fill="none" stroke={bgColor} strokeWidth="12" />
          <circle
            cx={cx} cy={cx} r={r}
            fill="none" stroke={color} strokeWidth="12"
            strokeLinecap="round"
            strokeDasharray={circumference}
            strokeDashoffset={offset}
          />
        </svg>
        <div className="absolute inset-0 flex items-center justify-center">
          <div className="text-3xl font-extrabold" style={{ color }}>{score}%</div>
        </div>
      </div>
      {label ? (
        <div className="mt-2 max-w-[7.5rem] px-2 text-center text-xs font-medium leading-tight text-[#6B5D52]">
          {label}
        </div>
      ) : null}
    </div>
  );
}

function SectionHeader({ skill }) {
  return (
    <div
      className="mb-6 flex flex-col items-center gap-4 rounded-2xl border-l-4 p-4 sm:mb-8 sm:flex-row sm:items-start sm:gap-6 sm:p-6"
      style={{ backgroundColor: skill.bgColor, borderColor: skill.color }}
    >
      <ScoreCircle score={skill.score} color={skill.color} bgColor={`${skill.bgColor}`} label={skill.level} size="sm" />
      <div className="flex-1 text-center sm:text-left">
        <div className="mb-2 flex items-center justify-center gap-2 sm:justify-start">
          <h2 className="text-xl font-extrabold text-[#4A1429] sm:text-2xl">{skill.label} Score</h2>
          <span className="px-3 py-1 rounded-full text-white text-xs font-bold" style={{ backgroundColor: skill.color }}>
            {skill.level}
          </span>
        </div>
        <p className="mb-2 font-semibold text-[#4A1429]">
          Your {skill.label.toLowerCase()} level is <strong>{skill.level}</strong>. {skill.score >= 80 ? 'Excellent work!' : 'Keep practicing to improve!'}
        </p>
        <div className="w-full h-3 bg-white rounded-full overflow-hidden shadow-inner">
          <motion.div
            initial={{ width: 0 }}
            animate={{ width: `${skill.score}%` }}
            transition={{ duration: 1, ease: 'easeOut' }}
            className="h-full rounded-full"
            style={{ backgroundColor: skill.color }}
          />
        </div>
        <div className="mt-1 text-xs text-[#6B5D52]">{skill.score}/100 points</div>
      </div>
    </div>
  );
}

function YouTubeModal({ videoId, onClose }) {
  if (!videoId) return null;

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4" onClick={onClose}>
      <div
        className="relative w-full max-w-2xl overflow-hidden rounded-2xl bg-black shadow-2xl"
        onClick={(event) => event.stopPropagation()}
      >
        <button
          onClick={onClose}
          className="absolute right-3 top-3 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-black/60 text-lg text-white hover:bg-black"
        >
          x
        </button>
        <div className="aspect-video">
          <iframe
            src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
            className="h-full w-full"
            allow="autoplay; encrypted-media"
            allowFullScreen
          />
        </div>
      </div>
    </div>
  );
}

function PronunciationDetails({ skill, guides = [] }) {
  const [youtubeVideoId, setYoutubeVideoId] = React.useState(null);
  const items = Array.isArray(guides)
    ? guides.filter((guide) => {
        const hasVideo = Boolean(guide?.video_id);
        const hasPreview = Boolean(String(guide?.preview_image || guide?.thumbnail || '').trim());
        return hasVideo && hasPreview;
      })
    : [];

  return (
    <div className="space-y-6">
      <YouTubeModal videoId={youtubeVideoId} onClose={() => setYoutubeVideoId(null)} />
      <SectionHeader skill={skill} />

      <div className="bg-white rounded-2xl shadow-md border border-[#E5DDD5] overflow-hidden">
        <div className="px-6 py-4 border-b-2 border-[#F5E6E0]" style={{ backgroundColor: skill.bgColor }}>
          <h3 className="text-lg font-extrabold text-[#4A1429]">Top Errors, Video Preview & Improvement Guides</h3>
        </div>
        {!items.length ? (
          <div className="p-6">
            <div className="rounded-xl border border-dashed border-[#E5DDD5] bg-[#FAF5F0] px-4 py-5 text-sm text-[#6B5D52]">
              No pronunciation guide matched the detected phonemes for this answer.
            </div>
          </div>
        ) : (
          <div className="grid max-h-[52rem] gap-4 overflow-y-auto p-6 pr-4 md:grid-cols-2 lg:grid-cols-3">
            {items.map((guide, i) => {
              const hasVideo = Boolean(guide?.video_id);
              const phonemeLabel = guide?.display_phoneme || `/${guide?.phoneme || ''}/`;
              const exampleWords = Array.isArray(guide?.example_words) ? guide.example_words.filter(Boolean) : [];
              const previewImage = guide?.preview_image || guide?.thumbnail || '';

              return (
                <motion.button
                  key={`${guide?.phoneme}-${guide?.video_id || 'guide'}`}
                  type="button"
                  initial={{ opacity: 0, scale: 0.95 }}
                  animate={{ opacity: 1, scale: 1 }}
                  transition={{ delay: i * 0.05 }}
                  onClick={() => {
                    if (hasVideo) setYoutubeVideoId(guide.video_id);
                  }}
                  className={`rounded-xl border-2 bg-[#FAF5F0] p-4 text-left transition-colors ${
                    hasVideo ? 'cursor-pointer border-[#E5DDD5] hover:border-[#A73121]' : 'cursor-default border-[#E5DDD5]'
                  }`}
                >
                  <div className="mb-4 overflow-hidden rounded-xl border border-[#E5DDD5] bg-white">
                    <div
                      className="relative h-32 w-full bg-[#E9DED3] bg-cover bg-center"
                      style={previewImage ? { backgroundImage: `url(${previewImage})` } : undefined}
                    >
                      {!previewImage ? (
                        <div className="flex h-full items-center justify-center bg-[#4A1429] text-2xl font-extrabold text-[#D4B038]">
                          {phonemeLabel}
                        </div>
                      ) : null}
                      <div className="absolute inset-0 flex items-center justify-center bg-black/25">
                        {hasVideo ? (
                          <div className="flex h-11 w-11 items-center justify-center rounded-full bg-black/55">
                            <Play className="ml-0.5 h-4 w-4 text-white" />
                          </div>
                        ) : (
                          <div className="rounded-full bg-white/90 px-3 py-1 text-[11px] font-bold text-[#4A1429]">
                            Guide
                          </div>
                        )}
                      </div>
                    </div>
                  </div>

                  <div className="flex items-center gap-2 mb-3">
                    <div
                      className="px-3 py-1.5 rounded-lg font-mono text-base font-extrabold text-white"
                      style={{
                        backgroundColor: skill.color,
                        fontFamily: '"Noto Sans IPA","Doulos SIL","Charis SIL","Segoe UI","Arial Unicode MS",sans-serif',
                      }}
                    >
                      {phonemeLabel}
                    </div>
                    {guide?.average_score ? (
                      <span className="rounded-full border border-[#E5DDD5] bg-white px-2 py-0.5 text-[10px] font-bold text-[#A73121]">
                        {guide.average_score}% avg
                      </span>
                    ) : null}
                  </div>

                  <div className="text-sm font-semibold text-[#DC2626] mb-2">
                    {hasVideo ? `Video preview for ${phonemeLabel}` : `Guide for ${phonemeLabel}`}
                  </div>

                  <div className="text-xs text-[#6B5D52] leading-relaxed border-t border-[#E5DDD5] pt-2">
                    <span className="font-bold text-[#4A1429]">Fix: </span>
                    {guide?.definition || 'Pronunciation guide available for this phoneme.'}
                  </div>

                  {exampleWords.length ? (
                    <div className="mt-3 text-[11px] font-medium text-[#8B7967]">
                      Heard in: {exampleWords.join(', ')}
                    </div>
                  ) : null}
                </motion.button>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

function IntonationWord({ word }) {
  const [tooltipOpen, setTooltipOpen] = React.useState(false);
  const type = word?.type || 'normal';
  const isOver = type === 'over';
  const isUnder = type === 'under';
  const isCorrect = type === 'correct';
  const isInteractive = isOver || isUnder;

  const style = isOver
    ? { color: '#DC2626', fontSize: '1.2em', fontWeight: 700, cursor: 'pointer' }
    : isUnder
      ? { color: '#DC2626', fontSize: '0.8em', fontWeight: 600, cursor: 'pointer' }
      : isCorrect
        ? { color: '#111827', fontSize: '1.15em', fontWeight: 700 }
        : { color: '#4A1429', fontSize: '1em' };

  return (
    <span className="relative inline-block mr-1">
      <span
        style={style}
        onClick={() => {
          if (isInteractive) setTooltipOpen((current) => !current);
        }}
        onMouseLeave={() => setTooltipOpen(false)}
      >
        {word?.text}
      </span>
      <AnimatePresence>
        {tooltipOpen && word?.tooltip ? (
          <motion.div
            initial={{ opacity: 0, y: -4 }}
            animate={{ opacity: 1, y: -8 }}
            exit={{ opacity: 0, y: -4 }}
            className="absolute bottom-full left-1/2 z-30 mb-1 w-56 -translate-x-1/2 rounded-xl border border-gray-200 bg-white px-3 py-2 text-xs leading-snug text-gray-700 shadow-xl pointer-events-none"
          >
            {word.tooltip}
            <div className="absolute top-full left-1/2 -mt-1 h-2 w-2 -translate-x-1/2 rotate-45 border-b border-r border-gray-200 bg-white" />
          </motion.div>
        ) : null}
      </AnimatePresence>
    </span>
  );
}

function IntonationDetails({ skill, transcriptRows = [], intonation = null }) {
  const [youtubeVideoId, setYoutubeVideoId] = React.useState(null);
  const analysisStatus = String(intonation?.analysis_status || 'ok').trim().toLowerCase();
  const analysisError = normalizeText(intonation?.analysis_error);
  const hasPitchMetric = Number.isFinite(Number(intonation?.pitch_variation));
  const score = clampScore(hasPitchMetric ? (intonation?.score ?? skill?.score ?? 0) : 0);
  const proficiencyLabel = analysisStatus === 'unavailable'
    ? 'Unavailable'
    : String(skill?.level || scoreToLevelLabel(score)).trim() || scoreToLevelLabel(score);
  const pitchVariation = hasPitchMetric ? safeNumber(intonation?.pitch_variation, 0) : null;
  const pitchState = hasPitchMetric
    ? normalizePitchState(intonation?.pitch_state || intonation?.level || skill?.level)
    : 'Unavailable';
  const chart = hasPitchMetric ? getResolvedPitchChart(intonation) : null;
  const pitchBars = useMemo(
    () => (hasPitchMetric ? buildPitchSparkBars(chart, pitchVariation) : []),
    [chart, hasPitchMetric, pitchVariation]
  );
  const suggestionText = hasPitchMetric
    ? buildIntonationRecommendation(pitchState, pitchVariation)
    : analysisError || 'We could not extract enough voiced frames from this recording to build detailed intonation metrics.';

  return (
    <div className="space-y-6">
      {youtubeVideoId && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4" onClick={() => setYoutubeVideoId(null)}>
          <div className="relative w-full max-w-2xl bg-black rounded-2xl overflow-hidden shadow-2xl" onClick={e => e.stopPropagation()}>
            <button onClick={() => setYoutubeVideoId(null)} className="absolute top-3 right-3 z-10 w-8 h-8 bg-black/60 hover:bg-black rounded-full flex items-center justify-center text-white text-lg">x</button>
            <div className="aspect-video"><iframe src={`https://www.youtube.com/embed/${youtubeVideoId}?autoplay=1`} className="w-full h-full" allow="autoplay; encrypted-media" allowFullScreen /></div>
          </div>
        </div>
      )}
      <SectionHeader skill={skill} />

      <div className="bg-white rounded-2xl shadow-md border border-[#E5DDD5] overflow-hidden">
        <div className="px-6 py-3 border-b border-[#E5DDD5]">
          <h3 className="text-base font-extrabold text-[#4A1429]">Intonation Highlights</h3>
        </div>
        <div className="p-6">
          <div className="flex flex-col md:flex-row gap-4">
            <motion.div
              initial={{ opacity: 0, y: 16 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{ duration: 0.35 }}
              className="flex flex-col items-center justify-center border-2 rounded-xl p-6 bg-[#FFFBEB] md:w-48 flex-shrink-0"
              style={{ borderColor: skill.color }}
            >
              <div className="w-20 h-20 rounded-xl flex items-center justify-center mb-3 shadow-md" style={{ backgroundColor: skill.color }}>
                <span className="text-2xl font-extrabold text-white">{score}%</span>
              </div>
              <div className="text-sm font-bold text-[#6B5D52]">Your Level</div>
              <div className="mt-1 max-w-[10rem] text-center text-base font-extrabold leading-tight" style={{ color: skill.color }}>
                {proficiencyLabel}
              </div>
            </motion.div>

            <div className="flex-1 flex flex-col md:flex-row gap-4">
              <motion.div
                initial={{ opacity: 0, y: 16 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ duration: 0.35, delay: 0.08 }}
                className="flex-1 flex flex-col items-center justify-center bg-[#FAF5F0] rounded-xl border border-[#E5DDD5] p-4 text-center min-h-[230px]"
              >
                <div className="flex items-center gap-1.5 mb-2">
                  <span className="text-sm font-extrabold text-[#4A1429]">Average Pitch Variation</span>
                  <Info className="w-3.5 h-3.5 text-[#6B5D52]" title="Pitch variation measures how much your voice rises and falls across the recording. Aim for a natural rise-and-fall pattern inside the target band." />
                </div>
                {hasPitchMetric ? (
                  <>
                    <div className="text-3xl font-extrabold mb-2" style={{ color: skill.color }}>
                      <AnimatedNumber target={Math.round(pitchVariation)} suffix=" Hz" />
                    </div>
                    <div className="flex items-end gap-0.5 mb-2 h-10">
                      {pitchBars.map((height, index) => (
                        <motion.div
                          key={`${height}-${index}`}
                          initial={{ height: 0, opacity: 0 }}
                          animate={{ height: `${height}px`, opacity: 0.82 }}
                          transition={{ duration: 0.45, ease: 'easeOut', delay: 0.16 + index * 0.03 }}
                          className="w-1.5 rounded-sm"
                          style={{ backgroundColor: skill.color }}
                        />
                      ))}
                    </div>
                    <div className="text-sm font-extrabold" style={{ color: skill.color }} title={pitchState}>
                      {pitchState}
                    </div>
                  </>
                ) : (
                  <div className="rounded-xl border border-dashed border-[#E5DDD5] bg-white px-4 py-5 text-sm leading-relaxed text-[#6B5D52]">
                    {analysisError || 'Pitch data is not available for this recording.'}
                  </div>
                )}
              </motion.div>

              <motion.div
                initial={{ opacity: 0, y: 16 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ duration: 0.35, delay: 0.16 }}
                className="flex-1 bg-[#FAF5F0] rounded-xl border border-[#E5DDD5] p-4"
              >
                <h4 className="text-sm font-extrabold text-[#4A1429] mb-1">Suggestions for Improvement</h4>
                <p className="text-xs text-[#6B5D52] mb-3 leading-relaxed">
                  {suggestionText}
                </p>
                <div
                  onClick={() => setYoutubeVideoId(INTONATION_TUTORIAL_VIDEO_ID)}
                  className="flex items-center gap-3 bg-white rounded-xl border border-[#E5DDD5] p-2 cursor-pointer hover:bg-[#F5EFE7] transition-colors"
                >
                  <div className="w-12 h-9 rounded-lg overflow-hidden flex-shrink-0 bg-gray-200 relative">
                    <img src={INTONATION_TUTORIAL_THUMBNAIL} alt="Intonation tutorial preview" className="w-full h-full object-cover" />
                    <div className="absolute inset-0 flex items-center justify-center bg-black/30"><Play className="w-3 h-3 text-white" /></div>
                  </div>
                  <span className="text-xs font-semibold text-[#4A1429]">Video tutorial Intonation</span>
                </div>
              </motion.div>
            </div>
          </div>
        </div>
      </div>

      <div className="bg-white rounded-2xl shadow-md border border-[#E5DDD5] overflow-hidden">
        <div className="border-b border-[#E5DDD5] bg-gradient-to-r from-[#FFF9EF] via-white to-[#FAF5F0] px-6 py-5">
          <div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
            <div>
              <div className="flex items-center gap-2">
                <h3 className="text-[28px] font-extrabold text-[#4A1429]">Pitch Variation</h3>
                <Info className="h-4 w-4 text-[#6B5D52]" title="This contour is computed from the recorded audio with Parselmouth and shows how your pitch moves over time." />
              </div>
              <p className="mt-2 max-w-3xl text-sm text-[#6B5D52]">
                Keep your pitch variation within the target range shown in green below.
              </p>
            </div>
            <div
              className="inline-flex items-center rounded-full border px-3 py-1.5 text-xs font-bold"
              style={{ color: '#B78714', borderColor: '#EED59A', backgroundColor: '#FFF8E7' }}
            >
              Parselmouth analysis
            </div>
          </div>
        </div>
        <div className="p-6">
          <PitchVariationChart chart={chart} errorMessage={analysisError} />
        </div>
      </div>

      <div className="bg-white rounded-2xl shadow-md border border-[#E5DDD5] overflow-hidden">
        <div className="px-6 py-4 border-b-2" style={{ backgroundColor: skill.bgColor, borderColor: `${skill.color}40` }}>
          <h3 className="text-lg font-extrabold text-[#4A1429]">📝 Transcript</h3>
          <div className="mt-2 flex flex-wrap gap-4 text-xs">
            <span className="flex items-center gap-1.5">
              <span className="text-base font-bold text-[#DC2626]">BIG</span>
              <span className="text-[#6B5D52]">= Over-emphasised (say softer)</span>
            </span>
            <span className="flex items-center gap-1.5">
              <span className="text-[10px] font-bold text-[#DC2626]">small</span>
              <span className="text-[#6B5D52]">= Under-emphasised (say louder)</span>
            </span>
            <span className="flex items-center gap-1.5">
              <span className="text-base font-bold text-[#111827]">Bold</span>
              <span className="text-[#6B5D52]">= Correctly emphasised</span>
            </span>
          </div>
        </div>
        <div className="p-6 space-y-5">
          {transcriptRows.map((entry, i) => (
            <div key={i}>
              <div className="flex items-center gap-1.5 mb-1.5 text-xs text-[#6B5D52] font-mono">
                <span>Time</span> {entry.time}
              </div>
              <p className="leading-loose">
                {(Array.isArray(entry?.words) ? entry.words : []).map((word, index) => (
                  <IntonationWord key={`${entry.time}-${index}-${word?.text || 'word'}`} word={word} />
                ))}
              </p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function getFluencyStateTone(state, kind = 'neutral') {
  const normalized = String(state || '').trim().toLowerCase();
  if (normalized === 'natural') {
    return {
      textColor: '#16A34A',
      bgColor: '#ECFDF5',
      borderColor: '#BBF7D0',
      progressColor: '#16A34A',
    };
  }
  if (normalized === 'slow' || normalized === 'needs tuning' || normalized === 'noticeable') {
    return {
      textColor: kind === 'pace' ? '#2563EB' : '#F59E0B',
      bgColor: kind === 'pace' ? '#EFF6FF' : '#FFFBEB',
      borderColor: kind === 'pace' ? '#BFDBFE' : '#FDE68A',
      progressColor: kind === 'pace' ? '#2563EB' : '#F59E0B',
    };
  }
  return {
    textColor: '#DC2626',
    bgColor: '#FEF2F2',
    borderColor: '#FECACA',
    progressColor: '#DC2626',
  };
}

function buildFluencyTips(fluency) {
  const source = Array.isArray(fluency?.tips) ? fluency.tips.map((item) => String(item || '').trim()).filter(Boolean) : [];
  if (source.length) return source;

  const paceState = String(fluency?.pace_state || '').trim();
  const pauseState = String(fluency?.pause_state || '').trim();
  const hesitationState = String(fluency?.hesitation_state || '').trim();
  const tips = [];

  if (paceState === 'Slow') {
    tips.push('Raise your energy slightly and complete short ideas without stretching them out.');
  } else if (paceState === 'Fast') {
    tips.push('Slow down around key points so the listener can follow your meaning more easily.');
  } else {
    tips.push('Your speaking pace is close to natural. Keep that steady rhythm.');
  }

  if (pauseState === 'Natural') {
    tips.push('Your pauses are mostly well placed. Keep pausing after complete ideas.');
  } else {
    tips.push('Reduce long pauses by planning the next phrase before you stop speaking.');
  }

  if (hesitationState === 'Frequent' || hesitationState === 'Noticeable') {
    tips.push("Replace filler sounds with a short silent pause and restart the sentence clearly.");
  } else {
    tips.push('You are controlling hesitations well. Keep linking ideas with clear transitions.');
  }

  return tips;
}

function FluencyMetricCard({ label, value, status, score, color }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      className="rounded-2xl border border-[#E5DDD5] bg-white p-6 text-center shadow-md"
    >
      <div className="mb-3 text-sm font-bold text-[#6B5D52]">{label}</div>
      <div className="mb-1 text-3xl font-extrabold text-[#4A1429]">{value}</div>
      <div className="mb-4 text-sm font-bold" style={{ color }}>{status}</div>
      <div className="h-3 overflow-hidden rounded-full bg-gray-100">
        <motion.div
          initial={{ width: 0 }}
          animate={{ width: `${clampScore(score)}%` }}
          transition={{ duration: 1, ease: 'easeOut' }}
          className="h-full rounded-full"
          style={{ backgroundColor: color }}
        />
      </div>
    </motion.div>
  );
}

function hasUsablePacePoints(points) {
  return Array.isArray(points) && points.some((point) => Number.isFinite(point?.wpm) && point.wpm > 0);
}

function buildPacePointsFromWords(words, durationSeconds, limit = 18) {
  const timeline = (Array.isArray(words) ? words : [])
    .map((word) => ({
      start_sec: safeNumber(word?.start_sec, NaN),
      end_sec: safeNumber(word?.end_sec, NaN),
    }))
    .filter((word) => Number.isFinite(word.start_sec) && Number.isFinite(word.end_sec) && word.end_sec >= word.start_sec)
    .sort((a, b) => a.start_sec - b.start_sec);

  if (timeline.length < 6) return [];

  const duration = Math.max(
    safeNumber(durationSeconds, 0),
    safeNumber(timeline[timeline.length - 1]?.end_sec, 0),
    1
  );
  const windowSeconds = duration >= 60 ? 10 : 5;
  const bucketCount = Math.max(1, Math.ceil(duration / windowSeconds));
  const points = Array.from({ length: bucketCount }, (_, bucket) => {
    const start = bucket * windowSeconds;
    const end = Math.min(duration, (bucket + 1) * windowSeconds);
    const wordsInWindow = timeline.filter((word) => {
      const midpoint = (word.start_sec + word.end_sec) / 2;
      return midpoint >= start && midpoint < end;
    }).length;
    return {
      time_sec: Number((start + ((end - start) / 2)).toFixed(2)),
      wpm: Number(((wordsInWindow * 60) / Math.max(0.001, end - start)).toFixed(2)),
    };
  });

  return reducePitchPoints(points, limit);
}

function PaceOverviewChart({ chart, words = [], averageWpm = null }) {
  const chartId = React.useId().replace(/:/g, '');
  const basePoints = reducePitchPoints(
    (chart?.derived_from_overall_speed ? [] : (Array.isArray(chart?.points) ? chart.points : []))
      .map((point) => ({
        time_sec: safeNumber(point?.time_sec, NaN),
        wpm: safeNumber(point?.wpm, NaN),
      }))
      .filter((point) => Number.isFinite(point.time_sec) && Number.isFinite(point.wpm))
      .sort((a, b) => a.time_sec - b.time_sec),
    18
  );

  const fallbackDuration = Math.max(
    safeNumber(chart?.duration_seconds, 0),
    basePoints.length ? safeNumber(basePoints[basePoints.length - 1]?.time_sec, 0) : 0,
    1
  );

  let points = basePoints;
  if (!hasUsablePacePoints(points)) {
    points = buildPacePointsFromWords(words, fallbackDuration, 18);
  }

  if (!hasUsablePacePoints(points)) {
    const overallPace = safeNumber(averageWpm, 0);
    return (
      <div className="rounded-2xl border border-dashed border-[#DCCFC3] bg-[#FAF5F0] px-6 py-10 text-center text-sm text-[#6B5D52]">
        Pace variation is not available because this recording has no word-level timing data.
        {overallPace > 0 ? ` Overall pace: ${Math.round(overallPace)} WPM.` : ''}
      </div>
    );
  }

  const duration = Math.max(
    safeNumber(chart?.duration_seconds, 0),
    safeNumber(points[points.length - 1]?.time_sec, 0),
    1
  );
  const yMin = safeNumber(chart?.y_min_wpm, 0);
  const observedPaceMax = points.reduce((max, point) => Math.max(max, safeNumber(point?.wpm, 0)), 0);
  const yMax = Math.max(safeNumber(chart?.y_max_wpm, 250), Math.ceil((observedPaceMax || 0) * 1.2), 220);
  const targetMin = safeNumber(chart?.target_min_wpm, 120);
  const targetMax = safeNumber(chart?.target_max_wpm, 180);
  const width = 980;
  const height = 340;
  const margin = { top: 22, right: 108, bottom: 58, left: 64 };
  const plotWidth = width - margin.left - margin.right;
  const plotHeight = height - margin.top - margin.bottom;

  const scaleX = (value) => margin.left + (Math.max(0, value) / duration) * plotWidth;
  const scaleY = (value) => margin.top + plotHeight - ((value - yMin) / Math.max(1, yMax - yMin)) * plotHeight;
  const yTicks = [];
  for (let tick = yMax; tick >= yMin; tick -= 50) {
    yTicks.push(tick);
  }
  const xTicks = buildTimeTicks(duration);
  const linePath = points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${scaleX(point.time_sec)} ${scaleY(point.wpm)}`).join(' ');
  const linePoints = points.map((point) => `${scaleX(point.time_sec)},${scaleY(point.wpm)}`).join(' ');
  const gradientId = `${chartId}-pace-line-gradient`;
  const glowId = `${chartId}-pace-glow`;

  const rightLabelX = width - 16;

  return (
    <div
      className="rounded-[28px] p-4"
      style={{ backgroundColor: chartUi.panelBg }}
    >
      <div className="overflow-x-auto">
        <svg viewBox={`0 0 ${width} ${height}`} className="min-w-[880px] w-full h-auto">
          <defs>
            <linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" stopColor="#1D8BF1" />
              <stop offset="100%" stopColor="#2196F3" />
            </linearGradient>
            <filter id={glowId} x="-20%" y="-20%" width="140%" height="140%">
              <feDropShadow dx="0" dy="10" stdDeviation="10" floodColor="#1D8BF1" floodOpacity="0.16" />
            </filter>
          </defs>

          <rect
            x={margin.left}
            y={scaleY(targetMax)}
            width={plotWidth}
            height={Math.max(0, scaleY(targetMin) - scaleY(targetMax))}
            rx="18"
            fill={chartUi.targetBand}
            fillOpacity="0.95"
          />

          {yTicks.map((tick) => (
            <g key={tick}>
              {tick !== 0 ? (
                <line
                  x1={margin.left}
                  y1={scaleY(tick)}
                  x2={margin.left + plotWidth}
                  y2={scaleY(tick)}
                  stroke={chartUi.gridMajor}
                  strokeDasharray="6 8"
                />
              ) : null}
              <text
                x={margin.left - 18}
                y={scaleY(tick) + 5}
                textAnchor="end"
                className="text-[14px] font-semibold"
                fill={chartUi.axisText}
              >
                {tick}
              </text>
            </g>
          ))}

          {xTicks.map((tick) => (
            <g key={tick}>
              <line
                x1={scaleX(tick)}
                y1={margin.top}
                x2={scaleX(tick)}
                y2={margin.top + plotHeight}
                stroke={chartUi.gridMinor}
                strokeDasharray="4 10"
              />
              <text
                x={scaleX(tick)}
                y={height - 14}
                textAnchor={tick === 0 ? 'start' : tick === xTicks[xTicks.length - 1] ? 'end' : 'middle'}
                className="text-[14px] font-semibold"
                fill={chartUi.axisText}
              >
                {formatChartTimestamp(tick)}
              </text>
            </g>
          ))}

          <polyline
            points={linePoints}
            fill="none"
            stroke="#1D8BF1"
            strokeOpacity="0.18"
            strokeWidth="8"
            strokeLinecap="round"
            strokeLinejoin="round"
          />
          <path
            d={linePath}
            fill="none"
            stroke={`url(#${gradientId})`}
            strokeWidth="4.5"
            strokeLinecap="round"
            strokeLinejoin="round"
            filter={`url(#${glowId})`}
          />

          {points.map((point, index) => (
            <circle
              key={`${point.time_sec}-${point.wpm}-${index}`}
              cx={scaleX(point.time_sec)}
              cy={scaleY(point.wpm)}
              r="7.5"
              fill="#1D8BF1"
              stroke="#FFFFFF"
              strokeWidth="3"
            />
          ))}

          <text x={margin.left - 54} y={margin.top + 18} className="text-[14px] font-bold" fill={chartUi.axisText}>
            Wpm
          </text>
          <text x={rightLabelX} y={scaleY(yMax - 12)} textAnchor="end" className="text-[14px] font-bold" fill={chartUi.danger}>
            Fast
          </text>
          <text x={rightLabelX} y={scaleY((targetMin + targetMax) / 2) + 5} textAnchor="end" className="text-[14px] font-bold" fill={chartUi.success}>
            Natural
          </text>
          <text x={rightLabelX} y={scaleY(40)} textAnchor="end" className="text-[14px] font-bold" fill={chartUi.danger}>
            Slow
          </text>
        </svg>
      </div>
    </div>
  );
}

function PauseOverviewChart({ chart }) {
  const events = (Array.isArray(chart?.events) ? chart.events : [])
    .map((event) => ({
      time_sec: safeNumber(event?.time_sec, NaN),
      duration_sec: safeNumber(event?.duration_sec, NaN),
      type: String(event?.type || '').trim().toLowerCase(),
    }))
    .filter((event) => Number.isFinite(event.time_sec) && Number.isFinite(event.duration_sec))
    .sort((a, b) => a.time_sec - b.time_sec);

  if (!events.length) {
    return (
      <div className="rounded-2xl border border-dashed border-[#DCCFC3] bg-[#FAF5F0] px-6 py-10 text-center text-sm text-[#6B5D52]">
        No measurable pause segments were detected in this recording.
      </div>
    );
  }

  const duration = Math.max(
    safeNumber(chart?.duration_seconds, 0),
    safeNumber(events[events.length - 1]?.time_sec, 0),
    1
  );
  const yMin = safeNumber(chart?.y_min_seconds, 0);
  const yMax = Math.max(safeNumber(chart?.y_max_seconds, 4), 2);
  const tickStep = yMax <= 2.5 ? 0.5 : 1;
  const yTicks = [];
  for (let tick = yMax; tick >= yMin; tick -= tickStep) {
    yTicks.push(Number(tick.toFixed(1)));
  }

  const width = 980;
  const height = 340;
  const margin = { top: 22, right: 42, bottom: 58, left: 64 };
  const plotWidth = width - margin.left - margin.right;
  const plotHeight = height - margin.top - margin.bottom;
  const barWidth = Math.max(10, Math.min(18, plotWidth / Math.max(events.length * 1.8, 24)));

  const scaleX = (value) => margin.left + (Math.max(0, value) / duration) * plotWidth;
  const scaleY = (value) => margin.top + plotHeight - ((value - yMin) / Math.max(0.5, yMax - yMin)) * plotHeight;
  const xTicks = buildTimeTicks(duration);
  const barColor = (type) => {
    if (type === 'good') return '#16A34A';
    if (type === 'optional') return '#7C83B4';
    return '#A73121';
  };

  return (
    <div
      className="rounded-[28px] p-4"
      style={{ backgroundColor: chartUi.panelBg }}
    >
      <div className="mb-4 flex flex-wrap items-center justify-between gap-3 px-2">
        <div className="text-sm text-[#6B5D52]">Keep an eye out for bad pauses and missed pauses. Red bars need the most attention.</div>
        <div
          className="flex flex-wrap items-center gap-4 rounded-2xl border px-4 py-2 text-sm shadow-sm"
          style={{ borderColor: chartUi.panelBorder, backgroundColor: '#FFFDF9', color: chartUi.body }}
        >
          {[
            { label: 'Good Pauses', color: '#16A34A' },
            { label: 'Optional Pauses', color: '#7C83B4' },
            { label: 'Bad Pauses', color: '#A73121' },
          ].map((item) => (
            <span key={item.label} className="flex items-center gap-2 font-semibold text-[#4A1429]">
              <span className="h-4 w-4 rounded-full" style={{ backgroundColor: item.color }} />
              {item.label}
            </span>
          ))}
        </div>
      </div>
      <div className="overflow-x-auto">
        <svg viewBox={`0 0 ${width} ${height}`} className="min-w-[880px] w-full h-auto">
          {yTicks.map((tick) => (
            <g key={tick}>
              {tick !== 0 ? (
                <line
                  x1={margin.left}
                  y1={scaleY(tick)}
                  x2={margin.left + plotWidth}
                  y2={scaleY(tick)}
                  stroke={chartUi.gridMajor}
                  strokeDasharray="6 8"
                />
              ) : null}
              <text
                x={margin.left - 18}
                y={scaleY(tick) + 5}
                textAnchor="end"
                className="text-[14px] font-semibold"
                fill={chartUi.axisText}
              >
                {tick}
              </text>
            </g>
          ))}

          {xTicks.map((tick) => (
            <g key={tick}>
              <line
                x1={scaleX(tick)}
                y1={margin.top}
                x2={scaleX(tick)}
                y2={margin.top + plotHeight}
                stroke={chartUi.gridMinor}
                strokeDasharray="4 10"
              />
              <text
                x={scaleX(tick)}
                y={height - 14}
                textAnchor={tick === 0 ? 'start' : tick === xTicks[xTicks.length - 1] ? 'end' : 'middle'}
                className="text-[14px] font-semibold"
                fill={chartUi.axisText}
              >
                {formatChartTimestamp(tick)}
              </text>
            </g>
          ))}

          {events.map((event, index) => {
            const x = scaleX(event.time_sec) - (barWidth / 2);
            const y = scaleY(event.duration_sec);
            const heightValue = Math.max(6, margin.top + plotHeight - y);
            return (
              <rect
                key={`${event.time_sec}-${event.type}-${index}`}
                x={x}
                y={y}
                width={barWidth}
                height={heightValue}
                rx="4"
                fill={barColor(event.type)}
              />
            );
          })}

          <text x={margin.left - 54} y={margin.top + 16} className="text-[14px] font-bold" fill={chartUi.axisText}>
            Seconds
          </text>
        </svg>
      </div>
    </div>
  );
}

function FluencyChartSection({ title, description, badge, children }) {
  return (
    <div className="bg-white rounded-2xl shadow-md border border-[#E5DDD5] overflow-hidden">
      <div className="border-b border-[#E5DDD5] bg-gradient-to-r from-[#FFF9EF] via-white to-[#FAF5F0] px-6 py-5">
        <div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
          <div>
            <h3 className="text-[28px] font-extrabold text-[#4A1429]">{title}</h3>
            <p className="mt-2 max-w-3xl text-sm text-[#6B5D52]">{description}</p>
          </div>
          {badge ? (
            <div
              className="inline-flex items-center rounded-full border px-3 py-1.5 text-xs font-bold"
              style={{ color: '#B78714', borderColor: '#EED59A', backgroundColor: '#FFF8E7' }}
            >
              {badge}
            </div>
          ) : null}
        </div>
      </div>
      <div className="p-6">
        {children}
      </div>
    </div>
  );
}

function FluencyDetails({ skill, fluency = null, measuredPaceWpm = null }) {
  const [youtubeVideoId, setYoutubeVideoId] = React.useState(null);
  const analysisError = normalizeText(fluency?.analysis_error || fluency?.recognition_error);
  const directPaceWpm = safeNumber(fluency?.words_per_minute ?? fluency?.articulation_rate_wpm, 0);
  const paceWpm = directPaceWpm > 0 ? directPaceWpm : safeNumber(measuredPaceWpm, 0);
  const available = Boolean(fluency?.available || paceWpm > 0);
  const measuredPaceScore = paceWpm >= 120 && paceWpm <= 180
    ? Math.max(82, Math.min(100, 100 - (Math.abs(paceWpm - 150) * 0.5)))
    : Math.max(15, Math.min(81, 86 - (Math.min(75, Math.abs(paceWpm < 120 ? 120 - paceWpm : paceWpm - 180) * 0.7))));
  const paceScore = directPaceWpm > 0
    ? safeNumber(fluency?.pace_score, measuredPaceScore)
    : Math.round(measuredPaceScore);
  const pauseScore = safeNumber(fluency?.pause_score, 0);
  const hesitationScore = safeNumber(fluency?.hesitation_score, 0);
  const derivedPaceState = paceWpm < 120 ? 'Slow' : paceWpm > 180 ? 'Fast' : 'Natural';
  const paceState = String(directPaceWpm > 0 ? (fluency?.pace_state || derivedPaceState) : derivedPaceState);
  const pauseState = String(fluency?.pause_state || 'Natural');
  const hesitationState = String(fluency?.hesitation_state || 'Natural');
  const paceTone = getFluencyStateTone(paceState, 'pace');
  const pauseTone = getFluencyStateTone(pauseState, 'pause');
  const hesitationTone = getFluencyStateTone(hesitationState, 'hesitation');
  const tips = buildFluencyTips(fluency);
  const hesitationEvents = Array.isArray(fluency?.hesitation_events) ? fluency.hesitation_events : [];
  const fluencyCards = [
    {
      label: 'Pace',
      value: `${Math.round(paceWpm)} wpm`,
      status: paceState === 'Natural' ? 'Good' : paceState,
      score: paceScore,
      color: paceTone.progressColor,
    },
    {
      label: 'Pausing Score',
      value: `${Math.round(pauseScore)}%`,
      status: scoreToLevelLabel(pauseScore),
      score: pauseScore,
      color: pauseTone.progressColor,
    },
    {
      label: 'Hesitations',
      value: hesitationState,
      status: scoreToLevelLabel(hesitationScore),
      score: hesitationScore,
      color: hesitationTone.progressColor,
    },
  ];
  const hesitationHeadline =
    hesitationState === 'Natural'
      ? 'You nailed it! Hesitations were natural.'
      : `Hesitations were ${hesitationState.toLowerCase()} in this recording.`;
  const hesitationBody =
    hesitationState === 'Natural'
      ? 'Brief pauses indicate thoughtful consideration. Your hesitation frequency and duration stayed within the natural range for confident speakers.'
      : `We detected ${safeNumber(fluency?.hesitation_count, 0)} hesitation events, including ${safeNumber(fluency?.filler_hesitation_count, 0)} filler hesitations. Focus on shorter silent pauses and clearer phrase planning.`;

  return (
    <div className="space-y-6">
      <YouTubeModal videoId={youtubeVideoId} onClose={() => setYoutubeVideoId(null)} />
      <SectionHeader skill={skill} />

      {!available ? (
        <div className="rounded-2xl border border-[#E5DDD5] bg-white p-6 shadow-md">
          <div className="text-lg font-extrabold text-[#4A1429] mb-2">Detailed fluency analysis is not available</div>
          <p className="text-sm text-[#6B5D52]">
            We could not build the VAD and word-timestamp timeline for this recording. If the source audio is available, reopening this result will try the analysis again.
          </p>
          {analysisError ? (
            <div className="mt-4 rounded-xl border border-[#FCA5A5] bg-[#FEF2F2] px-4 py-3 text-sm text-[#7F1D1D]">
              Analyzer detail: {analysisError}
            </div>
          ) : null}
        </div>
      ) : (
        <>
          <div className="grid gap-4 md:grid-cols-3">
            {fluencyCards.map((card) => (
              <FluencyMetricCard key={card.label} {...card} />
            ))}
          </div>

          <div className="rounded-2xl border border-[#E5DDD5] bg-white p-6 shadow-md">
            <h3 className="text-lg font-extrabold text-[#4A1429] mb-4">Hesitation Analysis</h3>
            <div
              className="rounded-xl border-2 p-5"
              style={{
                backgroundColor: hesitationState === 'Natural' ? '#ECFDF5' : hesitationTone.bgColor,
                borderColor: hesitationState === 'Natural' ? '#86EFAC' : hesitationTone.borderColor,
              }}
            >
              <div className="flex items-start gap-4">
                <div className="text-4xl">{hesitationState === 'Natural' ? '😀' : hesitationState === 'Noticeable' ? '🙂' : '⚠️'}</div>
                <div>
                  <div className="mb-1 font-extrabold text-[#4A1429]">{hesitationHeadline}</div>
                  <p className="text-sm leading-relaxed text-[#6B5D52]">{hesitationBody}</p>
                </div>
              </div>
            </div>

            <div className="mt-6 grid gap-4 text-sm md:grid-cols-2">
              {tips.map((tip, index) => (
                <div key={`${tip}-${index}`} className="flex items-start gap-3 rounded-lg border border-[#E5DDD5] bg-[#FAF5F0] p-3">
                  <div className="mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-[#2563EB] text-xs font-bold text-white">
                    {index + 1}
                  </div>
                  <p className="leading-relaxed text-[#6B5D52]">{tip}</p>
                </div>
              ))}
            </div>

            <button
              type="button"
              onClick={() => setYoutubeVideoId(FLUENCY_TUTORIAL_VIDEO_ID)}
              className="mt-5 flex w-full items-center gap-3 rounded-xl border border-[#E5DDD5] bg-[#FAF5F0] p-3 text-left transition-colors hover:bg-[#F5EFE7]"
            >
              <div className="relative h-12 w-20 overflow-hidden rounded-lg bg-gray-200">
                <img src={FLUENCY_TUTORIAL_THUMBNAIL} alt="Fluency tutorial preview" className="h-full w-full object-cover" />
                <div className="absolute inset-0 flex items-center justify-center bg-black/25">
                  <Play className="h-4 w-4 text-white" />
                </div>
              </div>
              <div>
                <div className="text-sm font-extrabold text-[#4A1429]">Video tutorial for Fluency skill</div>
                <div className="text-xs text-[#6B5D52]">Learn how to reduce rushed phrases, long pauses, and filler hesitations.</div>
              </div>
            </button>
          </div>

          <FluencyChartSection
            title="An overview of your Pace during this recording"
            description="Make sure you maintain a natural pace between 120 and 180 words per minute while you speak. This helps your audience stay connected to your ideas."
            badge="Fluency analysis"
          >
            <PaceOverviewChart
              chart={fluency?.pace_chart}
              words={fluency?.words}
              averageWpm={paceWpm}
            />
          </FluencyChartSection>

          <FluencyChartSection
            title="An overview of your Pauses during this recording"
            description="Keep an eye out for bad pauses and missed pauses. These appear on the timeline below so you can see where your delivery broke."
            badge="Pause analysis"
          >
            <PauseOverviewChart chart={fluency?.pause_chart} />
          </FluencyChartSection>

          {hesitationEvents.length ? (
            <div className="rounded-2xl border border-[#E5DDD5] bg-white p-6 shadow-md">
              <div className="mb-4 flex items-center justify-between gap-3">
                <div>
                  <h3 className="text-lg font-extrabold text-[#4A1429]">Detected Hesitation Events</h3>
                  <p className="mt-1 text-sm text-[#6B5D52]">
                    These timestamps come from merging VAD pause segments with word timing events when available.
                  </p>
                </div>
                <span
                  className="rounded-full border px-3 py-1 text-xs font-bold"
                  style={{ color: hesitationTone.textColor, borderColor: hesitationTone.borderColor, backgroundColor: hesitationTone.bgColor }}
                >
                  {safeNumber(fluency?.hesitation_count, 0)} events
                </span>
              </div>
              <div className="space-y-3">
                {hesitationEvents.slice(0, 8).map((event, index) => (
                  <div key={`${event?.time_sec}-${event?.type}-${index}`} className="flex items-start gap-3 rounded-xl border border-[#E5DDD5] bg-[#FAF5F0] p-3">
                    <div className="flex h-10 w-10 items-center justify-center rounded-full bg-[#4A1429] text-xs font-extrabold text-white">
                      {formatChartTimestamp(event?.time_sec)}
                    </div>
                    <div className="min-w-0 flex-1">
                      <div className="text-sm font-extrabold text-[#4A1429]">
                        {String(event?.type || '').replace(/_/g, ' ')}
                      </div>
                      <div className="mt-1 text-sm text-[#6B5D52]">
                        {event?.label || 'Detected hesitation event'}
                      </div>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          ) : null}
        </>
      )}
    </div>
  );
}

function CollapsibleSection({ title, badge, badgeColor, bgColor, borderColor, children, toggleSection, expandedSections, id }) {
  return (
    <div className="rounded-xl overflow-hidden border-2" style={{ borderColor: borderColor || '#E5DDD5' }}>
      <button
        onClick={() => toggleSection(id)}
        className="w-full flex items-center justify-between p-4 transition-colors"
        style={{ backgroundColor: bgColor || '#FAF5F0' }}
      >
        <div className="flex items-center gap-3">
          <span className="font-bold text-[#4A1429]">{title}</span>
          {badge && (
            <span className="px-3 py-1 text-white text-xs rounded-full font-bold" style={{ backgroundColor: badgeColor }}>
              {badge}
            </span>
          )}
        </div>
        {expandedSections[id] ? <ChevronUp className="w-5 h-5 text-[#4A1429]" /> : <ChevronDown className="w-5 h-5 text-[#4A1429]" />}
      </button>
      <AnimatePresence>
        {expandedSections[id] && (
          <motion.div
            initial={{ height: 0, opacity: 0 }}
            animate={{ height: 'auto', opacity: 1 }}
            exit={{ height: 0, opacity: 0 }}
            className="overflow-hidden"
          >
            <div className="p-5 bg-white border-t border-[#E5DDD5]">{children}</div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

function TypingText({ text, active, className = '' }) {
  const content = String(text || '').trim();
  const [visibleCount, setVisibleCount] = React.useState(0);

  React.useEffect(() => {
    if (!active || !content) {
      setVisibleCount(0);
      return undefined;
    }

    setVisibleCount(0);
    let timerId = null;

    const getStep = (current) => {
      if (current < 160) return 1;
      if (current < 320) return 2;
      return 3;
    };

    const getDelay = (nextCount) => {
      const previousChar = content.charAt(Math.max(0, nextCount - 1));
      if (/[.!?]/.test(previousChar)) return 260;
      if (/[,;:]/.test(previousChar)) return 180;
      if (/\s/.test(previousChar)) return 85;
      return 52;
    };

    const tick = () => {
      setVisibleCount((current) => {
        const step = getStep(current);
        const next = Math.min(content.length, current + step);
        if (next < content.length) {
          timerId = setTimeout(tick, getDelay(next));
        }
        return next;
      });
    };

    timerId = setTimeout(tick, 260);
    return () => {
      if (timerId) clearTimeout(timerId);
    };
  }, [active, content]);

  const visibleText = active ? content.slice(0, visibleCount) : '';

  return (
    <p className={className}>
      {visibleText}
      {active && visibleText.length < content.length ? (
        <span className="ml-0.5 inline-block h-4 w-0.5 animate-pulse align-middle bg-[#4A1429]" />
      ) : null}
    </p>
  );
}

function GrammarDetails({ skill, toggleSection, expandedSections, recommendation, rangeItems = [], errorItems = [] }) {
  const displayRange = rangeItems.filter(Boolean);
  const displayErrors = errorItems.filter(Boolean);

  const recommendationText = recommendation || 'We could not generate a detailed grammar coaching paragraph for this attempt yet.';
  return (
    <div className="space-y-6">
      <SectionHeader skill={skill} />
      <div className="space-y-3">
        <CollapsibleSection id="cambridge-rec" title="Cambridge Veritas Recommendation" badge="NEW" badgeColor="#1E3A8A" bgColor="#FEF2F2" borderColor="#FCA5A5" toggleSection={toggleSection} expandedSections={expandedSections}>
          <TypingText
            active={Boolean(expandedSections['cambridge-rec'])}
            text={recommendationText}
            className="text-sm leading-relaxed text-[#6B5D52]"
          />
        </CollapsibleSection>

        <CollapsibleSection id="grammar-range" title="Your Grammatical Range" badge={skill?.level || 'Grammar'} badgeColor="#DC2626" bgColor="#FFF7ED" borderColor="#FED7AA" toggleSection={toggleSection} expandedSections={expandedSections}>
          <h4 className="font-bold text-[#4A1429] mb-3">Structures You Used Well</h4>
          {displayRange.length ? (
            <div className="grid md:grid-cols-2 gap-3 text-sm">
              {displayRange.map((item, i) => {
                const type = typeof item === 'string' ? 'Structure' : (item.type || item.label || 'Structure');
                const example = typeof item === 'string' ? item : (item.example || item.sample || '');
                return (
                  <div key={i} className="bg-[#ECFDF5] rounded-lg p-3 border border-[#16a34a]/20">
                    <div className="font-bold text-[#059669]">{type}</div>
                    {example && <div className="text-[#6B5D52] italic">{example}</div>}
                  </div>
                );
              })}
            </div>
          ) : (
            <div className="rounded-xl border border-dashed border-[#E5DDD5] bg-[#FAF5F0] px-4 py-5 text-sm text-[#6B5D52]">
              No reliable transcript-matched grammar range examples were returned for this attempt.
            </div>
          )}
        </CollapsibleSection>

        <CollapsibleSection id="top-errors" title="Your Top Grammatical Errors" bgColor="#FFF5F5" borderColor="#FCA5A5" toggleSection={toggleSection} expandedSections={expandedSections}>
          {displayErrors.length ? (
            <div className="space-y-4 text-sm">
              {displayErrors.map((item, i) => {
                const errorText = typeof item === 'string' ? item : (item.error || item.title || 'Grammar issue');
                const suggestion = typeof item === 'string' ? '' : (item.suggestion || item.fix || '');
                const explanation = typeof item === 'string' ? '' : (item.explanation || item.detail || '');
                return (
                  <div key={i} className="bg-[#FFF5F5] rounded-lg p-4 border-l-4 border-[#DC2626]">
                    <div className="font-bold text-[#DC2626] mb-1">Error: {errorText}</div>
                    {suggestion && <div className="text-[#059669] font-semibold mb-1">Fix: {suggestion}</div>}
                    {explanation && <div className="text-[#6B5D52]">{explanation}</div>}
                  </div>
                );
              })}
            </div>
          ) : (
            <div className="rounded-xl border border-dashed border-[#E5DDD5] bg-[#FAF5F0] px-4 py-5 text-sm text-[#6B5D52]">
              No clear transcript-matched grammatical errors were identified for this attempt.
            </div>
          )}
        </CollapsibleSection>
      </div>
    </div>
  );
}

function clampVocabularyPercent(value) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) return 0;
  return Math.max(0, Math.min(100, Math.round(numeric)));
}

function buildVocabularyDistribution(vocabularyStats) {
  const stats = vocabularyStats && typeof vocabularyStats === 'object' ? vocabularyStats : {};
  const levels = [
    { level: 'C2', percent: clampVocabularyPercent(stats?.CEFR_C2_pct), color: '#1E40AF' },
    { level: 'C1', percent: clampVocabularyPercent(stats?.CEFR_C1_pct), color: '#3B82F6' },
    { level: 'B2', percent: clampVocabularyPercent(stats?.CEFR_B2_pct), color: '#60A5FA' },
    { level: 'B1', percent: clampVocabularyPercent(stats?.CEFR_B1_pct), color: '#93C5FD' },
    { level: 'A2', percent: clampVocabularyPercent(stats?.CEFR_A2_pct), color: '#BFDBFE' },
    { level: 'A1', percent: clampVocabularyPercent(stats?.CEFR_A1_pct), color: '#DBEAFE' },
  ];

  return {
    levels,
    advancedPercent: clampVocabularyPercent(
      levels
        .filter((item) => ['B2', 'C1', 'C2'].includes(item.level))
        .reduce((sum, item) => sum + item.percent, 0)
    ),
  };
}

function VocabularyDetails({ skill, toggleSection, expandedSections, recommendation, repetitiveWords = [], activeVocabulary = [], vocabularyStats = null }) {
  const { levels, advancedPercent } = React.useMemo(
    () => buildVocabularyDistribution(vocabularyStats),
    [vocabularyStats]
  );
  const recommendationText = recommendation || 'A rich vocabulary enables precise, versatile communication. Use a thesaurus to explore synonyms and incorporate high-impact words naturally into your speech.';
  const upgradeRows = activeVocabulary
    .map((item) => {
      const text = String(item || '').trim();
      const parts = splitAlternativeText(text);
      return {
        source: parts[0] || '',
        alternatives: parts.length > 1 ? parts.slice(1).join(' → ') : '',
      };
    })
    .filter((item) => item.source && item.alternatives);

  return (
    <div className="space-y-6">
      <SectionHeader skill={skill} />
      <div className="space-y-3">
        <CollapsibleSection id="vocab-rec" title="Cambridge Veritas Recommendation" badge="NEW" badgeColor="#1E3A8A" bgColor="#FEF2F2" borderColor="#FCA5A5" toggleSection={toggleSection} expandedSections={expandedSections}>
          <TypingText
            active={Boolean(expandedSections['vocab-rec'])}
            text={recommendationText}
            className="text-sm leading-relaxed text-[#6B5D52]"
          />
        </CollapsibleSection>

        <CollapsibleSection id="vocab-dist" title="Vocabulary Distribution (CEFR)" bgColor={skill.bgColor} borderColor={`${skill.color}40`} toggleSection={toggleSection} expandedSections={expandedSections}>
          <div className="space-y-3">
            <div className="text-right mb-2">
              <span className="text-3xl font-extrabold text-[#DC2626]">{advancedPercent}%</span>
              <span className="text-sm text-[#6B5D52] ml-2">Advanced Vocabulary Used</span>
            </div>
            {levels.map((item) => (
              <div key={item.level} className="flex items-center gap-3">
                <div className="w-10 h-10 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0" style={{ backgroundColor: item.color }}>
                  {item.level}
                </div>
                <div className="flex-1">
                  <div className="flex justify-between text-xs mb-1">
                    <span className="text-[#6B5D52]">{item.level}</span>
                    <span className="font-bold text-[#4A1429]">{item.percent}%</span>
                  </div>
                  <div className="h-2.5 bg-gray-100 rounded-full overflow-hidden">
                    <motion.div
                      initial={{ width: 0 }}
                      animate={{ width: `${item.percent}%` }}
                      transition={{ duration: 0.8 }}
                      className="h-full rounded-full"
                      style={{ backgroundColor: item.color }}
                    />
                  </div>
                </div>
              </div>
            ))}
          </div>
        </CollapsibleSection>

        <CollapsibleSection id="areas-improve" title="Words to Upgrade" bgColor="#F5F3FF" borderColor="#7C3AED40" toggleSection={toggleSection} expandedSections={expandedSections}>
          {upgradeRows.length ? (
            <div className="grid gap-3 md:grid-cols-2">
              <h4 className="font-bold text-[#4A1429]">Word From Your Transcript</h4>
              <h4 className="font-bold text-[#4A1429]">Better Alternatives</h4>
              {upgradeRows.map((item, index) => (
                <React.Fragment key={`${item.source}-${index}`}>
                  <div
                    className="px-4 py-2 bg-[#FFF5F5] rounded-lg border-l-4 border-[#DC2626] text-[#DC2626] font-bold"
                    style={{ fontFamily: CODING_FONT_STACK }}
                  >
                    {`"${item.source}"`}
                  </div>
                  <div className="rounded-lg border border-[#16a34a]/20 bg-[#ECFDF5] px-4 py-2 text-sm text-[#166534]">
                    {item.alternatives}
                  </div>
                </React.Fragment>
              ))}
            </div>
          ) : (
            <div className="rounded-xl border border-dashed border-[#E5DDD5] bg-[#FAF5F0] px-4 py-5 text-sm text-[#6B5D52]">
              No reliable transcript-matched word upgrades were returned for this attempt.
            </div>
          )}
          {repetitiveWords.length ? (
            <div className="mt-5 border-t border-[#E5DDD5] pt-4">
              <h4 className="mb-2 font-bold text-[#4A1429]">Repeated 3 or More Times</h4>
              <div className="flex flex-wrap gap-2">
                {repetitiveWords.map((word) => (
                  <span key={word} className="rounded-full bg-[#FFF5F5] px-3 py-1 text-sm font-bold text-[#DC2626]">
                    {word}
                  </span>
                ))}
              </div>
            </div>
          ) : null}
        </CollapsibleSection>
      </div>
    </div>
  );
}

function TranscriptSection({ transcript, highlightWords, audioUrl, totalSeconds }) {
  const transcriptRows = Array.isArray(transcript) ? transcript.filter((entry) => String(entry?.text || '').trim()) : [];
  const highlightSet = new Set((highlightWords || []).map((word) => String(word || '').toLowerCase()));

  const [isPlaying, setIsPlaying] = useState(false);
  const audioRef = useRef(null);
  const [audioPosition, setAudioPosition] = useState(0);
  const [audioDuration, setAudioDuration] = useState(0);

  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return undefined;

    const resolveDuration = () => {
      const directDuration = Number(audio.duration);
      if (Number.isFinite(directDuration) && directDuration > 0) {
        return directDuration;
      }

      if (audio.seekable && audio.seekable.length > 0) {
        const seekableEnd = Number(audio.seekable.end(audio.seekable.length - 1));
        if (Number.isFinite(seekableEnd) && seekableEnd > 0) {
          return seekableEnd;
        }
      }

      return safeNumber(totalSeconds, 0);
    };

    const syncPlayback = () => {
      const nextPosition = Number(audio.currentTime);
      setAudioPosition(Number.isFinite(nextPosition) ? nextPosition : 0);
      setAudioDuration(resolveDuration());
    };

    const handleEnded = () => {
      syncPlayback();
      setIsPlaying(false);
    };
    const handlePlay = () => setIsPlaying(true);
    const handlePause = () => setIsPlaying(false);

    syncPlayback();
    audio.addEventListener('timeupdate', syncPlayback);
    audio.addEventListener('loadedmetadata', syncPlayback);
    audio.addEventListener('loadeddata', syncPlayback);
    audio.addEventListener('durationchange', syncPlayback);
    audio.addEventListener('canplay', syncPlayback);
    audio.addEventListener('play', handlePlay);
    audio.addEventListener('pause', handlePause);
    audio.addEventListener('ended', handleEnded);

    return () => {
      audio.removeEventListener('timeupdate', syncPlayback);
      audio.removeEventListener('loadedmetadata', syncPlayback);
      audio.removeEventListener('loadeddata', syncPlayback);
      audio.removeEventListener('durationchange', syncPlayback);
      audio.removeEventListener('canplay', syncPlayback);
      audio.removeEventListener('play', handlePlay);
      audio.removeEventListener('pause', handlePause);
      audio.removeEventListener('ended', handleEnded);
    };
  }, [audioUrl, totalSeconds]);

  const togglePlayback = async () => {
    if (!audioRef.current) return;
    if (isPlaying) {
      audioRef.current.pause();
    } else {
      try {
        await audioRef.current.play();
      } catch {
        setIsPlaying(false);
      }
    }
  };

  const displayDuration = Number.isFinite(audioDuration) && audioDuration > 0
    ? audioDuration
    : safeNumber(totalSeconds, 0);
  const progressPercent = displayDuration > 0
    ? Math.min(100, Math.max(0, (audioPosition / displayDuration) * 100))
    : 0;

  return (
    <div className="mt-6 bg-white rounded-2xl shadow-md border border-[#E5DDD5] overflow-hidden">
      <audio ref={audioRef} onEnded={() => setIsPlaying(false)} preload="metadata" src={audioUrl || undefined} />

      <div className="flex items-center justify-between gap-3 bg-gradient-to-r from-[#4A1429] to-[#5A1839] px-4 py-3 sm:px-6 sm:py-4">
        <h3 className="text-base font-extrabold text-white sm:text-lg">📝 Transcript</h3>
        <button
          onClick={togglePlayback}
          disabled={!audioUrl}
          className="flex items-center gap-1.5 rounded-lg bg-[#D4B038] px-3 py-1.5 text-xs font-bold text-[#4A1429] transition-colors hover:bg-[#BFA030] disabled:opacity-60 sm:gap-2 sm:px-4 sm:py-2 sm:text-sm"
        >
          {isPlaying ? <Pause className="h-3.5 w-3.5 sm:h-4 sm:w-4" /> : <Play className="h-3.5 w-3.5 sm:h-4 sm:w-4" />}
          {isPlaying ? 'Pause Recording' : 'Play Recording'}
        </button>
      </div>
      <div className="space-y-4 p-4 sm:p-6">
        {transcriptRows.length ? transcriptRows.map((entry, i) => (
          <div key={i} className="flex items-start gap-3 sm:gap-4">
            <div className="flex-shrink-0 rounded-lg bg-[#D4B038] px-2 py-1 text-xs font-bold font-mono text-[#4A1429]">
              {entry.time}
            </div>
            <p className="text-[#4A1429] leading-relaxed text-sm">
              {entry.text.split(' ').map((word, j) => {
                const clean = word.toLowerCase().replace(/[,.'\"-]/g, '');
                return (
                  <span key={j} className={highlightSet.has(clean) ? 'font-bold text-[#A73121] underline decoration-dotted underline-offset-2' : ''}>
                    {word}{' '}
                  </span>
                );
              })}
            </p>
          </div>
        )) : (
          <div className="rounded-xl border border-dashed border-[#E5DDD5] bg-[#FAF5F0] px-4 py-5 text-sm text-[#6B5D52]">
            Transcript text is not available for this recording.
          </div>
        )}
      </div>
      <div className="px-6 pb-5">
        <div className="text-xs text-[#6B5D52] font-mono">
          {formatDuration(audioPosition)} / {formatDuration(displayDuration)}
        </div>
        <div className="w-full h-1.5 bg-[#FAF5F0] rounded-full border border-[#E5DDD5] mt-2">
          <div
            className="h-full bg-[#4A1429] rounded-full"
            style={{ width: `${progressPercent}%` }}
          />
        </div>
      </div>
    </div>
  );
}
