import { parseTermCode, pickTermEra } from '@app/degree-planner/shared/utils';
import { UIActionTypes } from './actions/ui.actions';
import {
  DegreePlannerState,
  INITIAL_DEGREE_PLANNER_STATE,
} from '@app/degree-planner/store/state';
import {
  PlanActionTypes,
  InitialLoadSuccess,
  SwitchPlan,
  SwitchPlanSuccess,
  MakePlanPrimary,
  MakePlanPrimarySuccess,
  MakePlanPrimaryFailure,
  ChangePlanNameSuccess,
  ChangePlanNameFailure,
  CreatePlanSuccess,
  DeletePlanSuccess,
  PlanError,
} from '@app/degree-planner/store/actions/plan.actions';
import {
  MoveCourseInsideTerm,
  CourseActionTypes,
  RemoveCourse,
  MoveCourseBetweenTerms,
  AddCourseSuccess,
  RemoveSaveForLater,
  AddSaveForLater,
  CourseError,
} from '@app/degree-planner/store/actions/course.actions';
import {
  NoteActionTypes,
  WriteNoteSuccess,
  DeleteNoteSuccess,
} from '@app/degree-planner/store/actions/note.actions';
import {
  AddAcademicYearActionTypes,
  AddAcademicYearRequest,
} from '@app/degree-planner/store/actions/addAcademicYear.actions';
import {
  ExpandAcademicYear,
  CollapseAcademicYear,
  OpenCourseSearch,
  CloseCourseSearch,
  ToggleCourseSearch,
  UpdateSearchTermCode,
} from '@app/degree-planner/store/actions/ui.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 { Note } from '@app/core/models/note';
import { PlannedTerm, PlannedTermEra } from '@app/core/models/planned-term';
import * as utils from '@app/degree-planner/shared/utils';

