import { State } from "./../index";
import { ActionContext, Module } from "vuex";
import {
  CalendarRange,
  CalendarRangeCreate,
  CalendarRangeEdit,
  isEventInputCalendarRange,
} from "../../../../types";
import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types";
import { fetchCalendarRangeForUser } from "../../../Calendar/api";
import { calendarRangeToFullCalendarEvent } from "../../../Calendar/store/utils";
import { EventInput } from "@fullcalendar/core";
import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import {
  datetimeToISO,
  dateToISO,
  ISOToDatetime,
} from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import type { EventInputCalendarRange } from "../../../../types";

export interface CalendarRangesState {
  ranges: (EventInput | EventInputCalendarRange)[];
  rangesLoaded: { start: number; end: number }[];
  rangesIndex: Set<string>;
  key: number;
}

type Context = ActionContext<CalendarRangesState, State>;

export default {
  namespaced: true,
  state: (): CalendarRangesState => ({
    ranges: [],
    rangesLoaded: [],
    rangesIndex: new Set<string>(),
    key: 0,
  }),
  getters: {
    isRangeLoaded:
      (state: CalendarRangesState) =>
      ({ start, end }: { start: Date; end: Date }): boolean => {
        for (const range of state.rangesLoaded) {
          if (start.getTime() === range.start && end.getTime() === range.end) {
            return true;
          }
        }

        return false;
      },
    getRangesOnDate:
      (state: CalendarRangesState) =>
      (date: Date): EventInputCalendarRange[] => {
        const founds = [];
        const dateStr = dateToISO(date) as string;

        for (const range of state.ranges) {
          if (
            isEventInputCalendarRange(range) &&
            range.start.startsWith(dateStr)
          ) {
            founds.push(range);
          }
        }

        return founds;
      },
    getRangesOnWeek:
      (state: CalendarRangesState) =>
      (mondayDate: Date): EventInputCalendarRange[] => {
        const founds = [];
        for (const d of Array.from(Array(7).keys())) {
          const dateOfWeek = new Date(mondayDate);
          dateOfWeek.setDate(mondayDate.getDate() + d);
          const dateStr = dateToISO(dateOfWeek) as string;
          for (const range of state.ranges) {
            if (
              isEventInputCalendarRange(range) &&
              range.start.startsWith(dateStr)
            ) {
              founds.push(range);
            }
          }
        }

        return founds;
      },
  },
  mutations: {
    addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
      const toAdd = ranges
        .map((cr) => calendarRangeToFullCalendarEvent(cr))
        .map((cr) => ({
          ...cr,
          backgroundColor: "white",
          borderColor: "#3788d8",
          textColor: "black",
        }))
        .filter((r) => !state.rangesIndex.has(r.id));

      toAdd.forEach((r) => {
        state.rangesIndex.add(r.id);
        state.ranges.push(r);
      });
      state.key = state.key + toAdd.length;
    },
    addExternals(
      state: CalendarRangesState,
      externalEvents: (EventInput & { id: string })[],
    ) {
      const toAdd = externalEvents.filter((r) => !state.rangesIndex.has(r.id));

      toAdd.forEach((r) => {
        state.rangesIndex.add(r.id);
        state.ranges.push(r);
      });
      state.key = state.key + toAdd.length;
    },
    addLoaded(state: CalendarRangesState, payload: { start: Date; end: Date }) {
      state.rangesLoaded.push({
        start: payload.start.getTime(),
        end: payload.end.getTime(),
      });
    },
    addRange(state: CalendarRangesState, payload: CalendarRange) {
      const asEvent = calendarRangeToFullCalendarEvent(payload);
      state.ranges.push({
        ...asEvent,
        backgroundColor: "white",
        borderColor: "#3788d8",
        textColor: "black",
      });
      state.rangesIndex.add(asEvent.id);
      state.key = state.key + 1;
    },
    removeRange(state: CalendarRangesState, calendarRangeId: number) {
      const found = state.ranges.find(
        (r) => r.calendarRangeId === calendarRangeId && r.is === "range",
      );

      if (found !== undefined) {
        state.ranges = state.ranges.filter(
          (r) => !(r.calendarRangeId === calendarRangeId && r.is === "range"),
        );

        if (typeof found.id === "string") {
          // should always be true
          state.rangesIndex.delete(found.id);
        }

        state.key = state.key + 1;
      }
    },
    updateRange(state: CalendarRangesState, range: CalendarRange) {
      const found = state.ranges.find(
        (r) => r.calendarRangeId === range.id && r.is === "range",
      );
      const newEvent = calendarRangeToFullCalendarEvent(range);

      if (found !== undefined) {
        found.start = newEvent.start;
        found.end = newEvent.end;
        found.locationId = range.location.id;
        found.locationName = range.location.name;
      }

      state.key = state.key + 1;
    },
  },
  actions: {
    fetchRanges(
      ctx: Context,
      payload: { start: Date; end: Date },
    ): Promise<null> {
      const start = payload.start;
      const end = payload.end;

      if (ctx.rootGetters["me/getMe"] === null) {
        return Promise.resolve(ctx.getters.getRangeSource);
      }

      if (ctx.getters.isRangeLoaded({ start, end })) {
        return Promise.resolve(ctx.getters.getRangeSource);
      }

      ctx.commit("addLoaded", {
        start: start,
        end: end,
      });

      return fetchCalendarRangeForUser(
        ctx.rootGetters["me/getMe"],
        start,
        end,
      ).then((ranges: CalendarRange[]) => {
        ctx.commit("addRanges", ranges);
        return Promise.resolve(null);
      });
    },
    createRange(
      ctx: Context,
      { start, end, location }: { start: Date; end: Date; location: Location },
    ): Promise<null> {
      const url = `/api/1.0/calendar/calendar-range.json?`;

      if (ctx.rootState.me.me === null) {
        throw new Error("user is currently null");
      }

      const body = {
        user: {
          id: ctx.rootState.me.me.id,
          type: "user",
        },
        startDate: {
          datetime: datetimeToISO(start),
        },
        endDate: {
          datetime: datetimeToISO(end),
        },
        location: {
          id: location.id,
          type: "location",
        },
      } as CalendarRangeCreate;

      return makeFetch<CalendarRangeCreate, CalendarRange>("POST", url, body)
        .then((newRange) => {
          ctx.commit("addRange", newRange);

          return Promise.resolve(null);
        })
        .catch((error) => {
          console.error(error);

          throw error;
        });
    },
    deleteRange(ctx: Context, calendarRangeId: number) {
      const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;

      makeFetch<undefined, never>("DELETE", url).then(() => {
        ctx.commit("removeRange", calendarRangeId);
      });
    },
    patchRangeTime(
      ctx,
      {
        calendarRangeId,
        start,
        end,
      }: { calendarRangeId: number; start: Date; end: Date },
    ): Promise<null> {
      const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
      const body = {
        startDate: {
          datetime: datetimeToISO(start),
        },
        endDate: {
          datetime: datetimeToISO(end),
        },
      } as CalendarRangeEdit;

      return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body)
        .then((range) => {
          ctx.commit("updateRange", range);
          return Promise.resolve(null);
        })
        .catch((error) => {
          console.error(error);
          return Promise.resolve(null);
        });
    },
    patchRangeLocation(
      ctx,
      {
        location,
        calendarRangeId,
      }: { location: Location; calendarRangeId: number },
    ): Promise<null> {
      const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
      const body = {
        location: {
          id: location.id,
          type: "location",
        },
      } as CalendarRangeEdit;

      return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body)
        .then((range) => {
          ctx.commit("updateRange", range);
          return Promise.resolve(null);
        })
        .catch((error) => {
          console.error(error);
          return Promise.resolve(null);
        });
    },
    copyFromDayToAnotherDay(
      ctx,
      { from, to }: { from: Date; to: Date },
    ): Promise<null> {
      const rangesToCopy: EventInputCalendarRange[] =
        ctx.getters["getRangesOnDate"](from);
      const promises = [];

      for (const r of rangesToCopy) {
        const start = new Date(ISOToDatetime(r.start) as Date);
        start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
        const end = new Date(ISOToDatetime(r.end) as Date);
        end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
        const location = ctx.rootGetters["locations/getLocationById"](
          r.locationId,
        );

        promises.push(ctx.dispatch("createRange", { start, end, location }));
      }

      return Promise.all(promises).then(() => Promise.resolve(null));
    },
    copyFromWeekToAnotherWeek(
      ctx: Context,
      { fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
    ): Promise<null> {
      const rangesToCopy: EventInputCalendarRange[] =
        ctx.getters["getRangesOnWeek"](fromMonday);
      const promises = [];
      const diffTime = toMonday.getTime() - fromMonday.getTime();
      for (const r of rangesToCopy) {
        const start = new Date(ISOToDatetime(r.start) as Date);
        const end = new Date(ISOToDatetime(r.end) as Date);
        start.setTime(start.getTime() + diffTime);
        end.setTime(end.getTime() + diffTime);
        const location = ctx.rootGetters["locations/getLocationById"](
          r.locationId,
        );

        promises.push(ctx.dispatch("createRange", { start, end, location }));
      }

      return Promise.all(promises).then(() => Promise.resolve(null));
    },
  },
} as Module<CalendarRangesState, State>;
