import { useEffect, useRef, useState } from "react";
import { api, ApiError } from "../api";
import type { BreedEstimateResult, PhotoSearchMatch, PhotoSearchResult } from "../types";
import { estimatedBreeds } from "./MatchCard";
import { prepareImage } from "../lib/prepareImage";
import BreedResults from "./BreedResults";
import ComparePhotosModal, { photosFromUrls } from "./ComparePhotosModal";
import SearchProgress, { MIN_PROGRESS_MS } from "./SearchProgress";
import TabButton from "./TabButton";
import { BreedNote, MatchingNote } from "./ToolNotes";

type Mode = "match" | "breed";
type Pool = "found" | "lost"; // "found": I lost my dog, search found dogs; "lost": I found a dog

interface PickedImage {
  file: File;
  /** Preview URL, or null when this browser couldn't decode the file (still uploadable). */
  url: string | null;
}

const MAX_IMAGES = 6;

/** Turn a failed request into something a visitor can act on. */
function describeRequestError(err: unknown): string {
  if (err instanceof ApiError) {
    // 400s carry a specific, already human-readable reason (unreadable format, too large, ...).
    if (err.status === 400) return err.message || "That image couldn't be read.";
    if (err.status === 413) return "That image is too large to upload. Try a smaller one.";
    if (err.status === 429) return "Too many requests just now. Give it a few seconds and retry.";
    if (err.status >= 500) return "The server had a problem running the model. Please try again.";
    return err.message;
  }
  // fetch() rejects (Safari says "Load failed") on a dropped or timed-out connection.
  return "The upload didn't complete. Check your connection and try again.";
}

