"use client";

import React, { useState, useRef, useEffect, useMemo, forwardRef, useImperativeHandle } from "react";
import { cn } from "@/lib/utils";

export interface TranscriptLine {
  speaker: "agent" | "user";
  text: string;
  offsetMs: number;
}

/**
 * Build a reliable per-line reveal schedule (in ms) for the chat stream.
 *
 * Transcript `offsetMs` values in the source data are sometimes degenerate —
 * every line pinned at 0, or a long tail all pinned at a single timestamp — so
 * trusting them verbatim makes the chat dump all at once instead of tracking
 * the audio. Once the real audio duration is known we repair the offsets into a
 * strictly non-decreasing, duration-bounded schedule:
 *   - Valid, strictly increasing offsets are used as-is.
 *   - Any flat/out-of-order run of lines is spread across the gap up to the next
 *     distinct offset (or, for a trailing run, the end of the audio), weighted
 *     by text length so longer utterances hold the screen a little longer.
 * A fully degenerate transcript (all identical offsets) becomes a single run
 * spread evenly across the whole duration. Before metadata loads (duration
 * unknown) we fall back to the raw offsets.
 */
function buildRevealSchedule(lines: TranscriptLine[], durationMs: number): number[] {
  const n = lines.length;
  if (n === 0) return [];
  // Duration not yet known: fall back to raw offsets.
  if (!(durationMs > 0)) return lines.map((l) => l.offsetMs);

  const clamp = (v: number) => Math.max(0, Math.min(durationMs, Number.isFinite(v) ? v : 0));
  const raw = lines.map((l) => clamp(l.offsetMs));
  const schedule = new Array<number>(n);

  let i = 0;
  while (i < n) {
    // Grow a run over any following lines that are not strictly greater than
    // the run's start — i.e. equal (plateau) or out-of-order offsets.
    let j = i;
    while (j + 1 < n && raw[j + 1] <= raw[i]) j++;

    const startT = raw[i];
    const endT = j + 1 < n ? raw[j + 1] : durationMs;
    const runLen = j - i + 1;

    if (runLen === 1) {
      schedule[i] = startT;
    } else {
      // Distribute the run across (startT, endT), weighted by text length.
      const weights: number[] = [];
      let totalW = 0;
      for (let k = i; k <= j; k++) {
        const w = Math.max(1, lines[k].text.trim().length);
        weights.push(w);
        totalW += w;
      }
      const span = Math.max(0, endT - startT);
      let acc = 0;
      for (let k = 0; k < runLen; k++) {
        schedule[i + k] = startT + span * (acc / totalW);
        acc += weights[k];
      }
    }
    i = j + 1;
  }

  // Guard against float rounding: keep the schedule non-decreasing.
  for (let k = 1; k < n; k++) {
    if (schedule[k] < schedule[k - 1]) schedule[k] = schedule[k - 1];
  }
  return schedule;
}

export interface CallEntry {
  kind: "call" | "whatsapp" | "browser";
  industry: string;
  title: string;
  brands?: string[];
  brandLogos?: string[];
  agentName: string;
  description: string;
  audioSrc?: string;
  videoSrc?: string;
  transcript?: TranscriptLine[];
}

export interface SyncedCallPlayerHandle {
  togglePlay: () => void;
  cycleRate: () => void;
  toggleMute: () => void;
  isPlaying: boolean;
  isMuted: boolean;
  playbackRate: number;
}

interface SyncedCallPlayerProps {
  entry: CallEntry;
  className?: string;
  onPlaybackChange?: (state: { isPlaying: boolean; isMuted: boolean; playbackRate: number }) => void;
}

