import {
  DegreePlannerState,
  INITIAL_DEGREE_PLANNER_STATE,
} from '@app/degree-planner/store/state';
import * as planActions from '@app/degree-planner/store/actions/plan.actions';
import * as courseActions from '@app/degree-planner/store/actions/course.actions';
import * as noteActions from '@app/degree-planner/store/actions/note.actions';
import * as uiActions from '@app/degree-planner/store/actions/ui.actions';
import * as globalActions from '@app/core/actions';
import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
import { DegreePlan } from '@app/core/models/degree-plan';
import { Year, YearMapping } from '@app/core/models/year';
import { Course } from '@app/core/models/course';
import { PlannedTerm, PlannedTermNote } from '@app/core/models/planned-term';
import { YearCode } from '@app/degree-planner/shared/term-codes/yearcode';
import { TermCode, Era } from '@app/degree-planner/shared/term-codes/termcode';

type SupportedActions =
  | planActions.PlanError
  | planActions.InitialLoadSuccess
  | planActions.SwitchPlan
  | planActions.SwitchPlanSuccess
  | noteActions.WriteNote
  | noteActions.WriteNoteSuccess
  | noteActions.DeleteNote
  | courseActions.MoveCourseInsideTerm
  | courseActions.MoveCourseInsideSFL
  | courseActions.MoveCourseBetweenTerms
  | courseActions.RemoveCourse
  | courseActions.AddCourse
  | courseActions.AddCourseSuccess
  | courseActions.RemoveSaveForLater
  | courseActions.AddSaveForLater
  | uiActions.AddAcademicYear
  | planActions.CreatePlan
  | planActions.CreatePlanSuccess
  | planActions.MakePlanPrimary
  | planActions.MakePlanPrimarySuccess
  | planActions.MakePlanPrimaryFailure
  | planActions.ChangePlanNameSuccess
  | planActions.ChangePlanNameFailure
  | planActions.DeletePlanSuccess
  | uiActions.ExpandAcademicYear
  | uiActions.CollapseAcademicYear
  | globalActions.DismissAlert
  | uiActions.OpenCourseSearch
  | uiActions.CloseCourseSearch
  | uiActions.ToggleCourseSearch
  | uiActions.OpenSidenav
  | uiActions.CloseSidenav
  | uiActions.UpdateSearchTermCode
  | planActions.ChangeGradeVisibility;

