import { Add, Check, Close, Delete } from "@mui/icons-material";
import { Box, Button, Tooltip, Typography } from "@mui/material";
import {
  FooterPropsOverrides,
  GridActionsCellItem,
  GridCellParams,
  GridColDef,
  GridEditInputCell,
  GridRenderCellParams,
  GridRenderEditCellParams,
  GridRowId,
  GridRowIdGetter,
  GridRowModel,
  GridRowModes,
  GridRowModesModel,
  GridToolbarContainer,
  useGridApiRef,
} from "@mui/x-data-grid";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useFormContext } from "react-hook-form";
import {
  RecordTimeLeaveInput,
  TimeEntry,
  TimeLeaveEntry,
} from "@/app/api/timesheets/actions";
import { manhourFormatter } from "@/app/utils/formatUtil";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import uniqBy from "lodash/uniqBy";
import { Task, TaskGroup } from "@/app/api/tasks";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import ProjectSelect from "../TimesheetTable/ProjectSelect";
import TaskGroupSelect, {
  TaskGroupSelectWithoutProject,
} from "../TimesheetTable/TaskGroupSelect";
import TaskSelect from "../TimesheetTable/TaskSelect";
import {
  DAILY_NORMAL_MAX_HOURS,
  LeaveEntryError,
  TimeEntryError,
  validateLeaveEntry,
  validateTimeEntry,
} from "@/app/api/timesheets/utils";
import { roundToNearestQuarter } from "@/app/utils/manhourUtils";
import FastTimeEntryModal from "../TimesheetTable/FastTimeEntryModal";
import { LeaveType } from "@/app/api/timesheets";
import DisabledEdit from "./DisabledEdit";
import TwoLineCell from "./TwoLineCell";

dayjs.extend(isBetween);

interface Props {
  day: string;
  isHoliday: boolean;
  allProjects: ProjectWithTasks[];
  assignedProjects: AssignedProject[];
  fastEntryEnabled?: boolean;
  leaveTypes: LeaveType[];
  miscTasks: Task[];
}

export type TimeLeaveRow = Partial<
  TimeLeaveEntry & {
    _isNew: boolean;
    _error: TimeEntryError | LeaveEntryError;
    _isPlanned?: boolean;
  }
>;

class ProcessRowUpdateError extends Error {
  public readonly row: TimeLeaveRow;
  public readonly errors: TimeEntryError | LeaveEntryError | undefined;
  constructor(
    row: TimeLeaveRow,
    message?: string,
    errors?: TimeEntryError | LeaveEntryError,
  ) {
    super(message);
    this.row = row;
    this.errors = errors;

    Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
  }
}

