import { buildStreamUrlForServer } from './api/subsonicStreamUrl';
import { getPlaybackCacheServerKey } from './utils/playback/playbackServer';
import type { QueueItemRef } from './store/playerStoreTypes';
import { resolveQueueTrack } from './utils/library/queueTrackView';
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from './store/authStore';
import { selectHotCacheEntries, useHotCacheStore } from './store/hotCacheStore';
import { useLocalPlaybackStore } from './store/localPlaybackStore';
import { getMediaDir } from './utils/media/mediaDir';
import { librarySqlServerId } from './api/coverCache';
import { usePlayerStore } from './store/playerStore';
import {
  bumpHotCachePreviousTrackGrace,
  clearHotCachePreviousGrace,
  getDeferHotCachePrefetch,
} from './utils/cache/hotCacheGate';
import {
  PREFETCH_AHEAD,
  type PrefetchJob,
  entryKey,
  sumCachedBytesInProtectedWindow,
  estimateTrackHotCacheBytes,
  hotCacheFrontendDebug,
  debounceMs,
} from './hotCachePrefetch/helpers';
import {
  scheduleAnalysisQueuePruneFromPlaybackQueue,
  resetAnalysisPruneState,
} from './hotCachePrefetch/analysisPrune';
import { reconcileEphemeralCache } from './utils/cache/ephemeralTierReconcile';
import { hasLocalPersistentPlaybackBytes } from './utils/offline/offlineLibraryHelpers';

/** Periodic index↔disk sync (stale rows + empty dirs); unindexed files evicted only on budget pressure. */
const EPHEMERAL_MAINTENANCE_MS = 10 * 60 * 1000;

let debounceTimer: ReturnType<typeof setTimeout> | null = null;
/** Fires `replanNow` once grace for the ex-current track ends so eviction can drop it. */
let graceEvictTimer: ReturnType<typeof setTimeout> | null = null;
const pendingQueue: PrefetchJob[] = [];
let workerRunning = false;

function scheduleEvictAfterPreviousGrace(): void {
  if (graceEvictTimer) {
    clearTimeout(graceEvictTimer);
    graceEvictTimer = null;
  }
  const ms = debounceMs();
  if (ms <= 0) {
    void replanNow();
    return;
  }
  graceEvictTimer = setTimeout(() => {
    graceEvictTimer = null;
    void replanNow();
  }, ms);
}

/** Prefetch the current (paused) track so cold resume can hit disk instead of HTTP. */
export function scheduleHotCachePrefetchForTrack(track: { id: string; suffix?: string }, serverId: string | null): void {
  const auth = useAuthStore.getState();
  if (!auth.isLoggedIn || !auth.hotCacheEnabled || !serverId) return;
  if (hasLocalPersistentPlaybackBytes(track.id, serverId)) return;
  const hotIndex = selectHotCacheEntries(useLocalPlaybackStore.getState().entries);
  if (hotIndex[entryKey(serverId, track.id)]) return;
  enqueueJobs([{ trackId: track.id, serverId, suffix: track.suffix || 'mp3' }]);
}

function enqueueJobs(jobs: PrefetchJob[]) {
  const seen = new Set(pendingQueue.map(j => `${j.serverId}:${j.trackId}`));
  let merged = 0;
  for (const j of jobs) {
    const k = `${j.serverId}:${j.trackId}`;
    if (seen.has(k)) continue;
    seen.add(k);
    pendingQueue.push(j);
    merged++;
  }
  if (merged > 0) {
    hotCacheFrontendDebug({
      event: 'prefetch-queue-jobs',
      added: merged,
      pendingTotal: pendingQueue.length,
      trackIds: jobs.map(j => j.trackId),
    });
  }
  void runWorker();
}

