import { isDesktop, isIOS, isMobile, isSafari } from "react-device-detect";
import { SearchResult } from "@/types/search";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import { getIconForLabel } from "@/utils/iconUtil";
import { useApiHost } from "@/api";
import { Button } from "../../ui/button";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import axios from "axios";
import { toast } from "sonner";
import { Textarea } from "../../ui/textarea";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import useOptimisticState from "@/hooks/use-optimistic-state";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Event } from "@/types/event";
import { baseUrl } from "@/api/baseUrl";
import { cn } from "@/lib/utils";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import {
  FaArrowRight,
  FaCheckCircle,
  FaChevronDown,
  FaChevronLeft,
  FaChevronRight,
} from "react-icons/fa";
import { TrackingDetails } from "./TrackingDetails";
import { AnnotationSettingsPane } from "./AnnotationSettingsPane";
import { DetailStreamProvider } from "@/context/detail-stream-context";
import {
  MobilePage,
  MobilePageContent,
  MobilePageDescription,
  MobilePageHeader,
  MobilePageTitle,
} from "@/components/mobile/MobilePage";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { REVIEW_PADDING } from "@/types/review";
import { capitalizeAll } from "@/utils/stringUtil";
import useGlobalMutation from "@/hooks/use-global-mutate";
import DetailActionsMenu from "./DetailActionsMenu";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import useImageLoaded from "@/hooks/use-image-loaded";
import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator";
import { GenericVideoPlayer } from "@/components/player/GenericVideoPlayer";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import { LuInfo } from "react-icons/lu";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import { FaPencilAlt } from "react-icons/fa";
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
import { Trans, useTranslation } from "react-i18next";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { getTranslatedLabel } from "@/utils/i18n";
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
import { DialogPortal } from "@radix-ui/react-dialog";
import { useDetailStream } from "@/context/detail-stream-context";
import { PiSlidersHorizontalBold } from "react-icons/pi";

const SEARCH_TABS = ["snapshot", "tracking_details"] as const;
export type SearchTab = (typeof SEARCH_TABS)[number];

type TabsWithActionsProps = {
  search: SearchResult;
  searchTabs: SearchTab[];
  pageToggle: SearchTab;
  setPageToggle: (v: SearchTab) => void;
  config?: FrigateConfig;
  setSearch: (s: SearchResult | undefined) => void;
  setSimilarity?: () => void;
  isPopoverOpen: boolean;
  setIsPopoverOpen: (open: boolean) => void;
};

function TabsWithActions({
  search,
  searchTabs,
  pageToggle,
  setPageToggle,
  config,
  setSearch,
  setSimilarity,
  isPopoverOpen,
  setIsPopoverOpen,
}: TabsWithActionsProps) {
  const { t } = useTranslation(["views/explore", "views/faceLibrary"]);

  if (!search) return null;

  return (
    <div className="flex items-center justify-between gap-1">
      <ScrollArea className="flex-1 whitespace-nowrap">
        <div className="mb-2 flex flex-row md:mb-0">
          <ToggleGroup
            className="*:rounded-md *:px-3 *:py-4"
            type="single"
            size="sm"
            value={pageToggle}
            onValueChange={(value: SearchTab) => {
              if (value) {
                setPageToggle(value);
              }
            }}
          >
            {Object.values(searchTabs).map((item) => (
              <ToggleGroupItem
                key={item}
                className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
                value={item}
                data-nav-item={item}
                aria-label={`Select ${item}`}
              >
                <div className="smart-capitalize">
                  {item === "snapshot"
                    ? search?.has_snapshot
                      ? t("type.snapshot")
                      : t("type.thumbnail")
                    : t(`type.${item}`)}
                </div>
              </ToggleGroupItem>
            ))}
          </ToggleGroup>
          <ScrollBar orientation="horizontal" className="h-0" />
        </div>
      </ScrollArea>
      <DetailActionsMenu
        search={search}
        config={config}
        setSearch={setSearch}
        setSimilarity={setSimilarity}
      />
      {pageToggle === "tracking_details" && (
        <AnnotationSettings
          search={search}
          open={isPopoverOpen}
          setIsOpen={setIsPopoverOpen}
        />
      )}
    </div>
  );
}

type AnnotationSettingsProps = {
  search: SearchResult;
  open: boolean;
  setIsOpen: (open: boolean) => void;
};