export function degreePlannerReducer(
  state = INITIAL_DEGREE_PLANNER_STATE,
  action: SupportedActions,
): DegreePlannerState {
  switch (action.type) {
    case planActions.PlanActionTypes.PlanError: {
      return { ...state, isLoadingPlan: false };
    }

    case planActions.PlanActionTypes.CreatePlan: {
      return { ...state, isLoadingPlan: true };
    }
    /**
     * The `InitialPlanLoadResponse` action is triggered on initial Degree
     * Planner app load. It downloads a list of the user's degree plans and
     * picks the primary plan from that list to load as the first visible plan.
     */
    case planActions.PlanActionTypes.InitialLoadSuccess: {
      return { ...action.payload };
    }

    case planActions.PlanActionTypes.SwitchPlan: {
      return { ...state, isLoadingPlan: true };
    }

    /**
     * The `SwitchPlanSuccess` action is triggered whenever the UI needs
     * to switch which degree plan is being shown and load the data associated
     * with that degree plan. The reducer extracts that downloaded data from the
     * action payload and builds a new state using that data.
     */
    case planActions.PlanActionTypes.SwitchPlanSuccess: {
      return { ...state, ...action.payload, isLoadingPlan: false };
    }

    /**
     * The `AddAcademicYear` action is triggered after `addAcademicYear()`
     * function runs. A new academic year container with three terms will be created.
     */
    case uiActions.UIActionTypes.AddAcademicYear: {
      const currentYearCodes = Object.keys(state.visibleYears);
      const largestYearCode = Math.max(
        ...currentYearCodes.map(yearCode => {
          return parseInt(yearCode, 10);
        }),
      );
      const nextYearCode = new YearCode(
        `${largestYearCode + 1}`,
        Era.Future,
        Era.Future,
        Era.Future,
      );
      const nextYear = emptyYear(
        (state.visibleDegreePlan as DegreePlan).roadmapId,
        nextYearCode,
      );
      const visibleYears: YearMapping = {
        ...state.visibleYears,
        [nextYearCode.toString()]: nextYear,
      };
      return { ...state, visibleYears };
    }

    case uiActions.UIActionTypes.ExpandAcademicYear: {
      const yearCode = action.payload ? action.payload.yearCode : undefined;
      const newState = { ...state };

      if (yearCode) {
        newState.visibleYears[yearCode.toString()].isExpanded = true;
      } else {
        Object.entries(newState.visibleYears).forEach(([code, year]) => {
          newState.visibleYears[code].isExpanded = true;
        });
      }

      return newState;
    }

    case uiActions.UIActionTypes.CollapseAcademicYear: {
      const yearCode = action.payload ? action.payload.yearCode : undefined;
      const newState = { ...state };

      if (yearCode) {
        newState.visibleYears[yearCode.toString()].isExpanded = false;
      } else {
        Object.entries(newState.visibleYears).forEach(([code, year]) => {
          newState.visibleYears[code].isExpanded = false;
        });
      }

      return newState;
    }

    case globalActions.NotificationActionTypes.DismissAlert: {
      const keyToRemove = action.payload.key;
      const newAlerts = state.alerts.filter(({ key }) => key !== keyToRemove);
      return { ...state, alerts: newAlerts };
    }

    /**
     * The `ToggleCourseSearch` action toggles the open and close state of the course search side nav
     */
    case uiActions.UIActionTypes.ToggleCourseSearch: {
      const newSearchState = {
        ...state.search,
        visible: !state.search.visible,
      };

      // If a term was passed into the action
      if (action.termCode) {
        newSearchState.selectedTerm = action.termCode;
      }

      return {
        ...state,
        search: newSearchState,
      };
    }

    /**
     * The `ToggleCourseSearch` action opens the course search side nav
     */
    case uiActions.UIActionTypes.OpenCourseSearch: {
      const newSearchState = {
        ...state.search,
        visible: true,
      };

      // If a term was passed into the action
      if (action.termCode) {
        newSearchState.selectedTerm = action.termCode;
      }

      return { ...state, search: newSearchState };
    }

    /**
     * The `ToggleCourseSearch` action closes the course search side nav
     */
    case uiActions.UIActionTypes.CloseCourseSearch: {
      return {
        ...state,
        search: { ...state.search, visible: false },
      };
    }

    /**
     * The `UpdateSearchTermCode` action changes the active seach term code.
     */
    case uiActions.UIActionTypes.UpdateSearchTermCode: {
      return {
        ...state,
        search: { ...state.search, selectedTerm: action.termCode },
      };
    }

    case uiActions.UIActionTypes.OpenSidenav: {
      return {
        ...state,
        isSidenavOpen: true,
      };
    }

    case uiActions.UIActionTypes.CloseSidenav: {
      return {
        ...state,
        isSidenavOpen: false,
      };
    }

    case noteActions.NoteActionTypes.WriteNote: {
      const { termCode, noteText } = action.payload;
      const { yearCode, termName } = termCode;
      const year = state.visibleYears[yearCode.toString()];
      const existingNote = year ? year[termName].note : undefined;

      const newNote: PlannedTermNote =
        existingNote && existingNote.isLoaded
          ? { isLoaded: true, text: noteText, id: existingNote.id }
          : { isLoaded: false, text: noteText };

      const visibleYears: YearMapping = {
        ...state.visibleYears,
        [yearCode.toString()]: createYearWithNote(
          (state.visibleDegreePlan as DegreePlan).roadmapId,
          termCode,
          newNote,
          state.visibleYears[yearCode.toString()],
        ),
      };

      return { ...state, visibleYears };
    }

    /**
     * The `WriteNoteResponse` action is dispatched by the `Note.write$` effect
     * upon a successful response from the `updateNote` or `createNote` API
     * endpoints. The reducer in this case either:
     *
     *  - Replaces a note on a term that already had a note.
     *  - *OR* adds a note to a term that didn't previously have a note.
     *  - *OR* adds a new term with the given note if no term exists with the note's termCode.
     */
    case noteActions.NoteActionTypes.WriteNoteSuccess: {
      const { termCode, updatedNote } = action.payload;
      const { yearCode } = termCode;

      const visibleYears: YearMapping = {
        ...state.visibleYears,
        [yearCode.toString()]: createYearWithNote(
          (state.visibleDegreePlan as DegreePlan).roadmapId,
          termCode,
          { isLoaded: true, text: updatedNote.note, id: updatedNote.id },
          state.visibleYears[yearCode.toString()],
        ),
      };

      return { ...state, visibleYears };
    }

    /**
     * The `DeleteNoteResponse` action is dispatched after the `deleteNote` API
     * has been called and it is okay to remote the note with the given
     * termCode from the degree planner state.
     */
    case noteActions.NoteActionTypes.DeleteNote: {
      const { termCode } = action.payload;
      const { yearCode } = termCode;

      const visibleYears: YearMapping = {
        ...state.visibleYears,
        [yearCode.toString()]: createYearWithoutNote(
          (state.visibleDegreePlan as DegreePlan).roadmapId,
          termCode,
          state.visibleYears[yearCode.toString()],
        ),
      };

      return { ...state, visibleYears };
    }

    case courseActions.CourseActionTypes.MoveCourseInsideTerm: {
      const { termCode, recordId, newIndex } = action.payload;
      const { yearCode, termName } = termCode;
      const year = state.visibleYears[yearCode.toString()];

      if (year) {
        const courses = year[termName].plannedCourses;
        const course = courses.find(course => course.id === recordId);
        const oldIndex = courses.findIndex(course => course.id === recordId);
        if (course) {
          const newCourses = courses.slice();
          newCourses.splice(oldIndex, 1);
          newCourses.splice(newIndex, 0, course);

          const visibleYears = {
            ...state.visibleYears,
            [yearCode.toString()]: {
              ...state.visibleYears[yearCode.toString()],
              [termName]: {
                ...state.visibleYears[yearCode.toString()][termName],
                plannedCourses: newCourses,
              },
            },
          };

          return { ...state, visibleYears };
        }
      }

      return state;
    }

    case courseActions.CourseActionTypes.MoveCourseInsideSFL: {
      const { courseId, newIndex } = action.payload;
      const courses = state.savedForLaterCourses.slice();
      const course = courses.find(c => c.courseId === courseId);

      if (course) {
        const oldIndex = courses.findIndex(c => c.courseId === courseId);
        courses.splice(oldIndex, 1);
        courses.splice(newIndex, 0, course);
        return { ...state, savedForLaterCourses: courses };
      }

      return state;
    }

    case courseActions.CourseActionTypes.MoveCourseBetweenTerms: {
      const {
        to: toTermCode,
        from: fromTermCode,
        id,
        newIndex,
      } = action.payload;
      const { yearCode: fromYearCode } = fromTermCode;
      const { yearCode: toYearCode } = toTermCode;
      const course = findCourse(state.visibleYears, fromTermCode, id);

      if (course && course.id !== null) {
        course.classNumber = null;
        const fromYear = createYearWithoutCourse(
          (state.visibleDegreePlan as DegreePlan).roadmapId,
          fromTermCode,
          course.id,
          state.visibleYears[fromYearCode.toString()],
        );

        const toYear = createYearWithCourse(
          (state.visibleDegreePlan as DegreePlan).roadmapId,
          toTermCode,
          { ...course, termCode: toTermCode.toString() },
          fromYearCode.equals(toYearCode)
            ? fromYear
            : state.visibleYears[toYearCode.toString()],
          newIndex,
        );

        const visibleYears = {
          ...state.visibleYears,
          [fromYearCode.toString()]: fromYear,
          [toYearCode.toString()]: toYear,
        };

        return { ...state, visibleYears };
      }

      return state;
    }

    case courseActions.CourseActionTypes.AddCourse: {
      const { termCode, newIndex } = action.payload;

      const course = {
        ...action.payload,
        id: null,
        termCode: action.payload.termCode.toString(),
      } as Course;

      const year: Year = createYearWithCourse(
        (state.visibleDegreePlan as DegreePlan).roadmapId,
        termCode,
        course,
        state.visibleYears[termCode.yearCode.toString()],
        newIndex,
      );

      const visibleYears: YearMapping = {
        ...state.visibleYears,
        [termCode.yearCode.toString()]: year,
      };

      return { ...state, visibleYears };
    }

    case courseActions.CourseActionTypes.AddCourseSuccess: {
      const { termCode, course, newIndex } = action.payload;
      const { yearCode } = termCode;

      const year: Year = createYearWithCourse(
        (state.visibleDegreePlan as DegreePlan).roadmapId,
        termCode,
        course,
        state.visibleYears[yearCode.toString()],
        newIndex,
      );

      const visibleYears = {
        ...state.visibleYears,
        [yearCode.toString()]: year,
      };

      return { ...state, visibleYears };
    }

    case courseActions.CourseActionTypes.RemoveCourse: {
      const { recordId, fromTermCode } = action.payload;
      const { yearCode } = fromTermCode;

      const year: Year = createYearWithoutCourse(
        (state.visibleDegreePlan as DegreePlan).roadmapId,
        fromTermCode,
        recordId,
        state.visibleYears[yearCode.toString()],
      );

      const visibleYears = {
        ...state.visibleYears,
        [yearCode.toString()]: year,
      };

      return { ...state, visibleYears };
    }

    case courseActions.CourseActionTypes.RemoveSaveForLater: {
      const { courseId, subjectCode } = action.payload;
      const newSavedForLater = state.savedForLaterCourses.filter(course => {
        return !(
          course.subjectCode === subjectCode && course.courseId === courseId
        );
      });

      return { ...state, savedForLaterCourses: newSavedForLater };
    }

    case courseActions.CourseActionTypes.AddSaveForLater: {
      const { courseId, subjectCode, newIndex } = action.payload;

      const savedForLaterCourses = state.savedForLaterCourses.filter(c => {
        return !(c.courseId === courseId && c.subjectCode === subjectCode);
      });

      const newSavedCourse: SavedForLaterCourse = {
        id: null,
        courseId: courseId,
        termCode: '0000',
        topicId: 0,
        subjectCode: subjectCode,
        title: action.payload.title,
        catalogNumber: action.payload.catalogNumber,
        courseOrder: 0,
      };

      savedForLaterCourses.splice(newIndex, 0, newSavedCourse);

      return { ...state, savedForLaterCourses };
    }

    case planActions.PlanActionTypes.CreatePlanSuccess: {
      const { newPlan, newYears } = action.payload;
      return {
        ...state,
        visibleDegreePlan: newPlan,
        visibleYears: newYears,
        allDegreePlans: state.allDegreePlans.concat(newPlan),
        isLoadingPlan: false,
      };
    }

    case planActions.PlanActionTypes.MakePlanPrimary: {
      // TODO add global loading state
      return state;
    }

    case planActions.PlanActionTypes.MakePlanPrimarySuccess: {
      // Get current roadmap ID
      const roadmapId = (state.visibleDegreePlan as DegreePlan).roadmapId;

      // Update the visible term object
      const newVisibleDegreePlan = {
        ...(state.visibleDegreePlan as DegreePlan),
        primary: true,
      };

      // Update all plans to only have the current one be primary
      const newPlans = state.allDegreePlans.map(plan => {
        if (plan.roadmapId !== roadmapId) {
          return { ...plan, primary: false };
        }
        return { ...plan, primary: true };
      });

      // Return the new state
      return {
        ...state,
        visibleDegreePlan: newVisibleDegreePlan,
        allDegreePlans: newPlans,
      };
    }

    case planActions.PlanActionTypes.MakePlanPrimaryFailure: {
      // TODO add error message
      return state;
    }

    case planActions.PlanActionTypes.ChangePlanNameSuccess: {
      const visibleDegreePlan = {
        ...(state.visibleDegreePlan as DegreePlan),
        name: action.payload.newName,
      };

      return {
        ...state,
        allDegreePlans: state.allDegreePlans.map(plan => {
          if (plan.roadmapId === action.payload.roadmapId) {
            return { ...plan, name: action.payload.newName };
          } else {
            return plan;
          }
        }),
        visibleDegreePlan,
      };
    }

    case planActions.PlanActionTypes.ChangePlanNameFailure: {
      const visibleDegreePlan = {
        ...(state.visibleDegreePlan as DegreePlan),
        name: action.payload.oldName,
      };

      return {
        ...state,
        allDegreePlans: state.allDegreePlans.map(plan => {
          if (plan.roadmapId === action.payload.roadmapId) {
            return { ...plan, name: action.payload.oldName };
          } else {
            return plan;
          }
        }),
        visibleDegreePlan,
      };
    }

    case planActions.PlanActionTypes.DeletePlanSuccess: {
      const allDegreePlans = state.allDegreePlans.filter(plan => {
        return plan.roadmapId !== action.payload.roadmapId;
      });
      return { ...state, allDegreePlans };
    }

    case planActions.PlanActionTypes.ChangeGradeVisibility: {
      return { ...state, showGrades: action.visibility };
    }

    /**
     * It's okay if the action didn't match any of the cases above. If that's
     * the case, just return the existing state object.
     */
    default:
      return state;
  }
}