const TimeLeaveInputTable: React.FC<Props> = ({
  day,
  allProjects,
  assignedProjects,
  isHoliday,
  fastEntryEnabled,
  leaveTypes,
  miscTasks,
}) => {
  const { t } = useTranslation("home");
  const taskGroupsByProject = useMemo(() => {
    return allProjects.reduce<{
      [projectId: AssignedProject["id"]]: {
        value: TaskGroup["id"];
        label: string;
      }[];
    }>((acc, project) => {
      return {
        ...acc,
        [project.id]: uniqBy(
          project.tasks.map((t) => ({
            value: t.taskGroup.id,
            label: t.taskGroup.name,
          })),
          "value",
        ),
      };
    }, {});
  }, [allProjects]);
  const taskGroupsWithoutProject = useMemo(
    () =>
      uniqBy(
        miscTasks.map((t) => t.taskGroup),
        "id",
      ),
    [miscTasks],
  );

  // To check for start / end planned dates
  const milestonesByProject = useMemo(() => {
    return assignedProjects.reduce<{
      [projectId: AssignedProject["id"]]: AssignedProject["milestones"];
    }>((acc, project) => {
      return { ...acc, [project.id]: { ...project.milestones } };
    }, {});
  }, [assignedProjects]);

  const { getValues, setValue, clearErrors, setError } =
    useFormContext<RecordTimeLeaveInput>();
  const currentEntries = getValues(day);

  const getRowId = useCallback<GridRowIdGetter<TimeLeaveRow>>(
    (row) => `${row.type}-${row.id}`,
    [],
  );

  const [entries, setEntries] = useState<TimeLeaveRow[]>(currentEntries || []);

  const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});

  const apiRef = useGridApiRef();
  const addRow = useCallback(() => {
    const type = "timeEntry" as const;
    const newEntry = { id: Date.now(), _isNew: true, type };
    setEntries((e) => [...e, newEntry]);
    setRowModesModel((model) => ({
      ...model,
      [getRowId(newEntry)]: {
        mode: GridRowModes.Edit,
        fieldToFocus: "projectId",
      },
    }));
  }, [getRowId]);

  const validateRow = useCallback(
    (row: TimeLeaveRow) => {
      if (row.type === "timeEntry") {
        return validateTimeEntry(row, isHoliday);
      } else {
        return validateLeaveEntry(row, isHoliday);
      }
    },
    [isHoliday],
  );

  const verifyIsPlanned = useCallback(
    (row: TimeLeaveRow) => {
      if (
        row.type === "timeEntry" &&
        row.projectId &&
        row.taskGroupId &&
        milestonesByProject[row.projectId]
      ) {
        const milestone =
          milestonesByProject[row.projectId][row.taskGroupId] || {};
        const { startDate, endDate } = milestone;
        // Check if the current day is between the start and end date inclusively
        return dayjs(day).isBetween(startDate, endDate, "day", "[]");
      }
    },
    [day, milestonesByProject],
  );

  const handleCancel = useCallback(
    (id: GridRowId) => () => {
      setRowModesModel((model) => ({
        ...model,
        [id]: { mode: GridRowModes.View, ignoreModifications: true },
      }));
      const editedRow = entries.find((entry) => getRowId(entry) === id);
      if (editedRow?._isNew) {
        setEntries((es) => es.filter((e) => getRowId(e) !== id));
      } else {
        setEntries((es) =>
          es.map((e) =>
            getRowId(e) === id
              ? { ...e, _error: undefined, _isPlanned: undefined }
              : e,
          ),
        );
      }
    },
    [entries, getRowId],
  );

  const handleDelete = useCallback(
    (id: GridRowId) => () => {
      setEntries((es) => es.filter((e) => getRowId(e) !== id));
    },
    [getRowId],
  );

  const handleSave = useCallback(
    (id: GridRowId) => () => {
      setRowModesModel((model) => ({
        ...model,
        [id]: { mode: GridRowModes.View },
      }));
    },
    [],
  );

  const processRowUpdate = useCallback(
    (
      newRow: GridRowModel<TimeLeaveRow>,
      originalRow: GridRowModel<TimeLeaveRow>,
    ) => {
      const errors = validateRow(newRow);
      if (errors) {
        throw new ProcessRowUpdateError(
          originalRow,
          "validation error",
          errors,
        );
      }

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { _isNew, _error, _isPlanned, ...updatedRow } = newRow;
      const newIsPlanned = verifyIsPlanned(updatedRow);

      const rowToSave = {
        ...updatedRow,
        ...(updatedRow.type === "timeEntry"
          ? {
              leaveTypeId: undefined,
            }
          : {
              projectId: undefined,
              taskGroupId: undefined,
              taskId: undefined,
            }),
        _isPlanned: newIsPlanned,
      } satisfies TimeLeaveRow;
      setEntries((es) =>
        es.map((e) => (getRowId(e) === getRowId(originalRow) ? rowToSave : e)),
      );
      return rowToSave;
    },
    [validateRow, verifyIsPlanned, getRowId],
  );

  const onProcessRowUpdateError = useCallback(
    (updateError: ProcessRowUpdateError) => {
      const errors = updateError.errors;
      const oldRow = updateError.row;

      apiRef.current.updateRows([{ ...oldRow, _error: errors }]);
    },
    [apiRef],
  );

  const columns = useMemo<GridColDef[]>(
    () => [
      {
        field: "projectId",
        editable: true,
      },
      {
        field: "leaveTypeId",
        editable: true,
      },
      {
        field: "type",
        headerName: t("Project or Leave"),
        width: 300,
        editable: true,
        valueFormatter(params) {
          const row = params.id
            ? params.api.getRow<TimeLeaveRow>(params.id)
            : null;
          if (!row) {
            return null;
          }

          if (row.type === "timeEntry") {
            const project = allProjects.find((p) => p.id === row.projectId);
            return project ? `${project.code} - ${project.name}` : t("None");
          } else if (row.type === "leaveEntry") {
            const leave = leaveTypes.find((l) => l.id === row.leaveTypeId);
            return leave?.name || "Unknown leave";
          }
        },
        renderCell(params: GridRenderCellParams<TimeLeaveRow, number>) {
          return <TwoLineCell>{params.formattedValue}</TwoLineCell>;
        },
        renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow, number>) {
          return (
            <ProjectSelect
              referenceDay={dayjs(day)}
              includeLeaves
              leaveTypes={leaveTypes}
              multiple={false}
              allProjects={allProjects}
              assignedProjects={assignedProjects}
              value={
                (params.row.type === "leaveEntry"
                  ? `leave-${params.row.leaveTypeId}`
                  : undefined) ||
                (params.row.type === "timeEntry"
                  ? params.row.projectId
                  : undefined)
              }
              onProjectSelect={async (projectOrLeaveId, isLeave) => {
                await params.api.setEditCellValue({
                  id: params.id,
                  field: params.field,
                  value: isLeave ? "leaveEntry" : "timeEntry",
                });

                await params.api.setEditCellValue({
                  id: params.id,
                  field: isLeave ? "leaveTypeId" : "projectId",
                  value: projectOrLeaveId,
                });

                await params.api.setEditCellValue({
                  id: params.id,
                  field: isLeave ? "projectId" : "leaveTypeId",
                  value: undefined,
                });

                await params.api.setEditCellValue({
                  id: params.id,
                  field: "taskGroupId",
                  value: undefined,
                });

                await params.api.setEditCellValue({
                  id: params.id,
                  field: "taskId",
                  value: undefined,
                });

                params.api.setCellFocus(
                  params.id,
                  isLeave || !projectOrLeaveId ? "inputHours" : "taskGroupId",
                );
              }}
            />
          );
        },
      },
      {
        field: "taskGroupId",
        headerName: t("Stage"),
        width: 200,
        editable: true,
        renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow, number>) {
          if (params.row.type === "timeEntry") {
            if (params.row.projectId) {
              return (
                <TaskGroupSelect
                  projectId={params.row.projectId}
                  value={params.value}
                  taskGroupsByProject={taskGroupsByProject}
                  onTaskGroupSelect={async (taskGroupId) => {
                    await params.api.setEditCellValue({
                      id: params.id,
                      field: params.field,
                      value: taskGroupId,
                    });
                    await params.api.setEditCellValue({
                      id: params.id,
                      field: "taskId",
                      value: undefined,
                    });
                    params.api.setCellFocus(params.id, "taskId");
                  }}
                />
              );
            } else {
              return (
                <TaskGroupSelectWithoutProject
                  value={params.value}
                  onTaskGroupSelect={async (taskGroupId) => {
                    await params.api.setEditCellValue({
                      id: params.id,
                      field: params.field,
                      value: taskGroupId,
                    });
                    await params.api.setEditCellValue({
                      id: params.id,
                      field: "taskId",
                      value: undefined,
                    });
                    params.api.setCellFocus(params.id, "taskId");
                  }}
                  taskGroups={taskGroupsWithoutProject}
                />
              );
            }
          } else {
            return <DisabledEdit />;
          }
        },
        valueFormatter(params) {
          if (!params.id) {
            return null;
          }

          const projectId = params.api.getRow(params.id).projectId;
          if (projectId) {
            const taskGroups =
              taskGroupsByProject[params.api.getRow(params.id).projectId] || [];
            const taskGroup = taskGroups.find(
              (tg) => tg.value === params.value,
            );
            return taskGroup ? taskGroup.label : t("None");
          } else {
            const taskGroupId = params.value;
            return (
              taskGroupsWithoutProject.find((tg) => tg.id === taskGroupId)
                ?.name || t("None")
            );
          }
        },
        renderCell(params: GridRenderCellParams<TimeLeaveRow, number>) {
          return <TwoLineCell>{params.formattedValue}</TwoLineCell>;
        },
      },
      {
        field: "taskId",
        headerName: t("Task"),
        width: 200,
        editable: true,
        renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow, number>) {
          if (params.row.type === "timeEntry") {
            return (
              <TaskSelect
                value={params.value}
                projectId={params.row.projectId}
                taskGroupId={params.row.taskGroupId}
                allProjects={allProjects}
                onTaskSelect={(taskId) => {
                  params.api.setEditCellValue({
                    id: params.id,
                    field: params.field,
                    value: taskId,
                  });
                  params.api.setCellFocus(params.id, "inputHours");
                }}
                miscTasks={miscTasks}
              />
            );
          } else {
            return <DisabledEdit />;
          }
        },
        valueFormatter(params) {
          const projectId = params.id
            ? params.api.getRow(params.id).projectId
            : undefined;

          const task = (
            projectId
              ? allProjects.find((p) => p.id === projectId)?.tasks || []
              : miscTasks
          ).find((t) => t.id === params.value);
          return task ? task.name : t("None");
        },
        renderCell(params: GridRenderCellParams<TimeLeaveRow, number>) {
          return <TwoLineCell>{params.formattedValue}</TwoLineCell>;
        },
      },
      {
        field: "inputHours",
        headerName: t("Hours"),
        width: 100,
        editable: true,
        type: "number",
        renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow>) {
          const errorMessage =
            params.row._error?.[
              params.field as keyof Omit<TimeLeaveEntry, "type">
            ];
          const content = (
            <GridEditInputCell {...params} inputProps={{ min: 0 }} />
          );
          return errorMessage ? (
            <Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}>
              <Box width="100%">{content}</Box>
            </Tooltip>
          ) : (
            content
          );
        },
        valueParser(value) {
          return value ? roundToNearestQuarter(value) : value;
        },
        valueFormatter(params) {
          return manhourFormatter.format(params.value || 0);
        },
      },
      {
        field: "otHours",
        headerName: t("Other Hours"),
        width: 150,
        editable: true,
        type: "number",
        renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow>) {
          if (params.row.type === "leaveEntry") {
            return <DisabledEdit />;
          }
          const errorMessage =
            params.row._error?.[
              params.field as keyof Omit<TimeLeaveEntry, "type">
            ];
          const content = (
            <GridEditInputCell {...params} inputProps={{ min: 0 }} />
          );
          return errorMessage ? (
            <Tooltip title={t(errorMessage)}>
              <Box width="100%">{content}</Box>
            </Tooltip>
          ) : (
            content
          );
        },
        valueParser(value) {
          return value ? roundToNearestQuarter(value) : value;
        },
        valueFormatter(params) {
          return manhourFormatter.format(params.value || 0);
        },
      },
      {
        field: "remark",
        headerName: t("Remarks"),
        sortable: false,
        flex: 1,
        editable: true,
        renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow>) {
          const errorMessage =
            params.row._error?.[
              params.field as keyof Omit<TimeLeaveEntry, "type">
            ];
          const content = <GridEditInputCell {...params} />;
          return errorMessage ? (
            <Tooltip title={t(errorMessage)}>
              <Box width="100%">{content}</Box>
            </Tooltip>
          ) : (
            content
          );
        },
        renderCell(params: GridRenderCellParams<TimeLeaveRow>) {
          return <TwoLineCell>{params.value}</TwoLineCell>;
        },
      },
      {
        type: "actions",
        field: "actions",
        headerName: t("Actions"),
        getActions: ({ id }) => {
          if (rowModesModel[id]?.mode === GridRowModes.Edit) {
            return [
              <GridActionsCellItem
                key="accpet-action"
                icon={<Check />}
                label={t("Save")}
                onClick={handleSave(id)}
              />,
              <GridActionsCellItem
                key="cancel-action"
                icon={<Close />}
                label={t("Cancel")}
                onClick={handleCancel(id)}
              />,
            ];
          }

          return [
            <GridActionsCellItem
              key="delete-action"
              icon={<Delete />}
              label={t("Remove")}
              onClick={handleDelete(id)}
            />,
          ];
        },
      },
    ],
    [
      t,
      rowModesModel,
      handleDelete,
      handleSave,
      handleCancel,
      allProjects,
      leaveTypes,
      assignedProjects,
      taskGroupsByProject,
      taskGroupsWithoutProject,
      miscTasks,
      day,
    ],
  );

  useEffect(() => {
    const newEntries: TimeLeaveEntry[] = entries
      .map((e) => {
        if (e._isNew || e._error || !e.id || !e.type) {
          return null;
        }

        return e;
      })
      .filter((e): e is TimeLeaveEntry => Boolean(e));

    setValue(day, newEntries);

    if (entries.some((e) => e._isNew)) {
      setError(day, {
        message: "There are some unsaved entries.",
        type: "custom",
      });
    } else {
      clearErrors(day);
    }
  }, [getValues, entries, setValue, day, clearErrors, setError]);

  const hasOutOfPlannedStages = entries.some(
    (entry) => entry._isPlanned !== undefined && !entry._isPlanned,
  );

  // Fast entry modal
  const [fastEntryModalOpen, setFastEntryModalOpen] = useState(false);
  const closeFastEntryModal = useCallback(() => {
    setFastEntryModalOpen(false);
  }, []);
  const openFastEntryModal = useCallback(() => {
    setFastEntryModalOpen(true);
  }, []);
  const onSaveFastEntry = useCallback(async (entries: TimeEntry[]) => {
    setEntries((e) => [
      ...e,
      ...entries.map((newEntry) => ({
        ...newEntry,
        type: "timeEntry" as const,
      })),
    ]);
    setFastEntryModalOpen(false);
  }, []);

  const footer = (
    <Box display="flex" gap={2} alignItems="center">
      <Button
        disableRipple
        variant="outlined"
        startIcon={<Add />}
        onClick={addRow}
        size="small"
      >
        {t("Record time or leave")}
      </Button>
      {fastEntryEnabled && (
        <Button
          disableRipple
          variant="outlined"
          startIcon={<Add />}
          onClick={openFastEntryModal}
          size="small"
        >
          {t("Fast time entry")}
        </Button>
      )}
      {hasOutOfPlannedStages && (
        <Typography color="warning.main" variant="body2">
          {t("There are entries for stages out of planned dates!")}
        </Typography>
      )}
    </Box>
  );

  return (
    <>
      <StyledDataGrid
        getRowId={getRowId}
        apiRef={apiRef}
        autoHeight
        sx={{
          "--DataGrid-overlayHeight": "100px",
          ".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
            border: "1px solid",
            borderColor: "error.main",
          },
          ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
            border: "1px solid",
            borderColor: "warning.main",
          },
        }}
        disableColumnMenu
        editMode="row"
        columnVisibilityModel={{ projectId: false, leaveTypeId: false }}
        rows={entries}
        rowModesModel={rowModesModel}
        onRowModesModelChange={setRowModesModel}
        processRowUpdate={processRowUpdate}
        onProcessRowUpdateError={onProcessRowUpdateError}
        columns={columns}
        getCellClassName={(params: GridCellParams<TimeLeaveRow>) => {
          let classname = "";
          if (
            params.row._error?.[
              params.field as keyof Omit<TimeLeaveEntry, "type">
            ]
          ) {
            classname = "hasError";
          } else if (
            params.field === "taskGroupId" &&
            params.row._isPlanned !== undefined &&
            !params.row._isPlanned
          ) {
            classname = "hasWarning";
          }
          return classname;
        }}
        slots={{
          footer: FooterToolbar,
          noRowsOverlay: NoRowsOverlay,
        }}
        slotProps={{
          footer: { child: footer },
        }}
      />
      {fastEntryEnabled && (
        <FastTimeEntryModal
          recordDate={day}
          allProjects={allProjects}
          assignedProjects={assignedProjects}
          open={fastEntryModalOpen}
          isHoliday={Boolean(isHoliday)}
          onClose={closeFastEntryModal}
          onSave={onSaveFastEntry}
        />
      )}
    </>
  );
};

const NoRowsOverlay: React.FC = () => {
  const { t } = useTranslation("home");
  return (
    <Box
      display="flex"
      justifyContent="center"
      alignItems="center"
      height="100%"
    >
      <Typography variant="caption">{t("Add some time entries!")}</Typography>
    </Box>
  );
};

const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
  return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};

export default TimeLeaveInputTable;