function AnnotationSettings({
  search,
  open,
  setIsOpen,
}: AnnotationSettingsProps) {
  const { t } = useTranslation(["views/explore"]);
  const { annotationOffset, setAnnotationOffset } = useDetailStream();

  const ignoreNextOpenRef = useRef(false);

  useEffect(() => {
    setIsOpen(false);
    ignoreNextOpenRef.current = false;
  }, [search, setIsOpen]);

  const handleOpenChange = useCallback(
    (nextOpen: boolean) => {
      if (nextOpen) {
        if (ignoreNextOpenRef.current) {
          ignoreNextOpenRef.current = false;
          return;
        }
        setIsOpen(true);
      } else {
        setIsOpen(false);
      }
    },
    [setIsOpen],
  );

  const registerTriggerCloseIntent = useCallback(() => {
    if (open) {
      ignoreNextOpenRef.current = true;
    }
  }, [open]);

  const Overlay = isDesktop ? Popover : Drawer;
  const Trigger = isDesktop ? PopoverTrigger : DrawerTrigger;
  const Content = isDesktop ? PopoverContent : DrawerContent;

  return (
    <div className="ml-2">
      <Overlay modal={isDesktop} open={open} onOpenChange={handleOpenChange}>
        <Trigger asChild>
          <Button
            type="button"
            className="size-7 p-1.5"
            variant={open ? "select" : "ghost"}
            aria-label={t("trackingDetails.adjustAnnotationSettings")}
            aria-expanded={open}
            onPointerDown={registerTriggerCloseIntent}
            onKeyDown={(event) => {
              if (open && (event.key === "Enter" || event.key === " ")) {
                registerTriggerCloseIntent();
              }
            }}
          >
            <PiSlidersHorizontalBold className="size-5" />
          </Button>
        </Trigger>

        <Content
          className={
            isDesktop
              ? "w-[90vw] max-w-md p-0"
              : "mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl px-4 pb-4"
          }
          {...(isDesktop ? { align: "end" } : {})}
          data-annotation-popover
        >
          <AnnotationSettingsPane
            event={search as unknown as Event}
            annotationOffset={annotationOffset}
            setAnnotationOffset={setAnnotationOffset}
          />
        </Content>
      </Overlay>
    </div>
  );
}

type DialogContentComponentProps = {
  page: SearchTab;
  search: SearchResult;
  isDesktop: boolean;
  apiHost: string;
  config?: FrigateConfig;
  searchTabs: SearchTab[];
  pageToggle: SearchTab;
  setPageToggle: (v: SearchTab) => void;
  setSearch: (s: SearchResult | undefined) => void;
  setInputFocused: React.Dispatch<React.SetStateAction<boolean>>;
  setSimilarity?: () => void;
  isPopoverOpen: boolean;
  setIsPopoverOpen: (open: boolean) => void;
};

function DialogContentComponent({
  page,
  search,
  isDesktop,
  apiHost,
  config,
  searchTabs,
  pageToggle,
  setPageToggle,
  setSearch,
  setInputFocused,
  setSimilarity,
  isPopoverOpen,
  setIsPopoverOpen,
}: DialogContentComponentProps) {
  if (page === "tracking_details") {
    return (
      <TrackingDetails
        className={cn("size-full", !isDesktop && "flex flex-col gap-4")}
        event={search as unknown as Event}
        tabs={
          isDesktop ? (
            <TabsWithActions
              search={search}
              searchTabs={searchTabs}
              pageToggle={pageToggle}
              setPageToggle={setPageToggle}
              config={config}
              setSearch={setSearch}
              setSimilarity={setSimilarity}
              isPopoverOpen={isPopoverOpen}
              setIsPopoverOpen={setIsPopoverOpen}
            />
          ) : undefined
        }
      />
    );
  }

  // Snapshot page content
  const snapshotElement = search.has_snapshot ? (
    <ObjectSnapshotTab
      className={isDesktop ? undefined : "mb-4"}
      search={
        {
          ...search,
          plus_id: config?.plus?.enabled ? search.plus_id : "not_enabled",
        } as unknown as Event
      }
    />
  ) : (
    <div className={cn(!isDesktop ? "mb-4 w-full" : "size-full")}>
      <img
        className="w-full select-none rounded-lg object-contain transition-opacity"
        style={
          isIOS
            ? {
                WebkitUserSelect: "none",
                WebkitTouchCallout: "none",
              }
            : undefined
        }
        draggable={false}
        src={`${apiHost}api/events/${search.id}/thumbnail.webp`}
      />
    </div>
  );

  if (isDesktop) {
    return (
      <div className="flex h-full gap-4 overflow-hidden">
        <div
          className={cn(
            "scrollbar-container flex-[3] overflow-y-hidden",
            !search.has_snapshot && "flex-[2]",
          )}
        >
          {snapshotElement}
        </div>
        <div className="flex flex-[2] flex-col gap-4 overflow-hidden">
          <TabsWithActions
            search={search}
            searchTabs={searchTabs}
            pageToggle={pageToggle}
            setPageToggle={setPageToggle}
            config={config}
            setSearch={setSearch}
            setSimilarity={setSimilarity}
            isPopoverOpen={isPopoverOpen}
            setIsPopoverOpen={setIsPopoverOpen}
          />
          <div className="scrollbar-container flex-1 overflow-y-auto">
            <ObjectDetailsTab
              search={search}
              config={config}
              setSearch={setSearch}
              setInputFocused={setInputFocused}
            />
          </div>
        </div>
      </div>
    );
  }

  // mobile
  return (
    <>
      {snapshotElement}
      <ObjectDetailsTab
        search={search}
        config={config}
        setSearch={setSearch}
        setInputFocused={setInputFocused}
      />
    </>
  );
}

type SearchDetailDialogProps = {
  search?: SearchResult;
  page: SearchTab;
  setSearch: (search: SearchResult | undefined) => void;
  setSearchPage: (page: SearchTab) => void;
  setSimilarity?: () => void;
  setInputFocused: React.Dispatch<React.SetStateAction<boolean>>;
  onPrevious?: () => void;
  onNext?: () => void;
};