async function runWorker() {
  if (workerRunning) return;
  workerRunning = true;
  try {
    while (pendingQueue.length > 0) {
      const auth = useAuthStore.getState();
      const playbackSid = getPlaybackCacheServerKey();
      if (!auth.isLoggedIn || !auth.hotCacheEnabled || !playbackSid) {
        hotCacheFrontendDebug({
          event: 'prefetch-worker-stop',
          reason: 'auth-disabled-or-logged-out',
          clearedPending: pendingQueue.length,
        });
        pendingQueue.length = 0;
        break;
      }

      while (getDeferHotCachePrefetch()) {
        await new Promise(r => setTimeout(r, 150));
      }

      const job = pendingQueue.shift();
      if (!job) break;

      const maxBytes = Math.max(0, auth.hotCacheMaxMb) * 1024 * 1024;
      if (maxBytes <= 0) {
        hotCacheFrontendDebug({ event: 'prefetch-skip-job', trackId: job.trackId, reason: 'max-mb-zero' });
        continue;
      }

      if (hasLocalPersistentPlaybackBytes(job.trackId, job.serverId)) {
        hotCacheFrontendDebug({
          event: 'prefetch-skip-job',
          trackId: job.trackId,
          reason: 'persistent-local-bytes',
        });
        continue;
      }
      const hotIndex = selectHotCacheEntries(useLocalPlaybackStore.getState().entries);
      if (hotIndex[entryKey(job.serverId, job.trackId)]) {
        hotCacheFrontendDebug({
          event: 'prefetch-skip-job',
          trackId: job.trackId,
          reason: 'already-in-hot-index',
        });
        continue;
      }

      const player = usePlayerStore.getState();
      const { queueItems, queueIndex } = player;
      const upcomingRefs = queueItems.slice(queueIndex + 1, queueIndex + 1 + PREFETCH_AHEAD);
      const wantIds = new Set(upcomingRefs.map(r => r.trackId));
      if (!wantIds.has(job.trackId)) {
        hotCacheFrontendDebug({
          event: 'prefetch-skip-job',
          trackId: job.trackId,
          reason: 'not-in-upcoming-window',
          queueIndex,
          window: PREFETCH_AHEAD,
        });
        continue;
      }

      // Thin-state: the upcoming window sits inside the resolver-warm range, so
      // resolveQueueTrack returns the full Track (placeholder only on a cold
      // miss, where the size estimate falls back to the bitrate heuristic).
      const jobRef = upcomingRefs.find(r => r.trackId === job.trackId);
      if (!jobRef) {
        hotCacheFrontendDebug({
          event: 'prefetch-skip-job',
          trackId: job.trackId,
          reason: 'track-not-in-queue',
        });
        continue;
      }
      const track = resolveQueueTrack(jobRef);
      const hotEntries = selectHotCacheEntries(useLocalPlaybackStore.getState().entries);
      const occupied = sumCachedBytesInProtectedWindow(queueItems, queueIndex, job.serverId, hotEntries);
      const est = estimateTrackHotCacheBytes(track);
      const isImmediateNext = queueItems[queueIndex + 1]?.trackId === job.trackId;
      if (!isImmediateNext && occupied + est > maxBytes) {
        hotCacheFrontendDebug({
          event: 'prefetch-skip-job',
          trackId: job.trackId,
          reason: 'budget-protected-window-plus-estimate',
          occupied,
          estimateBytes: est,
          maxBytes,
        });
        continue;
      }

      const url = buildStreamUrlForServer(job.serverId, job.trackId);
      try {
        const mediaDir = getMediaDir();
        hotCacheFrontendDebug({ event: 'prefetch-invoke', trackId: job.trackId });
        const res = await invoke<{ path: string; size: number; layoutFingerprint: string }>('download_track_local', {
          tier: 'ephemeral',
          trackId: job.trackId,
          serverIndexKey: job.serverId,
          libraryServerId: librarySqlServerId(job.serverId),
          url,
          suffix: job.suffix,
          mediaDir,
          downloadId: null,
        });
        useHotCacheStore.getState().setEntry(
          job.trackId,
          job.serverId,
          res.path,
          res.size,
          'prefetch',
          res.layoutFingerprint,
          job.suffix,
        );
        hotCacheFrontendDebug({ event: 'prefetch-stored', trackId: job.trackId, sizeBytes: res.size });
        const fresh = usePlayerStore.getState();
        const authAfter = useAuthStore.getState();
        const maxAfter = Math.max(0, authAfter.hotCacheMaxMb) * 1024 * 1024;
        await useHotCacheStore.getState().evictToFit(
          fresh.queueItems,
          fresh.queueIndex,
          maxAfter,
          getPlaybackCacheServerKey(),
          getMediaDir(),
        );
      } catch (e: unknown) {
        const msg = String(e);
        if (msg.includes('TRACK_NOT_INDEXED')) {
          hotCacheFrontendDebug({
            event: 'prefetch-skip-job',
            trackId: job.trackId,
            reason: 'track-not-indexed',
          });
          continue;
        }
        hotCacheFrontendDebug({ event: 'prefetch-download-failed', trackId: job.trackId, error: msg });
      }
    }
  } finally {
    workerRunning = false;
    if (pendingQueue.length > 0) void runWorker();
  }
}

