import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import axios from "axios";
import { isDesktop, isMobile } from "react-device-detect";
import { baseUrl } from "@/api/baseUrl";
import Logo from "@/components/Logo";
import { FrigateConfig } from "@/types/frigateConfig";
import { TimeRange } from "@/types/timeline";
import { RecordingsSummary } from "@/types/review";
import { ExportMode } from "@/types/filter";
import {
  MotionSearchRequest,
  MotionSearchStartResponse,
  MotionSearchResult,
  MotionSearchMetrics,
  MotionSearchJobResults,
  MotionSearchJobPayload,
} from "@/types/motionSearch";
import { useJobStatus } from "@/api/ws";

import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Progress } from "@/components/ui/progress";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

import DynamicVideoPlayer from "@/components/player/dynamic/DynamicVideoPlayer";
import { DynamicVideoController } from "@/components/player/dynamic/DynamicVideoController";
import { DetailStreamProvider } from "@/context/detail-stream-context";
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
import CalendarFilterButton from "@/components/filter/CalendarFilterButton";
import ExportDialog from "@/components/overlay/ExportDialog";
import SaveExportOverlay from "@/components/overlay/SaveExportOverlay";
import ReviewActivityCalendar from "@/components/overlay/ReviewActivityCalendar";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import { SelectSeparator } from "@/components/ui/select";

import { useResizeObserver } from "@/hooks/resize-observer";
import { useFullscreen } from "@/hooks/use-fullscreen";
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
import { useCameraPreviews } from "@/hooks/use-camera-previews";
import { getChunkedTimeDay } from "@/utils/timelineUtil";

import { MotionData, REVIEW_PADDING, ZoomLevel } from "@/types/review";
import {
  ASPECT_VERTICAL_LAYOUT,
  ASPECT_WIDE_LAYOUT,
  Recording,
  RecordingSegment,
} from "@/types/record";
import { VideoResolutionType } from "@/types/live";
import {
  useFormattedTimestamp,
  useFormattedRange,
  use24HourTime,
} from "@/hooks/use-date-utils";
import MotionSearchROICanvas from "./MotionSearchROICanvas";
import MotionSearchDialog from "./MotionSearchDialog";
import { IoMdArrowRoundBack } from "react-icons/io";
import { FaArrowDown, FaCalendarAlt, FaCog, FaFire } from "react-icons/fa";
import { useNavigate } from "react-router-dom";
import { LuSearch, LuChevronRight, LuX } from "react-icons/lu";

type MotionSearchViewProps = {
  config: FrigateConfig;
  cameras: string[];
  selectedCamera: string | null;
  onCameraSelect: (camera: string) => void;
  cameraLocked?: boolean;
  selectedDay: Date | undefined;
  onDaySelect: (day: Date | undefined) => void;
  timeRange: TimeRange;
  timezone: string | undefined;
  onBack?: () => void;
};

const DEFAULT_EXPORT_WINDOW_SECONDS = 60;

