import ActivityIndicator from "@/components/indicators/activity-indicator";
import useApiFilter from "@/hooks/use-api-filter";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { useCameraPreviews } from "@/hooks/use-camera-previews";
import { useTimezone } from "@/hooks/use-date-utils";
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
import { useUserPersistence } from "@/hooks/use-user-persistence";
import { FrigateConfig } from "@/types/frigateConfig";
import { RecordingStartingPoint } from "@/types/record";
import {
  RecordingsSummary,
  REVIEW_PADDING,
  ReviewFilter,
  ReviewSegment,
  ReviewSeverity,
  ReviewSummary,
  SegmentedReviewData,
} from "@/types/review";
import { TimelineType } from "@/types/timeline";
import {
  getBeginningOfDayTimestamp,
  getEndOfDayTimestamp,
} from "@/utils/dateUtil";
import {
  parseRecordingReviewLink,
  RECORDING_REVIEW_LINK_PARAM,
} from "@/utils/recordingReviewUrl";
import EventView from "@/views/events/EventView";
import MotionSearchView from "@/views/motion-search/MotionSearchView";
import { RecordingView } from "@/views/recording/RecordingView";
import { useFrigateReviews } from "@/api/ws";
import axios from "axios";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import useSWR from "swr";

export default function Events() {
  const { t } = useTranslation(["views/events"]);

  const { data: config } = useSWR<FrigateConfig>("config", {
    revalidateOnFocus: false,
  });
  const timezone = useTimezone(config);
  const allowedCameras = useAllowedCameras();

  // recordings viewer

  const [severity, setSeverity] = useOverlayState<ReviewSeverity>(
    "severity",
    "alert",
  );

  const [showReviewed, setShowReviewed] = useUserPersistence(
    "showReviewed",
    false,
  );

  const [recording, setRecording] = useOverlayState<
    RecordingStartingPoint | undefined
  >("recording", undefined, false);
  const [motionPreviewsCamera, setMotionPreviewsCamera] = useOverlayState<
    string | undefined
  >("motionPreviewsCamera", undefined);

  const [motionSearchCamera, setMotionSearchCamera] = useState<string | null>(
    null,
  );
  const [motionSearchDay, setMotionSearchDay] = useState<Date | undefined>(
    undefined,
  );

  const reviewCameras = useMemo(() => {
    if (!config?.cameras) {
      return [] as string[];
    }

    return Object.keys(config.cameras).filter(
      (cam) =>
        allowedCameras.includes(cam) &&
        config.cameras[cam]?.ui?.review !== false,
    );
  }, [allowedCameras, config?.cameras]);

  const selectedMotionSearchCamera = useMemo(() => {
    if (!motionSearchCamera) {
      return null;
    }

    if (reviewCameras.includes(motionSearchCamera)) {
      return motionSearchCamera;
    }

    return reviewCameras[0] ?? null;
  }, [motionSearchCamera, reviewCameras]);

  const motionSearchTimeRange = useMemo(() => {
    if (motionSearchDay) {
      return {
        after: getBeginningOfDayTimestamp(new Date(motionSearchDay)),
        before: getEndOfDayTimestamp(new Date(motionSearchDay)),
      };
    }

    const now = Date.now() / 1000;
    return {
      after: now - 86400,
      before: now,
    };
  }, [motionSearchDay]);

  const closeMotionSearch = useCallback(() => {
    setMotionSearchCamera(null);
    setMotionSearchDay(undefined);
    setBeforeTs(Date.now() / 1000);
  }, []);

  const handleMotionSearchCameraSelect = useCallback((camera: string) => {
    setMotionSearchCamera(camera);
  }, []);

  const handleMotionSearchDaySelect = useCallback((day: Date | undefined) => {
    if (day == undefined) {
      setMotionSearchDay(undefined);
      return;
    }

    const normalizedDay = new Date(day);
    normalizedDay.setHours(0, 0, 0, 0);
    setMotionSearchDay(normalizedDay);
  }, []);

  const [notificationTab, setNotificationTab] =
    useState<TimelineType>("timeline");

  const getReviewDayBounds = useCallback((date: Date) => {
    const now = Date.now() / 1000;
    return {
      after: getBeginningOfDayTimestamp(date),
      before: Math.min(getEndOfDayTimestamp(date), now),
    };
  }, []);

  useSearchEffect("tab", (tab: string) => {
    if (tab === "timeline" || tab === "events" || tab === "detail") {
      setNotificationTab(tab as TimelineType);
    }
    return true;
  });

  useSearchEffect("id", (reviewId: string) => {
    axios
      .get(`review/${reviewId}`)
      .then((resp) => {
        if (resp.status == 200 && resp.data) {
          const startTime = resp.data.start_time - REVIEW_PADDING;
          const date = new Date(startTime * 1000);

          setReviewFilter(getReviewDayBounds(date));
          setRecording(
            {
              camera: resp.data.camera,
              startTime,
              severity: resp.data.severity,
              timelineType: notificationTab,
            },
            true,
          );
        }
      })
      .catch(() => {});

    return true;
  });

  const [startTime, setStartTime] = useState<number>();

  useEffect(() => {
    if (recording) {
      document.title = t("recordings.documentTitle");
    } else {
      document.title = t("documentTitle");
    }
  }, [recording, severity, t]);

  // review filter

  const [reviewFilter, setReviewFilter, reviewSearchParams] =
    useApiFilter<ReviewFilter>();

  useSearchEffect("cameras", (cameras: string) => {
    setReviewFilter({
      ...reviewFilter,
      cameras: cameras.includes(",") ? cameras.split(",") : [cameras],
    });
    return true;
  });

  useSearchEffect("labels", (labels: string) => {
    setReviewFilter({
      ...reviewFilter,
      labels: labels.includes(",") ? labels.split(",") : [labels],
    });
    return true;
  });

  useSearchEffect("zones", (zones: string) => {
    setReviewFilter({
      ...reviewFilter,
      zones: zones.includes(",") ? zones.split(",") : [zones],
    });
    return true;
  });

  useSearchEffect("group", (reviewGroup) => {
    if (config && reviewGroup && reviewGroup != "default") {
      const group = config.camera_groups[reviewGroup];
      const isBirdseyeOnly =
        group.cameras.length == 1 && group.cameras[0] == "birdseye";

      if (group && !isBirdseyeOnly) {
        setReviewFilter({
          ...reviewFilter,
          cameras: group.cameras,
        });
      }

      return true;
    }

    return false;
  });

  const onUpdateFilter = useCallback(
    (newFilter: ReviewFilter) => {
      setReviewFilter(newFilter);

      // update recording start time if filter
      // was changed on recording page
      if (recording != undefined && newFilter.after != undefined) {
        setRecording({ ...recording, startTime: newFilter.after }, true);
      }
    },
    [recording, setRecording, setReviewFilter],
  );

  useSearchEffect(RECORDING_REVIEW_LINK_PARAM, (reviewLinkValue: string) => {
    if (!config) {
      return false;
    }

    const reviewLink = parseRecordingReviewLink(reviewLinkValue);

    if (!reviewLink) {
      toast.error(t("recordings.invalidSharedLink"), {
        position: "top-center",
      });
      return true;
    }

    const validCamera =
      config.cameras[reviewLink.camera] &&
      allowedCameras.includes(reviewLink.camera);

    if (!validCamera) {
      toast.error(t("recordings.invalidSharedCamera"), {
        position: "top-center",
      });
      return true;
    }

    setReviewFilter({
      ...reviewFilter,
      ...getReviewDayBounds(new Date(reviewLink.timestamp * 1000)),
    });
    setRecording(
      {
        camera: reviewLink.camera,
        startTime: reviewLink.timestamp,
        // severity not actually applicable here, but the type requires it
        // this pattern is also used LiveCameraView to enter recording view
        severity: "alert",
        timelineType: notificationTab,
        navigationSource: "shared-link",
      },
      true,
    );

    return true;
  });

  // review paging

  const [beforeTs, setBeforeTs] = useState(Math.ceil(Date.now() / 1000));
  const last24Hours = useMemo(() => {
    return { before: beforeTs, after: getHoursAgo(24) };
  }, [beforeTs]);
  const selectedTimeRange = useMemo(() => {
    if (reviewSearchParams["after"] == undefined) {
      return last24Hours;
    }

    return {
      before: Math.ceil(reviewSearchParams["before"]),
      after: Math.floor(reviewSearchParams["after"]),
    };
  }, [last24Hours, reviewSearchParams]);

  // we want to update the items whenever the severity changes
  useEffect(() => {
    if (recording) {
      return;
    }

    const now = Date.now() / 1000;

    if (now - beforeTs > 60) {
      setBeforeTs(now);
    }

    // only refresh when severity changes
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [severity]);

  const reviewSegmentFetcher = useCallback((key: Array<string> | string) => {
    const [path, params] = Array.isArray(key) ? key : [key, undefined];
    return axios.get(path, { params }).then((res) => res.data);
  }, []);

  const getKey = useCallback(() => {
    const params = {
      cameras: reviewSearchParams["cameras"],
      labels: reviewSearchParams["labels"],
      zones: reviewSearchParams["zones"],
      reviewed: null, // We want both reviewed and unreviewed items as we filter in the UI
      before: reviewSearchParams["before"] || last24Hours.before,
      after: reviewSearchParams["after"] || last24Hours.after,
    };
    return ["review", params];
  }, [reviewSearchParams, last24Hours]);

  const { data: reviews, mutate: updateSegments } = useSWR<ReviewSegment[]>(
    getKey,
    reviewSegmentFetcher,
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
    },
  );

  const reviewItems = useMemo<SegmentedReviewData>(() => {
    if (!reviews) {
      return undefined;
    }

    const all: ReviewSegment[] = [];
    const alerts: ReviewSegment[] = [];
    const detections: ReviewSegment[] = [];
    const motion: ReviewSegment[] = [];

    reviews?.forEach((segment) => {
      if (config?.cameras[segment.camera]?.ui?.review === false) {
        return;
      }

      all.push(segment);

      switch (segment.severity) {
        case "alert":
          alerts.push(segment);
          break;
        case "detection":
          detections.push(segment);
          break;
        default:
          motion.push(segment);
          break;
      }
    });

    return {
      all: all,
      alert: alerts,
      detection: detections,
      significant_motion: motion,
    };
  }, [reviews, config?.cameras]);

  // update review items in place when a review segment ends
  const reviewUpdate = useFrigateReviews();
  const [endedReviews, setEndedReviews] = useState(
    new Map<string, ReviewSegment>(),
  );

  useEffect(() => {
    if (reviewUpdate?.type === "end" && reviews) {
      updateSegments(
        (data) => {
          if (!data) return data;
          return data.map((seg) =>
            seg.id === reviewUpdate.after.id ? reviewUpdate.after : seg,
          );
        },
        { revalidate: false, populateCache: true },
      );
      setEndedReviews((prev) =>
        new Map(prev).set(reviewUpdate.after.id, reviewUpdate.after),
      );
    }
    // reviews is intentionally excluded - only used to guard against
    // updating the SWR cache before data has loaded
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [reviewUpdate, updateSegments]);

  const currentItems = useMemo(() => {
    if (!reviewItems || !severity) {
      return null;
    }

    let current;

    if (reviewFilter?.showAll) {
      current = reviewItems.all;
    } else {
      current = reviewItems[severity];
    }

    if (!current || current.length == 0) {
      return [];
    }

    if (!showReviewed) {
      return current.filter((seg) => !seg.has_been_reviewed);
    } else {
      return current;
    }
    // only refresh when severity or filter changes
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [severity, reviewFilter, showReviewed, reviewItems?.all.length]);

  // overlay end_time updates onto currentItems without re-running
  // the has_been_reviewed filter, so hover-reviewed items stay visible
  const displayItems = useMemo(() => {
    if (!currentItems || endedReviews.size === 0) return currentItems;
    return currentItems.map((seg) => endedReviews.get(seg.id) ?? seg);
  }, [currentItems, endedReviews]);

  // review summary

  const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>(
    [
      "review/summary",
      {
        timezone: timezone,
        cameras: reviewSearchParams["cameras"] ?? null,
        labels: reviewSearchParams["labels"] ?? null,
        zones: reviewSearchParams["zones"] ?? null,
      },
    ],
    {
      revalidateOnFocus: true,
      refreshInterval: 30000,
      revalidateOnReconnect: false,
    },
  );

  const reloadData = useCallback(() => {
    setBeforeTs(Date.now() / 1000);
    updateSummary();
  }, [updateSummary]);

  // recordings summary

  const { data: recordingsSummary } = useSWR<RecordingsSummary>([
    "recordings/summary",
    {
      timezone: timezone,
      cameras: reviewSearchParams["cameras"] ?? null,
    },
  ]);

  // preview videos
  const previewTimes = useMemo(() => {
    const startDate = new Date(selectedTimeRange.after * 1000);
    startDate.setUTCMinutes(0, 0, 0);

    const endDate = new Date(selectedTimeRange.before * 1000);
    endDate.setHours(endDate.getHours() + 1, 0, 0, 0);

    return {
      after: startDate.getTime() / 1000,
      before: endDate.getTime() / 1000,
    };
  }, [selectedTimeRange]);

  const allPreviews = useCameraPreviews(
    previewTimes ?? { after: 0, before: 0 },
    {
      fetchPreviews: previewTimes != undefined,
    },
  );

  // review status

  const markItemsAsReviewed = useCallback(
    async (
      currentItems: ReviewSegment[],
      itemsToMarkReviewed: ReviewSegment[] | undefined = undefined,
    ) => {
      if (currentItems.length == 0) {
        return;
      }

      const severity = currentItems[0].severity;
      updateSegments(
        (data: ReviewSegment[] | undefined) => {
          if (!data) {
            return data;
          }

          const newData = [...data];

          newData.forEach((seg) => {
            if (seg.end_time && seg.severity == severity) {
              seg.has_been_reviewed = true;
            }
          });

          return newData;
        },
        { revalidate: false, populateCache: true },
      );

      const reviewList =
        itemsToMarkReviewed?.map((seg) => seg.id) ||
        currentItems?.filter((seg) => seg.end_time)?.map((seg) => seg.id) ||
        [];

      if (reviewList.length > 0) {
        await axios.post(`reviews/viewed`, {
          ids: reviewList,
          reviewed: true,
        });
        reloadData();

        if (reviewSearchParams["after"] != undefined) {
          updateSegments();
        }
      }
    },
    [reloadData, updateSegments, reviewSearchParams],
  );

  const markItemAsReviewed = useCallback(
    async (review: ReviewSegment) => {
      const resp = await axios.post(`reviews/viewed`, {
        ids: [review.id],
        reviewed: true,
      });

      if (resp.status == 200) {
        updateSegments(
          (data: ReviewSegment[] | undefined) => {
            if (!data) {
              return data;
            }

            const reviewIndex = data.findIndex((item) => item.id == review.id);
            if (reviewIndex == -1) {
              return data;
            }

            const newData = [
              ...data.slice(0, reviewIndex),
              { ...data[reviewIndex], has_been_reviewed: true },
              ...data.slice(reviewIndex + 1),
            ];

            return newData;
          },
          { revalidate: false, populateCache: true },
        );

        updateSummary(
          (data: ReviewSummary | undefined) => {
            if (!data) {
              return data;
            }

            const day = new Date(review.start_time * 1000);
            const today = new Date();
            today.setHours(0, 0, 0, 0);

            let key;
            if (day.getTime() > today.getTime()) {
              key = "last24Hours";
            } else {
              key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
            }

            if (!Object.keys(data).includes(key)) {
              return data;
            }

            const item = data[key];
            return {
              ...data,
              [key]: {
                ...item,
                reviewed_alert:
                  review.severity == "alert"
                    ? item.reviewed_alert + 1
                    : item.reviewed_alert,
                reviewed_detection:
                  review.severity == "detection"
                    ? item.reviewed_detection + 1
                    : item.reviewed_detection,
              },
            };
          },
          { revalidate: false, populateCache: true },
        );
      }
    },
    [updateSegments, updateSummary],
  );

  // selected items

  const selectedReviewData = useMemo(() => {
    if (!recording) {
      return undefined;
    }

    if (!config) {
      return undefined;
    }

    if (!reviews) {
      return undefined;
    }

    setStartTime(recording.startTime);
    const allCameras = reviewFilter?.cameras ?? reviewCameras;

    return {
      camera: recording.camera,
      start_time: recording.startTime,
      allCameras: allCameras,
    };

    // previews will not update after item is selected
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [recording, reviews]);

  if (!timezone) {
    return <ActivityIndicator />;
  }

  if (recording) {
    if (selectedReviewData) {
      return (
        <RecordingView
          key={selectedTimeRange.before}
          startCamera={selectedReviewData.camera}
          startTime={selectedReviewData.start_time}
          allCameras={selectedReviewData.allCameras}
          reviewItems={reviews}
          reviewSummary={reviewSummary}
          allPreviews={allPreviews}
          timeRange={selectedTimeRange}
          filter={reviewFilter}
          updateFilter={onUpdateFilter}
          refreshData={reloadData}
          onMotionSearch={(camera) => {
            setMotionSearchCamera(camera);
            setRecording(undefined);
          }}
        />
      );
    }
  } else {
    return motionSearchCamera ? (
      !config || !selectedMotionSearchCamera ? (
        <ActivityIndicator />
      ) : (
        <MotionSearchView
          config={config}
          cameras={reviewCameras}
          selectedCamera={selectedMotionSearchCamera}
          onCameraSelect={handleMotionSearchCameraSelect}
          cameraLocked={true}
          selectedDay={motionSearchDay}
          onDaySelect={handleMotionSearchDaySelect}
          timeRange={motionSearchTimeRange}
          timezone={timezone}
          onBack={closeMotionSearch}
        />
      )
    ) : (
      <EventView
        reviewItems={reviewItems}
        currentReviewItems={displayItems}
        reviewSummary={reviewSummary}
        recordingsSummary={recordingsSummary}
        relevantPreviews={allPreviews}
        timeRange={selectedTimeRange}
        filter={reviewFilter}
        severity={severity ?? "alert"}
        startTime={startTime}
        showReviewed={showReviewed ?? false}
        setShowReviewed={setShowReviewed}
        setSeverity={setSeverity}
        markItemAsReviewed={markItemAsReviewed}
        markItemsAsReviewed={markItemsAsReviewed}
        onOpenRecording={setRecording}
        motionPreviewsCamera={motionPreviewsCamera ?? null}
        setMotionPreviewsCamera={(camera) =>
          setMotionPreviewsCamera(camera ?? undefined)
        }
        setMotionSearchCamera={setMotionSearchCamera}
        pullLatestData={reloadData}
        updateFilter={onUpdateFilter}
      />
    );
  }
}

function getHoursAgo(hours: number): number {
  const now = new Date();
  now.setHours(now.getHours() - hours);
  return Math.ceil(now.getTime() / 1000);
}
