import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  cancelDownload, deleteDownload, downloadVideo, onRefreshProgress, refreshFeeds,
} from "./api";
import Player from "./components/Player";
import Settings from "./components/Settings";
import Sidebar from "./components/Sidebar";
import TopBar, { type ViewMode } from "./components/TopBar";
import { Dialog, Toast } from "./components/ui";
import VideoRow from "./components/VideoRow";
import VideoTile from "./components/VideoTile";
import { useAppearance } from "./hooks/useAppearance";
import { useConnectivity } from "./hooks/useConnectivity";
import { useDownloads } from "./hooks/useDownloads";
import { useFeed } from "./hooks/useFeed";
import { useWindowFullscreen } from "./hooks/useWindowFullscreen";
import {
  QUALITIES, STREAM_QUALITIES,
  type FeedFilter, type FeedItem, type Quality, type RefreshProgress,
} from "./types";

const TOAST_MS = 2400;
/** How often to pull new videos while online, so the feed stays live. */
const AUTO_REFRESH_MS = 10 * 60 * 1000;

function remembered(key: string): boolean {
  try {
    return localStorage.getItem(`flighttube.${key}`) === "1";
  } catch {
    return false;
  }
}

export default function App() {
  const [channelId, setChannelId] = useState<string | null>(null);
  const [search, setSearch] = useState("");
  const [downloadedOnly, setDownloadedOnly] = useState(() => remembered("downloadedOnly"));
  const [hideShorts, setHideShorts] = useState(() => remembered("hideShorts"));
  const [sidebarHidden, setSidebarHidden] = useState(() => remembered("sidebarHidden"));
  const [sidebarPeek, setSidebarPeek] = useState(false);
  const [view, setView] = useState<ViewMode>(() => {
    try {
      return localStorage.getItem("flighttube.view") === "grid" ? "grid" : "list";
    } catch {
      return "list";
    }
  });
  const [showSettings, setShowSettings] = useState(false);
  // Index into the current feed, so the player can step through it.
  const [playingIndex, setPlayingIndex] = useState<number | null>(null);
  const [streamQuality, setStreamQuality] = useState<Quality>(() => {
    try {
      const stored = localStorage.getItem("flighttube.streamQuality");
      return STREAM_QUALITIES.some((q) => q.value === stored) ? (stored as Quality) : "best";
    } catch {
      return "best";
    }
  });
  const [quality, setQuality] = useState<Quality>(() => {
    try {
      const stored = localStorage.getItem("flighttube.quality");
      return QUALITIES.some((q) => q.value === stored) ? (stored as Quality) : "best";
    } catch {
      return "best";
    }
  });
  const [refreshing, setRefreshing] = useState(false);
  const [refreshProgress, setRefreshProgress] = useState<RefreshProgress | null>(null);
  const [toast, setToast] = useState<string | null>(null);
  const [failure, setFailure] = useState<string | null>(null);

  const { mode, setMode } = useAppearance();
  const windowFullscreen = useWindowFullscreen();
  const { online, reachable, forcedOffline, setForcedOffline, probe } = useConnectivity();

  // A toast reports success and fades; a modal reports a failure or asks a
  // question. Never make someone dismiss a box to be told it worked.
  const toastTimer = useRef<number | undefined>(undefined);
  const say = useCallback((message: string) => {
    setToast(message);
    window.clearTimeout(toastTimer.current);
    toastTimer.current = window.setTimeout(() => setToast(null), TOAST_MS);
  }, []);
  useEffect(() => () => window.clearTimeout(toastTimer.current), []);

  useEffect(() => {
    try {
      localStorage.setItem("flighttube.view", view);
      localStorage.setItem("flighttube.quality", quality);
      localStorage.setItem("flighttube.streamQuality", streamQuality);
      localStorage.setItem("flighttube.downloadedOnly", downloadedOnly ? "1" : "0");
      localStorage.setItem("flighttube.hideShorts", hideShorts ? "1" : "0");
      localStorage.setItem("flighttube.sidebarHidden", sidebarHidden ? "1" : "0");
    } catch {
      /* storage blocked */
    }
  }, [view, quality, streamQuality, downloadedOnly, hideShorts, sidebarHidden]);

  // Offline, the only videos that can be played are the ones already on disk,
  // so the feed collapses to those regardless of the toggle.
  const effectiveDownloadedOnly = downloadedOnly || !online;

  const filter: FeedFilter = useMemo(
    () => ({
      channel_id: channelId,
      search: search.trim() || null,
      downloaded_only: effectiveDownloadedOnly,
      hide_shorts: hideShorts,
      limit: 500,
    }),
    [channelId, search, effectiveDownloadedOnly, hideShorts],
  );

  const { items, channels, loading, error, reload } = useFeed(filter);
  const { live, clear: clearLive } = useDownloads(reload);

  useEffect(() => {
    let un: (() => void) | undefined;
    onRefreshProgress(setRefreshProgress).then((u) => (un = u));
    return () => un?.();
  }, []);

  useEffect(() => {
    if (error) setFailure(error);
  }, [error]);

  const totals = useMemo(
    () =>
      channels.reduce(
        (acc, c) => ({
          videos: acc.videos + c.video_count,
          downloaded: acc.downloaded + c.downloaded_count,
        }),
        { videos: 0, downloaded: 0 },
      ),
    [channels],
  );

  const doRefresh = useCallback(async () => {
    setRefreshing(true);
    try {
      const s = await refreshFeeds();
      await reload();
      const failed = s.failures.length;
      say(
        failed
          ? `Checked ${s.channels} channels · ${failed} failed`
          : `Checked ${s.channels} channel${s.channels === 1 ? "" : "s"}`,
      );
    } catch (e) {
      setFailure(String(e));
    } finally {
      setRefreshing(false);
      setRefreshProgress(null);
    }
  }, [reload, say]);

  // Downloaded plays from disk; anything else streams. Only being offline with
  // no local copy leaves nothing to play.
  const playableAt = useCallback(
    (i: number): { item: FeedItem; path: string | null } | null => {
      const item = items[i];
      if (!item) return null;
      const path = live[item.id]?.path ?? item.path;
      const done = (live[item.id]?.state ?? item.state) === "done";
      if (done && path) return { item, path };
      return online ? { item, path: null } : null;
    },
    [items, live, online],
  );

  const openIndex = useCallback(
    (i: number) => {
      if (playableAt(i)) setPlayingIndex(i);
      else setFailure("That video isn't downloaded, and you're offline.");
    },
    [playableAt],
  );

  /** Next/previous item that can actually be played right now. */
  const stepFrom = useCallback(
    (from: number, dir: 1 | -1): number | null => {
      for (let i = from + dir; i >= 0 && i < items.length; i += dir) {
        if (playableAt(i)) return i;
      }
      return null;
    },
    [items.length, playableAt],
  );

  const playing = playingIndex == null ? null : playableAt(playingIndex);
  // The feed can change under an open player (a refresh, a filter change).
  useEffect(() => {
    if (playingIndex != null && !items[playingIndex]) setPlayingIndex(null);
  }, [items, playingIndex]);

  // Keep the feed live while online. Skipped whenever a refresh is already
  // running or the player is open, so it never yanks the list under you.
  useEffect(() => {
    if (!online) return;
    const id = setInterval(() => {
      if (!refreshing && playingIndex == null) void doRefresh();
    }, AUTO_REFRESH_MS);
    return () => clearInterval(id);
  }, [online, refreshing, playingIndex, doRefresh]);

  const emptyMessage = () => {
    if (loading) return "Loading…";
    if (channels.length === 0)
      return "No subscriptions yet. Open Settings — it walks you through exporting them from Google Takeout.";
    if (totals.videos === 0) return "Subscriptions imported. Hit Refresh to pull in their latest videos.";
    if (!online) return "You're offline, and nothing has been downloaded yet.";
    if (effectiveDownloadedOnly) return "No downloaded videos match this filter.";
    return "Nothing matches this filter.";
  };

  return (
    <div className="flex h-screen flex-col">
      {/* The webview paints the title bar itself. It sits directly above the
          sidebar and top bar, so it takes the panel colour, not the page's.
          In macOS window fullscreen the traffic lights are gone, so the strip
          would just be a blank bar — it collapses instead. */}
      {!windowFullscreen && (
        <div
          data-tauri-drag-region
          className="h-9 shrink-0 bg-white dark:bg-slate-900"
        />
      )}

      <div className="relative flex min-h-0 flex-1 flex-col lg:flex-row">
        {/* With the sidebar hidden, a thin strip along the left edge brings it
            back on hover. */}
        {sidebarHidden && (
          <div
            onMouseEnter={() => setSidebarPeek(true)}
            className="absolute inset-y-0 left-0 z-20 w-3"
            aria-hidden
          />
        )}
        {sidebarHidden && sidebarPeek && (
          <div
            onMouseLeave={() => setSidebarPeek(false)}
            className="absolute inset-y-0 left-0 z-30 w-[280px]"
          >
            <Sidebar
              floating
              channels={channels}
              activeChannel={channelId}
              onSelect={(id) => { setChannelId(id); setSidebarPeek(false); }}
              onOpenSettings={() => setShowSettings(true)}
              totalVideos={totals.videos}
              totalDownloaded={totals.downloaded}
              onHide={() => { setSidebarHidden(true); setSidebarPeek(false); }}
            />
          </div>
        )}

        {!sidebarHidden && (
          <Sidebar
            channels={channels}
            activeChannel={channelId}
            onSelect={setChannelId}
            onOpenSettings={() => setShowSettings(true)}
            totalVideos={totals.videos}
            totalDownloaded={totals.downloaded}
            onHide={() => setSidebarHidden(true)}
          />
        )}

      <main className="flex min-h-0 min-w-0 flex-1 flex-col">
        <TopBar
          search={search} onSearch={setSearch}
          downloadedOnly={effectiveDownloadedOnly} onDownloadedOnly={setDownloadedOnly}
          hideShorts={hideShorts} onHideShorts={setHideShorts}
          online={online} reachable={reachable} forcedOffline={forcedOffline}
          onToggleForcedOffline={() => { setForcedOffline(!forcedOffline); probe(); }}
          onRefresh={doRefresh} refreshing={refreshing} refreshProgress={refreshProgress}
          resultCount={items.length}
          view={view} onView={setView}
          sidebarHidden={sidebarHidden}
          onShowSidebar={() => { setSidebarHidden(false); setSidebarPeek(false); }}
        />

        {!online && (
          <div
            className="border-b border-amber-500/30 bg-amber-500/10 px-4 py-2 text-[11px]
                       leading-snug text-amber-700 dark:text-amber-300"
          >
            Offline — showing only videos you've downloaded.
            {forcedOffline && " Offline mode is forced on."}
          </div>
        )}

        <div className="min-h-0 flex-1 overflow-y-auto p-3 lg:p-5">
          {items.length === 0 ? (
            <div className="grid h-full place-items-center">
              <p className="max-w-sm text-center text-[12px] leading-relaxed text-slate-500 dark:text-slate-400">
                {emptyMessage()}
              </p>
            </div>
          ) : (
            <ul
              className={
                view === "grid"
                  ? "mx-auto grid max-w-7xl grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-x-4 gap-y-6"
                  : "mx-auto max-w-4xl space-y-1.5"
              }
            >
              {items.map((item, idx) => {
                const shared = {
                  item,
                  live: live[item.id],
                  online,
                  onOpen: () => openIndex(idx),
                  onDownload: () =>
                    downloadVideo(item.id, quality).catch((e) => setFailure(String(e))),
                  onCancel: () =>
                    cancelDownload(item.id).catch((e) => setFailure(String(e))),
                  onDelete: () =>
                    deleteDownload(item.id)
                      .then(() => { clearLive(item.id); reload(); say("Download deleted"); })
                      .catch((e) => setFailure(String(e))),
                };
                return view === "grid" ? (
                  <VideoTile key={item.id} {...shared} />
                ) : (
                  <VideoRow key={item.id} {...shared} />
                );
              })}
            </ul>
          )}
        </div>
      </main>
      </div>

      {playing && playingIndex != null && (
        <Player
          key={playing.item.id}
          item={playing.item}
          path={playing.path}
          index={playingIndex}
          total={items.length}
          maxHeight={streamQuality === "best" ? null : Number(streamQuality)}
          onPrev={
            stepFrom(playingIndex, -1) != null
              ? () => setPlayingIndex(stepFrom(playingIndex, -1))
              : undefined
          }
          onNext={
            stepFrom(playingIndex, 1) != null
              ? () => setPlayingIndex(stepFrom(playingIndex, 1))
              : undefined
          }
          onDownload={
            playing.path === null
              ? () => {
                  downloadVideo(playing.item.id, quality).catch((e) => setFailure(String(e)));
                  say("Download started");
                }
              : undefined
          }
          downloading={["queued", "running"].includes(
            live[playing.item.id]?.state ?? playing.item.state ?? "",
          )}
          onClose={() => { setPlayingIndex(null); reload(); }}
          onDelete={async () => {
            await deleteDownload(playing.item.id);
            clearLive(playing.item.id);
            setPlayingIndex(null);
            reload();
            say("Download deleted");
          }}
        />
      )}

      {showSettings && (
        <Settings
          onClose={() => setShowSettings(false)}
          appearance={mode}
          onAppearance={setMode}
          quality={quality}
          onQuality={setQuality}
          streamQuality={streamQuality}
          onStreamQuality={setStreamQuality}
          onError={setFailure}
          onImported={(n) => {
            reload();
            say(`Imported ${n} subscription${n === 1 ? "" : "s"}`);
          }}
        />
      )}

      {failure && (
        <Dialog title="Something went wrong" onCancel={() => setFailure(null)}>
          <p className="break-words">{failure}</p>
        </Dialog>
      )}

      <Toast message={toast} />
    </div>
  );
}