export default function SearchDetailDialog({
  search,
  page,
  setSearch,
  setSearchPage,
  setSimilarity,
  setInputFocused,
  onPrevious,
  onNext,
}: SearchDetailDialogProps) {
  const { t } = useTranslation(["views/explore", "views/faceLibrary"]);
  const { data: config } = useSWR<FrigateConfig>("config", {
    revalidateOnFocus: false,
  });
  const apiHost = useApiHost();

  // tabs

  const [pageToggle, setPageToggle] = useOptimisticState(
    page,
    setSearchPage,
    100,
  );

  // dialog and mobile page

  const [isOpen, setIsOpen] = useState(search != undefined);
  const [isPopoverOpen, setIsPopoverOpen] = useState(false);

  const handleOpenChange = useCallback(
    (open: boolean) => {
      setIsPopoverOpen(open);
      setIsOpen(open);
      if (!open) {
        // short timeout to allow the mobile page animation
        // to complete before updating the state
        setTimeout(() => {
          setSearch(undefined);
        }, 300);
      }
    },
    [setSearch],
  );

  useEffect(() => {
    if (search) {
      setIsOpen(search != undefined);
    }
  }, [search]);

  // show/hide annotation settings is handled inside TabsWithActions

  const searchTabs = useMemo(() => {
    if (!config || !search) {
      return [];
    }

    const views = [...SEARCH_TABS];

    if (search.data.type != "object" || !search.has_clip) {
      const index = views.indexOf("tracking_details");
      views.splice(index, 1);
    }

    return views;
  }, [config, search]);

  useEffect(() => {
    if (searchTabs.length == 0) {
      return;
    }

    if (!searchTabs.includes(pageToggle)) {
      setSearchPage("snapshot");
    }
  }, [pageToggle, searchTabs, setSearchPage]);

  if (!search) {
    return;
  }

  // content

  const Overlay = isDesktop ? Dialog : MobilePage;
  const Content = isDesktop ? DialogContent : MobilePageContent;
  const Header = isDesktop ? DialogHeader : MobilePageHeader;
  const Title = isDesktop ? DialogTitle : MobilePageTitle;
  const Description = isDesktop ? DialogDescription : MobilePageDescription;

  return (
    <DetailStreamProvider
      isDetailMode={true}
      currentTime={(search as unknown as Event)?.start_time ?? 0}
      camera={(search as unknown as Event)?.camera ?? ""}
      initialSelectedObjectIds={[(search as unknown as Event).id as string]}
    >
      <Overlay
        open={isOpen}
        onOpenChange={handleOpenChange}
        enableHistoryBack={true}
      >
        {isDesktop && onPrevious && onNext && (
          <DialogPortal>
            <div className="pointer-events-none fixed inset-0 z-[200] flex items-center justify-center">
              <div
                className={cn(
                  "relative flex items-center justify-between",
                  "w-full",
                  // match dialog's max-width classes
                  "sm:max-w-xl md:max-w-4xl lg:max-w-[75%] xl:max-w-[80%]",
                )}
              >
                <Tooltip>
                  <TooltipTrigger asChild>
                    <button
                      onClick={(e) => {
                        e.stopPropagation();
                        onPrevious?.();
                      }}
                      className="nav-button pointer-events-auto absolute -left-16 rounded-lg border bg-secondary/60 p-2 text-primary-variant shadow-lg backdrop-blur-sm hover:bg-secondary/80 hover:text-primary"
                      aria-label={t("searchResult.previousTrackedObject")}
                    >
                      <FaChevronLeft className="size-4" />
                    </button>
                  </TooltipTrigger>
                  <TooltipContent>
                    {t("searchResult.previousTrackedObject")}
                  </TooltipContent>
                </Tooltip>

                <Tooltip>
                  <TooltipTrigger asChild>
                    <button
                      onClick={(e) => {
                        e.stopPropagation();
                        onNext?.();
                      }}
                      className="nav-button pointer-events-auto absolute -right-16 rounded-lg border bg-secondary/60 p-2 text-primary-variant shadow-lg backdrop-blur-sm hover:bg-secondary/80 hover:text-primary"
                      aria-label={t("searchResult.nextTrackedObject")}
                    >
                      <FaChevronRight className="size-4" />
                    </button>
                  </TooltipTrigger>
                  <TooltipContent>
                    {t("searchResult.nextTrackedObject")}
                  </TooltipContent>
                </Tooltip>
              </div>
            </div>
          </DialogPortal>
        )}
        <Content
          className={cn(
            "scrollbar-container overflow-y-auto",
            isDesktop &&
              "max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-[75%] xl:max-w-[80%]",
            isMobile && "px-4",
          )}
          onInteractOutside={(e) => {
            if (isPopoverOpen) {
              e.preventDefault();
            }
            const target = e.target as HTMLElement;
            if (target.closest(".nav-button")) {
              e.preventDefault();
            }
          }}
        >
          <Header>
            <Title>{t("trackedObjectDetails")}</Title>
            <Description className="sr-only">
              {t("trackedObjectDetails")}
              <span className="sr-only" tabIndex={0} />
            </Description>
          </Header>

          {!isDesktop && (
            <div className="flex w-full flex-col justify-center gap-4">
              <TabsWithActions
                search={search}
                searchTabs={searchTabs}
                pageToggle={pageToggle}
                setPageToggle={setPageToggle}
                config={config}
                setSearch={setSearch}
                setSimilarity={setSimilarity}
                isPopoverOpen={isPopoverOpen}
                setIsPopoverOpen={setIsPopoverOpen}
              />
            </div>
          )}

          <DialogContentComponent
            page={page}
            search={search}
            isDesktop={isDesktop}
            apiHost={apiHost}
            config={config}
            searchTabs={searchTabs}
            pageToggle={pageToggle}
            setPageToggle={setPageToggle}
            setSearch={setSearch}
            setInputFocused={setInputFocused}
            setSimilarity={setSimilarity}
            isPopoverOpen={isPopoverOpen}
            setIsPopoverOpen={setIsPopoverOpen}
          />
        </Content>
      </Overlay>
    </DetailStreamProvider>
  );
}

