import { useCallback, useEffect, useRef, useState } from "react";
import {
  fetchSubtitles, fileUrl, listSubtitles, openExternal, resolveStream, savePlayback,
} from "../api";
import { DEFAULT_SUB_LANG, type FeedItem } from "../types";
import { compactViews, relativeTime, subtitleLabel } from "./format";
import PlayerControls from "./PlayerControls";
import { Badge, BTN, Spinner } from "./ui";

interface Props {
  item: FeedItem;
  /** Local file when downloaded; null means resolve and stream it instead. */
  path: string | null;
  onClose: () => void;
  onDelete: () => void;
  onPrev?: () => void;
  onNext?: () => void;
  /** Present only while streaming, so the video can be saved from here. */
  onDownload?: () => void;
  downloading?: boolean;
  /** Max height for streaming, or null to let the player adapt. */
  maxHeight: number | null;
  /** Preferred subtitle language, or "off". */
  subLang: string;
  /** Persists a subtitle choice made from the transport bar. */
  onSubLang: (l: string) => void;
  /** Position in the current feed, for the "3 of 180" readout. */
  index: number;
  total: number;
  /** False in window fullscreen, where there are no traffic lights to clear. */
  titleBarInset: boolean;
}

/**
 * Releases a <video> completely.
 *
 * Detaching the element is not enough: WebKit keeps a Picture-in-Picture
 * session (and its audio) running after the element leaves the DOM, so closing
 * the player or stepping to the next video would leave the previous one playing
 * with no way to stop it. Every exit path goes through here.
 */
function teardown(v: HTMLVideoElement | null) {
  if (!v) return;
  // Safari's PiP is the non-standard presentation-mode API; the spec one is
  // tried too, since either may be the live implementation.
  const webkit = v as WebkitVideo;
  try {
    if (webkit.webkitPresentationMode && webkit.webkitPresentationMode !== "inline") {
      webkit.webkitSetPresentationMode?.("inline");
    }
  } catch {
    /* not supported here */
  }
  try {
    if (webkit.webkitDisplayingFullscreen) webkit.webkitExitFullscreen?.();
  } catch {
    /* not supported here */
  }
  try {
    if (document.pictureInPictureElement) void document.exitPictureInPicture();
  } catch {
    /* not supported here */
  }
  try {
    if (document.fullscreenElement) void document.exitFullscreen();
  } catch {
    /* not supported here */
  }
  try {
    v.pause();
    // Dropping the source is what actually frees the decoder and the audio.
    v.removeAttribute("src");
    v.load();
  } catch {
    /* already gone */
  }
}

/** The WebKit-only members we rely on for PiP and native video fullscreen. */
type WebkitVideo = HTMLVideoElement & {
  webkitPresentationMode?: string;
  webkitSetPresentationMode?: (mode: string) => void;
  webkitEnterFullscreen?: () => void;
  webkitExitFullscreen?: () => void;
  webkitDisplayingFullscreen?: boolean;
};

/** Save at most this often while playing; also saved on close. */
const SAVE_EVERY_MS = 5000;
/** Ignore a saved position this close to either end — nothing useful to resume. */
const RESUME_EDGE_S = 5;

/**
 * Two sources, one player element.
 *
 * Downloaded: the local file through Tauri's asset protocol. Every download is
 * H.264/AAC in MP4 precisely so WKWebView can decode it natively.
 *
 * Not downloaded: YouTube's HLS master playlist, resolved by yt-dlp. Its
 * variants are H.264 + AAC up to 1080p, which AVFoundation streams natively —
 * so watching still happens here rather than in a browser. The iframe embed
 * cannot be used: it rejects a `tauri://` origin with "Error 153".
 */
