import { useCallback, useEffect, useState } from 'react';
import type {
  IncomingTransfer,
  TransferHandle,
  TransferOutput,
  TransferProgress,
  TransferResult,
  ThumbnailMime,
} from '@shade/sdk';
import { isAllowedThumbnailMime } from '@shade/sdk';
import { useShadeRuntime } from './ShadeRuntimeProvider.js';
import { useThumbnailCache } from './useShadeThumbnails.js';

export interface DownloadEntry {
  incoming: IncomingTransfer;
  handle: TransferHandle | null;
  progress: TransferProgress | null;
  error: unknown;
  done: boolean;
}

export interface UseShadeDownloadOptions {
  /**
   * If provided, accept transfers automatically with the supplied output
   * (called once per incoming). Useful for "auto-save to /uploads" servers.
   */
  autoAccept?: (incoming: IncomingTransfer) => Promise<TransferOutput | null>;
}

export interface UseShadeDownloadResult {
  /** Pending transfers awaiting accept/decline. */
  pending: DownloadEntry[];
  /** In-flight transfers being received. */
  active: DownloadEntry[];
  /** Manually accept a pending transfer with a chosen output. */
  accept: (streamId: string, output: TransferOutput) => Promise<TransferHandle>;
  /** Manually decline a pending transfer. */
  decline: (streamId: string, reason?: string) => Promise<void>;
  dismiss: (streamId: string) => void;
}

/**
 * Headless hook for download tracking. Subscribes to incoming transfers
 * and exposes a list of pending and active downloads.
 */
export function useShadeDownload(
  options: UseShadeDownloadOptions = {},
): UseShadeDownloadResult {
  const shade = useShadeRuntime();
  const thumbnailCache = useThumbnailCache();
  const [pending, setPending] = useState<DownloadEntry[]>([]);
  const [active, setActive] = useState<DownloadEntry[]>([]);

  const trackHandle = useCallback(
    (incoming: IncomingTransfer, handle: TransferHandle) => {
      const entry: DownloadEntry = {
        incoming,
        handle,
        progress: null,
        error: null,
        done: false,
      };
      setPending((prev) => prev.filter((p) => p.incoming.streamId !== incoming.streamId));
      setActive((prev) => [entry, ...prev]);

      void (async () => {
        try {
          for await (const ev of handle.events) {
            if (ev.type === 'progress') {
              setActive((prev) =>
                prev.map((a) =>
                  a.incoming.streamId === incoming.streamId ? { ...a, progress: ev.progress } : a,
                ),
              );
            } else if (ev.type === 'complete') {
              setActive((prev) =>
                prev.map((a) =>
                  a.incoming.streamId === incoming.streamId ? { ...a, done: true } : a,
                ),
              );
            } else if (ev.type === 'error') {
              setActive((prev) =>
                prev.map((a) =>
                  a.incoming.streamId === incoming.streamId
                    ? { ...a, error: ev.error, done: true }
                    : a,
                ),
              );
            }
          }
        } catch (err) {
          setActive((prev) =>
            prev.map((a) =>
              a.incoming.streamId === incoming.streamId
                ? { ...a, error: err, done: true }
                : a,
            ),
          );
        }
      })();
    },
    [],
  );

  useEffect(() => {
    let unsubscribe: (() => void) | null = null;
    let cancelled = false;
    void (async () => {
      const cleanup = await shade.onIncomingTransfer(async (incoming) => {
        // V3.9 — auto-accept thumbnail streams into the in-memory cache.
        // Sender marks them with `userMetadata.shadeThumbnail === '1'`.
        if (incoming.metadata.userMetadata?.shadeThumbnail === '1') {
          const handle = await incoming.accept({ output: { kind: 'buffer' } });
          void handle
            .done()
            .then((res: TransferResult) => {
              const bytes = (res as TransferResult & { bytes?: Uint8Array }).bytes;
              const declaredMime = incoming.metadata.contentType;
              if (
                bytes !== undefined &&
                declaredMime !== undefined &&
                isAllowedThumbnailMime(declaredMime)
              ) {
                thumbnailCache.put(
                  incoming.streamId,
                  bytes,
                  declaredMime as ThumbnailMime,
                );
              }
            })
            .catch(() => {
              /* preview failures are non-fatal */
            });
          return;
        }
        // V3.9 — when the main `stream-init` arrives, register the
        // expected thumbnail hash so a previously-arrived (but not yet
        // verified) thumbnail can be matched + made visible to widgets.
        const fm = incoming.metadata.fileMetadata;
        if (fm?.thumbnailStreamId !== undefined && fm.thumbnailHash !== undefined) {
          thumbnailCache.setExpectedHash(fm.thumbnailStreamId, fm.thumbnailHash);
        }
        if (options.autoAccept !== undefined) {
          const output = await options.autoAccept(incoming);
          if (output !== null) {
            const handle = await incoming.accept({ output });
            trackHandle(incoming, handle);
            return;
          }
        }
        const entry: DownloadEntry = {
          incoming,
          handle: null,
          progress: null,
          error: null,
          done: false,
        };
        setPending((prev) => [entry, ...prev]);
      });
      if (cancelled) cleanup();
      else unsubscribe = cleanup;
    })();
    return () => {
      cancelled = true;
      unsubscribe?.();
    };
  }, [shade, options.autoAccept, trackHandle, thumbnailCache]);

  const accept = useCallback(
    async (streamId: string, output: TransferOutput): Promise<TransferHandle> => {
      const entry = pending.find((p) => p.incoming.streamId === streamId);
      if (entry === undefined) {
        throw new Error(`No pending transfer with streamId=${streamId}`);
      }
      const handle = await entry.incoming.accept({ output });
      trackHandle(entry.incoming, handle);
      return handle;
    },
    [pending, trackHandle],
  );

  const decline = useCallback(
    async (streamId: string, reason?: string) => {
      const entry = pending.find((p) => p.incoming.streamId === streamId);
      if (entry !== undefined) {
        await entry.incoming.decline(reason);
        setPending((prev) => prev.filter((p) => p.incoming.streamId !== streamId));
      }
    },
    [pending],
  );

  const dismiss = useCallback((streamId: string) => {
    setPending((prev) => prev.filter((p) => p.incoming.streamId !== streamId));
    setActive((prev) => prev.filter((a) => a.incoming.streamId !== streamId));
  }, []);

  return { pending, active, accept, decline, dismiss };
}
