import { useCallback, useEffect, useRef, useState } from "react";
import { fileUrl, listSubtitles, openExternal, resolveStream, savePlayback } from "../api";
import type { FeedItem } from "../types";
import { compactViews, relativeTime } from "./format";
import PlayerControls from "./PlayerControls";
import { 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;
  /** 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, 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);
  // WebVTT files yt-dlp saved next to a download, so subtitles work offline.
  const [sidecars, setSidecars] = useState<Array<[string, string]>>([]);

  useEffect(() => {
    if (!path) {
      setSidecars([]);
      return;
    }
    let cancelled = false;
    listSubtitles(item.id)
      .then((s) => !cancelled && setSidecars(s))
      .catch(() => !cancelled && setSidecars([]));
    return () => {
      cancelled = true;
    };
  }, [item.id, path]);
  // 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);
  const stageRef = useRef<HTMLDivElement>(null);
  const lastSave = useRef(0);

  useEffect(() => {
    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 navBtn =
    "rounded-lg border border-slate-300 px-2 py-1.5 text-[11px] 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={`${navBtn} w-[30px] p-0`}>
          <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 && (
          <span className="text-[11px] text-slate-400 dark:text-slate-500">
            {error ? "Unavailable" : src && !buffering ? "Streaming" : "Loading…"}
          </span>
        )}
      </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)}
            className="absolute inset-0 size-full object-contain"
          >
            {sidecars.map(([lang, file]) => (
              <track
                key={file}
                kind="subtitles"
                srcLang={lang}
                label={lang}
                src={fileUrl(file)}
              />
            ))}
          </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}
            />
          </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}
                className={`${navBtn} whitespace-nowrap`}
              >
                {downloading ? "Downloading…" : "Download"}
              </button>
            )}
            {!streaming && (
              <button
                onClick={onDelete}
                title="Delete this download"
                className={`${navBtn} whitespace-nowrap hover:border-red-500! hover:text-red-600!
                            dark:hover:border-red-500! dark:hover:text-red-400!`}
              >
                Delete
              </button>
            )}
            <button
              onClick={() => openExternal(`https://www.youtube.com/watch?v=${item.id}`)}
              title="Open on YouTube"
              aria-label="Open on YouTube"
              className={`${navBtn} w-[30px] p-0`}
            >
              <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>
  );
}