export default function Player({
  item, path, onClose, onDelete, onPrev, onNext, onDownload, downloading,
  maxHeight, subLang, onSubLang, index, total, titleBarInset,
}: Props) {
  const streaming = path === null;
  const [src, setSrc] = useState<string | null>(path ? fileUrl(path) : null);
  const [error, setError] = useState<string | null>(null);
  const [buffering, setBuffering] = useState(true);
  // The height actually being decoded. With an adaptive stream this changes as
  // the player switches rendition, so it is read from the element rather than
  // assumed from the setting.
  const [height, setHeight] = useState(0);
  /**
   * Subtitle tracks as (language, URL), from whichever source has them:
   * WebVTT files yt-dlp saved beside a download, or a fetch.
   *
   * A stream has no other source — YouTube's HLS manifest carries a dozen
   * audio renditions and no subtitles at all — and a download saved before
   * subtitles were switched on has nothing beside it either.
   *
   * Fetching does not depend on the Settings preference. That preference says
   * which language is switched on by itself; it must not decide whether
   * subtitles exist to be chosen at all, or "None" would quietly empty the
   * player's subtitle menu as well.
   *
   * Fetched cues become blob URLs, which share the document's origin — a
   * file:// or 127.0.0.1 track would not.
   */
  const [tracks, setTracks] = useState<Array<[string, string]>>([]);
  const [fetchingSubs, setFetchingSubs] = useState(false);
  // The language to fetch, which is NOT the preference: turning subtitles on
  // from the player's menu moves the preference from "off" to that language,
  // and refetching then would tear down the tracks — and the stream with them —
  // the instant one is chosen.
  const wantLang = subLang === "off" ? DEFAULT_SUB_LANG : subLang;

  useEffect(() => {
    let cancelled = false;
    const blobs: string[] = [];
    setTracks([]);
    setFetchingSubs(false);

    // A download made since subtitles became embedded carries them inside the
    // file, where the element exposes them itself. Give it until the metadata
    // is parsed to say so before going to the network for something already on
    // disk — which would also fail offline.
    const hasEmbedded = async () => {
      const v = videoRef.current;
      if (!v) return false;
      for (let i = 0, settled = 0; i < 20 && settled < 3; i++) {
        if (v.textTracks.length > 0) return true;
        if (v.readyState >= 1) settled++;
        await new Promise((r) => setTimeout(r, 150));
      }
      return (videoRef.current?.textTracks.length ?? 0) > 0;
    };

    const load = async () => {
      if (path) {
        const local = await listSubtitles(item.id).catch(() => []);
        if (cancelled) return;
        if (local.length > 0) {
          setTracks(local.map(([lang, file]) => [lang, fileUrl(file)]));
          return;
        }
        if (await hasEmbedded()) return;
      }
      if (cancelled) return;
      setFetchingSubs(true);
      try {
        const list = await fetchSubtitles(item.id, wantLang);
        if (cancelled) return;
        setTracks(
          list.map(([lang, text]) => {
            const url = URL.createObjectURL(new Blob([text], { type: "text/vtt" }));
            blobs.push(url);
            return [lang, url] as [string, string];
          }),
        );
      } catch {
        /* a video with no captions in this language is an ordinary outcome */
      } finally {
        if (!cancelled) setFetchingSubs(false);
      }
    };
    void load();

    return () => {
      cancelled = true;
      for (const u of blobs) URL.revokeObjectURL(u);
    };
  }, [item.id, path, wantLang]);

  // Controls and edge arrows fade away while you are just watching.
  const [chromeVisible, setChromeVisible] = useState(true);
  const hideTimer = useRef<number | undefined>(undefined);
  const videoRef = useRef<HTMLVideoElement>(null);
  // Quality is the short side, so a portrait Short reads 1080p, not 1920p.
  const measure = useCallback(() => {
    const v = videoRef.current;
    if (!v?.videoHeight) return;
    setHeight(Math.min(v.videoWidth, v.videoHeight));
  }, []);
  const stageRef = useRef<HTMLDivElement>(null);
  const lastSave = useRef(0);

  useEffect(() => {
    setHeight(0);
    if (path) {
      setSrc(fileUrl(path));
      return;
    }
    let cancelled = false;
    setSrc(null);
    setError(null);
    resolveStream(item.id, maxHeight)
      .then((u) => !cancelled && setSrc(u))
      .catch((e) => !cancelled && setError(String(e)));
    return () => {
      cancelled = true;
    };
  }, [item.id, path, maxHeight]);


  const persist = useCallback(() => {
    const v = videoRef.current;
    if (!v || !Number.isFinite(v.duration) || v.duration <= 0) return;
    savePlayback(item.id, v.currentTime, v.duration).catch(() => {
      /* a lost position is not worth interrupting playback for */
    });
  }, [item.id]);

  // Save on the way out, including when switching to another video. Declared
  // before the teardown effect so React runs this cleanup first — the position
  // has to be read off the element before its source is dropped.
  useEffect(() => () => persist(), [persist]);

  useEffect(() => {
    const v = videoRef.current;
    return () => teardown(v);
  }, [src]);

  const onTimeUpdate = () => {
    // Frames are flowing, so whatever the media events claimed, we are not
    // buffering. A resume-seek can fire `waiting` after `playing` and leave the
    // spinner stuck otherwise.
    const v = videoRef.current;
    if (v && v.readyState >= 3 && !v.paused) setBuffering(false);

    const now = Date.now();
    if (now - lastSave.current < SAVE_EVERY_MS) return;
    lastSave.current = now;
    persist();
  };

  // Pick up where this video was left off, unless that was right at either end.
  const onLoadedMetadata = () => {
    const v = videoRef.current;
    const at = item.position ?? 0;
    if (!v || !Number.isFinite(v.duration)) return;
    if (at > RESUME_EDGE_S && at < v.duration - RESUME_EDGE_S) v.currentTime = at;
  };

  const leave = useCallback(() => onClose(), [onClose]);

  // Escape backs out, as it does everywhere else in the app.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      // In fullscreen the browser already handles Escape; closing the player
      // as well would drop you all the way back to the feed.
      if (e.key === "Escape" && !document.fullscreenElement) leave();
      // Arrow keys only when the video does not own them for seeking.
      if (e.key === "ArrowLeft" && e.shiftKey) onPrev?.();
      if (e.key === "ArrowRight" && e.shiftKey) onNext?.();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [leave, onPrev, onNext]);

  const edgeBtn =
    "absolute top-1/2 z-10 -translate-y-1/2 grid size-11 place-items-center rounded-full " +
    "bg-slate-950/55 text-2xl leading-none text-white backdrop-blur cursor-pointer " +
    "transition-opacity duration-200 hover:bg-slate-950/80 disabled:hidden " +
    (chromeVisible ? "opacity-100" : "pointer-events-none opacity-0");

  const showChrome = useCallback(() => {
    setChromeVisible(true);
    window.clearTimeout(hideTimer.current);
    hideTimer.current = window.setTimeout(() => {
      // Never hide while paused — there would be no way back to play.
      if (!videoRef.current?.paused) setChromeVisible(false);
    }, 2600);
  }, []);

  useEffect(() => {
    showChrome();
    return () => window.clearTimeout(hideTimer.current);
  }, [showChrome, src]);

  const navIcon =
    "grid h-[30px] w-[30px] shrink-0 place-items-center rounded-lg border border-slate-300 " +
    "cursor-pointer hover:border-sky-500 hover:text-sky-600 disabled:opacity-30 " +
    "disabled:cursor-not-allowed dark:border-slate-700 dark:hover:border-sky-500 " +
    "dark:hover:text-sky-400";

  const navBtn =
    "inline-flex h-[30px] items-center rounded-lg border border-slate-300 px-2.5 text-[12px] font-medium cursor-pointer " +
    "hover:border-sky-500 hover:text-sky-600 disabled:opacity-30 disabled:cursor-not-allowed " +
    "disabled:hover:border-slate-300 disabled:hover:text-inherit " +
    "dark:border-slate-700 dark:hover:border-sky-500 dark:hover:text-sky-400";

  return (
    <div className="fixed inset-0 z-50 flex flex-col bg-slate-100 dark:bg-slate-950">
      {titleBarInset && <div data-tauri-drag-region className="h-9 shrink-0" />}
      <header
        className="flex items-center gap-2 border-b border-slate-200 bg-white px-4 py-3
                   dark:border-slate-800 dark:bg-slate-900"
      >
        <button onClick={leave} title="Back to the feed (Esc)" aria-label="Back"
          className={navIcon}>
          <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
            <path strokeLinecap="round" strokeLinejoin="round" d="M15 6l-6 6 6 6" />
          </svg>
        </button>

        <button onClick={onPrev} disabled={!onPrev} title="Previous video" className={navBtn}>
          ‹ Prev
        </button>
        <button onClick={onNext} disabled={!onNext} title="Next video" className={navBtn}>
          Next ›
        </button>
        <span className="font-mono text-[11px] tabular-nums text-slate-400 dark:text-slate-500">
          {index + 1}/{total}
        </span>

        <span className="min-w-0 flex-1 truncate text-[12px] text-slate-500 dark:text-slate-400">
          {item.channel_title}
        </span>

        {streaming ? (
          error ? (
            <Badge tone="danger">Unavailable</Badge>
          ) : src && !buffering ? (
            <Badge tone="accent" title={`Streaming at ${height || "an unknown"}p`}>
              Streaming{height ? ` · ${height}p` : ""}
            </Badge>
          ) : (
            <Badge>Loading…</Badge>
          )
        ) : (
          height > 0 && (
            <Badge title={`This copy on your Mac is ${height}p`}>
              Downloaded · {height}p
            </Badge>
          )
        )}
      </header>

      {/* Absolute fill + object-contain, so portrait Shorts and landscape
          videos are both letterboxed to the pane instead of overflowing it. */}
      <div
        ref={stageRef}
        onContextMenu={(e) => e.preventDefault()}
        onMouseMove={showChrome}
        onMouseLeave={() => !videoRef.current?.paused && setChromeVisible(false)}
        className={`relative min-h-0 flex-1 bg-slate-950 ${chromeVisible ? "" : "cursor-none"}`}
      >
        {/* Edge arrows, the way a player wants them: big targets on the left and
            right of the picture. They fade in on hover so they never sit on top
            of the video while you are watching it. */}
        <button
          onClick={onPrev}
          disabled={!onPrev}
          title="Previous video"
          aria-label="Previous video"
          className={`${edgeBtn} left-3`}
        >
          ‹
        </button>
        <button
          onClick={onNext}
          disabled={!onNext}
          title="Next video"
          aria-label="Next video"
          className={`${edgeBtn} right-3`}
        >
          ›
        </button>

        {/* Resolving the stream and buffering it are the same wait as far as
            you are concerned, so they get the same spinner in the same place. */}
        {(!src || buffering) && !error && (
          <div className="pointer-events-none absolute inset-0 z-30 grid place-items-center">
            <Spinner className="size-8 text-white/80" />
          </div>
        )}

        {src ? (
          <video
            ref={videoRef}
            key={src}
            src={src}
            autoPlay
            onContextMenu={(e) => e.preventDefault()}
            onClick={() => {
              const v = videoRef.current;
              if (!v) return;
              if (v.paused) void v.play().catch(() => {});
              else v.pause();
            }}
            onTimeUpdate={onTimeUpdate}
            onLoadedMetadata={onLoadedMetadata}
            onPause={persist}
            onLoadStart={() => setBuffering(true)}
            onWaiting={() => setBuffering(true)}
            onStalled={() => setBuffering(true)}
            onCanPlay={() => setBuffering(false)}
            onPlaying={() => setBuffering(false)}
            onSeeked={() => setBuffering(false)}
            onResize={measure}
            onLoadedData={measure}
            className="absolute inset-0 size-full object-contain"
          >
            {tracks.map(([lang, url]) => (
              <track key={url} kind="subtitles" srcLang={lang} label={subtitleLabel(lang)} src={url} />
            ))}
          </video>
        ) : (
          error && (
            <div className="absolute inset-0 grid place-items-center px-6 text-center">
              <div className="max-w-sm">
                <p className="text-[13px] text-red-400">{error}</p>
                <button
                  onClick={() => openExternal(`https://www.youtube.com/watch?v=${item.id}`)}
                  className={`${BTN} mt-3 cursor-pointer py-1.5`}
                >
                  Open on YouTube instead
                </button>
              </div>
            </div>
          )
        )}
        {src && !error && (
          <div
            className={`transition-opacity duration-200 ${
              chromeVisible ? "opacity-100" : "pointer-events-none opacity-0"
            }`}
          >
            <PlayerControls
              videoRef={videoRef}
              stageRef={stageRef}
              onActivity={showChrome}
              subLang={subLang}
              onSubLang={onSubLang}
              subsLoading={fetchingSubs}
            />
          </div>
        )}
      </div>

      <footer
        className="max-h-52 shrink-0 overflow-y-auto border-t border-slate-200 bg-white px-4 py-3
                   dark:border-slate-800 dark:bg-slate-900"
      >
        <div className="flex items-start justify-between gap-3">
          <div className="min-w-0">
            <h2 className="truncate text-[15px] font-semibold tracking-tight">{item.title}</h2>
            <div className="mt-0.5 text-[11px] text-slate-400 dark:text-slate-500">
              {[compactViews(item.views), relativeTime(item.published)]
                .filter(Boolean)
                .join(" · ")}
            </div>
          </div>
          <div className="flex shrink-0 items-center gap-2">
            {onDownload && (
              <button
                onClick={onDownload}
                disabled={downloading}
                title={downloading ? "Downloading…" : "Download for offline"}
                aria-label="Download"
                className={navIcon}
              >
                {downloading ? (
                  <Spinner className="size-4" />
                ) : (
                  <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
                    <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4M4 20h16" />
                  </svg>
                )}
              </button>
            )}
            {!streaming && (
              <button
                onClick={onDelete}
                title="Delete this download"
                aria-label="Delete download"
                className={`${navIcon} hover:border-red-500! hover:text-red-600!
                            dark:hover:border-red-500! dark:hover:text-red-400!`}
              >
                <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
                  <path strokeLinecap="round" strokeLinejoin="round"
                    d="M4 7h16M9 7V5h6v2M6 7l1 13h10l1-13M10 11v6M14 11v6" />
                </svg>
              </button>
            )}
            <button
              onClick={() => openExternal(`https://www.youtube.com/watch?v=${item.id}`)}
              title="Open on YouTube"
              aria-label="Open on YouTube"
              className={navIcon}
            >
              <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
                <path strokeLinecap="round" strokeLinejoin="round"
                  d="M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 01-1 1H5a1 1 0 01-1-1V7a1 1 0 011-1h5" />
              </svg>
            </button>
          </div>
        </div>

        {/* Collapsed by default — the description is rarely what you came for. */}
        {item.description && (
          <details className="group mt-2">
            <summary
              className="cursor-pointer list-none text-[11px] font-medium text-slate-500
                         hover:text-sky-600 dark:text-slate-400 dark:hover:text-sky-400"
            >
              <span className="inline-block transition-transform group-open:rotate-90">›</span>{" "}
              Description
            </summary>
            <p className="mt-2 whitespace-pre-wrap text-[12.5px] leading-relaxed text-slate-600 dark:text-slate-300">
              {item.description}
            </p>
          </details>
        )}
      </footer>
    </div>
  );
}
