import { useEffect, useRef, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { createPortal } from "react-dom";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertTriangle, ArrowLeft, Check, CheckCheck, ExternalLink, SkipBack, SkipForward, X } from "lucide-react";
import Avatar from "./Avatar";
import AddToPlaylist from "./AddToPlaylist";
import { api, type Video } from "../lib/api";
import { channelYouTubeUrl, formatDuration, formatViews, relativeTime } from "../lib/format";

// Turn a description into clickable nodes:
//  - bare timestamps (mm:ss / hh:mm:ss) → seek the inline player
//  - a YouTube link to the *current* video → seek (to its t= if any)
//  - a YouTube link to *another* video → load it in the inline player
//  - emails → mailto:, hashtags → YouTube's hashtag page, other URLs → new tab
const URL_RE = /(https?:\/\/[^\s<>]+)/g;
// One pass over plain text for the three inline token kinds (email | hashtag | timestamp).
const INLINE_RE =
  /([A-Za-z0-9._%+-]+@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+)|(#[\p{L}\p{N}_]+)|(\b(?:\d{1,2}:)?\d{1,2}:\d{2}\b)/gu;

function tsToSeconds(ts: string): number {
  const parts = ts.split(":").map((n) => parseInt(n, 10));
  return parts.length === 3
    ? parts[0] * 3600 + parts[1] * 60 + parts[2]
    : parts[0] * 60 + parts[1];
}

// Parse a YouTube `t`/`start` param: "90", "90s", or "1h2m3s".
function parseStart(t: string | null): number | null {
  if (!t) return null;
  if (/^\d+$/.test(t)) return parseInt(t, 10) || null;
  const m = t.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
  if (!m) return null;
  const total =
    parseInt(m[1] || "0", 10) * 3600 +
    parseInt(m[2] || "0", 10) * 60 +
    parseInt(m[3] || "0", 10);
  return total || null;
}

// Extract a video id (+ optional start) from a YouTube URL, else null.
function parseYouTube(url: string): { id: string; start: number | null } | null {
  let u: URL;
  try {
    u = new URL(url);
  } catch {
    return null;
  }
  const host = u.hostname.replace(/^www\./, "");
  let id: string | null = null;
  if (host === "youtu.be") {
    id = u.pathname.slice(1) || null;
  } else if (host === "youtube.com" || host === "m.youtube.com" || host === "music.youtube.com") {
    if (u.pathname === "/watch") id = u.searchParams.get("v");
    else {
      const m = u.pathname.match(/^\/(?:shorts|embed|live)\/([^/?#]+)/);
      if (m) id = m[1];
    }
  }
  if (!id) return null;
  return { id, start: parseStart(u.searchParams.get("t") || u.searchParams.get("start")) };
}

function renderDescription(
  text: string,
  opts: {
    currentId: string;
    onSeek: (seconds: number) => void;
    onLoadVideo: (id: string, start: number | null) => void;
    t: TFunction;
  }
): ReactNode[] {
  const { t } = opts;
  const out: ReactNode[] = [];
  let key = 0;
  const linkCls = "text-accent hover:underline break-all";
  // Tidy YouTube descriptions: drop trailing spaces and remove blank lines entirely
  // (they're just noise in the popover), keeping single line breaks.
  const clean = text.replace(/[ \t]+\n/g, "\n").replace(/\n{2,}/g, "\n").trim();
  for (const chunk of clean.split(URL_RE)) {
    if (/^https?:\/\//.test(chunk)) {
      const href = chunk.replace(/[.,;:!?)\]]+$/, "");
      const yt = parseYouTube(href);
      if (yt) {
        const sameVideo = yt.id === opts.currentId;
        out.push(
          <button
            key={key++}
            onClick={() =>
              sameVideo ? opts.onSeek(yt.start ?? 0) : opts.onLoadVideo(yt.id, yt.start)
            }
            className={linkCls}
            title={sameVideo ? t("player.jumpToTime") : t("player.playInApp")}
          >
            {href}
          </button>
        );
      } else {
        out.push(
          <a key={key++} href={href} target="_blank" rel="noreferrer" className={linkCls}>
            {href}
          </a>
        );
      }
      continue;
    }
    // Plain text: pull out emails, hashtags and bare timestamps.
    INLINE_RE.lastIndex = 0;
    let last = 0;
    let m: RegExpExecArray | null;
    while ((m = INLINE_RE.exec(chunk))) {
      if (m.index > last) out.push(<span key={key++}>{chunk.slice(last, m.index)}</span>);
      if (m[1]) {
        // Email — trim trailing punctuation the domain rule may have swallowed.
        const email = m[1].replace(/[.,;:]+$/, "");
        out.push(
          <a key={key++} href={`mailto:${email}`} className={linkCls}>
            {email}
          </a>
        );
        if (m[1].length > email.length) out.push(<span key={key++}>{m[1].slice(email.length)}</span>);
      } else if (m[2]) {
        // Hashtag → YouTube's hashtag feed (mirrors native YouTube behavior).
        const tag = m[2].slice(1);
        out.push(
          <a
            key={key++}
            href={`https://www.youtube.com/hashtag/${encodeURIComponent(tag)}`}
            target="_blank"
            rel="noreferrer"
            className={linkCls}
          >
            {m[2]}
          </a>
        );
      } else {
        const ts = m[3];
        out.push(
          <button
            key={key++}
            onClick={() => opts.onSeek(tsToSeconds(ts))}
            className="text-accent hover:underline"
          >
            {ts}
          </button>
        );
      }
      last = m.index + m[0].length;
    }
    if (last < chunk.length) out.push(<span key={key++}>{chunk.slice(last)}</span>);
  }
  return out;
}

// Experiment (branch experiment/inline-player): play the video in-app via the
// YouTube IFrame Player API (not a bare embed) so we can read playback position
// and resume where the user left off. The modal closes via the in-card Close
// button, the backdrop, or ESC (ESC only while focus is on our page, not inside
// the cross-origin player iframe — a browser security boundary we can't cross).

// How close to the end (seconds) counts as "finished" → auto-mark watched.
const FINISH_MARGIN = 10;

// --- IFrame Player API loader (singleton) ---
let apiPromise: Promise<any> | null = null;
function loadYouTubeApi(): Promise<any> {
  if (apiPromise) return apiPromise;
  apiPromise = new Promise((resolve) => {
    const w = window as any;
    if (w.YT && w.YT.Player) return resolve(w.YT);
    const prev = w.onYouTubeIframeAPIReady;
    w.onYouTubeIframeAPIReady = () => {
      if (typeof prev === "function") prev();
      resolve(w.YT);
    };
    const tag = document.createElement("script");
    tag.src = "https://www.youtube.com/iframe_api";
    document.head.appendChild(tag);
  });
  return apiPromise;
}

export default function PlayerModal({
  video,
  startAt,
  queue,
  startIndex,
  onClose,
  onState,
}: {
  video: Video;
  // Where to start the opened video: a number of seconds (0 = Restart), or null/undefined
  // to resume from the server-saved position carried on the video.
  startAt?: number | null;
  // Optional playback queue (e.g. a playlist). When present, the player advances through
  // it (auto-advance on end + prev/next), recreating per item to reuse the single-video
  // resume / auto-watch / progress logic.
  queue?: Video[];
  startIndex?: number;
  onClose: () => void;
  onState: (id: string, status: string) => void;
}) {
  const { t, i18n } = useTranslation();
  const qc = useQueryClient();
  // Precise upload date shown inline after the relative time (e.g. "9 yr ago · 12 Jan 2017").
  const fullDate = (d: string | null | undefined) =>
    d
      ? new Date(d).toLocaleDateString(i18n.language, {
          year: "numeric",
          month: "short",
          day: "numeric",
        })
      : "";
  // Track the playing item by id (not a frozen index) so it survives the queue changing
  // under us — e.g. an item removed in another tab. The index is derived from the *live*
  // queue, so the N / M counter and prev/next neighbours stay correct without disrupting
  // playback of the current video.
  const [playingId, setPlayingId] = useState<string>(
    queue?.[startIndex ?? 0]?.id ?? video.id
  );
  let index = queue ? queue.findIndex((v) => v.id === playingId) : 0;
  if (index < 0) index = 0;
  const active = queue && queue[index] ? queue[index] : video;
  const hasQueue = !!queue && queue.length > 1;
  // startAt only applies to the item we opened with; advanced items resume their own pos.
  const resumeAt =
    startAt != null && active.id === video.id ? startAt : active.position_seconds || 0;
  const mountRef = useRef<HTMLDivElement | null>(null);
  const playerRef = useRef<any>(null);
  const autoMarkedRef = useRef(false);

  // The player can navigate to other videos (YouTube links in a description). The
  // currently-playing id may differ from the active item.
  const [currentVideoId, setCurrentVideoId] = useState(active.id);
  const currentIdRef = useRef(active.id);
  const navigated = currentVideoId !== active.id;
  // Title/author of a navigated-to video, read from the player (free, no API call).
  const [liveData, setLiveData] = useState<{ title?: string; author?: string } | null>(null);
  // IFrame Player API error code, when the current video can't play in the embed (e.g. an
  // auto-generated Topic "Art Track" with embedding disabled → 101/150, or removed/private →
  // 100). We overlay a friendly message + "Open on YouTube" instead of YouTube's bare error.
  const [playerError, setPlayerError] = useState<number | null>(null);

  const loadVideo = (id: string, start: number | null) => {
    const p = playerRef.current;
    if (!p || typeof p.loadVideoById !== "function") return;
    // Explicit start wins; otherwise resume the active item, start others at 0.
    const startSeconds = start != null ? start : id === active.id ? resumeAt : 0;
    setPlayerError(null);
    p.loadVideoById({ videoId: id, startSeconds: startSeconds || 0 });
    currentIdRef.current = id;
    setCurrentVideoId(id);
    setLiveData(null);
  };

  // Local mirror of watch status so the toggle reacts instantly; changes are
  // propagated to the feed (and server) via onState.
  const [status, setStatus] = useState(active.status);
  const watched = status === "watched";
  const setWatched = (on: boolean) => {
    const next = on ? "watched" : "new";
    setStatus(next);
    onState(active.id, next);
  };

  // On queue advance (active changes), sync local state to the new item.
  useEffect(() => {
    setStatus(active.status);
    setCurrentVideoId(active.id);
    currentIdRef.current = active.id;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [active.id]);

  const seekTo = (seconds: number) => {
    const p = playerRef.current;
    if (p && typeof p.seekTo === "function") {
      p.seekTo(seconds, true);
      p.playVideo?.();
    }
  };

  // Lazy description (fetched only when the title is hovered). The popover is
  // portaled to <body> with fixed positioning so the modal card's overflow-y-auto
  // can't clip it. A small close grace lets the mouse travel title → popover.
  const [showDesc, setShowDesc] = useState(false);
  // `bottom` anchors the popover just above the title so it grows upward (over the
  // player) instead of downward off-screen / behind the OS taskbar.
  const [descRect, setDescRect] = useState<{ left: number; bottom: number; width: number } | null>(
    null
  );
  const titleRef = useRef<HTMLSpanElement | null>(null);
  const closeTimer = useRef<number | undefined>(undefined);
  const openDesc = () => {
    window.clearTimeout(closeTimer.current);
    const el = titleRef.current;
    if (el) {
      const r = el.getBoundingClientRect();
      const width = Math.min(560, window.innerWidth - 32);
      const left = Math.max(16, Math.min(r.left, window.innerWidth - width - 16));
      setDescRect({ left, bottom: window.innerHeight - r.top + 8, width });
    }
    setShowDesc(true);
  };
  const scheduleCloseDesc = () => {
    closeTimer.current = window.setTimeout(() => setShowDesc(false), 150);
  };
  const detail = useQuery({
    queryKey: ["video-detail", currentVideoId],
    queryFn: () => api.videoDetail(currentVideoId),
    // On hover (for the description) and eagerly when navigated, so we can link the
    // linked video's channel. Both share the one cached result.
    enabled: showDesc || navigated,
    staleTime: 5 * 60_000,
  });

  // ESC + background scroll lock.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    window.addEventListener("keydown", onKey);
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = prevOverflow;
      window.clearTimeout(closeTimer.current);
    };
  }, [onClose]);

  // Create the player, resume from the saved position, persist progress, and
  // auto-mark watched once playback reaches the end.
  useEffect(() => {
    let cancelled = false;
    const id = active.id;
    autoMarkedRef.current = false;
    setPlayerError(null);

    // Auto-watch only applies to the active item — not to other videos the player
    // navigated to via description links.
    const maybeAutoWatch = (current: number, duration: number): boolean => {
      if (autoMarkedRef.current || duration <= 0 || currentIdRef.current !== id) return false;
      if (current > duration - FINISH_MARGIN) {
        autoMarkedRef.current = true;
        setWatched(true);
        return true;
      }
      return false;
    };
    const persist = (): Promise<unknown> | void => {
      const p = playerRef.current;
      if (!p || typeof p.getCurrentTime !== "function") return;
      try {
        const cur = p.getCurrentTime();
        const dur = typeof p.getDuration === "function" ? p.getDuration() : 0;
        // If we just auto-marked watched, skip the near-end progress checkpoint: it would
        // only clear the resume position, and firing both at once needlessly races the
        // watched write against the progress row it deletes.
        if (maybeAutoWatch(cur, dur)) return;
        // Only checkpoint the feed video we opened with — a navigated-to video may not
        // be in this user's library, so the server would 404 on it.
        if (currentIdRef.current === id) {
          return api.saveProgress(id, cur, dur).catch(() => {});
        }
      } catch {
        /* player may be tearing down */
      }
    };

    loadYouTubeApi().then((YT) => {
      if (cancelled || !mountRef.current) return;
      playerRef.current = new YT.Player(mountRef.current, {
        width: "100%",
        height: "100%",
        videoId: id,
        playerVars: {
          autoplay: 1,
          start: resumeAt || undefined,
          rel: 0, // limit "related" to the same channel (full removal is no longer possible)
          enablejsapi: 1,
          origin: window.location.origin,
          playsinline: 1,
        },
        events: {
          onStateChange: (e: any) => {
            // 1 === playing → sync the navigated-to video's title/author for display.
            if (e?.data === 1) {
              const p = playerRef.current;
              const d = p && typeof p.getVideoData === "function" ? p.getVideoData() : null;
              if (d) setLiveData({ title: d.title, author: d.author });
            }
            // 0 === ended → mark watched, then advance to the next queue item if any.
            if (e?.data === 0 && currentIdRef.current === id) {
              if (!autoMarkedRef.current) {
                autoMarkedRef.current = true;
                setWatched(true);
              }
              if (queue && index < queue.length - 1) setPlayingId(queue[index + 1].id);
            }
          },
          // The embed couldn't play this video (embedding disabled, removed, private…).
          // Surface our own message + an "Open on YouTube" escape hatch.
          onError: (e: any) => setPlayerError(typeof e?.data === "number" ? e.data : -1),
        },
      });
    });

    // Periodic checkpoint so progress (and auto-watch) survive a crash/refresh.
    const timer = window.setInterval(persist, 5000);

    return () => {
      cancelled = true;
      window.clearInterval(timer);
      // Final flush, then refresh the feed + playlists so resume bars reflect this session.
      Promise.resolve(persist()).finally(() => {
        qc.invalidateQueries({ queryKey: ["feed"] });
        qc.invalidateQueries({ queryKey: ["playlist"] });
      });
      const p = playerRef.current;
      if (p && typeof p.destroy === "function") {
        try {
          p.destroy();
        } catch {
          /* ignore */
        }
      }
      playerRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [active.id]);

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/80 backdrop-blur-sm"
      onClick={onClose}
      role="dialog"
      aria-modal="true"
    >
      <div
        className="glass-card relative w-full max-w-4xl max-h-full overflow-y-auto rounded-2xl shadow-2xl"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="relative aspect-video w-full bg-black rounded-t-2xl overflow-hidden">
          {/* Hide the iframe entirely on error so YouTube's own error screen can't bleed
              through our overlay. */}
          <div ref={mountRef} className={`w-full h-full ${playerError != null ? "invisible" : ""}`} />
          {playerError != null && (
            <div className="absolute inset-0 grid place-items-center bg-bg p-6 text-center">
              <div className="max-w-sm">
                <AlertTriangle className="w-8 h-8 text-amber-500 mx-auto mb-3" />
                <h3 className="text-base font-semibold">{t("player.unavailableTitle")}</h3>
                <p className="text-sm text-muted mt-1.5 mb-4">
                  {playerError === 101 || playerError === 150
                    ? t("player.embedDisabledBody")
                    : t("player.unavailableBody")}
                </p>
                <a
                  href={`https://www.youtube.com/watch?v=${currentVideoId}`}
                  target="_blank"
                  rel="noreferrer"
                  className="inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
                >
                  <ExternalLink className="w-4 h-4" />
                  {t("player.openOnYoutube")}
                </a>
              </div>
            </div>
          )}
        </div>

        {hasQueue && (
          <div className="flex items-center justify-center gap-5 px-4 py-2 border-b border-border text-sm">
            <button
              onClick={() => index > 0 && setPlayingId(queue![index - 1].id)}
              disabled={index === 0}
              className="inline-flex items-center gap-1.5 text-muted enabled:hover:text-fg disabled:opacity-40 transition"
            >
              <SkipBack className="w-4 h-4" /> {t("player.previous")}
            </button>
            <span className="text-muted tabular-nums">
              {index + 1} / {queue!.length}
            </span>
            <button
              onClick={() => index < queue!.length - 1 && setPlayingId(queue![index + 1].id)}
              disabled={index === queue!.length - 1}
              className="inline-flex items-center gap-1.5 text-muted enabled:hover:text-fg disabled:opacity-40 transition"
            >
              {t("player.next")} <SkipForward className="w-4 h-4" />
            </button>
          </div>
        )}

        <div className="p-4 sm:p-5">
          {/* Title row — title (with hover description) on the left, Close on the right. */}
          <div className="flex items-start gap-3">
            <h2 className="min-w-0 flex-1 text-lg font-semibold leading-snug">
              {/* Hover target is the text itself (inline), not the whole row. */}
              <span
                ref={titleRef}
                className="cursor-default"
                onMouseEnter={openDesc}
                onMouseLeave={scheduleCloseDesc}
              >
                {navigated ? liveData?.title ?? t("player.loading") : active.title}
              </span>
            </h2>
            {navigated && (
              <button
                onClick={() => loadVideo(active.id, null)}
                title={t("player.backToOriginal")}
                className="shrink-0 inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg text-muted hover:text-fg hover:bg-surface transition"
              >
                <ArrowLeft className="w-4 h-4" />
                {t("player.back")}
              </button>
            )}
            {showDesc &&
              descRect &&
              createPortal(
                <div
                  className="fixed z-[60] bg-surface border border-border rounded-xl shadow-2xl p-4"
                  style={{
                    left: descRect.left,
                    bottom: descRect.bottom,
                    width: descRect.width,
                  }}
                  onMouseEnter={openDesc}
                  onMouseLeave={scheduleCloseDesc}
                >
                  <div className="text-xs uppercase tracking-wide text-muted mb-2">
                    {t("player.description")}
                  </div>
                  {detail.isLoading ? (
                    <div className="text-sm text-muted">{t("player.loading")}</div>
                  ) : detail.data?.description ? (
                    <div className="text-sm whitespace-pre-wrap break-words max-h-64 overflow-y-auto leading-relaxed">
                      {renderDescription(detail.data.description, {
                        currentId: currentVideoId,
                        onSeek: seekTo,
                        onLoadVideo: loadVideo,
                        t,
                      })}
                    </div>
                  ) : (
                    <div className="text-sm text-muted">{t("player.noDescription")}</div>
                  )}
                </div>,
                document.body
              )}
            <button
              onClick={onClose}
              title={t("player.closeEsc")}
              className="shrink-0 inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg text-muted hover:text-fg hover:bg-surface transition"
            >
              {t("player.close")}
              <X className="w-4 h-4" />
            </button>
          </div>

          {/* Channel + meta on one line, with the watched toggle pushed to the right.
              When navigated to a linked video we only have its author (from the player),
              so we show that as plain text and hide feed-video-specific bits. */}
          <div className="flex items-center gap-3 mt-3">
            {!navigated && (
              <Avatar
                src={active.channel_thumbnail}
                fallback={active.channel_title ?? ""}
                className="w-9 h-9 rounded-full shrink-0"
              />
            )}
            {navigated ? (
              detail.data?.channel_id ? (
                <a
                  href={channelYouTubeUrl(detail.data.channel_id)}
                  target="_blank"
                  rel="noreferrer"
                  className="font-medium hover:text-accent shrink-0"
                >
                  {liveData?.author ?? detail.data.channel_title ?? t("player.channel")}
                </a>
              ) : (
                <span className="font-medium shrink-0">{liveData?.author ?? ""}</span>
              )
            ) : (
              <a
                href={active.channel_url}
                target="_blank"
                rel="noreferrer"
                className="font-medium hover:text-accent shrink-0"
              >
                {active.channel_title}
              </a>
            )}
            {!navigated ? (
              <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-muted min-w-0">
                {active.view_count != null && (
                  <span>· {t("player.views", { count: active.view_count, formattedCount: formatViews(active.view_count) })}</span>
                )}
                <span>
                  · {relativeTime(active.published_at)}
                  {active.published_at && ` · ${fullDate(active.published_at)}`}
                </span>
                {active.duration_seconds != null && (
                  <span>· {formatDuration(active.duration_seconds)}</span>
                )}
                {active.live_status === "was_live" && (
                  <span className="bg-accent text-accent-fg text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded">
                    {t("player.stream")}
                  </span>
                )}
              </div>
            ) : (
              // Linked video's stats come from the (already-fetched) video detail.
              <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-muted min-w-0">
                {detail.data?.view_count != null && (
                  <span>· {t("player.views", { count: detail.data.view_count, formattedCount: formatViews(detail.data.view_count) })}</span>
                )}
                {detail.data?.published_at && (
                  <span>
                    · {relativeTime(detail.data.published_at)} · {fullDate(detail.data.published_at)}
                  </span>
                )}
                {detail.data?.duration_seconds != null && (
                  <span>· {formatDuration(detail.data.duration_seconds)}</span>
                )}
              </div>
            )}

            {!navigated && (
              <AddToPlaylist
                videoId={active.id}
                className="ml-auto shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg text-muted hover:text-fg hover:bg-surface border border-border transition"
              />
            )}
            {!navigated && (
              <button
                onClick={() => setWatched(!watched)}
                title={watched ? t("player.watchedUnmark") : t("player.markWatched")}
                className={
                  "shrink-0 inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg transition " +
                  (watched
                    ? "bg-accent text-accent-fg shadow-sm hover:opacity-90"
                    : "text-muted hover:text-fg hover:bg-surface border border-border")
                }
              >
                {watched ? <CheckCheck className="w-4 h-4" /> : <Check className="w-4 h-4" />}
                {watched ? t("player.watched") : t("player.markWatched")}
              </button>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}