type ObjectDetailsTabProps = {
  search: SearchResult;
  config?: FrigateConfig;
  setSearch: (search: SearchResult | undefined) => void;
  setInputFocused: React.Dispatch<React.SetStateAction<boolean>>;
};
function ObjectDetailsTab({
  search,
  config,
  setSearch,
  setInputFocused,
}: ObjectDetailsTabProps) {
  const { t, i18n } = useTranslation([
    "views/explore",
    "views/faceLibrary",
    "components/dialog",
  ]);

  const apiHost = useApiHost();

  // mutation / revalidation

  const mutate = useGlobalMutation();

  // users

  const isAdmin = useIsAdmin();

  // data

  const [desc, setDesc] = useState(search?.data.description);
  const [isSubLabelDialogOpen, setIsSubLabelDialogOpen] = useState(false);
  const [isLPRDialogOpen, setIsLPRDialogOpen] = useState(false);

  const handleDescriptionFocus = useCallback(() => {
    setInputFocused(true);
  }, [setInputFocused]);

  const handleDescriptionBlur = useCallback(() => {
    setInputFocused(false);
  }, [setInputFocused]);

  // we have to make sure the current selected search item stays in sync
  useEffect(() => setDesc(search?.data.description ?? ""), [search]);

  const formattedDate = useFormattedTimestamp(
    search?.start_time ?? 0,
    config?.ui.time_format == "24hour"
      ? t("time.formattedTimestampMonthDayYearHourMinute.24hour", {
          ns: "common",
        })
      : t("time.formattedTimestampMonthDayYearHourMinute.12hour", {
          ns: "common",
        }),
    config?.ui.timezone,
  );

  const topScore = useMemo(() => {
    if (!search) {
      return 0;
    }

    const value = search.data.top_score ?? search.top_score ?? 0;

    return Math.round(value * 100);
  }, [search]);

  const subLabelScore = useMemo(() => {
    if (!search) {
      return undefined;
    }

    if (search.sub_label && search.data?.sub_label_score) {
      return Math.round((search.data?.sub_label_score ?? 0) * 100);
    } else {
      return undefined;
    }
  }, [search]);

  const recognizedLicensePlateScore = useMemo(() => {
    if (!search) {
      return undefined;
    }

    if (
      search.data.recognized_license_plate &&
      search.data?.recognized_license_plate_score
    ) {
      return Math.round(
        (search.data?.recognized_license_plate_score ?? 0) * 100,
      );
    } else {
      return undefined;
    }
  }, [search]);

  const snapScore = useMemo(() => {
    if (!search?.has_snapshot) {
      return undefined;
    }

    const value = search.data.score ?? search.score ?? 0;

    return Math.floor(value * 100);
  }, [search]);

  const averageEstimatedSpeed = useMemo(() => {
    if (!search || !search.data?.average_estimated_speed) {
      return undefined;
    }

    if (search.data?.average_estimated_speed != 0) {
      return search.data?.average_estimated_speed.toFixed(1);
    } else {
      return undefined;
    }
  }, [search]);

  const velocityAngle = useMemo(() => {
    if (!search || !search.data?.velocity_angle) {
      return undefined;
    }

    if (search.data?.velocity_angle != 0) {
      return search.data?.velocity_angle.toFixed(1);
    } else {
      return undefined;
    }
  }, [search]);

  const updateDescription = useCallback(() => {
    if (!search) {
      return;
    }

    axios
      .post(`events/${search.id}/description`, { description: desc })
      .then((resp) => {
        if (resp.status == 200) {
          toast.success(t("details.tips.descriptionSaved"), {
            position: "top-center",
          });
        }
        mutate(
          (key) =>
            typeof key === "string" &&
            (key.includes("events") ||
              key.includes("events/search") ||
              key.includes("events/explore")),
          (currentData: SearchResult[][] | SearchResult[] | undefined) => {
            if (!currentData) return currentData;
            // optimistic update
            return currentData
              .flat()
              .map((event) =>
                event.id === search.id
                  ? { ...event, data: { ...event.data, description: desc } }
                  : event,
              );
          },
          {
            optimisticData: true,
            rollbackOnError: true,
            revalidate: false,
          },
        );
      })
      .catch((error) => {
        const errorMessage =
          error.response?.data?.message ||
          error.response?.data?.detail ||
          "Unknown error";
        toast.error(
          t("details.tips.saveDescriptionFailed", {
            errorMessage,
          }),
          {
            position: "top-center",
          },
        );
        setDesc(search.data.description);
      });
  }, [desc, search, mutate, t]);

  const regenerateDescription = useCallback(
    (source: "snapshot" | "thumbnails") => {
      if (!search) {
        return;
      }

      axios
        .put(`events/${search.id}/description/regenerate?source=${source}`)
        .then((resp) => {
          if (resp.status == 200) {
            toast.success(
              t("details.item.toast.success.regenerate", {
                provider: capitalizeAll(
                  config?.genai.provider.replaceAll("_", " ") ??
                    t("generativeAI"),
                ),
              }),
              {
                position: "top-center",
                duration: 7000,
              },
            );
          }
        })
        .catch((error) => {
          const errorMessage =
            error.response?.data?.message ||
            error.response?.data?.detail ||
            "Unknown error";
          toast.error(
            t("details.item.toast.error.regenerate", {
              provider: capitalizeAll(
                config?.genai.provider.replaceAll("_", " ") ??
                  t("generativeAI"),
              ),
              errorMessage,
            }),
            { position: "top-center" },
          );
        });
    },
    [search, config, t],
  );

  const handleSubLabelSave = useCallback(
    (text: string) => {
      if (!search) return;

      // set score to 1.0 if we're manually entering a sub label
      const subLabelScore =
        text === "" ? undefined : search.data?.sub_label_score || 1.0;

      axios
        .post(`${apiHost}api/events/${search.id}/sub_label`, {
          camera: search.camera,
          subLabel: text,
          subLabelScore: subLabelScore,
        })
        .then((response) => {
          if (response.status === 200) {
            toast.success(t("details.item.toast.success.updatedSublabel"), {
              position: "top-center",
            });

            mutate(
              (key) =>
                typeof key === "string" &&
                (key.includes("events") ||
                  key.includes("events/search") ||
                  key.includes("events/explore")),
              (currentData: SearchResult[][] | SearchResult[] | undefined) => {
                if (!currentData) return currentData;
                return currentData.flat().map((event) =>
                  event.id === search.id
                    ? {
                        ...event,
                        sub_label: text,
                        data: {
                          ...event.data,
                          sub_label_score: subLabelScore,
                        },
                      }
                    : event,
                );
              },
              {
                optimisticData: true,
                rollbackOnError: true,
                revalidate: false,
              },
            );

            setSearch({
              ...search,
              sub_label: text,
              data: {
                ...search.data,
                sub_label_score: subLabelScore,
              },
            });
            setIsSubLabelDialogOpen(false);
          }
        })
        .catch((error) => {
          const errorMessage =
            error.response?.data?.message ||
            error.response?.data?.detail ||
            "Unknown error";
          toast.error(
            t("details.item.toast.error.updatedSublabelFailed", {
              errorMessage,
            }),
            {
              position: "top-center",
            },
          );
        });
    },
    [search, apiHost, mutate, setSearch, t],
  );

  // recognized plate

  const handleLPRSave = useCallback(
    (text: string) => {
      if (!search) return;

      // set score to 1.0 if we're manually entering a new plate
      const plateScore = text === "" ? undefined : 1.0;

      axios
        .post(`${apiHost}api/events/${search.id}/recognized_license_plate`, {
          recognizedLicensePlate: text,
          recognizedLicensePlateScore: plateScore,
        })
        .then((response) => {
          if (response.status === 200) {
            toast.success(t("details.item.toast.success.updatedLPR"), {
              position: "top-center",
            });

            mutate(
              (key) =>
                typeof key === "string" &&
                (key.includes("events") ||
                  key.includes("events/search") ||
                  key.includes("events/explore")),
              (currentData: SearchResult[][] | SearchResult[] | undefined) => {
                if (!currentData) return currentData;
                return currentData.flat().map((event) =>
                  event.id === search.id
                    ? {
                        ...event,
                        data: {
                          ...event.data,
                          recognized_license_plate: text,
                          recognized_license_plate_score: plateScore,
                        },
                      }
                    : event,
                );
              },
              {
                optimisticData: true,
                rollbackOnError: true,
                revalidate: false,
              },
            );

            setSearch({
              ...search,
              data: {
                ...search.data,
                recognized_license_plate: text,
                recognized_license_plate_score: plateScore,
              },
            });
            setIsLPRDialogOpen(false);
          }
        })
        .catch((error) => {
          const errorMessage =
            error.response?.data?.message ||
            error.response?.data?.detail ||
            "Unknown error";
          toast.error(
            t("details.item.toast.error.updatedLPRFailed", {
              errorMessage,
            }),
            {
              position: "top-center",
            },
          );
        });
    },
    [search, apiHost, mutate, setSearch, t],
  );

  // speech transcription

  const onTranscribe = useCallback(() => {
    axios
      .put(`/audio/transcribe`, { event_id: search.id })
      .then((resp) => {
        if (resp.status == 202) {
          toast.success(t("details.item.toast.success.audioTranscription"), {
            position: "top-center",
          });
        }
      })
      .catch((error) => {
        const errorMessage =
          error.response?.data?.message ||
          error.response?.data?.detail ||
          "Unknown error";
        toast.error(
          t("details.item.toast.error.audioTranscription", {
            errorMessage,
          }),
          {
            position: "top-center",
          },
        );
      });
  }, [search, t]);

  // frigate+ submission

  type SubmissionState = "reviewing" | "uploading" | "submitted";
  const [state, setState] = useState<SubmissionState>(
    search?.plus_id ? "submitted" : "reviewing",
  );

  useEffect(
    () => setState(search?.plus_id ? "submitted" : "reviewing"),
    [search],
  );

  const onSubmitToPlus = useCallback(
    async (falsePositive: boolean) => {
      if (!search) {
        return;
      }

      falsePositive
        ? axios.put(`events/${search.id}/false_positive`)
        : axios.post(`events/${search.id}/plus`, {
            include_annotation: 1,
          });

      setState("submitted");
      setSearch({
        ...search,
        plus_id: "new_upload",
      });
    },
    [search, setSearch],
  );

  const popoverContainerRef = useRef<HTMLDivElement | null>(null);
  return (
    <div ref={popoverContainerRef} className="flex flex-col gap-5">
      <div className="flex w-full flex-row">
        <div className="flex w-full flex-col gap-3">
          <div className="w-full">
            <div className="flex w-full flex-row flex-wrap gap-6">
              <div className="min-w-0 flex-1">
                <div className="flex flex-col gap-3">
                  <div className="flex flex-col gap-1.5">
                    <div className="text-sm text-primary/40">
                      {t("details.label")}
                    </div>
                    <div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
                      {getIconForLabel(search.label, "size-4 text-primary")}
                      {getTranslatedLabel(search.label)}
                      {search.sub_label && ` (${search.sub_label})`}
                      {isAdmin && search.end_time && (
                        <Tooltip>
                          <TooltipTrigger asChild>
                            <span>
                              <FaPencilAlt
                                className="size-4 cursor-pointer text-primary/40 hover:text-primary/80"
                                onClick={() => setIsSubLabelDialogOpen(true)}
                              />
                            </span>
                          </TooltipTrigger>
                          <TooltipPortal>
                            <TooltipContent>
                              {t("details.editSubLabel.title")}
                            </TooltipContent>
                          </TooltipPortal>
                        </Tooltip>
                      )}
                    </div>
                  </div>

                  <div className="flex flex-col gap-1.5">
                    <div className="text-sm text-primary/40">
                      <div className="flex flex-row items-center gap-1">
                        {t("details.topScore.label")}
                        <Popover>
                          <PopoverTrigger asChild>
                            <div className="cursor-pointer p-0">
                              <LuInfo className="size-4" />
                              <span className="sr-only">Info</span>
                            </div>
                          </PopoverTrigger>
                          <PopoverContent
                            container={popoverContainerRef.current}
                            className="w-80 text-xs"
                          >
                            {t("details.topScore.info")}
                          </PopoverContent>
                        </Popover>
                      </div>
                    </div>
                    <div className="text-sm">
                      {topScore}%{subLabelScore && ` (${subLabelScore}%)`}
                    </div>
                  </div>

                  <div className="flex flex-col gap-1.5">
                    <div className="text-sm text-primary/40">
                      {t("details.camera")}
                    </div>
                    <div className="text-sm smart-capitalize">
                      <CameraNameLabel camera={search.camera} />
                    </div>
                  </div>
                </div>
              </div>

              <div className="min-w-0 flex-1">
                <div className="flex flex-col gap-3">
                  {snapScore != undefined && (
                    <div className="flex flex-col gap-1.5">
                      <div className="text-sm text-primary/40">
                        <div className="flex flex-row items-center gap-1">
                          {t("details.snapshotScore.label")}
                        </div>
                      </div>
                      <div className="text-sm">{snapScore}%</div>
                    </div>
                  )}

                  {averageEstimatedSpeed && (
                    <div className="flex flex-col gap-1.5">
                      <div className="text-sm text-primary/40">
                        {t("details.estimatedSpeed")}
                      </div>
                      <div className="flex flex-col space-y-0.5 text-sm">
                        <div className="flex flex-row items-center gap-2">
                          {averageEstimatedSpeed}{" "}
                          {config?.ui.unit_system == "imperial"
                            ? t("unit.speed.mph", { ns: "common" })
                            : t("unit.speed.kph", { ns: "common" })}
                          {velocityAngle != undefined && (
                            <span className="text-primary/40">
                              <FaArrowRight
                                size={10}
                                style={{
                                  transform: `rotate(${(360 - Number(velocityAngle)) % 360}deg)`,
                                }}
                              />
                            </span>
                          )}
                        </div>
                      </div>
                    </div>
                  )}

                  <div className="flex flex-col gap-1.5">
                    <div className="text-sm text-primary/40">
                      {t("details.timestamp")}
                    </div>
                    <div className="text-sm">{formattedDate}</div>
                  </div>
                </div>
              </div>
            </div>
          </div>
          {search?.data.recognized_license_plate && (
            <div className="flex flex-col gap-1.5">
              <div className="text-sm text-primary/40">
                {t("details.recognizedLicensePlate")}
              </div>
              <div className="flex flex-col space-y-0.5 text-sm">
                <div className="flex flex-row items-center gap-2">
                  {search.data.recognized_license_plate}{" "}
                  {recognizedLicensePlateScore &&
                    ` (${recognizedLicensePlateScore}%)`}
                  {isAdmin && (
                    <Tooltip>
                      <TooltipTrigger asChild>
                        <span>
                          <FaPencilAlt
                            className="size-4 cursor-pointer text-primary/40 hover:text-primary/80"
                            onClick={() => setIsLPRDialogOpen(true)}
                          />
                        </span>
                      </TooltipTrigger>
                      <TooltipPortal>
                        <TooltipContent>
                          {t("details.editLPR.title")}
                        </TooltipContent>
                      </TooltipPortal>
                    </Tooltip>
                  )}
                </div>
              </div>
            </div>
          )}
        </div>
      </div>

      <div
        className={cn(
          "my-2 flex w-full flex-col justify-between gap-1.5",
          state == "submitted" && "flex-row",
        )}
      >
        <div className="text-sm text-primary/40">
          <div className="flex flex-row items-center gap-1">
            {t("explore.plus.submitToPlus.label", {
              ns: "components/dialog",
            })}
            <Popover>
              <PopoverTrigger asChild>
                <div className="cursor-pointer p-0">
                  <LuInfo className="size-4" />
                  <span className="sr-only">Info</span>
                </div>
              </PopoverTrigger>
              <PopoverContent
                container={popoverContainerRef.current}
                className="w-80 text-xs"
              >
                {t("explore.plus.submitToPlus.desc", {
                  ns: "components/dialog",
                })}
              </PopoverContent>
            </Popover>
          </div>
        </div>

        <div className="flex flex-row items-center justify-between gap-2 text-sm">
          {state == "reviewing" && (
            <>
              <div>
                {i18n.language === "en" ? (
                  // English with a/an logic plus label
                  <>
                    {/^[aeiou]/i.test(search?.label || "") ? (
                      <Trans
                        ns="components/dialog"
                        values={{ label: search?.label }}
                      >
                        explore.plus.review.question.ask_an
                      </Trans>
                    ) : (
                      <Trans
                        ns="components/dialog"
                        values={{ label: search?.label }}
                      >
                        explore.plus.review.question.ask_a
                      </Trans>
                    )}
                  </>
                ) : (
                  // For other languages
                  <Trans
                    ns="components/dialog"
                    values={{
                      untranslatedLabel: search?.label,
                      translatedLabel: getTranslatedLabel(search?.label),
                    }}
                  >
                    explore.plus.review.question.ask_full
                  </Trans>
                )}
              </div>
              <div className="flex max-w-xl flex-row gap-2">
                <Button
                  className="flex-1 bg-success"
                  aria-label={t("button.yes", { ns: "common" })}
                  onClick={() => {
                    setState("uploading");
                    onSubmitToPlus(false);
                  }}
                >
                  {t("button.yes", { ns: "common" })}
                </Button>
                <Button
                  className="flex-1 text-white"
                  aria-label={t("button.no", { ns: "common" })}
                  variant="destructive"
                  onClick={() => {
                    setState("uploading");
                    onSubmitToPlus(true);
                  }}
                >
                  {t("button.no", { ns: "common" })}
                </Button>
              </div>
            </>
          )}
          {state == "uploading" && <ActivityIndicator />}
          {state == "submitted" && (
            <div className="flex flex-row items-center justify-center gap-2">
              <FaCheckCircle className="size-4 text-success" />
              {t("explore.plus.review.state.submitted")}
            </div>
          )}
        </div>
      </div>
      <div className="flex flex-col gap-1.5">
        {config?.cameras[search.camera].objects.genai.enabled &&
        !search.end_time &&
        (config.cameras[search.camera].objects.genai.required_zones.length ===
          0 ||
          search.zones.some((zone) =>
            config.cameras[search.camera].objects.genai.required_zones.includes(
              zone,
            ),
          )) &&
        (config.cameras[search.camera].objects.genai.objects.length === 0 ||
          config.cameras[search.camera].objects.genai.objects.includes(
            search.label,
          )) ? (
          <>
            <div className="text-sm text-primary/40">
              {t("details.description.label")}
            </div>
            <div className="flex h-64 flex-col items-center justify-center gap-3 border p-4 text-sm text-primary/40">
              <div className="flex">
                <ActivityIndicator />
              </div>
              <div className="flex">{t("details.description.aiTips")}</div>
            </div>
          </>
        ) : (
          <>
            <div className="text-sm text-primary/40"></div>
            <Textarea
              className="text-md h-64"
              placeholder={t("details.description.placeholder")}
              value={desc}
              onChange={(e) => setDesc(e.target.value)}
              onFocus={handleDescriptionFocus}
              onBlur={handleDescriptionBlur}
            />
          </>
        )}

        <div className="flex w-full flex-row justify-end gap-2">
          {config?.cameras[search?.camera].audio_transcription.enabled &&
            search?.label == "speech" &&
            search?.end_time && (
              <Button onClick={onTranscribe}>
                <div className="flex gap-1">
                  {t("itemMenu.audioTranscription.label")}
                </div>
              </Button>
            )}
          {config?.cameras[search.camera].objects.genai.enabled &&
            search.end_time && (
              <div className="flex items-start">
                <Button
                  className="rounded-r-none border-r-0"
                  aria-label={t("details.button.regenerate.label")}
                  onClick={() => regenerateDescription("thumbnails")}
                >
                  {t("details.button.regenerate.title")}
                </Button>
                {search.has_snapshot && (
                  <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                      <Button
                        className="rounded-l-none border-l-0 px-2"
                        aria-label={t("details.expandRegenerationMenu")}
                      >
                        <FaChevronDown className="size-3" />
                      </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent>
                      <DropdownMenuItem
                        className="cursor-pointer"
                        aria-label={t("details.regenerateFromSnapshot")}
                        onClick={() => regenerateDescription("snapshot")}
                      >
                        {t("details.regenerateFromSnapshot")}
                      </DropdownMenuItem>
                      <DropdownMenuItem
                        className="cursor-pointer"
                        aria-label={t("details.regenerateFromThumbnails")}
                        onClick={() => regenerateDescription("thumbnails")}
                      >
                        {t("details.regenerateFromThumbnails")}
                      </DropdownMenuItem>
                    </DropdownMenuContent>
                  </DropdownMenu>
                )}
              </div>
            )}
          {((config?.cameras[search.camera].objects.genai.enabled &&
            search.end_time) ||
            !config?.cameras[search.camera].objects.genai.enabled) && (
            <Button
              variant="select"
              aria-label={t("button.save", { ns: "common" })}
              onClick={updateDescription}
            >
              {t("button.save", { ns: "common" })}
            </Button>
          )}

          <TextEntryDialog
            open={isSubLabelDialogOpen}
            setOpen={setIsSubLabelDialogOpen}
            title={t("details.editSubLabel.title")}
            description={
              search.label
                ? t("details.editSubLabel.desc", {
                    label: search.label,
                  })
                : t("details.editSubLabel.descNoLabel")
            }
            onSave={handleSubLabelSave}
            defaultValue={search?.sub_label || ""}
            allowEmpty={true}
          />
          <TextEntryDialog
            open={isLPRDialogOpen}
            setOpen={setIsLPRDialogOpen}
            title={t("details.editLPR.title")}
            description={
              search.label
                ? t("details.editLPR.desc", {
                    label: search.label,
                  })
                : t("details.editLPR.descNoLabel")
            }
            onSave={handleLPRSave}
            defaultValue={search?.data.recognized_license_plate || ""}
            allowEmpty={true}
          />
        </div>
      </div>
    </div>
  );
}