type SupportedActions =
  | PlanError
  | InitialLoadSuccess
  | SwitchPlan
  | SwitchPlanSuccess
  | WriteNoteSuccess
  | DeleteNoteSuccess
  | MoveCourseInsideTerm
  | MoveCourseBetweenTerms
  | RemoveCourse
  | AddCourseSuccess
  | RemoveSaveForLater
  | AddSaveForLater
  | AddAcademicYearRequest
  | CreatePlanSuccess
  | MakePlanPrimary
  | MakePlanPrimarySuccess
  | MakePlanPrimaryFailure
  | ChangePlanNameSuccess
  | ChangePlanNameFailure
  | DeletePlanSuccess
  | ExpandAcademicYear
  | CollapseAcademicYear
  | OpenCourseSearch
  | CloseCourseSearch
  | ToggleCourseSearch
  | UpdateSearchTermCode;

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

    /**
     * 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 PlanActionTypes.InitialLoadSuccess: {
      return { ...action.payload };
    }

    case 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 PlanActionTypes.SwitchPlanSuccess: {
      return { ...state, ...action.payload, isLoadingPlan: false };
    }

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

    case UIActionTypes.ExpandAcademicYear: {
      const yearCode = action.payload.yearCode;
      return {
        ...state,
        visibleYears: {
          ...state.visibleYears,
          [yearCode]: { ...state.visibleYears[yearCode], isExpanded: true },
        },
      };
    }

    case UIActionTypes.CollapseAcademicYear: {
      const yearCode = action.payload.yearCode;
      return {
        ...state,
        visibleYears: {
          ...state.visibleYears,
          [yearCode]: { ...state.visibleYears[yearCode], isExpanded: false },
        },
      };
    }

    /**
     * The `ToggleCourseSearch` action toggles the open and close state of the course search side nav
     */
    case 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 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 UIActionTypes.CloseCourseSearch: {
      return {
        ...state,
        search: { ...state.search, visible: false, selectedTerm: '0000' },
      };
    }

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

    /**
     * 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 NoteActionTypes.WriteNoteSuccess: {
      const { updatedNote } = action.payload;
      const { termCode } = updatedNote;
      const { yearCode } = parseTermCode(termCode);

      const visibleYears: YearMapping = {
        ...state.visibleYears,
        [yearCode]: createYearWithNote(
          termCode,
          updatedNote,
          state.activeTermCodes,
          state.visibleYears[yearCode],
        ),
      };

      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 NoteActionTypes.DeleteNoteSuccess: {
      const { termCode } = action.payload;
      const { yearCode } = parseTermCode(termCode);

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

      return { ...state, visibleYears };
    }

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

      if (year) {
        const courses = year[termName].courses;
        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]: {
              ...state.visibleYears[yearCode],
              [termName]: {
                ...state.visibleYears[yearCode][termName],
                courses: newCourses,
              },
            },
          };

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

      return state;
    }

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

      if (course && course.id !== null) {
        const fromYear = createYearWithoutCourse(
          fromTermCode,
          course.id,
          state.activeTermCodes,
          state.visibleYears[fromYearCode],
        );

        const toYear = createYearWithCourse(
          toTermCode,
          course,
          newIndex,
          state.activeTermCodes,
          fromYearCode === toYearCode
            ? fromYear
            : state.visibleYears[toYearCode],
        );

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

        return { ...state, visibleYears };
      }

      return state;
    }

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

      const year: Year = createYearWithCourse(
        termCode,
        course,
        newIndex,
        state.activeTermCodes,
        state.visibleYears[yearCode],
      );

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

      return { ...state, visibleYears };
    }

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

      const year: Year = createYearWithoutCourse(
        fromTermCode,
        recordId,
        state.activeTermCodes,
        state.visibleYears[yearCode],
      );

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

      return { ...state, visibleYears };
    }

    case CourseActionTypes.RemoveSaveForLater: {
      const { courseId, subjectCode } = action.payload;

      // // Create new saved for later array
      const newSavedForLater = state.savedForLaterCourses.filter(
        course =>
          course.subjectCode !== subjectCode && course.courseId !== courseId,
      );

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

    case CourseActionTypes.AddSaveForLater: {
      const { newIndex } = action.payload;
      const newSavedCourse: SavedForLaterCourse = {
        id: null,
        courseId: action.payload.courseId,
        termCode: '0000',
        topicId: 0,
        subjectCode: action.payload.subjectCode,
        title: action.payload.title,
        catalogNumber: action.payload.catalogNumber,
        courseOrder: 0,
        subject: state.subjects[action.payload.subjectCode],
      };
      const savedForLaterCoursesArr = state.savedForLaterCourses.slice();

      savedForLaterCoursesArr.splice(newIndex, 0, newSavedCourse);
      return { ...state, savedForLaterCourses: savedForLaterCoursesArr };
    }

    case PlanActionTypes.CreatePlanSuccess: {
      const { newPlan } = action.payload;
      return {
        ...state,
        allDegreePlans: state.allDegreePlans.concat(newPlan),
      };
    }

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

    case 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 PlanActionTypes.MakePlanPrimaryFailure: {
      // TODO add error message
      return state;
    }

    case 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 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 PlanActionTypes.DeletePlanSuccess: {
      const allDegreePlans = state.allDegreePlans.filter(plan => {
        return plan.roadmapId !== action.payload.roadmapId;
      });
      return { ...state, allDegreePlans };
    }

    /**
     * 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 termCodeExists = (termCode: string, things: { termCode: string }[]) => {
  return things.some(thing => thing.termCode === termCode);
};

const emptyTerm = (termCode: string, era: PlannedTermEra): PlannedTerm => {
  return { termCode, era, courses: [] };
};

const emptyYear = (yearCode: string, activeTermCodes: string[]): Year => {
  const fall = `${yearCode}2`;
  const spring = `${yearCode}4`;
  const summer = `${yearCode}6`;
  return {
    yearCode,
    isExpanded: utils.pickYearEra(yearCode, activeTermCodes) !== 'past',
    fall: emptyTerm(fall, pickTermEra(fall, activeTermCodes)),
    spring: emptyTerm(spring, pickTermEra(spring, activeTermCodes)),
    summer: emptyTerm(summer, pickTermEra(summer, activeTermCodes)),
  };
};

const generateYearForTermCode = (
  termCode: string,
  activeTermCodes: string[],
): Year => {
  const { yearCode } = parseTermCode(termCode);
  return emptyYear(yearCode, activeTermCodes);
};

const createYearWithNote = (
  termCode: string,
  note: Note | undefined,
  activeTermCodes: string[],
  year = generateYearForTermCode(termCode, activeTermCodes),
): Year => {
  const { termName } = parseTermCode(termCode);
  const term = year[termName];
  return { ...year, [termName]: { ...term, note } };
};

const createYearWithoutNote = (
  termCode: string,
  activeTermCodes: string[],
  year?: Year,
) => {
  return createYearWithNote(termCode, undefined, activeTermCodes, year);
};

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

const createYearWithCourse = (
  termCode: string,
  course: Course,
  newIndex: number,
  activeTermCodes: string[],
  year = generateYearForTermCode(termCode, activeTermCodes),
): Year => {
  const { termName } = parseTermCode(termCode);
  const term = year[termName];
  const courses = term.courses.slice();
  courses.splice(newIndex, 0, course);
  return { ...year, [termName]: { ...term, courses } };
};

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