import React, { useCallback, useEffect, useMemo, useState } from "react";

import { HolidaysResult } from "@/app/api/holidays";
import { LeaveType, TeamLeaves, TeamTimeSheets } from "@/app/api/timesheets";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import { Autocomplete, Stack, TextField, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import transform from "lodash/transform";
import {
  getHolidayForDate,
  getPublicHolidaysForNYears,
} from "@/app/utils/holidayUtils";
import {
  INPUT_DATE_FORMAT,
  convertDateArrayToString,
} from "@/app/utils/formatUtil";
import StyledFullCalendar from "../StyledFullCalendar";
import { ProjectWithTasks } from "@/app/api/projects";
import {
  LeaveEntry,
  TimeEntry,
  deleteMemberEntry,
  deleteMemberLeave,
  saveMemberEntry,
  saveMemberLeave,
} from "@/app/api/timesheets/actions";
import TimesheetEditModal, {
  Props as TimesheetEditModalProps,
} from "../TimesheetTable/TimesheetEditModal";
import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal";
import LeaveEditModal from "../LeaveTable/LeaveEditModal";
import dayjs from "dayjs";
import { checkTotalHours } from "@/app/api/timesheets/utils";
import unionBy from "lodash/unionBy";

export interface Props {
  leaveTypes: LeaveType[];
  teamLeaves: TeamLeaves;
  teamTimesheets: TeamTimeSheets;
  companyHolidays: HolidaysResult[];
  allProjects: ProjectWithTasks[];
}

type MemberOption = TeamTimeSheets[0] & TeamLeaves[0] & { id: string };

interface EventClickArg {
  event: {
    start: Date | null;
    startStr: string;
    extendedProps: {
      calendar?: string;
      entry?: TimeEntry | LeaveEntry;
      memberId?: string;
    };
  };
}

const TimesheetAmendment: React.FC<Props> = ({
  teamTimesheets,
  teamLeaves,
  companyHolidays,
  allProjects,
  leaveTypes,
}) => {
  const { t } = useTranslation(["home", "common"]);

  const theme = useTheme();

  const projectMap = useMemo(() => {
    return allProjects.reduce<{
      [id: ProjectWithTasks["id"]]: ProjectWithTasks;
    }>((acc, project) => {
      return { ...acc, [project.id]: project };
    }, {});
  }, [allProjects]);

  const leaveMap = useMemo(() => {
    return leaveTypes.reduce<{ [id: LeaveType["id"]]: string }>(
      (acc, leaveType) => ({ ...acc, [leaveType.id]: leaveType.name }),
      {},
    );
  }, [leaveTypes]);

  // Use a local state to manage updates after a mutation
  const [localTeamTimesheets, setLocalTeamTimesheets] =
    useState(teamTimesheets);
  const [localTeamLeaves, setLocalTeamLeaves] = useState(teamLeaves);

  // member select
  const allMembers = useMemo(() => {
    return transform<TeamTimeSheets[0], MemberOption[]>(
      localTeamTimesheets,
      (acc, memberTimesheet, id) => {
        const leaves = localTeamLeaves[parseInt(id)];
        return acc.push({
          ...leaves,
          ...memberTimesheet,
          id,
        });
      },
      [],
    );
  }, [localTeamLeaves, localTeamTimesheets]);

  const [selectedStaff, setSelectedStaff] = useState<MemberOption>(
    allMembers[0],
  );
  useEffect(() => {
    setSelectedStaff(
      (currentStaff) =>
        allMembers.find((member) => member.id === currentStaff.id) ||
        allMembers[0],
    );
  }, [allMembers]);

  // edit modal related
  const [editModalProps, setEditModalProps] = useState<
    Partial<TimesheetEditModalProps>
  >({});
  const [editModalOpen, setEditModalOpen] = useState(false);

  const openEditModal = useCallback(
    (defaultValues?: TimeEntry, recordDate?: string, isHoliday?: boolean) => {
      setEditModalProps({
        defaultValues: defaultValues ? { ...defaultValues } : undefined,
        recordDate,
        isHoliday,
        onDelete: defaultValues
          ? async () => {
              const intStaffId = parseInt(selectedStaff.id);
              const newMemberTimesheets = await deleteMemberEntry({
                staffId: intStaffId,
                entryId: defaultValues.id,
              });
              setLocalTeamTimesheets((timesheets) => ({
                ...timesheets,
                [intStaffId]: {
                  ...timesheets[intStaffId],
                  timeEntries: newMemberTimesheets,
                },
              }));
              setEditModalOpen(false);
            }
          : undefined,
      });
      setEditModalOpen(true);
    },
    [selectedStaff.id],
  );

  const closeEditModal = useCallback(() => {
    setEditModalOpen(false);
  }, []);

  // leave edit modal related
  const [leaveEditModalProps, setLeaveEditModalProps] = useState<
    Partial<LeaveEditModalProps>
  >({});
  const [leaveEditModalOpen, setLeaveEditModalOpen] = useState(false);

  const openLeaveEditModal = useCallback(
    (defaultValues?: LeaveEntry, recordDate?: string, isHoliday?: boolean) => {
      setLeaveEditModalProps({
        defaultValues: defaultValues ? { ...defaultValues } : undefined,
        recordDate,
        isHoliday,
        onDelete: defaultValues
          ? async () => {
              const intStaffId = parseInt(selectedStaff.id);
              const newMemberLeaves = await deleteMemberLeave({
                staffId: intStaffId,
                entryId: defaultValues.id,
              });
              setLocalTeamLeaves((leaves) => ({
                ...leaves,
                [intStaffId]: {
                  ...leaves[intStaffId],
                  leaveEntries: newMemberLeaves,
                },
              }));
              setLeaveEditModalOpen(false);
            }
          : undefined,
      });
      setLeaveEditModalOpen(true);
    },
    [selectedStaff.id],
  );

  const closeLeaveEditModal = useCallback(() => {
    setLeaveEditModalOpen(false);
  }, []);

  // calendar related
  const holidays = useMemo(() => {
    return [
      ...getPublicHolidaysForNYears(2),
      ...companyHolidays.map((h) => ({
        title: h.name,
        date: convertDateArrayToString(h.date, INPUT_DATE_FORMAT),
        extendedProps: {
          calender: "holiday",
        },
      })),
    ].map((e) => ({
      ...e,
      backgroundColor: theme.palette.error.main,
      borderColor: theme.palette.error.main,
    }));
  }, [companyHolidays, theme.palette.error.main]);

  const leaveEntries = useMemo(
    () =>
      Object.keys(selectedStaff.leaveEntries).flatMap((date, index) => {
        return selectedStaff.leaveEntries[date].map((entry) => ({
          id: `${date}-${index}-leave-${entry.id}`,
          date,
          title: `${t("{{count}} hour", {
            ns: "common",
            count: entry.inputHours || 0,
          })} (${leaveMap[entry.leaveTypeId]})`,
          backgroundColor: theme.palette.warning.light,
          borderColor: theme.palette.warning.light,
          textColor: theme.palette.text.primary,
          extendedProps: {
            calendar: "leaveEntry",
            entry,
            memberId: selectedStaff.id,
          },
        }));
      }),
    [leaveMap, selectedStaff, t, theme],
  );

  const timeEntries = useMemo(
    () =>
      Object.keys(selectedStaff.timeEntries).flatMap((date, index) => {
        return selectedStaff.timeEntries[date].map((entry) => ({
          id: `${date}-${index}-time-${entry.id}`,
          date,
          title: `${t("{{count}} hour", {
            ns: "common",
            count: (entry.inputHours || 0) + (entry.otHours || 0),
          })} (${
            entry.projectId
              ? projectMap[entry.projectId].code
              : t("Non-billable task")
          })`,
          backgroundColor: theme.palette.info.main,
          borderColor: theme.palette.info.main,
          extendedProps: {
            calendar: "timeEntry",
            entry,
            memberId: selectedStaff.id,
          },
        }));
      }),
    [projectMap, selectedStaff, t, theme],
  );

  const handleEventClick = useCallback(
    ({ event }: EventClickArg) => {
      const dayJsObj = dayjs(event.startStr);
      const holiday = getHolidayForDate(event.startStr, companyHolidays);
      const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;

      if (
        event.extendedProps.calendar === "timeEntry" &&
        event.extendedProps.entry
      ) {
        openEditModal(
          event.extendedProps.entry as TimeEntry,
          event.startStr,
          Boolean(isHoliday),
        );
      } else if (
        event.extendedProps.calendar === "leaveEntry" &&
        event.extendedProps.entry
      ) {
        openLeaveEditModal(
          event.extendedProps.entry as LeaveEntry,
          event.startStr,
          Boolean(isHoliday),
        );
      }
    },
    [companyHolidays, openEditModal, openLeaveEditModal],
  );

  const handleDateClick = useCallback(
    (e: { dateStr: string }) => {
      const dayJsObj = dayjs(e.dateStr);
      const holiday = getHolidayForDate(e.dateStr, companyHolidays);
      const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;
      openEditModal(undefined, e.dateStr, Boolean(isHoliday));
    },
    [companyHolidays, openEditModal],
  );

  const checkTotalHoursForDate = useCallback(
    (newEntry: TimeEntry | LeaveEntry, date?: string) => {
      if (!date) {
        throw Error("Invalid date");
      }
      const intStaffId = parseInt(selectedStaff.id);
      const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || [];
      const timesheets =
        localTeamTimesheets[intStaffId].timeEntries[date] || [];

      let totalHourError;
      if ((newEntry as LeaveEntry).leaveTypeId) {
        // newEntry is a leave entry
        const leavesWithNewEntry = unionBy(
          [newEntry as LeaveEntry],
          leaves,
          "id",
        );
        totalHourError = checkTotalHours(timesheets, leavesWithNewEntry);
      } else {
        // newEntry is a timesheet entry
        const timesheetsWithNewEntry = unionBy(
          [newEntry as TimeEntry],
          timesheets,
          "id",
        );
        totalHourError = checkTotalHours(timesheetsWithNewEntry, leaves);
      }
      if (totalHourError) throw Error(totalHourError);
    },
    [localTeamLeaves, localTeamTimesheets, selectedStaff.id],
  );

  const handleSave = useCallback(
    async (timeEntry: TimeEntry, recordDate?: string) => {
      // TODO: should be fine, but can handle parse error
      const intStaffId = parseInt(selectedStaff.id);
      checkTotalHoursForDate(timeEntry, recordDate);
      const newMemberTimesheets = await saveMemberEntry({
        staffId: intStaffId,
        entry: timeEntry,
        recordDate,
      });
      setLocalTeamTimesheets((timesheets) => ({
        ...timesheets,
        [intStaffId]: {
          ...timesheets[intStaffId],
          timeEntries: newMemberTimesheets,
        },
      }));
      setEditModalOpen(false);
    },
    [checkTotalHoursForDate, selectedStaff.id],
  );

  const handleSaveLeave = useCallback(
    async (leaveEntry: LeaveEntry, recordDate?: string) => {
      const intStaffId = parseInt(selectedStaff.id);
      checkTotalHoursForDate(leaveEntry, recordDate);
      const newMemberLeaves = await saveMemberLeave({
        staffId: intStaffId,
        recordDate,
        entry: leaveEntry,
      });
      setLocalTeamLeaves((leaves) => ({
        ...leaves,
        [intStaffId]: {
          ...leaves[intStaffId],
          leaveEntries: newMemberLeaves,
        },
      }));
      setLeaveEditModalOpen(false);
    },
    [checkTotalHoursForDate, selectedStaff.id],
  );

  return (
    <Stack spacing={2}>
      <Autocomplete
        sx={{ maxWidth: 400 }}
        noOptionsText={t("No team members")}
        value={selectedStaff}
        onChange={(_, value) => {
          if (value) setSelectedStaff(value);
        }}
        options={allMembers}
        isOptionEqualToValue={(option, value) => option.id === value.id}
        getOptionLabel={(option) => `${option.staffId} - ${option.name}`}
        renderInput={(params) => <TextField {...params} />}
      />
      <StyledFullCalendar
        plugins={[dayGridPlugin, interactionPlugin]}
        initialView="dayGridMonth"
        buttonText={{ today: t("Today") }}
        events={[...holidays, ...timeEntries, ...leaveEntries]}
        eventClick={handleEventClick}
        dateClick={handleDateClick}
      />
      <TimesheetEditModal
        modalSx={{ maxWidth: 400 }}
        allProjects={allProjects}
        assignedProjects={[]}
        open={editModalOpen}
        onClose={closeEditModal}
        onSave={handleSave}
        {...editModalProps}
      />
      <LeaveEditModal
        modalSx={{ maxWidth: 400 }}
        leaveTypes={leaveTypes}
        open={leaveEditModalOpen}
        onClose={closeLeaveEditModal}
        onSave={handleSaveLeave}
        {...leaveEditModalProps}
      />
    </Stack>
  );
};

export default TimesheetAmendment;