export const SyncedCallPlayer = forwardRef<SyncedCallPlayerHandle, SyncedCallPlayerProps>(
  function SyncedCallPlayer({ entry, className, onPlaybackChange }, ref) {
    const audioRef = useRef<HTMLAudioElement | null>(null);
    const scrollRef = useRef<HTMLDivElement | null>(null);
    const isScrubbingRef = useRef(false);

    const [isPlaying, setIsPlaying] = useState(false);
    const [currentTime, setCurrentTime] = useState(0);
    const [duration, setDuration] = useState(0);
    const [playbackRate, setPlaybackRate] = useState(1);
    const [isMuted, setIsMuted] = useState(false);

    const hasAudio = !!entry.audioSrc;

    // Transcript source (may be empty). Declared here — before the effects that
    // depend on it — so the virtual-clock effects can reference it.
    // eslint-disable-next-line react-hooks/exhaustive-deps -- transcript is entry.transcript || []; it only feeds the pure buildRevealSchedule + virtualDurationSec, so recomputing per render is intended and output-identical
    const transcript = entry.transcript || [];

    // Synthetic duration (seconds) for entries with no audio file: the last
    // transcript offset plus a 2.6s tail so the closing line lingers before the
    // virtual clock stops.
    const virtualDurationSec = useMemo(() => {
      if (!transcript.length) return 0;
      const last = transcript.reduce((m, l) => Math.max(m, l.offsetMs), 0);
      return (last + 2600) / 1000;
    }, [transcript]);

    // Expose controls to parent (for the floating control bar)
    useImperativeHandle(ref, () => ({
      togglePlay,
      cycleRate,
      toggleMute,
      isPlaying,
      isMuted,
      playbackRate,
    }));

    // Handle entry switch: pause + reset the audio element (when present) and
    // reset the playback clock. The state resets live outside the audioRef guard
    // so switching between two audio-less entries also rewinds the virtual clock
    // (audioSrc is undefined for both, so keying on it alone would never fire).
    useEffect(() => {
      if (audioRef.current) {
        audioRef.current.pause();
        audioRef.current.currentTime = 0;
      }
      setIsPlaying(false);
      setCurrentTime(0);
      onPlaybackChange?.({ isPlaying: false, isMuted, playbackRate });
      // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: reset only when the call entry changes (keyed on title + audioSrc so audio-less entries reset too); adding isMuted/playbackRate/onPlaybackChange would pause + reset on every mute/rate toggle or parent re-render
    }, [entry.title, entry.audioSrc]);

    // Audio-less entries have no <audio> to report a duration, so feed the
    // reveal schedule + scrubber a synthetic one derived from the transcript.
    // (Guarded by !hasAudio, so react-hooks/set-state-in-effect does not flag it.)
    useEffect(() => {
      if (!hasAudio) setDuration(virtualDurationSec);
    }, [hasAudio, virtualDurationSec]);

    // Virtual playback clock: for entries with no audio file, advance
    // `currentTime` with requestAnimationFrame so the transcript reveal +
    // scrubber animate exactly as they do for audio-backed entries. Reads the
    // latest time via the functional updater, so mid-play scrubbing is honoured.
    useEffect(() => {
      if (hasAudio || !isPlaying) return;
      let raf = 0;
      let prev: number | null = null;
      let stopped = false;
      const tick = (ts: number) => {
        if (prev === null) prev = ts;
        // Clamp dt so a backgrounded tab (rAF paused) doesn't lurch the clock
        // forward by seconds when it resumes.
        const dt = Math.min(0.1, (ts - prev) / 1000);
        prev = ts;
        // The updater stays pure: it only computes the next time and flags when
        // the clock hits the end. The end-of-play side effects (setIsPlaying +
        // onPlaybackChange, which updates the PARENT) run AFTER, never inside the
        // updater — calling a parent setter from within a state updater triggers
        // React's "setState while rendering a different component" warning.
        let reachedEnd = false;
        setCurrentTime((t) => {
          if (stopped) return t;
          const next = t + dt * playbackRate;
          if (next >= virtualDurationSec) {
            reachedEnd = true;
            return virtualDurationSec;
          }
          return next;
        });
        if (reachedEnd) {
          stopped = true;
          setIsPlaying(false);
          onPlaybackChange?.({ isPlaying: false, isMuted, playbackRate });
        }
        if (!stopped) raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
      return () => cancelAnimationFrame(raf);
      // eslint-disable-next-line react-hooks/exhaustive-deps -- isMuted/onPlaybackChange are intentionally omitted so the rAF loop isn't torn down + restarted on every mute toggle or parent re-render; they're only read when the clock reaches the end (mirrors handleEnded)
    }, [hasAudio, isPlaying, playbackRate, virtualDurationSec]);

    // Sync time updates
    const handleTimeUpdate = () => {
      if (audioRef.current) {
        setCurrentTime(audioRef.current.currentTime);
      }
    };

    const handleLoadedMetadata = () => {
      if (audioRef.current) {
        setDuration(audioRef.current.duration || 0);
      }
    };

    const handleEnded = () => {
      setIsPlaying(false);
      setCurrentTime(0);
      if (audioRef.current) {
        audioRef.current.currentTime = 0;
      }
      onPlaybackChange?.({ isPlaying: false, isMuted, playbackRate });
    };

    const togglePlay = () => {
      if (hasAudio) {
        if (!audioRef.current) return;
        if (isPlaying) {
          audioRef.current.pause();
          setIsPlaying(false);
          onPlaybackChange?.({ isPlaying: false, isMuted, playbackRate });
        } else {
          audioRef.current
            .play()
            .then(() => {
              setIsPlaying(true);
              onPlaybackChange?.({ isPlaying: true, isMuted, playbackRate });
            })
            .catch((err) => {
              console.warn("Audio autoplay blocked:", err);
            });
        }
        return;
      }
      // Virtual clock (no audio file): drive isPlaying directly; the rAF effect
      // advances currentTime while playing.
      if (isPlaying) {
        setIsPlaying(false);
        onPlaybackChange?.({ isPlaying: false, isMuted, playbackRate });
      } else {
        if (currentTime >= virtualDurationSec) setCurrentTime(0);
        setIsPlaying(true);
        onPlaybackChange?.({ isPlaying: true, isMuted, playbackRate });
      }
    };

    const cycleRate = () => {
      const rates = [1, 1.25, 1.5, 2];
      const nextRate = rates[(rates.indexOf(playbackRate) + 1) % rates.length];
      setPlaybackRate(nextRate);
      if (audioRef.current) {
        audioRef.current.playbackRate = nextRate;
      }
      onPlaybackChange?.({ isPlaying, isMuted, playbackRate: nextRate });
    };

    const toggleMute = () => {
      if (!audioRef.current) return;
      const nextMuted = !isMuted;
      audioRef.current.muted = nextMuted;
      setIsMuted(nextMuted);
      onPlaybackChange?.({ isPlaying, isMuted: nextMuted, playbackRate });
    };

    const seekToOffset = (offsetMs: number) => {
      if (!hasAudio) {
        setCurrentTime(Math.max(0, offsetMs / 1000));
        return;
      }
      if (!audioRef.current) return;
      const target = Math.max(0, offsetMs / 1000);
      audioRef.current.currentTime = target;
      setCurrentTime(target);
      if (!isPlaying) {
        audioRef.current
          .play()
          .then(() => {
            setIsPlaying(true);
            onPlaybackChange?.({ isPlaying: true, isMuted, playbackRate });
          })
          .catch(() => {});
      }
    };

    // Seek by fraction (0..1) of the audio — used by the waveform scrubber.
    const seekToPct = (pct: number) => {
      const clamped = Math.max(0, Math.min(1, pct));
      if (!hasAudio) {
        setCurrentTime(clamped * virtualDurationSec);
        return;
      }
      const audio = audioRef.current;
      if (!audio || duration <= 0) return;
      const target = clamped * duration;
      audio.currentTime = target;
      setCurrentTime(target);
    };

    const pctFromClientX = (clientX: number, el: HTMLElement) => {
      const rect = el.getBoundingClientRect();
      if (rect.width <= 0) return 0;
      return (clientX - rect.left) / rect.width;
    };

    const handleScrubPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
      if (duration <= 0) return;
      isScrubbingRef.current = true;
      try {
        e.currentTarget.setPointerCapture(e.pointerId);
      } catch {
        // setPointerCapture can throw in rare cases; scrubbing still works.
      }
      seekToPct(pctFromClientX(e.clientX, e.currentTarget));
    };

    const handleScrubPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
      if (!isScrubbingRef.current) return;
      seekToPct(pctFromClientX(e.clientX, e.currentTarget));
    };

    const handleScrubPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
      if (!isScrubbingRef.current) return;
      isScrubbingRef.current = false;
      try {
        e.currentTarget.releasePointerCapture(e.pointerId);
      } catch {
        // ignore
      }
      if (!hasAudio) {
        // Virtual clock: resume playing from the scrubbed position.
        if (!isPlaying) {
          setIsPlaying(true);
          onPlaybackChange?.({ isPlaying: true, isMuted, playbackRate });
        }
        return;
      }
      const audio = audioRef.current;
      if (audio && !isPlaying) {
        audio
          .play()
          .then(() => {
            setIsPlaying(true);
            onPlaybackChange?.({ isPlaying: true, isMuted, playbackRate });
          })
          .catch(() => {});
      }
    };

    // Reveal schedule (ms per line), derived from the audio (or virtual)
    // duration so the chat tracks playback even when the source offsets are
    // degenerate.
    const schedule = useMemo(
      () => buildRevealSchedule(transcript, duration * 1000),
      [transcript, duration],
    );

    // Calculate visible lines: before audio starts playing, display the initial
    // greeting lines (up to 2). Once playing or scrubbing, reveal every line
    // whose scheduled time has been reached. `schedule` is non-decreasing, so
    // the filtered result is always a leading prefix of the transcript (its
    // indexes stay aligned with `schedule`).
    const nowMs = currentTime * 1000;
    const revealed = transcript.filter((_, idx) => nowMs >= (schedule[idx] ?? 0));
    // While playing, reveal on schedule but never drop below the 2-line preview —
    // avoids an empty flash at t≈0 and stays sensible if rAF is throttled.
    const visibleLines =
      isPlaying || currentTime > 0
        ? revealed.length >= 2
          ? revealed
          : transcript.slice(0, 2)
        : transcript.slice(0, 2);

    useEffect(() => {
      if (scrollRef.current) {
        scrollRef.current.scrollTo({
          top: scrollRef.current.scrollHeight,
          behavior: "smooth",
        });
      }
    }, [visibleLines.length]);

    const initial = entry.agentName.charAt(0).toUpperCase() || "A";

    return (
      <div className={cn("relative flex size-full flex-col bg-background text-foreground select-none", className)}>
        {/* Hidden HTML5 Audio Element */}
        {entry.audioSrc && (
          <audio
            ref={audioRef}
            src={entry.audioSrc}
            onTimeUpdate={handleTimeUpdate}
            onLoadedMetadata={handleLoadedMetadata}
            onEnded={handleEnded}
            preload="metadata"
          />
        )}

        {/* In-Call iOS Status & Agent Bar (Exact Ringg.ai Spec) */}
        <div className="flex shrink-0 items-center gap-3 border-b border-border/60 bg-secondary/30 px-5 pt-11 pb-3.5">
          {/* Avatar circle */}
          <div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-gradient-brand text-xs font-bold text-white shadow-2xs">
            {initial}
          </div>

          <div className="flex min-w-0 flex-1 flex-col">
            <span className="truncate text-sm font-semibold text-foreground leading-tight">
              {entry.agentName}
            </span>
            <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
              <span className="size-1.5 rounded-full bg-[#4FDAF1] animate-pulse motion-reduce:animate-none" aria-hidden="true" />
              <span>{entry.kind === "call" ? "Live call" : "Live chat"}</span>
            </span>
          </div>
        </div>

        {/* Transcript Chat Stream (Exact Ringg.ai typography & styling) */}
        <div
          ref={scrollRef}
          className="relative flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto px-4 py-3.5 text-sm leading-snug scrollbar-none pb-16"
        >
          {visibleLines.length === 0 && (
            <div className="flex h-32 flex-col items-center justify-center text-center text-muted-foreground text-xs">
              <p>Audio demo ready</p>
            </div>
          )}

          {visibleLines.map((line, idx) => {
            const isUser = line.speaker === "user";
            return (
              <button
                type="button"
                key={idx}
                onClick={() => seekToOffset(schedule[idx] ?? line.offsetMs)}
                aria-label={`Seek to this line: ${line.text.slice(0, 60)}`}
                className={cn(
                  "flex w-full cursor-pointer rounded-2xl transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
                  isUser ? "justify-end" : "justify-start",
                )}
              >
                <div
                  className={cn(
                    "max-w-[85%] rounded-2xl px-3.5 py-2.5 text-left text-sm leading-snug text-pretty transition-transform hover:scale-[1.01]",
                    isUser
                      ? "rounded-br-xs bg-foreground text-background shadow-2xs"
                      : "rounded-bl-xs bg-card text-foreground border border-[#4FDAF1]/35 shadow-2xs",
                  )}
                >
                  {line.text}
                </div>
              </button>
            );
          })}
        </div>

        {/* Waveform scrubber at the bottom of the phone frame. Fills progressively
            with playback and supports click + pointer-drag seeking; the chat
            follows automatically because it derives from audio currentTime. */}
        <div
          role="slider"
          tabIndex={0}
          aria-label="Seek audio"
          aria-valuemin={0}
          aria-valuemax={100}
          aria-valuenow={duration > 0 ? Math.round((currentTime / duration) * 100) : 0}
          onPointerDown={handleScrubPointerDown}
          onPointerMove={handleScrubPointerMove}
          onPointerUp={handleScrubPointerUp}
          onPointerCancel={handleScrubPointerUp}
          onKeyDown={(e) => {
            if (!(duration > 0)) return;
            const stepMs = Math.max(1000, Math.round((duration * 1000) / 20));
            const curMs = currentTime * 1000;
            let nextMs: number | null = null;
            if (e.key === "ArrowRight" || e.key === "ArrowUp") nextMs = Math.min(duration * 1000, curMs + stepMs);
            else if (e.key === "ArrowLeft" || e.key === "ArrowDown") nextMs = Math.max(0, curMs - stepMs);
            else if (e.key === "Home") nextMs = 0;
            else if (e.key === "End") nextMs = Math.max(0, duration * 1000 - 1);
            if (nextMs !== null) {
              e.preventDefault();
              seekToOffset(nextMs);
            }
          }}
          className="absolute inset-x-3 bottom-3 z-20 flex h-7.5 cursor-pointer touch-none items-center justify-center gap-0.5 rounded-full bg-background/90 px-3 backdrop-blur-md border border-border/50 shadow-xs group/wave focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
          title="Click or drag to seek audio"
        >
          {Array.from({ length: 36 }).map((_, i) => {
            const progress = duration > 0 ? currentTime / duration : 0;
            const filledCount = Math.floor(progress * 36);
            const isFilled = i <= filledCount;
            const isCurrentHead = isPlaying && i === filledCount;
            const playedColor = i < 12 ? "bg-[#4FDAF1]" : i < 24 ? "bg-[#F79C09]" : "bg-[#E26B0B]";
            const baseHeight = Math.round(5 + Math.sin(i * 0.35) * 6 + ((i * 5) % 7));
            const activeHeight = isCurrentHead
              ? Math.min(22, baseHeight + 6)
              : isFilled && isPlaying && (i + Math.floor(currentTime * 4)) % 3 === 0
                ? Math.min(20, baseHeight + 3)
                : baseHeight;

            return (
              <span
                key={i}
                className={cn(
                  "w-0.5 rounded-full transition-all duration-100",
                  isFilled
                    ? playedColor
                    : "bg-muted-foreground/25 group-hover/wave:bg-muted-foreground/35",
                  isCurrentHead && "scale-y-125 brightness-125 motion-reduce:scale-y-100 shadow-[0_0_6px_#4FDAF1]",
                )}
                style={{ height: `${activeHeight}px` }}
              />
            );
          })}
        </div>
      </div>
    );
  },
);