// Public home-page tool: upload up to 6 dog images, then run image matching, breed estimation, or
// both. More images (different angles/lighting) only help matching — the server scores every query
// image against every candidate image and keeps the best pair. Breed estimation uses the first
// image. Read-only — stores nothing and creates no case. Match results carry their own photos so a
// viewer can open without an authenticated endpoint.
// `lockedPool` forces the search direction and hides the toggle — used by the demo, whose haystack is
// a single found/unclaimed pool (there is no registered-lost pool to search). `haystackHint` is the
// searchable-dog count shown in the progress indicator while a match runs.
export default function PhotoTools({
  lockedPool,
  haystackHint,
}: { lockedPool?: Pool; haystackHint?: number } = {}) {
  const [images, setImages] = useState<PickedImage[]>([]);
  const BREED_COUNT = 10; // always show the model's full top-10; no picker to fiddle with
  const [pool, setPool] = useState<Pool>(lockedPool ?? "found");
  const [dragging, setDragging] = useState(false);
  const [preparing, setPreparing] = useState(false);
  const [tab, setTab] = useState<Mode>("match");
  const [running, setRunning] = useState<Mode | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [matchData, setMatchData] = useState<PhotoSearchResult | null>(null);
  const [breedData, setBreedData] = useState<BreedEstimateResult | null>(null);
  const [active, setActive] = useState<PhotoSearchMatch | null>(null);

  // Release every preview blob URL when the component unmounts (removeImage handles the rest).
  const imagesRef = useRef(images);
  imagesRef.current = images;
  useEffect(
    () => () => imagesRef.current.forEach((i) => i.url && URL.revokeObjectURL(i.url)),
    []
  );

  // NOTE: we deliberately don't filter on `file.type` — iOS often reports an empty type for HEIC
  // and for files picked out of the Files app, which silently dropped valid photos. Instead every
  // file is decoded and re-encoded here (see prepareImage), so what reaches the server is always a
  // modest JPEG and anything genuinely unreadable is reported by name.
  async function addImages(list: FileList | File[] | null) {
    if (!list) return;
    const incoming = Array.from(list);
    if (incoming.length === 0) return;
    setError(null);
    setPreparing(true);
    try {
      const room = MAX_IMAGES - images.length;
      const picked: PickedImage[] = [];
      let fellBack = 0;
      for (const file of incoming.slice(0, room)) {
        try {
          const prepared = await prepareImage(file);
          picked.push({ file: prepared, url: URL.createObjectURL(prepared) });
        } catch {
          // This browser can't decode it (e.g. HEIC outside Safari). Send the original anyway —
          // the server reads HEIC too — and skip the preview rather than showing a broken image.
          picked.push({ file, url: null });
          fellBack++;
        }
      }
      if (picked.length > 0) {
        setImages((prev) => [...prev, ...picked]);
        setMatchData(null);
        setBreedData(null);
      }
      if (fellBack > 0) {
        setError(
          `${fellBack === 1 ? "One image" : `${fellBack} images`} couldn't be previewed in this ` +
            "browser, but will still be searched. If it fails, try a JPEG or PNG."
        );
      }
    } finally {
      setPreparing(false);
    }
  }

  function removeImage(idx: number) {
    setImages((prev) => {
      const gone = prev[idx].url;
      if (gone) URL.revokeObjectURL(gone);
      return prev.filter((_, i) => i !== idx);
    });
    setMatchData(null);
    setBreedData(null);
  }

  async function run(mode: Mode) {
    if (images.length === 0) return;
    setRunning(mode);
    setError(null);
    const start = Date.now();
    try {
      if (mode === "match") {
        const d = await api.searchByPhoto(
          images.map((i) => i.file),
          undefined,
          pool
        );
        const wait = MIN_PROGRESS_MS - (Date.now() - start);
        if (wait > 0) await new Promise((r) => setTimeout(r, wait));
        setMatchData(d);
      } else {
        const d = await api.estimateBreed(
          images.map((i) => i.file),
          BREED_COUNT
        );
        const wait = MIN_PROGRESS_MS - (Date.now() - start);
        if (wait > 0) await new Promise((r) => setTimeout(r, wait));
        setBreedData(d);
      }
    } catch (err) {
      setError(describeRequestError(err));
    } finally {
      setRunning(null);
    }
  }

  const busy = running !== null || preparing;
  const hasImages = images.length > 0;

  return (
    <section className="card mt-4">
      <h2 className="text-xl font-display font-semibold">Try it with an image</h2>
      <p className="text-sm text-gray-600 mt-1">
        Upload up to 6 images of a dog, and search for look-alikes in the found-dog database or
        estimate its breed. Pictures from more angles help the matching process. Use clear photos of
        a single dog. Both models only know dogs, giving them anything else will return a meaningless
        answer.
      </p>

      <input
        id="pt-file"
        type="file"
        accept="image/*"
        multiple
        className="sr-only"
        onChange={(e) => addImages(e.target.files)}
      />

      {!hasImages ? (
        // Big dropzone before anything is picked.
        <label
          htmlFor="pt-file"
          onDragOver={(e) => {
            e.preventDefault();
            setDragging(true);
          }}
          onDragLeave={() => setDragging(false)}
          onDrop={(e) => {
            e.preventDefault();
            setDragging(false);
            addImages(e.dataTransfer.files);
          }}
          className={`mt-5 block w-full cursor-pointer rounded-xl border-2 border-dashed px-6 py-14 text-center transition ${
            dragging
              ? "border-leaf-500 bg-leaf-50"
              : "border-brand-400 bg-gray-50 hover:border-leaf-400 hover:bg-leaf-50/50"
          }`}
        >
          <p className="text-lg font-semibold text-gray-800">Upload images of your dog</p>
          <p className="mt-1 text-sm text-gray-500">Click to choose up to 6, or drag them here</p>
          <p className="mt-2 text-xs text-gray-400">Nothing will be saved to the database.</p>
        </label>
      ) : (
        // Thumbnail grid + an add-more tile once at least one image is picked.
        <div className="mt-5">
          <div className="flex flex-wrap justify-center gap-3">
            {images.map((img, i) => (
              <div
                key={`${img.file.name}-${i}`}
                className="relative h-32 w-32 sm:h-44 sm:w-44 overflow-hidden rounded-lg border border-gray-200 bg-gray-100 shadow-sm"
              >
                {img.url ? (
                  <img
                    src={img.url}
                    alt={`Your dog, image ${i + 1}`}
                    className="h-full w-full object-cover"
                  />
                ) : (
                  <div className="flex h-full w-full flex-col items-center justify-center gap-1 p-2 text-center">
                    <span className="text-xs font-medium text-gray-500">No preview</span>
                    <span className="text-[10px] text-gray-400 break-all">{img.file.name}</span>
                  </div>
                )}
                {running ? (
                  <>
                    <div className="absolute inset-0 bg-leaf-500/10" aria-hidden />
                    <div
                      className="scan-sweep absolute inset-x-0 h-0.5 bg-leaf-400 shadow-[0_0_10px_2px] shadow-leaf-400/80"
                      aria-hidden
                    />
                  </>
                ) : (
                  <button
                    type="button"
                    onClick={() => removeImage(i)}
                    aria-label={`Remove image ${i + 1}`}
                    className="absolute top-1.5 right-1.5 flex h-6 w-6 items-center justify-center rounded-full bg-black/60 text-sm leading-none text-white hover:bg-black/80"
                  >
                    &times;
                  </button>
                )}
              </div>
            ))}
            {images.length < MAX_IMAGES && !running && (
              <label
                htmlFor="pt-file"
                onDragOver={(e) => {
                  e.preventDefault();
                  setDragging(true);
                }}
                onDragLeave={() => setDragging(false)}
                onDrop={(e) => {
                  e.preventDefault();
                  setDragging(false);
                  addImages(e.dataTransfer.files);
                }}
                className={`flex h-32 w-32 sm:h-44 sm:w-44 cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed text-center transition ${
                  dragging
                    ? "border-leaf-500 bg-leaf-50"
                    : "border-brand-400 bg-gray-50 hover:border-leaf-400 hover:bg-leaf-50/50"
                }`}
              >
                <span className="text-3xl leading-none text-gray-400">+</span>
                <span className="mt-1.5 px-2 text-xs text-gray-500">Add more</span>
              </label>
            )}
          </div>
          <p className="mt-3 text-center text-xs text-gray-400">
            {preparing
              ? "Preparing images…"
              : `${images.length} of ${MAX_IMAGES} images · nothing will be saved to the database`}
          </p>
        </div>
      )}

      {/* Two tabs under the images: image matching | breed estimation (both use the same images) */}
      <div className="mt-6">
        <div
          role="tablist"
          aria-label="Image tools"
          className="flex justify-center gap-1 border-b border-gray-200"
        >
          <TabButton
            active={tab === "match"}
            onClick={() => {
              setTab("match");
              setError(null);
            }}
          >
            Image matching
          </TabButton>
          <TabButton
            active={tab === "breed"}
            onClick={() => {
              setTab("breed");
              setError(null);
            }}
          >
            Breed estimation
          </TabButton>
        </div>

        <div className="pt-4">
          {tab === "match" ? (
            <div>
              {!lockedPool && (
                <div className="text-center">
                  <span className="label block mb-1">What are you searching?</span>
                  <div className="inline-flex rounded-md border border-gray-200 overflow-hidden">
                    <button
                      type="button"
                      className={`px-3 py-2 text-sm ${
                        pool === "found" ? "bg-brand-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"
                      }`}
                      aria-pressed={pool === "found"}
                      onClick={() => setPool("found")}
                    >
                      I lost my dog
                    </button>
                    <button
                      type="button"
                      className={`px-3 py-2 text-sm border-l border-gray-200 ${
                        pool === "lost" ? "bg-brand-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"
                      }`}
                      aria-pressed={pool === "lost"}
                      onClick={() => setPool("lost")}
                    >
                      I found a dog
                    </button>
                  </div>
                  <p className="text-xs text-gray-500 mt-1">
                    {pool === "found"
                      ? "Searches found & unclaimed dogs in the system for a look-alike."
                      : "Searches currently-lost registered dogs to help find the owner."}
                  </p>
                </div>
              )}
              <button
                type="button"
                className={`btn-primary mx-auto block px-8 py-3 text-base ${lockedPool ? "" : "mt-4"}`}
                disabled={!hasImages || busy}
                onClick={() => run("match")}
              >
                {running === "match" ? "Searching…" : "Find matches"}
              </button>
              <MatchingNote />
              {running === "match" && (
                <div className="mt-4">
                  <SearchProgress mode="match" count={haystackHint} />
                </div>
              )}
              {matchData && <MatchResults data={matchData} onOpen={setActive} />}
            </div>
          ) : (
            <div>
              <button
                type="button"
                className="btn-primary mx-auto block px-8 py-3 text-base"
                disabled={!hasImages || busy}
                onClick={() => run("breed")}
              >
                {running === "breed" ? "Estimating…" : "Estimate breed"}
              </button>
              <BreedNote />
              {images.length > 1 && (
                <p className="text-xs text-gray-400 mt-3 text-center">
                  Averaged across all {images.length} images, since a single photo can easily flip the
                  top breed.
                </p>
              )}
              {running === "breed" && (
                <div className="mt-4">
                  <SearchProgress mode="breed" />
                </div>
              )}
              {breedData && <BreedResults data={breedData} />}
            </div>
          )}
        </div>
      </div>

      {error && <p className="text-red-600 text-sm mt-3">{error}</p>}

      {active && (
        <ComparePhotosModal
          match={active}
          queryPhotos={photosFromUrls(images.map((i) => i.url).filter((u): u is string => !!u))}
          queryLabel={`Your image${images.length > 1 ? "s" : ""}`}
          onClose={() => setActive(null)}
        />
      )}
    </section>
  );
}

