'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast';

// Per-photo download state for the public gallery. The photo-api serves full
// quality originals, which takes a few seconds, so downloads run through
// fetch() instead of a bare <a download> — that gives us a spinner, a real
// success/failure signal, a timeout and abort-on-unmount.

// Originals run to ~20 MB, so a single flat timeout would either be too short
// for a phone on mobile data or useless as a stall detector. Time out on the
// response headers instead, then give the body a generous ceiling.
const HEADERS_TIMEOUT_MS = 30_000;
const BODY_TIMEOUT_MS = 10 * 60_000;

export interface DownloadRequest {
  /** Photo id; keys the in-flight state so tiles stay independent. */
  id: string;
  url: string;
  filename?: string;
}

/** Saves fetched bytes without navigating away from the gallery. */
function saveBlob(blob: Blob, filename: string) {
  const objectUrl = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = objectUrl;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  // Give the browser time to start the save before the URL is invalidated.
  setTimeout(() => URL.revokeObjectURL(objectUrl), 10_000);
}

/**
 * Plain-anchor download, i.e. what the page did before this hook existed.
 * Used as a fallback when fetch() itself fails: with S3 storage the file
 * endpoint 302s to a presigned URL on another origin, which a cross-origin
 * fetch cannot read but a navigation downloads fine (the presigned URL
 * carries its own Content-Disposition).
 */
function navigateToDownload({ url, filename }: DownloadRequest) {
  const a = document.createElement('a');
  a.href = url;
  if (filename) a.download = filename;
  a.rel = 'noopener';
  document.body.appendChild(a);
  a.click();
  a.remove();
}

export function useDownloads(es: boolean) {
  const [pending, setPending] = useState<ReadonlySet<string>>(() => new Set());
  const controllers = useRef(new Map<string, AbortController>());
  const unmounted = useRef(false);
  // Lets the retry button in the error toast call the latest `start`.
  const startRef = useRef<(req: DownloadRequest) => void>(() => {});

  useEffect(() => {
    unmounted.current = false;
    return () => {
      unmounted.current = true;
      controllers.current.forEach((c) => c.abort());
      controllers.current.clear();
    };
  }, []);

  const start = useCallback(
    async (req: DownloadRequest) => {
      // Repeat clicks while the same photo is in flight are no-ops; other
      // photos are unaffected because state is keyed by id.
      if (controllers.current.has(req.id)) return;

      const controller = new AbortController();
      controllers.current.set(req.id, controller);
      setPending((prev) => new Set(prev).add(req.id));

      let timer = setTimeout(() => controller.abort(), HEADERS_TIMEOUT_MS);
      const timedOut = () => controller.signal.aborted && !unmounted.current;

      try {
        const res = await fetch(req.url, {
          credentials: 'same-origin',
          signal: controller.signal,
        });
        clearTimeout(timer);
        timer = setTimeout(() => controller.abort(), BODY_TIMEOUT_MS);
        if (!res.ok) throw new Error(`Download failed (${res.status})`);
        const blob = await res.blob();
        saveBlob(blob, req.filename || `${req.id}.jpg`);
      } catch (err) {
        if (unmounted.current) return; // page gone, nothing to report
        if (err instanceof TypeError) {
          // Network-level failure — most likely a cross-origin presigned
          // redirect. Hand it to the browser, which can follow it.
          navigateToDownload(req);
        } else {
          const message = timedOut()
            ? es
              ? 'La descarga tardó demasiado.'
              : 'The download timed out.'
            : es
              ? 'No se pudo descargar la foto.'
              : 'Could not download the photo.';
          toast.error(
            (t) => (
              <span className="flex items-center gap-3">
                {message}
                <button
                  type="button"
                  onClick={() => {
                    toast.dismiss(t.id);
                    startRef.current(req);
                  }}
                  className="font-medium underline underline-offset-2 whitespace-nowrap"
                >
                  {es ? 'Reintentar' : 'Retry'}
                </button>
              </span>
            ),
            { duration: 6000 }
          );
        }
      } finally {
        clearTimeout(timer);
        controllers.current.delete(req.id);
        if (!unmounted.current) {
          setPending((prev) => {
            const next = new Set(prev);
            next.delete(req.id);
            return next;
          });
        }
      }
    },
    [es]
  );

  useEffect(() => {
    startRef.current = start;
  }, [start]);

  const isPending = useCallback((id: string) => pending.has(id), [pending]);

  return { start, isPending };
}