type ObjectSnapshotTabProps = {
  search: Event;
  className?: string;
  onEventUploaded?: () => void;
};
export function ObjectSnapshotTab({
  search,
  className,
}: ObjectSnapshotTabProps) {
  const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();

  return (
    <div className={cn("relative w-full", className)}>
      <ImageLoadingIndicator
        className="absolute inset-0 aspect-video min-h-[60dvh] w-full"
        imgLoaded={imgLoaded}
      />
      <div className={`${imgLoaded ? "visible" : "invisible"}`}>
        <TransformWrapper minScale={1.0} wheel={{ smoothStep: 0.005 }}>
          <div className="flex flex-col space-y-3">
            <TransformComponent
              wrapperStyle={{
                width: "100%",
                height: "100%",
              }}
              contentStyle={{
                position: "relative",
                width: "100%",
                height: "100%",
              }}
            >
              {search?.id && (
                <div className="relative mx-auto">
                  <img
                    ref={imgRef}
                    className="mx-auto max-h-[60dvh] rounded-lg bg-background object-contain"
                    src={`${baseUrl}api/events/${search?.id}/snapshot.jpg`}
                    alt={`${search?.label}`}
                    loading={isSafari ? "eager" : "lazy"}
                    onLoad={() => {
                      onImgLoad();
                    }}
                  />
                </div>
              )}
            </TransformComponent>
          </div>
        </TransformWrapper>
      </div>
    </div>
  );
}

type VideoTabProps = {
  search: SearchResult;
};

export function VideoTab({ search }: VideoTabProps) {
  const clipTimeRange = useMemo(() => {
    const startTime = search.start_time - REVIEW_PADDING;
    const endTime = (search.end_time ?? Date.now() / 1000) + REVIEW_PADDING;
    return `start/${startTime}/end/${endTime}`;
  }, [search]);

  const source = `${baseUrl}vod/${search.camera}/${clipTimeRange}/index.m3u8`;

  return (
    <>
      <span tabIndex={0} className="sr-only" />
      <GenericVideoPlayer source={source} />
    </>
  );
}
