import { player } from './store.svelte';
import { api } from '$lib/api/client';
import { getOrCreateClientId } from './clientId';
import type { PlayStartedResponse, EventOkResponse } from '$lib/api/types';

// useEventsDispatcher installs $effect-based watchers that emit play events
// to /api/events. Call once from +layout.svelte's mount.
//
// The dispatcher is a small state machine over (current track id, state).
// Per spec data-flow:
//   - First transition into 'playing' for a new track → POST play_started.
//   - Track id changes while a play is open → POST play_skipped on the prior.
//   - Natural completion (state moves from 'playing' to 'paused' AND
//     position >= duration > 0) → POST play_ended on the open row.
//   - pagehide with a live row → navigator.sendBeacon a play_skipped.
export function useEventsDispatcher(): void {
  let openPlayEventId: string | null = null;
  let openTrackId: string | null = null;
  let lastPositionMs = 0;
  let prevTrackId: string | null = null;
  let prevState: string | null = null;
  const clientId = getOrCreateClientId();

  // Track the latest position for use by close events. Cheap; no API call.
  $effect(() => {
    lastPositionMs = Math.round(player.position * 1000);
  });

  // Main state machine. Reacts only to (current track id, state) transitions.
  $effect(() => {
    const t = player.current;
    const s = player.state;
    const tid = t?.id ?? null;

    // Track changed: close any prior open play as a skip.
    if (tid !== prevTrackId && openPlayEventId) {
      const id = openPlayEventId;
      const pos = lastPositionMs;
      openPlayEventId = null;
      openTrackId = null;
      api.post<EventOkResponse>('/api/events', {
        type: 'play_skipped',
        play_event_id: id,
        position_ms: pos
      }).catch(() => {});
    }

    // Entered playing for a new track (no open row yet for this track).
    if (tid && s === 'playing' && openTrackId !== tid) {
      void startNew(tid);
    }

    // Natural end: state went playing -> paused while at/past duration.
    if (
      prevState === 'playing' &&
      s === 'paused' &&
      openPlayEventId &&
      player.duration > 0 &&
      player.position >= player.duration
    ) {
      const id = openPlayEventId;
      const dur = lastPositionMs;
      openPlayEventId = null;
      openTrackId = null;
      api.post<EventOkResponse>('/api/events', {
        type: 'play_ended',
        play_event_id: id,
        duration_played_ms: dur
      }).catch(() => {});
    }

    prevTrackId = tid;
    prevState = s;
  });

  // Page close: sendBeacon a play_skipped if an open play exists.
  if (typeof window !== 'undefined') {
    const onPageHide = () => {
      if (!openPlayEventId) return;
      const payload = JSON.stringify({
        type: 'play_skipped',
        play_event_id: openPlayEventId,
        position_ms: lastPositionMs
      });
      navigator.sendBeacon('/api/events', new Blob([payload], { type: 'application/json' }));
    };
    window.addEventListener('pagehide', onPageHide);
  }

  async function startNew(trackId: string): Promise<void> {
    try {
      const res = await api.post<PlayStartedResponse>('/api/events', {
        type: 'play_started',
        track_id: trackId,
        client_id: clientId
      });
      // The user may have moved on by the time the response arrives. Only
      // adopt the id if we're still on the same track and still playing.
      if (player.current?.id === trackId && player.state === 'playing') {
        openPlayEventId = res.play_event_id;
        openTrackId = trackId;
      }
    } catch {
      // Best-effort; missed events are OK in v1.
    }
  }
}