function scheduleReplan() {
  const auth = useAuthStore.getState();
  const playbackSid = getPlaybackCacheServerKey();
  if (!auth.isLoggedIn || !auth.hotCacheEnabled || !playbackSid) {
    if (debounceTimer) {
      clearTimeout(debounceTimer);
      debounceTimer = null;
    }
    return;
  }

  if (debounceTimer) clearTimeout(debounceTimer);
  const ms = debounceMs();
  debounceTimer = setTimeout(() => {
    debounceTimer = null;
    void replanNow();
  }, ms);
}

async function replanNow() {
  const auth = useAuthStore.getState();
  const playbackSid = getPlaybackCacheServerKey();
  if (!auth.isLoggedIn || !auth.hotCacheEnabled || !playbackSid) return;

  const serverId = playbackSid;
  const maxBytes = Math.max(0, auth.hotCacheMaxMb) * 1024 * 1024;
  const mediaDir = getMediaDir();
  if (maxBytes <= 0) return;

  const { queueItems, queueIndex, currentRadio } = usePlayerStore.getState();
  if (currentRadio) {
    hotCacheFrontendDebug({ event: 'replan-skip', reason: 'radio-mode' });
    return;
  }

  await useHotCacheStore.getState().evictToFit(queueItems, queueIndex, maxBytes, serverId, mediaDir);

  // Must read entries after eviction: the pre-evict snapshot still lists removed keys and would
  // skip prefetch for upcoming tracks that no longer have on-disk rows.
  const hotEntries = selectHotCacheEntries(useLocalPlaybackStore.getState().entries);

  // Thin-state: resolve only the small upcoming window (within the resolver-warm
  // range) to full Tracks for the size estimates / suffix.
  const targetRefs = queueItems.slice(queueIndex + 1, queueIndex + 1 + PREFETCH_AHEAD);
  const targets = targetRefs.map(r => resolveQueueTrack(r));
  const immediateNextId = queueItems[queueIndex + 1]?.trackId;
  let projectedOccupied = sumCachedBytesInProtectedWindow(queueItems, queueIndex, serverId, hotEntries);
  const jobs: PrefetchJob[] = [];
  const skipped: { trackId: string; reason: string }[] = [];
  for (const t of targets) {
    if (hasLocalPersistentPlaybackBytes(t.id, serverId)) {
      skipped.push({ trackId: t.id, reason: 'persistent-local-bytes' });
      continue;
    }
    if (hotEntries[entryKey(serverId, t.id)]) {
      skipped.push({ trackId: t.id, reason: 'already-in-hot-index' });
      continue;
    }
    const isImmediateNext = t.id === immediateNextId;
    if (isImmediateNext) {
      jobs.push({ trackId: t.id, serverId, suffix: t.suffix || 'mp3' });
      continue;
    }
    const est = estimateTrackHotCacheBytes(t);
    if (projectedOccupied + est > maxBytes) {
      skipped.push({ trackId: t.id, reason: 'budget-cap-rest-deferred' });
      break;
    }
    projectedOccupied += est;
    jobs.push({ trackId: t.id, serverId, suffix: t.suffix || 'mp3' });
  }
  hotCacheFrontendDebug({
    event: 'replan',
    queueIndex,
    aheadCount: targets.length,
    scheduledIds: jobs.map(j => j.trackId),
    skipped,
    projectedOccupiedBytes: projectedOccupied,
    maxBytes,
  });
  enqueueJobs(jobs);
}

