import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils";
import { HolidaysResult } from "../holidays";
import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions";
import { INPUT_DATE_FORMAT, convertDateArrayToString } from "@/app/utils/formatUtil";
import compact from "lodash/compact";
import dayjs, { Dayjs } from "dayjs";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
dayjs.extend(isSameOrAfter);

export type TimeEntryError = {
  [field in keyof TimeEntry]?: string;
};

/**
 * @param entry - the time entry
 * @returns an object where the keys are the error fields and the values the error message, and undefined if there are no errors
 */
export const validateTimeEntry = (
  entry: Partial<TimeEntry>,
  isHoliday: boolean,
): TimeEntryError | undefined => {
  // Test for errors
  const error: TimeEntryError = {};

  // Either normal or other hours need to be inputted
  if (!entry.inputHours && !entry.otHours) {
    error[isHoliday ? "otHours" : "inputHours"] = "Required";
  } else if (entry.inputHours && isHoliday) {
    error.inputHours = "Cannot input normal hours on holidays";
  } else if (entry.inputHours && entry.inputHours <= 0) {
    error.inputHours =
      "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
  } else if (entry.inputHours && entry.inputHours > DAILY_NORMAL_MAX_HOURS) {
    error.inputHours =
      "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
  } else if (entry.otHours && entry.otHours <= 0) {
    error.otHours = "Hours should be bigger than 0";
  }

  // If there is a project id, there should also be taskGroupId, taskId, inputHours
  if (entry.projectId) {
    if (!entry.taskGroupId) {
      error.taskGroupId = "Required";
    } else if (!entry.taskId) {
      error.taskId = "Required";
    }
  } else {
    if (entry.taskGroupId && !entry.taskId) {
      error.taskId = "Required";
    } else if (!entry.taskGroupId && !entry.remark) {
      error.remark = "Required for non-billable tasks";
    }
  }

  return Object.keys(error).length > 0 ? error : undefined;
};

export type LeaveEntryError = {
  [field in keyof LeaveEntry]?: string;
};

export const validateLeaveEntry = (
  entry: Partial<LeaveEntry>,
  isHoliday: boolean,
): LeaveEntryError | undefined => {
  // Test for errrors
  const error: LeaveEntryError = {};
  if (!entry.leaveTypeId) {
    error.leaveTypeId = "Required";
  } else if (entry.inputHours && isHoliday) {
    error.inputHours = "Cannot input normal hours on holidays";
  } else if (!entry.inputHours) {
    error.inputHours = "Required";
  } else if (
    entry.inputHours &&
    (entry.inputHours <= 0 || entry.inputHours > DAILY_NORMAL_MAX_HOURS)
  ) {
    error.inputHours =
      "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
  }

  return Object.keys(error).length > 0 ? error : undefined;
};

export const validateTimeLeaveRecord = (
  records: RecordTimeLeaveInput,
  companyHolidays: HolidaysResult[],
  isFullTime?: boolean,
  joinDate?: Dayjs,
): { [date: string]: string } | undefined => {
  const errors: { [date: string]: string } = {};

  const holidays = new Set(
    compact([
      ...getPublicHolidaysForNYears(2).map((h) => h.date),
      ...companyHolidays.map((h) => convertDateArrayToString(h.date, INPUT_DATE_FORMAT)),
    ]),
  );

  Object.keys(records).forEach((date) => {
    const dayJsObj = dayjs(date);
    const isHoliday =
      holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6;

    const entries = records[date];
    // Check each entry
    for (const entry of entries) {
      let entryError;
      if (entry.type === "leaveEntry") {
        entryError = validateLeaveEntry(entry, isHoliday);
      } else {
        entryError = validateTimeEntry(entry, isHoliday);
      }

      if (entryError) {
        errors[date] = "There are errors in the entries";
        return;
      }
    }

    // Check total hours
    const totalHourError = checkTotalHours(
      dayJsObj,
      entries.filter((e) => e.type === "timeEntry") as TimeEntry[],
      entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[],
      isHoliday,
      isFullTime,
      joinDate,
    );

    if (totalHourError) {
      errors[date] = totalHourError;
    }
  });

  const hasErrors = Object.keys(errors).length > 0;
  const temporarilySaveable = isTemporarilySaveable(records, errors, holidays);

  return !temporarilySaveable && hasErrors ? errors : undefined;
};

export const checkTotalHours = (
  dayJsObj: Dayjs,
  timeEntries: TimeEntry[],
  leaves: LeaveEntry[],
  isHoliday?: boolean,
  isFullTime?: boolean,
  joinDate?: Dayjs,
): string | undefined => {
  const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0);

  const totalInputHours = timeEntries.reduce((acc, entry) => {
    return acc + (entry.inputHours || 0);
  }, 0);

  const totalOtHours = timeEntries.reduce((acc, entry) => {
    return acc + (entry.otHours || 0);
  }, 0);

  const totalHours = totalInputHours + leaveHours;

  const isDayToCheckAfterJoinDate =
    joinDate && joinDate.isValid() ? dayJsObj.isSameOrAfter(joinDate) : true;

  if (!isDayToCheckAfterJoinDate && totalHours > 0) {
    return "Cannot input hours before join date.";
  }

  if (totalHours > DAILY_NORMAL_MAX_HOURS) {
    return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours.";
  } else if (
    isFullTime &&
    isDayToCheckAfterJoinDate &&
    !isHoliday &&
    !dayJsObj.isSame(dayjs(), "day") &&
    totalHours !== DAILY_NORMAL_MAX_HOURS
  ) {
    return "The daily normal hours (timesheet hours + leave hours) for full-time staffs should be {{DAILY_NORMAL_MAX_HOURS}}.";
  } else if (totalHours + totalOtHours > TIMESHEET_DAILY_MAX_HOURS) {
    return "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}";
  }
};

export const DAILY_NORMAL_MAX_HOURS = 8;
export const TIMESHEET_DAILY_MAX_HOURS = 20;

export const isTemporarilySaveable = (
  records: RecordTimeLeaveInput,
  errors: { [date: string]: string },
  holidays: Set<string>,
): boolean => {
  const filledDates = Object.keys(records)
    .reduce<{ date: string; hasFilled: boolean }[]>((acc, date) => {
      const dayJsObj = dayjs(date);
      const isHoliday =
        holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6;
      if (isHoliday) {
        return acc;
      }
      return [
        ...acc,
        { date, hasFilled: !Boolean(errors[date]) && records[date].length > 0 },
      ];
    }, [])
    .sort((a, b) => dayjs(a.date).diff(dayjs(b.date)));

  const isConsecutivelyFilled = filledDates.every((currentDate, index) => {
    if (index === 0) {
      return true;
    }

    if (currentDate.hasFilled && !filledDates[index - 1].hasFilled) {
      return false;
    }

    return true;
  });

  return isConsecutivelyFilled;
};