function pct(score: number): string {
  return `${(score * 100).toFixed(1)}%`;
}

function MatchResults({
  data,
  onOpen,
}: {
  data: PhotoSearchResult;
  onOpen: (m: PhotoSearchMatch) => void;
}) {
  const noun = data.pool === "lost" ? "lost dog" : "found dog";
  return (
    <div className="mt-6">
      <h3 className="font-semibold mb-2">Closest {noun}s</h3>
      {data.results.length === 0 ? (
        <p className="text-gray-500 text-sm">
          No {noun}s to compare against yet. {" "}
          {data.pool === "lost"
            ? "Report the dog as found so we keep watching for the owner."
            : "Report your dog as lost so we keep watching for new matches."}
        </p>
      ) : (
        <>
          <p className="text-xs text-gray-500 mb-2">
            Closest of {data.candidate_count} {noun}(s)
            {data.zip ? ` near ${data.zip}` : ""}. Tap a card to see all its images. The scores measure
            image similarity only, so always confirm with a person.
          </p>
          <div className="grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
            {/* Ten, to match the "top 10" figure quoted under the button. */}
            {data.results.slice(0, 10).map((m) => (
              <ResultCard key={`${m.dog.kind}-${m.dog.id}`} match={m} onOpen={() => onOpen(m)} />
            ))}
          </div>
        </>
      )}
    </div>
  );
}