/**
 * Subscribe to queue/auth changes and run debounced prefetch.
 * Call once from the app shell.
 */
export function initHotCachePrefetch(): () => void {
  let lastQueueRef: unknown = null;
  let lastQueueIndex = -1;
  const unsubPlayer = usePlayerStore.subscribe(state => {
    const q = state.queueItems;
    const i = state.queueIndex;
    if (q === lastQueueRef && i === lastQueueIndex) return;
    const prevIdx = lastQueueIndex;
    const prevQ = lastQueueRef;
    const onlyIndexMoved = q === lastQueueRef && i !== lastQueueIndex;
    lastQueueRef = q;
    lastQueueIndex = i;
    scheduleAnalysisQueuePruneFromPlaybackQueue();
    if (onlyIndexMoved && i > prevIdx && prevIdx >= 0 && Array.isArray(prevQ)) {
      const left = (prevQ as QueueItemRef[])[prevIdx];
      const a = useAuthStore.getState();
      const graceSid = getPlaybackCacheServerKey();
      if (left && graceSid) {
        bumpHotCachePreviousTrackGrace(left.trackId, graceSid, a.hotCacheDebounceSec);
        scheduleEvictAfterPreviousGrace();
      }
    }
    if (onlyIndexMoved) void replanNow();
    else scheduleReplan();
  });

  let lastAuthSig = '';
  const unsubAuth = useAuthStore.subscribe((state, prev) => {
    const sig = `${state.hotCacheEnabled}:${state.hotCacheDebounceSec}:${state.hotCacheMaxMb}:${state.mediaDir ?? ''}:${state.activeServerId ?? ''}:${state.isLoggedIn}`;
    if (sig === lastAuthSig) return;
    lastAuthSig = sig;

    if (debounceTimer) {
      clearTimeout(debounceTimer);
      debounceTimer = null;
    }

    if (!state.hotCacheEnabled || !state.isLoggedIn) {
      hotCacheFrontendDebug({ event: 'prefetch-auth-off', clearedPending: pendingQueue.length });
      pendingQueue.length = 0;
      clearHotCachePreviousGrace();
      return;
    }

    const budgetSettingsChanged =
      !prev ||
      state.hotCacheMaxMb !== prev.hotCacheMaxMb ||
      state.mediaDir !== prev.mediaDir ||
      state.hotCacheEnabled !== prev.hotCacheEnabled ||
      state.activeServerId !== prev.activeServerId ||
      state.isLoggedIn !== prev.isLoggedIn;

    const onlyDebounceChanged =
      !!prev &&
      state.hotCacheDebounceSec !== prev.hotCacheDebounceSec &&
      !budgetSettingsChanged;

    if (budgetSettingsChanged) {
      if (prev && state.hotCacheMaxMb < prev.hotCacheMaxMb) {
        hotCacheFrontendDebug({
          event: 'prefetch-pending-cleared',
          reason: 'hot-cache-max-mb-decreased',
          prevMb: prev.hotCacheMaxMb,
          nextMb: state.hotCacheMaxMb,
          droppedJobs: pendingQueue.length,
        });
        pendingQueue.length = 0;
      }
      void replanNow();
    } else if (onlyDebounceChanged) {
      scheduleReplan();
    }
  });

  void replanNow();
  scheduleAnalysisQueuePruneFromPlaybackQueue();

  const maintenanceTimer = window.setInterval(() => {
    const auth = useAuthStore.getState();
    if (!auth.isLoggedIn || !auth.hotCacheEnabled) return;
    void reconcileEphemeralCache();
  }, EPHEMERAL_MAINTENANCE_MS);

  return () => {
    unsubPlayer();
    unsubAuth();
    if (debounceTimer) clearTimeout(debounceTimer);
    debounceTimer = null;
    if (graceEvictTimer) clearTimeout(graceEvictTimer);
    graceEvictTimer = null;
    window.clearInterval(maintenanceTimer);
    resetAnalysisPruneState();
    pendingQueue.length = 0;
    clearHotCachePreviousGrace();
  };
}