export default function MotionSearchView({
  config,
  cameras,
  selectedCamera,
  onCameraSelect,
  cameraLocked = false,
  selectedDay,
  onDaySelect,
  timeRange,
  timezone,
  onBack,
}: MotionSearchViewProps) {
  const { t } = useTranslation([
    "views/motionSearch",
    "common",
    "views/recording",
  ]);
  const navigate = useNavigate();

  const is24Hour = use24HourTime(config);
  const resultTimestampFormat = useMemo(
    () =>
      is24Hour
        ? t("time.formattedTimestamp.24hour", { ns: "common" })
        : t("time.formattedTimestamp.12hour", { ns: "common" }),
    [is24Hour, t],
  );

  // Refs
  const contentRef = useRef<HTMLDivElement | null>(null);
  const mainLayoutRef = useRef<HTMLDivElement | null>(null);
  const timelineRef = useRef<HTMLDivElement | null>(null);
  const mainControllerRef = useRef<DynamicVideoController | null>(null);
  const jobIdRef = useRef<string | null>(null);
  const jobCameraRef = useRef<string | null>(null);

  const [isSearchDialogOpen, setIsSearchDialogOpen] = useState(true);
  const [isMobileSettingsOpen, setIsMobileSettingsOpen] = useState(false);
  const [mobileSettingsMode, setMobileSettingsMode] = useState<
    "actions" | "calendar"
  >("actions");

  // Recordings summary for the calendars (main view + search dialog). Fetched
  // whenever a camera is selected so it is cached and ready by the time the
  // dialog's date pickers render, regardless of open/closed state.
  const { data: recordingsSummary } = useSWR<RecordingsSummary>(
    selectedCamera
      ? [
          "recordings/summary",
          {
            timezone: timezone,
            cameras: selectedCamera,
          },
        ]
      : null,
  );

  // Camera previews – defer until dialog is closed
  const allPreviews = useCameraPreviews(timeRange, {
    camera: selectedCamera ?? undefined,
    fetchPreviews: !isSearchDialogOpen,
  });

  // ROI state
  const [polygonPoints, setPolygonPoints] = useState<number[][]>([]);
  const [isDrawingROI, setIsDrawingROI] = useState(true);

  // Search settings
  const [parallelMode, setParallelMode] = useState(false);
  const [threshold, setThreshold] = useState(30);
  const [minArea, setMinArea] = useState(20);
  const [maxResults, setMaxResults] = useState(25);

  // Job state
  const [jobId, setJobId] = useState<string | null>(null);
  const [jobCamera, setJobCamera] = useState<string | null>(null);
  const { payload: jobPayload } =
    useJobStatus<MotionSearchJobResults>("motion_search");
  const jobStatus = jobPayload as MotionSearchJobPayload | null;
  const formattedScanningTimestamp = useFormattedTimestamp(
    jobStatus?.scanning_timestamp ?? 0,
    resultTimestampFormat,
    timezone,
  );

  // Search state
  const [isSearching, setIsSearching] = useState(false);
  const [searchResults, setSearchResults] = useState<MotionSearchResult[]>([]);
  const [showSegmentHeatmap, setShowSegmentHeatmap] = useState(false);
  const [searchMetrics, setSearchMetrics] =
    useState<MotionSearchMetrics | null>(null);
  const [hasSearched, setHasSearched] = useState(false);
  const [searchRange, setSearchRange] = useState<TimeRange | undefined>(
    undefined,
  );
  const [pendingSeekTime, setPendingSeekTime] = useState<number | null>(null);
  const pendingSeekTimeRef = useRef<number | null>(null);

  // Formatted search window shown above the results (same date+time convention).
  const formattedSearchRange = useFormattedRange(
    searchRange?.after ?? 0,
    searchRange?.before ?? 0,
    resultTimestampFormat,
    timezone,
  );

  // Export state
  const [exportMode, setExportMode] = useState<ExportMode>("none");
  const [exportRange, setExportRange] = useState<TimeRange>();
  const [showExportPreview, setShowExportPreview] = useState(false);

  // Timeline state
  const initialStartTime = timeRange.before - 60;
  const [scrubbing, setScrubbing] = useState(false);
  const [currentTime, setCurrentTime] = useState<number>(initialStartTime);
  const [playerTime, setPlayerTime] = useState<number>(initialStartTime);
  const [playbackStart, setPlaybackStart] = useState(initialStartTime);

  const chunkedTimeRange = useMemo(
    () => getChunkedTimeDay(timeRange),
    [timeRange],
  );

  const [selectedRangeIdx, setSelectedRangeIdx] = useState(() => {
    const ranges = getChunkedTimeDay(timeRange);
    const index = ranges.findIndex(
      (chunk) =>
        chunk.after <= initialStartTime && chunk.before >= initialStartTime,
    );
    return index === -1 ? ranges.length - 1 : index;
  });

  const currentTimeRange = useMemo<TimeRange>(
    () =>
      chunkedTimeRange[selectedRangeIdx] ??
      chunkedTimeRange[chunkedTimeRange.length - 1],
    [selectedRangeIdx, chunkedTimeRange],
  );

  const clampExportTime = useCallback(
    (value: number) =>
      Math.min(timeRange.before, Math.max(timeRange.after, value)),
    [timeRange.after, timeRange.before],
  );

  const buildDefaultExportRange = useCallback(
    (anchorTime: number): TimeRange => {
      const halfWindow = DEFAULT_EXPORT_WINDOW_SECONDS / 2;
      let after = clampExportTime(anchorTime - halfWindow);
      let before = clampExportTime(anchorTime + halfWindow);

      if (before <= after) {
        before = clampExportTime(timeRange.before);
        after = clampExportTime(before - DEFAULT_EXPORT_WINDOW_SECONDS);
      }

      return { after, before };
    },
    [clampExportTime, timeRange.before],
  );

  const setExportStartTime = useCallback<
    React.Dispatch<React.SetStateAction<number>>
  >(
    (value) => {
      setExportRange((prev) => {
        const resolvedValue =
          typeof value === "function"
            ? value(prev?.after ?? currentTime)
            : value;
        const after = clampExportTime(resolvedValue);
        const before = Math.max(
          after,
          clampExportTime(
            prev?.before ?? after + DEFAULT_EXPORT_WINDOW_SECONDS,
          ),
        );
        return { after, before };
      });
    },
    [clampExportTime, currentTime],
  );

  const setExportEndTime = useCallback<
    React.Dispatch<React.SetStateAction<number>>
  >(
    (value) => {
      setExportRange((prev) => {
        const resolvedValue =
          typeof value === "function"
            ? value(prev?.before ?? currentTime)
            : value;
        const before = clampExportTime(resolvedValue);
        const after = Math.min(
          before,
          clampExportTime(
            prev?.after ?? before - DEFAULT_EXPORT_WINDOW_SECONDS,
          ),
        );
        return { after, before };
      });
    },
    [clampExportTime, currentTime],
  );

  useEffect(() => {
    if (
      (exportMode !== "timeline" && exportMode !== "timeline_multi") ||
      exportRange
    ) {
      return;
    }

    setExportRange(buildDefaultExportRange(currentTime));
  }, [exportMode, exportRange, buildDefaultExportRange, currentTime]);

  const handleExportPreview = useCallback(() => {
    if (!exportRange) {
      toast.error(
        t("export.toast.error.noVaildTimeSelected", {
          ns: "components/dialog",
        }),
        {
          position: "top-center",
        },
      );
      return;
    }

    setShowExportPreview(true);
  }, [exportRange, setShowExportPreview, t]);

  const handleExportCancel = useCallback(() => {
    setShowExportPreview(false);
    setExportRange(undefined);
    setExportMode("none");
  }, [setExportMode, setExportRange, setShowExportPreview]);

  const setExportRangeWithPause = useCallback(
    (range: TimeRange | undefined) => {
      setExportRange(range);

      if (range != undefined) {
        mainControllerRef.current?.pause();
      }
    },
    [setExportRange],
  );

  const openMobileExport = useCallback(() => {
    const now = new Date(timeRange.before * 1000);
    now.setHours(now.getHours() - 1);

    setExportRangeWithPause({
      before: timeRange.before,
      after: now.getTime() / 1000,
    });
    setExportMode("select");
    setIsMobileSettingsOpen(false);
    setMobileSettingsMode("actions");
  }, [setExportRangeWithPause, timeRange.before]);

  const handleExportSave = useCallback(() => {
    if (!exportRange || !selectedCamera) {
      toast.error(
        t("export.toast.error.noVaildTimeSelected", {
          ns: "components/dialog",
        }),
        {
          position: "top-center",
        },
      );
      return;
    }

    if (exportRange.before < exportRange.after) {
      toast.error(
        t("export.toast.error.endTimeMustAfterStartTime", {
          ns: "components/dialog",
        }),
        { position: "top-center" },
      );
      return;
    }

    axios
      .post(
        `export/${selectedCamera}/start/${Math.round(exportRange.after)}/end/${Math.round(exportRange.before)}`,
        {
          playback: "realtime",
        },
      )
      .then((response) => {
        if (response.status < 300) {
          toast.success(
            t("export.toast.success", { ns: "components/dialog" }),
            {
              position: "top-center",
              action: (
                <a
                  href={`${baseUrl}export`}
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  <Button>
                    {t("export.toast.view", { ns: "components/dialog" })}
                  </Button>
                </a>
              ),
            },
          );
          setShowExportPreview(false);
          setExportRange(undefined);
          setExportMode("none");
        }
      })
      .catch((error) => {
        const errorMessage =
          error.response?.data?.message ||
          error.response?.data?.detail ||
          "Unknown error";
        toast.error(
          t("export.toast.error.failed", {
            ns: "components/dialog",
            error: errorMessage,
          }),
          { position: "top-center" },
        );
      });
  }, [
    exportRange,
    selectedCamera,
    setExportMode,
    setExportRange,
    setShowExportPreview,
    t,
  ]);

  useEffect(() => {
    if (!searchRange) {
      setSearchRange(timeRange);
    }
  }, [searchRange, timeRange]);

  // Video player state
  const [fullResolution, setFullResolution] = useState<VideoResolutionType>({
    width: 0,
    height: 0,
  });

  // Fullscreen
  const { fullscreen, toggleFullscreen, supportsFullScreen } =
    useFullscreen(mainLayoutRef);

  // Timeline zoom settings
  const [zoomSettings, setZoomSettings] = useState({
    segmentDuration: 30,
    timestampSpread: 15,
  });

  const possibleZoomLevels: ZoomLevel[] = useMemo(
    () => [
      { segmentDuration: 30, timestampSpread: 15 },
      { segmentDuration: 15, timestampSpread: 5 },
      { segmentDuration: 5, timestampSpread: 1 },
    ],
    [],
  );

  const handleZoomChange = useCallback(
    (newZoomLevel: number) => {
      setZoomSettings(possibleZoomLevels[newZoomLevel]);
    },
    [possibleZoomLevels],
  );

  const currentZoomLevel = useMemo(
    () =>
      possibleZoomLevels.findIndex(
        (level) => level.segmentDuration === zoomSettings.segmentDuration,
      ),
    [possibleZoomLevels, zoomSettings.segmentDuration],
  );

  const { isZooming, zoomDirection } = useTimelineZoom({
    zoomSettings,
    zoomLevels: possibleZoomLevels,
    onZoomChange: handleZoomChange,
    timelineRef: timelineRef,
    timelineDuration: timeRange.after - timeRange.before,
  });

  // Motion data for timeline
  const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
    { segmentDuration: zoomSettings.segmentDuration },
  );

  const alignedAfter = alignStartDateToTimeline(timeRange.after);
  const alignedBefore = alignEndDateToTimeline(timeRange.before);

  const { data: motionData, isLoading: isMotionLoading } = useSWR<MotionData[]>(
    selectedCamera && !isSearchDialogOpen
      ? [
          "review/activity/motion",
          {
            before: alignedBefore,
            after: alignedAfter,
            scale: Math.round(zoomSettings.segmentDuration / 2),
            cameras: selectedCamera,
          },
        ]
      : null,
  );

  const { data: noRecordings } = useSWR<RecordingSegment[]>(
    selectedCamera && !isSearchDialogOpen
      ? [
          "recordings/unavailable",
          {
            before: alignedBefore,
            after: alignedAfter,
            scale: Math.round(zoomSettings.segmentDuration),
            cameras: selectedCamera,
          },
        ]
      : null,
  );

  const recordingParams = useMemo(
    () => ({
      before: currentTimeRange.before,
      after: currentTimeRange.after,
    }),
    [currentTimeRange],
  );

  const { data: playbackRecordings } = useSWR<Recording[]>(
    selectedCamera && !isSearchDialogOpen
      ? [`${selectedCamera}/recordings`, recordingParams]
      : null,
    { revalidateOnFocus: false },
  );

  const activeSegmentHeatmap = useMemo(() => {
    if (!showSegmentHeatmap || !playbackRecordings?.length) {
      return null;
    }

    const activeSegment = playbackRecordings.find(
      (recording) =>
        recording.start_time <= currentTime &&
        recording.end_time >= currentTime,
    );

    return activeSegment?.motion_heatmap ?? null;
  }, [currentTime, playbackRecordings, showSegmentHeatmap]);

  // Camera aspect ratio
  const getCameraAspect = useCallback(
    (cam: string) => {
      if (!config) return undefined;
      if (
        cam === selectedCamera &&
        fullResolution.width &&
        fullResolution.height
      ) {
        return fullResolution.width / fullResolution.height;
      }
      const camera = config.cameras[cam];
      if (!camera) return undefined;
      return camera.detect.width / camera.detect.height;
    },
    [config, fullResolution, selectedCamera],
  );

  const mainCameraAspect = useMemo(() => {
    if (!selectedCamera) return "normal";
    const aspectRatio = getCameraAspect(selectedCamera);
    if (!aspectRatio) return "normal";
    if (aspectRatio > ASPECT_WIDE_LAYOUT) return "wide";
    if (aspectRatio < ASPECT_VERTICAL_LAYOUT) return "tall";
    return "normal";
  }, [getCameraAspect, selectedCamera]);

  const grow = useMemo(() => {
    if (mainCameraAspect === "wide") return "w-full aspect-wide";
    if (mainCameraAspect === "tall") {
      return isDesktop
        ? "size-full aspect-tall flex flex-col justify-center"
        : "size-full";
    }
    return "w-full aspect-video";
  }, [mainCameraAspect]);

  // Container resize observer
  const [{ width: containerWidth, height: containerHeight }] =
    useResizeObserver(mainLayoutRef);

  const useHeightBased = useMemo(() => {
    if (!containerWidth || !containerHeight || !selectedCamera) return false;
    const cameraAspectRatio = getCameraAspect(selectedCamera);
    if (!cameraAspectRatio) return false;
    const availableAspectRatio = containerWidth / containerHeight;
    return availableAspectRatio >= cameraAspectRatio;
  }, [containerWidth, containerHeight, getCameraAspect, selectedCamera]);

  const onClipEnded = useCallback(() => {
    if (!mainControllerRef.current) {
      return;
    }

    if (selectedRangeIdx < chunkedTimeRange.length - 1) {
      setSelectedRangeIdx(selectedRangeIdx + 1);
    }
  }, [selectedRangeIdx, chunkedTimeRange]);

  const updateSelectedSegment = useCallback(
    (nextTime: number, updateStartTime: boolean) => {
      const index = chunkedTimeRange.findIndex(
        (segment) => segment.after <= nextTime && segment.before >= nextTime,
      );

      if (index != -1) {
        if (updateStartTime) {
          setPlaybackStart(nextTime);
        }

        setSelectedRangeIdx(index);
      }
    },
    [chunkedTimeRange],
  );

  // Handle scrubbing
  useEffect(() => {
    if (scrubbing || exportRange) {
      if (
        currentTime > currentTimeRange.before + 60 ||
        currentTime < currentTimeRange.after - 60
      ) {
        updateSelectedSegment(currentTime, false);
        return;
      }

      mainControllerRef.current?.scrubToTimestamp(currentTime);
    }
    // we only want to seek when current time updates
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    currentTime,
    scrubbing,
    timeRange,
    currentTimeRange,
    updateSelectedSegment,
  ]);

  useEffect(() => {
    if (pendingSeekTimeRef.current != null) {
      return;
    }

    const nextTime = timeRange.before - 60;
    const index = chunkedTimeRange.findIndex(
      (segment) => segment.after <= nextTime && segment.before >= nextTime,
    );

    setCurrentTime(nextTime);
    setPlayerTime(nextTime);
    setPlaybackStart(nextTime);
    setSelectedRangeIdx(index === -1 ? chunkedTimeRange.length - 1 : index);
    mainControllerRef.current?.seekToTimestamp(nextTime, true);
  }, [timeRange, chunkedTimeRange]);

  useEffect(() => {
    if (!scrubbing) {
      if (Math.abs(currentTime - playerTime) > 10) {
        if (
          currentTimeRange.after <= currentTime &&
          currentTimeRange.before >= currentTime
        ) {
          mainControllerRef.current?.seekToTimestamp(currentTime, true);
        } else {
          updateSelectedSegment(currentTime, true);
        }
      } else if (playerTime != currentTime) {
        mainControllerRef.current?.play();
      }
    }
    // we only want to seek when current time doesn't match the player update time
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [currentTime, scrubbing, playerTime]);

  // Manually seek to timestamp
  const manuallySetCurrentTime = useCallback(
    (time: number, play: boolean = false) => {
      if (!currentTimeRange) {
        return;
      }

      setCurrentTime(time);

      if (currentTimeRange.after <= time && currentTimeRange.before >= time) {
        mainControllerRef.current?.seekToTimestamp(time, play);
      } else {
        updateSelectedSegment(time, true);
      }
    },
    [currentTimeRange, updateSelectedSegment],
  );

  const canStartSearch = Boolean(
    selectedCamera &&
      searchRange &&
      searchRange.before >= searchRange.after &&
      polygonPoints.length >= 3 &&
      !isDrawingROI,
  );

  const cancelMotionSearchJob = useCallback(
    async (jobIdToCancel: string | null, cameraToCancel: string | null) => {
      if (!jobIdToCancel || !cameraToCancel) {
        return;
      }

      try {
        await axios.post(
          `${cameraToCancel}/search/motion/${jobIdToCancel}/cancel`,
        );
      } catch {
        // Best effort cancellation.
      }
    },
    [],
  );

  const cancelMotionSearchJobViaBeacon = useCallback(
    (jobIdToCancel: string | null, cameraToCancel: string | null) => {
      if (!jobIdToCancel || !cameraToCancel) {
        return;
      }

      const url = `${window.location.origin}/api/${cameraToCancel}/search/motion/${jobIdToCancel}/cancel`;

      const xhr = new XMLHttpRequest();
      try {
        xhr.open("POST", url, false);
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.setRequestHeader("X-CSRF-TOKEN", "1");
        xhr.setRequestHeader("X-CACHE-BYPASS", "1");
        xhr.withCredentials = true;
        xhr.send("{}");
      } catch {
        // Best effort cancellation during unload.
      }
    },
    [],
  );

  useEffect(() => {
    jobIdRef.current = jobId;
  }, [jobId]);

  useEffect(() => {
    jobCameraRef.current = jobCamera;
  }, [jobCamera]);

  useEffect(() => {
    return () => {
      void cancelMotionSearchJob(jobIdRef.current, jobCameraRef.current);
    };
  }, [cancelMotionSearchJob]);

  useEffect(() => {
    const handleBeforeUnload = () => {
      cancelMotionSearchJobViaBeacon(jobIdRef.current, jobCameraRef.current);
    };

    window.addEventListener("beforeunload", handleBeforeUnload);

    return () => {
      window.removeEventListener("beforeunload", handleBeforeUnload);
    };
  }, [cancelMotionSearchJobViaBeacon]);

  const handleNewSearch = useCallback(() => {
    if (jobId && jobCamera) {
      void cancelMotionSearchJob(jobId, jobCamera);
      if (isSearching) {
        toast.message(t("searchCancelled"));
      }
    }
    setSearchResults([]);
    setSearchMetrics(null);
    setIsSearching(false);
    setJobId(null);
    setJobCamera(null);
    setHasSearched(false);
    setPendingSeekTime(null);
    setSearchRange(timeRange);
    setIsSearchDialogOpen(true);
  }, [cancelMotionSearchJob, isSearching, jobCamera, jobId, t, timeRange]);

  // Perform motion search
  const performSearch = useCallback(async () => {
    if (!selectedCamera) {
      toast.error(t("errors.noCamera"));
      return;
    }

    if (polygonPoints.length < 3) {
      toast.error(t("errors.polygonTooSmall"));
      return;
    }

    if (!searchRange) {
      toast.error(t("errors.noTimeRange"));
      return;
    }

    if (searchRange.before < searchRange.after) {
      toast.error(t("errors.invalidTimeRange"));
      return;
    }

    setIsSearching(true);
    setSearchResults([]);
    setHasSearched(true);

    try {
      const request: MotionSearchRequest = {
        start_time: searchRange.after,
        end_time: searchRange.before,
        polygon_points: polygonPoints,
        parallel: parallelMode,
        threshold,
        min_area: minArea,
        max_results: maxResults,
      };

      const response = await axios.post<MotionSearchStartResponse>(
        `${selectedCamera}/search/motion`,
        request,
      );

      if (response.data.success) {
        setJobId(response.data.job_id);
        setJobCamera(selectedCamera);
        setIsSearchDialogOpen(false);
        toast.success(t("searchStarted"));
      } else {
        toast.error(
          t("errors.searchFailed", { message: response.data.message }),
        );
        setIsSearching(false);
      }
    } catch (error) {
      let errorMessage = t("errors.unknown");

      if (axios.isAxiosError<{ message?: string; detail?: string }>(error)) {
        const responseData = error.response?.data as
          | {
              message?: unknown;
              detail?: unknown;
              error?: unknown;
              errors?: unknown;
            }
          | string
          | undefined;

        if (typeof responseData === "string") {
          errorMessage = responseData;
        } else if (responseData) {
          const apiMessage =
            responseData.message ??
            responseData.detail ??
            responseData.error ??
            responseData.errors;

          if (Array.isArray(apiMessage)) {
            errorMessage = apiMessage
              .map((item) =>
                typeof item === "string"
                  ? item
                  : ((item as { msg?: string })?.msg ?? JSON.stringify(item)),
              )
              .join(", ");
          } else if (typeof apiMessage === "string") {
            errorMessage = apiMessage;
          } else if (apiMessage) {
            errorMessage = JSON.stringify(apiMessage);
          } else {
            errorMessage = error.message || errorMessage;
          }
        } else {
          errorMessage = error.message || errorMessage;
        }
      } else if (error instanceof Error) {
        errorMessage = error.message;
      }

      toast.error(t("errors.searchFailed", { message: errorMessage }));
      setIsSearching(false);
    }
  }, [
    selectedCamera,
    polygonPoints,
    searchRange,
    parallelMode,
    threshold,
    minArea,
    maxResults,
    t,
  ]);

  // Monitor job status and update UI when complete
  useEffect(() => {
    if (!jobStatus) {
      return;
    }

    if (!jobId || jobStatus.id !== jobId) {
      return;
    }

    const resultList = jobStatus.results?.results;

    if (jobStatus.status === "success") {
      setSearchResults(resultList ?? []);
      setSearchMetrics(jobStatus.metrics ?? null);
      setIsSearching(false);
      setJobId(null);
      setJobCamera(null);
      toast.success(t("changesFound", { count: resultList?.length ?? 0 }));
    } else if (
      jobStatus.status === "queued" ||
      jobStatus.status === "running"
    ) {
      setSearchMetrics(jobStatus.metrics ?? null);
      // Stream partial results as they arrive
      if (resultList && resultList.length > 0) {
        setSearchResults(resultList);
      }
    } else if (jobStatus.status === "failed") {
      setIsSearching(false);
      setJobId(null);
      setJobCamera(null);
      toast.error(
        t("errors.searchFailed", {
          message: jobStatus.error_message || t("errors.unknown"),
        }),
      );
    } else if (jobStatus.status === "cancelled") {
      setIsSearching(false);
      setJobId(null);
      setJobCamera(null);
      toast.message(t("searchCancelled"));
    }
  }, [jobStatus, jobId, t]);

  // Handle result click
  const handleResultClick = useCallback(
    (result: MotionSearchResult) => {
      if (
        result.timestamp < timeRange.after ||
        result.timestamp > timeRange.before
      ) {
        pendingSeekTimeRef.current = result.timestamp;
        setPendingSeekTime(result.timestamp);
        onDaySelect(new Date(result.timestamp * 1000));
        return;
      }

      // start playback a few seconds before the change so the motion is in view
      manuallySetCurrentTime(result.timestamp - REVIEW_PADDING, true);
    },
    [manuallySetCurrentTime, onDaySelect, timeRange],
  );

  useEffect(() => {
    if (pendingSeekTime == null) {
      return;
    }

    if (
      pendingSeekTime >= timeRange.after &&
      pendingSeekTime <= timeRange.before
    ) {
      manuallySetCurrentTime(pendingSeekTime - REVIEW_PADDING, true);
      setPendingSeekTime(null);
      pendingSeekTimeRef.current = null;
    }
  }, [pendingSeekTime, timeRange, manuallySetCurrentTime]);

  if (!selectedCamera) {
    return (
      <div className="flex size-full items-center justify-center">
        <p className="text-muted-foreground">{t("selectCamera")}</p>
      </div>
    );
  }

  const timelinePanel = (
    <>
      <div className="pointer-events-none absolute inset-x-0 top-0 z-20 h-[30px] w-full bg-gradient-to-b from-secondary to-transparent" />
      <div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 h-[30px] w-full bg-gradient-to-t from-secondary to-transparent" />

      <SaveExportOverlay
        className="pointer-events-none absolute inset-x-0 top-0 z-30"
        show={
          (exportMode === "timeline" || exportMode === "timeline_multi") &&
          Boolean(exportRange)
        }
        hidePreview={exportMode === "timeline_multi"}
        saveLabel={
          exportMode === "timeline_multi"
            ? t("export.fromTimeline.useThisRange", { ns: "components/dialog" })
            : undefined
        }
        onPreview={handleExportPreview}
        onSave={() => {
          if (exportMode === "timeline_multi") {
            setExportMode("select");
            return;
          }

          handleExportSave();
        }}
        onCancel={handleExportCancel}
      />

      {!isMotionLoading ? (
        <MotionReviewTimeline
          timelineRef={timelineRef}
          segmentDuration={zoomSettings.segmentDuration}
          timestampSpread={zoomSettings.timestampSpread}
          timelineStart={timeRange.before}
          timelineEnd={timeRange.after}
          showHandlebar={true}
          handlebarTime={currentTime}
          setHandlebarTime={setCurrentTime}
          events={[]}
          motion_events={motionData ?? []}
          noRecordingRanges={noRecordings ?? []}
          contentRef={contentRef}
          onHandlebarDraggingChange={(dragging) => setScrubbing(dragging)}
          showExportHandles={
            (exportMode === "timeline" || exportMode === "timeline_multi") &&
            Boolean(exportRange)
          }
          exportStartTime={exportRange?.after}
          exportEndTime={exportRange?.before}
          setExportStartTime={setExportStartTime}
          setExportEndTime={setExportEndTime}
          isZooming={isZooming}
          zoomDirection={zoomDirection}
          onZoomChange={handleZoomChange}
          possibleZoomLevels={possibleZoomLevels}
          currentZoomLevel={currentZoomLevel}
        />
      ) : (
        <Skeleton className="size-full" />
      )}
    </>
  );

  const progressMetrics = jobStatus?.metrics ?? searchMetrics;
  const progressValue = (() => {
    if (jobStatus?.progress != null) {
      return Math.min(100, Math.max(0, jobStatus.progress * 100));
    }
    if (!progressMetrics || progressMetrics.segments_scanned <= 0) {
      return 0;
    }
    const skipped =
      progressMetrics.heatmap_roi_skip_segments +
      progressMetrics.metadata_inactive_segments;
    const totalWork = progressMetrics.segments_scanned - skipped;
    const doneWork = progressMetrics.segments_processed - skipped;
    if (totalWork <= 0) {
      return 100;
    }
    return Math.min(100, Math.max(0, (doneWork / totalWork) * 100));
  })();

  const wallTimeLabel = searchMetrics
    ? searchMetrics.wall_time_seconds >= 60
      ? t("metrics.minutesSeconds", {
          minutes: Math.floor(searchMetrics.wall_time_seconds / 60),
          seconds: Math.round(searchMetrics.wall_time_seconds % 60),
        })
      : t("metrics.seconds", {
          seconds: searchMetrics.wall_time_seconds.toFixed(1),
        })
    : "";

  const resultsPanel = (
    <>
      {(hasSearched || isSearching) && (
        <div className="flex flex-col gap-1 px-3 py-2.5">
          {searchRange && (
            <div className="text-sm font-medium text-foreground">
              {formattedSearchRange}
            </div>
          )}
          {searchMetrics && (
            <div className="text-xs text-muted-foreground">
              {t("metrics.scanSummary", {
                segments: searchMetrics.segments_scanned,
                time: wallTimeLabel,
              })}
            </div>
          )}
        </div>
      )}

      <ScrollArea className="flex-1 [&>[data-radix-scroll-area-viewport]>div]:!block">
        {isSearching && (
          <div className="flex flex-col gap-1.5 border-b p-3">
            <div className="flex items-center gap-2">
              <Progress className="h-1.5 flex-1" value={progressValue} />
              <Button
                variant="ghost"
                size="icon"
                className="size-6 shrink-0 text-muted-foreground"
                aria-label={t("cancelSearch")}
                title={t("cancelSearch")}
                onClick={() => {
                  void cancelMotionSearchJob(jobId, jobCamera);
                  setIsSearching(false);
                  setJobId(null);
                  setJobCamera(null);
                  toast.success(t("searchCancelled"));
                }}
              >
                <LuX className="size-4" />
              </Button>
            </div>
            <div className="text-xs text-muted-foreground">
              {jobStatus?.scanning_timestamp != null
                ? t("scanning", { time: formattedScanningTimestamp })
                : t("searching")}
            </div>
          </div>
        )}
        {searchMetrics &&
          (isSearching || searchResults.length > 0 || hasSearched) && (
            <Collapsible>
              <CollapsibleTrigger className="group flex w-full items-center gap-1 px-3 py-2.5 text-left text-xs text-muted-foreground hover:bg-accent">
                <LuChevronRight className="size-3 shrink-0 transition-transform group-data-[state=open]:rotate-90" />
                {t("metrics.title")}
              </CollapsibleTrigger>
              <CollapsibleContent>
                <div className="space-y-0.5 px-3 pb-3 text-xs text-muted-foreground">
                  {searchMetrics.segments_processed > 0 && (
                    <div className="flex justify-between font-medium">
                      <span>{t("metrics.segmentsProcessed")}</span>
                      <span className="text-primary-variant">
                        {searchMetrics.segments_processed}
                      </span>
                    </div>
                  )}
                  {searchMetrics.metadata_inactive_segments > 0 && (
                    <div className="flex justify-between">
                      <span>{t("metrics.segmentsSkippedInactive")}</span>
                      <span className="text-primary-variant">
                        {searchMetrics.metadata_inactive_segments}
                      </span>
                    </div>
                  )}
                  {searchMetrics.heatmap_roi_skip_segments > 0 && (
                    <div className="flex justify-between">
                      <span>{t("metrics.segmentsSkippedHeatmap")}</span>
                      <span className="text-primary-variant">
                        {searchMetrics.heatmap_roi_skip_segments}
                      </span>
                    </div>
                  )}
                  {searchMetrics.fallback_full_range_segments > 0 && (
                    <div className="flex justify-between">
                      <span>{t("metrics.fallbackFullRange")}</span>
                      <span className="text-primary-variant">
                        {searchMetrics.fallback_full_range_segments}
                      </span>
                    </div>
                  )}
                  <div className="flex justify-between">
                    <span>{t("metrics.framesDecoded")}</span>
                    <span className="text-primary-variant">
                      {searchMetrics.frames_decoded}
                    </span>
                  </div>
                  <div className="flex justify-between">
                    <span>{t("metrics.wallTime")}</span>
                    <span className="text-primary-variant">
                      {wallTimeLabel}
                    </span>
                  </div>
                  {searchMetrics.segments_with_errors > 0 && (
                    <div className="flex justify-between text-destructive">
                      <span>{t("metrics.segmentErrors")}</span>
                      <span className="text-primary-variant">
                        {searchMetrics.segments_with_errors}
                      </span>
                    </div>
                  )}
                </div>
              </CollapsibleContent>
            </Collapsible>
          )}

        {(searchResults.length > 0 || (hasSearched && !isSearching)) && (
          <div className="border-t px-1.5 pb-1.5 pt-3">
            <h3 className="text-sm font-medium tracking-wide text-muted-foreground">
              {searchResults.length > 0 && (
                <span className="ml-1.5">{searchResults.length}</span>
              )}{" "}
              {t("results")}
            </h3>
          </div>
        )}

        {searchResults.length === 0 && !isSearching ? (
          <div className="p-4 text-center text-sm text-muted-foreground">
            {hasSearched ? t("noChangesFound") : t("noResultsYet")}
          </div>
        ) : searchResults.length > 0 ? (
          <div className="flex flex-col gap-1 px-1 pb-2">
            {searchResults.map((result, index) => (
              <SearchResultItem
                key={index}
                result={result}
                timezone={timezone}
                timestampFormat={resultTimestampFormat}
                onClick={() => handleResultClick(result)}
              />
            ))}
          </div>
        ) : null}
      </ScrollArea>
    </>
  );

  return (
    <DetailStreamProvider
      isDetailMode={false}
      currentTime={currentTime}
      camera={selectedCamera ?? ""}
    >
      <div ref={contentRef} className="flex size-full flex-col pt-2">
        <Toaster closeButton={true} position="top-center" />
        <MotionSearchDialog
          open={isSearchDialogOpen}
          onOpenChange={setIsSearchDialogOpen}
          config={config}
          cameras={cameras}
          selectedCamera={selectedCamera}
          onCameraSelect={onCameraSelect}
          cameraLocked={cameraLocked}
          polygonPoints={polygonPoints}
          setPolygonPoints={setPolygonPoints}
          isDrawingROI={isDrawingROI}
          setIsDrawingROI={setIsDrawingROI}
          parallelMode={parallelMode}
          setParallelMode={setParallelMode}
          threshold={threshold}
          setThreshold={setThreshold}
          minArea={minArea}
          setMinArea={setMinArea}
          maxResults={maxResults}
          setMaxResults={setMaxResults}
          searchRange={searchRange}
          setSearchRange={setSearchRange}
          defaultRange={timeRange}
          isSearching={isSearching}
          canStartSearch={canStartSearch}
          onStartSearch={performSearch}
          timezone={timezone}
        />

        {/* Header */}
        <div className="relative mb-2 flex h-11 w-full items-center justify-between px-2">
          {isMobile && (
            <Logo className="absolute inset-x-1/2 h-8 -translate-x-1/2" />
          )}
          {(cameraLocked || onBack) && (
            <div className="flex items-center gap-2">
              <Button
                className="flex items-center gap-2.5 rounded-lg"
                aria-label={t("label.back", { ns: "common" })}
                size="sm"
                onClick={() => (onBack ? onBack() : navigate(-1))}
              >
                <IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
                {isDesktop && (
                  <div className="text-primary">
                    {t("button.back", { ns: "common" })}
                  </div>
                )}
              </Button>
            </div>
          )}
          <div className="flex w-full items-center justify-end gap-2">
            <div className="hidden h-9 cursor-pointer items-center justify-start rounded-md bg-secondary p-2 text-sm hover:bg-secondary/80 md:flex">
              <Switch
                id="heatmap-toggle"
                checked={showSegmentHeatmap}
                onCheckedChange={setShowSegmentHeatmap}
              />
              <label
                htmlFor="heatmap-toggle"
                className="ml-2 cursor-pointer text-sm text-primary"
              >
                {t("motionHeatmapLabel")}
              </label>
            </div>
            <Button
              className="rounded-lg md:hidden"
              variant={showSegmentHeatmap ? "select" : "default"}
              size="sm"
              aria-label={t("motionHeatmapLabel")}
              onClick={() => setShowSegmentHeatmap((prev) => !prev)}
            >
              <FaFire className="size-4" />
            </Button>
            {isDesktop ? (
              <>
                <ExportDialog
                  camera={selectedCamera}
                  currentTime={currentTime}
                  latestTime={timeRange.before}
                  mode={exportMode}
                  range={exportRange}
                  showPreview={showExportPreview}
                  setRange={setExportRangeWithPause}
                  setMode={setExportMode}
                  setShowPreview={setShowExportPreview}
                />
                <CalendarFilterButton
                  recordingsSummary={recordingsSummary}
                  day={selectedDay}
                  updateSelectedDay={onDaySelect}
                />
              </>
            ) : (
              <Drawer
                open={isMobileSettingsOpen}
                onOpenChange={(open) => {
                  setIsMobileSettingsOpen(open);

                  if (!open) {
                    setMobileSettingsMode("actions");
                  }
                }}
              >
                <DrawerTrigger asChild>
                  <Button
                    className="rounded-lg"
                    size="sm"
                    aria-label={t("filters", { ns: "views/recording" })}
                    onClick={() => setMobileSettingsMode("actions")}
                  >
                    <FaCog className="size-5 text-secondary-foreground" />
                  </Button>
                </DrawerTrigger>
                <DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl px-4 pb-4">
                  {mobileSettingsMode == "actions" ? (
                    <div className="flex w-full flex-col gap-2 p-4">
                      <Button
                        className="flex w-full items-center justify-center gap-2"
                        aria-label={t("menu.export", { ns: "common" })}
                        onClick={openMobileExport}
                      >
                        <FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
                        {t("menu.export", { ns: "common" })}
                      </Button>
                      <Button
                        className="flex w-full items-center justify-center gap-2"
                        aria-label={t("calendar", { ns: "views/recording" })}
                        variant={selectedDay ? "select" : "default"}
                        onClick={() => setMobileSettingsMode("calendar")}
                      >
                        <FaCalendarAlt
                          className={
                            selectedDay
                              ? "text-selected-foreground"
                              : "text-secondary-foreground"
                          }
                        />
                        {t("calendar", { ns: "views/recording" })}
                      </Button>
                    </div>
                  ) : (
                    <div className="flex w-full flex-col">
                      <div className="relative h-8 w-full">
                        <div
                          className="absolute left-0 text-selected"
                          onClick={() => setMobileSettingsMode("actions")}
                        >
                          {t("button.back", { ns: "common" })}
                        </div>
                        <div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
                          {t("calendar", { ns: "views/recording" })}
                        </div>
                      </div>
                      <div className="flex w-full flex-row justify-center">
                        <ReviewActivityCalendar
                          recordingsSummary={recordingsSummary}
                          selectedDay={selectedDay}
                          onSelect={(day) => {
                            onDaySelect(day);
                            setIsMobileSettingsOpen(false);
                            setMobileSettingsMode("actions");
                          }}
                        />
                      </div>
                      <SelectSeparator />
                      <div className="flex items-center justify-center p-2">
                        <Button
                          aria-label={t("button.reset", { ns: "common" })}
                          onClick={() => {
                            onDaySelect(undefined);
                            setIsMobileSettingsOpen(false);
                            setMobileSettingsMode("actions");
                          }}
                        >
                          {t("button.reset", { ns: "common" })}
                        </Button>
                      </div>
                    </div>
                  )}
                </DrawerContent>
              </Drawer>
            )}
            <Button
              variant="select"
              size="sm"
              aria-label={t("newSearch")}
              onClick={handleNewSearch}
            >
              {isDesktop ? t("newSearch") : <LuSearch className="size-5" />}
            </Button>
          </div>
        </div>

        {!isDesktop && (
          <div className="hidden">
            <ExportDialog
              camera={selectedCamera}
              currentTime={currentTime}
              latestTime={timeRange.before}
              mode={exportMode}
              range={exportRange}
              showPreview={showExportPreview}
              setRange={setExportRangeWithPause}
              setMode={setExportMode}
              setShowPreview={setShowExportPreview}
            />
          </div>
        )}

        {/* Main Content */}
        <div
          className={cn(
            "flex flex-1 overflow-hidden",
            isDesktop ? "flex-row" : "flex-col gap-2 landscape:flex-row",
          )}
        >
          {/* Video Player with ROI Canvas */}
          <div
            ref={mainLayoutRef}
            className={cn(
              "flex flex-col overflow-hidden",
              isDesktop
                ? mainCameraAspect === "tall"
                  ? "mr-2 h-full min-h-0 min-w-0 flex-1 items-center"
                  : "mr-2 h-full min-h-0 min-w-0 flex-1"
                : mainCameraAspect === "tall"
                  ? "flex-1 portrait:h-[40dvh] portrait:max-h-[40dvh] portrait:flex-shrink-0 portrait:flex-grow-0 portrait:basis-auto portrait:items-center portrait:justify-center"
                  : "flex-1 portrait:max-h-[40dvh] portrait:flex-shrink-0 portrait:flex-grow-0 portrait:basis-auto landscape:items-center landscape:justify-center",
            )}
          >
            <div
              className={cn(
                "relative flex max-h-full min-h-0 min-w-0 max-w-full items-center justify-center",
                isDesktop
                  ? mainCameraAspect === "tall" || useHeightBased
                    ? "h-full"
                    : "w-full"
                  : mainCameraAspect == "tall"
                    ? "aspect-tall h-full w-auto max-w-full flex-shrink-0 landscape:h-full"
                    : cn(
                        "flex-shrink-0 portrait:w-full landscape:h-full",
                        mainCameraAspect == "wide"
                          ? "aspect-wide"
                          : "aspect-video",
                      ),
              )}
              style={{
                aspectRatio: getCameraAspect(selectedCamera),
              }}
            >
              {/* Video Player */}
              <DynamicVideoPlayer
                className={grow}
                camera={selectedCamera}
                timeRange={currentTimeRange}
                cameraPreviews={allPreviews ?? []}
                startTimestamp={playbackStart}
                hotKeys={exportMode != "select"}
                fullscreen={fullscreen}
                onTimestampUpdate={(timestamp) => {
                  setPlayerTime(timestamp);
                  setCurrentTime(timestamp);
                }}
                onClipEnded={onClipEnded}
                onSeekToTime={manuallySetCurrentTime}
                onControllerReady={(controller) => {
                  mainControllerRef.current = controller;
                }}
                isScrubbing={
                  scrubbing ||
                  exportMode == "timeline" ||
                  exportMode == "timeline_multi"
                }
                supportsFullscreen={supportsFullScreen}
                setFullResolution={setFullResolution}
                toggleFullscreen={toggleFullscreen}
                containerRef={mainLayoutRef}
                transformedOverlay={
                  <MotionSearchROICanvas
                    camera={selectedCamera}
                    width={config.cameras[selectedCamera]?.detect.width ?? 1920}
                    height={
                      config.cameras[selectedCamera]?.detect.height ?? 1080
                    }
                    polygonPoints={polygonPoints}
                    setPolygonPoints={setPolygonPoints}
                    isDrawing={isDrawingROI}
                    setIsDrawing={setIsDrawingROI}
                    isInteractive={false}
                    motionHeatmap={activeSegmentHeatmap}
                    showMotionHeatmap={showSegmentHeatmap}
                  />
                }
              />
            </div>
          </div>

          {isDesktop ? (
            <>
              <div className="relative w-[100px] flex-shrink-0 overflow-hidden">
                {timelinePanel}
              </div>

              <div className="flex w-64 flex-col border-l">{resultsPanel}</div>
            </>
          ) : (
            <div className="flex min-h-0 flex-1 flex-col overflow-hidden landscape:flex-row">
              <div className="relative min-h-0 basis-1/2 overflow-hidden landscape:w-[100px] landscape:flex-shrink-0 landscape:basis-auto">
                {timelinePanel}
              </div>

              <div className="flex min-h-0 basis-1/2 flex-col border-t landscape:flex-1 landscape:border-l landscape:border-t-0">
                {resultsPanel}
              </div>
            </div>
          )}
        </div>
      </div>
    </DetailStreamProvider>
  );
}

type SearchResultItemProps = {
  result: MotionSearchResult;
  timezone: string | undefined;
  timestampFormat: string;
  onClick: () => void;
};

function SearchResultItem({
  result,
  timezone,
  timestampFormat,
  onClick,
}: SearchResultItemProps) {
  const { t } = useTranslation(["views/motionSearch"]);
  const formattedTime = useFormattedTimestamp(
    result.timestamp,
    timestampFormat,
    timezone,
  );

  return (
    <button
      className="flex w-full items-center justify-between gap-2 rounded-md p-2 text-left hover:bg-accent"
      onClick={onClick}
      title={t("jumpToTime")}
    >
      <span className="min-w-0 truncate text-sm font-medium">
        {formattedTime}
      </span>
      <span
        className="shrink-0 text-xs tabular-nums text-muted-foreground"
        title={t("changePercentage", {
          percentage: result.change_percentage.toFixed(1),
        })}
      >
        {result.change_percentage.toFixed(1)}%
      </span>
    </button>
  );
}