const emptyTerm = (roadmapId: number, termCode: TermCode): PlannedTerm => {
  return { roadmapId, termCode, plannedCourses: [], enrolledCourses: [] };
};

const emptyYear = (roadmapId: number, yearCode: YearCode): Year => {
  return {
    yearCode,
    isExpanded: !(
      yearCode.fall().isPast() &&
      yearCode.spring().isPast() &&
      yearCode.summer().isPast()
    ),
    fall: emptyTerm(roadmapId, yearCode.fall()),
    spring: emptyTerm(roadmapId, yearCode.spring()),
    summer: emptyTerm(roadmapId, yearCode.summer()),
  };
};

const generateYearForTermCode = (
  roadmapId: number,
  termCode: TermCode,
): Year => {
  return emptyYear(roadmapId, termCode.yearCode);
};

const createYearWithNote = (
  roadmapId: number,
  termCode: TermCode,
  note: PlannedTermNote | undefined,
  year = generateYearForTermCode(roadmapId, termCode),
): Year => {
  const term = year[termCode.termName];
  return { ...year, [termCode.termName]: { ...term, note } };
};

const createYearWithoutNote = (
  roadmapId: number,
  termCode: TermCode,
  year?: Year,
) => {
  return createYearWithNote(roadmapId, termCode, undefined, year);
};

const findCourse = (
  years: YearMapping,
  termCode: TermCode,
  recordId: number,
) => {
  const year = years[termCode.yearCode.toString()];
  if (year) {
    const term = year[termCode.termName];
    return term.plannedCourses.find(course => course.id === recordId);
  }
};

const createYearWithCourse = (
  roadmapId: number,
  termCode: TermCode,
  course: Course,
  year = generateYearForTermCode(roadmapId, termCode),
  newIndex?: number,
): Year => {
  const term = year[termCode.termName];
  const courses = term.plannedCourses.filter(
    c => c.courseId !== course.courseId,
  );
  newIndex = newIndex !== undefined ? newIndex : courses.length;
  courses.splice(newIndex, 0, course);
  return { ...year, [termCode.termName]: { ...term, plannedCourses: courses } };
};

const createYearWithoutCourse = (
  roadmapId: number,
  termCode: TermCode,
  recordId: number,
  year = generateYearForTermCode(roadmapId, termCode),
): Year => {
  const term = year[termCode.termName];
  const courses = term.plannedCourses.filter(course => course.id !== recordId);
  return {
    ...year,
    [termCode.termName]: { ...term, plannedCourses: courses },
  };
};