function ResultCard({ match, onOpen }: { match: PhotoSearchMatch; onOpen: () => void }) {
  const { dog } = match;
  const thumb = match.photos[0]?.url || dog.thumb_url || "/paw-placeholder.svg";
  return (
    <button className="card text-left hover:ring-2 hover:ring-brand-300" onClick={onOpen}>
      <div className="relative">
        <img src={thumb} alt={dog.name} className="h-40 w-full rounded-md object-contain bg-gray-100" />
        <span className="badge absolute top-2 left-2 bg-brand-600 text-white">{pct(match.score)} match</span>
        {dog.picture_count > 1 && (
          <span className="badge absolute top-2 right-2 bg-black/60 text-white">
            {dog.picture_count} images
          </span>
        )}
      </div>
      <div className="mt-2">
        <h3 className="font-semibold truncate" title={dog.name}>
          {dog.name}
        </h3>
        <p className="text-xs text-gray-500 truncate">{estimatedBreeds(dog.predicted_breeds)}</p>
        <p className="text-xs text-gray-400 mt-0.5">
          {dog.zip ? `ZIP ${dog.zip}` : "no ZIP"}
          {match.distance_miles != null ? ` · ${match.distance_miles} mi away` : ""}
        </p>
      </div>
    </button>
  );
}
