// Libraries
import { Injectable } from '@angular/core';
import { ROOT_EFFECTS_INIT, Actions, Effect, ofType } from '@ngrx/effects';
import { Observable, forkJoin, of } from 'rxjs';
import {
  tap,
  map,
  flatMap,
  withLatestFrom,
  catchError,
  filter,
} from 'rxjs/operators';
import { GlobalState } from '@app/core/state';
import { Store, Action } from '@ngrx/store';
import { MatSnackBar } from '@angular/material';

// Services
import { DegreePlannerApiService } from '@app/degree-planner/services/api.service';
import { getDegreePlannerState } from '@app/degree-planner/store/selectors';

// Actions
import {
  InitialLoadSuccess,
  SwitchPlan,
  SwitchPlanSuccess,
  PlanActionTypes,
  PlanError,
  MakePlanPrimary,
  MakePlanPrimarySuccess,
  MakePlanPrimaryFailure,
  ChangePlanName,
  ChangePlanNameSuccess,
  ChangePlanNameFailure,
  CreatePlan,
  CreatePlanSuccess,
  DeletePlan,
  DeletePlanSuccess,
} from '@app/degree-planner/store/actions/plan.actions';

// Models
import { DegreePlan } from '@app/core/models/degree-plan';
import { PlannedTerm } from '@app/core/models/planned-term';
import { SubjectMapping } from '@app/core/models/course';
import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
import {
  DegreePlannerState,
  INITIAL_DEGREE_PLANNER_STATE,
} from '@app/degree-planner/store/state';

@Injectable()
export class DegreePlanEffects {
  constructor(
    private actions$: Actions,
    private api: DegreePlannerApiService,
    private store$: Store<GlobalState>,
    private snackBar: MatSnackBar,
  ) {}

  @Effect()
  init$ = this.actions$.pipe(
    ofType(ROOT_EFFECTS_INIT),
    // Load the list of degree plans and data used by all degree plans.
    flatMap(() => {
      const activeTermCodes = this.api
        .getActiveTerms()
        .pipe(map(terms => terms.map(term => term.termCode)));

      return forkJoinWithKeys({
        allDegreePlans: this.api.getAllDegreePlans(),
        subjects: this.api.getAllSubjects(),
        activeTermCodes,
      });
    }),
    // Load data specific to the primary degree plan.
    flatMap(({ allDegreePlans, subjects, activeTermCodes }) => {
      const savedForLaterCourses = this.loadSavedForLaterCourses(subjects);
      const visibleDegreePlan = pickPrimaryDegreePlan(allDegreePlans);
      const visibleTerms = loadPlanTerms(
        this.api,
        visibleDegreePlan,
        subjects,
        activeTermCodes,
      );

      return forkJoinWithKeys({
        visibleDegreePlan: of(visibleDegreePlan),
        visibleTerms,
        savedForLaterCourses,
        activeTermCodes: of(activeTermCodes),
        allDegreePlans: of(allDegreePlans),
        subjects: of(subjects),
      });
    }),
    map(payload => {
      const allTerms = payload.visibleTerms.map(term => term.termCode);
      const currentIndex = allTerms.indexOf(payload.activeTermCodes[0]);
      const expandedTerms = allTerms.slice(currentIndex - allTerms.length);
      const expandedYearsDups = expandedTerms.map(term => term.substr(1, 2));
      const expandedYears = expandedYearsDups.filter(function(item, pos, self) {
        return self.indexOf(item) === pos;
      });

      return { ...payload, expandedYears };
    }),
    map(
      payload =>
        new InitialLoadSuccess({ ...INITIAL_DEGREE_PLANNER_STATE, ...payload }),
    ),
    catchError(error => {
      return of(
        new PlanError({
          message: 'Something went wrong',
          duration: 2000,
          error,
        }),
      );
    }),
  );

  @Effect()
  switch$ = this.actions$.pipe(
    ofType<SwitchPlan>(PlanActionTypes.SwitchPlan),
    withLatestFrom(this.store$.select(getDegreePlannerState)),
    flatMap(([action, state]) => {
      const visibleDegreePlan = state.allDegreePlans.find(plan => {
        return plan.roadmapId === action.payload.newVisibleRoadmapId;
      }) as DegreePlan;

      const visibleTerms = loadPlanTerms(
        this.api,
        visibleDegreePlan,
        state.subjects,
        state.activeTermCodes,
      );

      return forkJoinWithKeys({
        visibleDegreePlan: of(visibleDegreePlan),
        visibleTerms,
      });
    }),
    map(payload => new SwitchPlanSuccess(payload)),
    tap(state => {
      const touchedPlan = state.payload.visibleDegreePlan.name;
      const message = `Switched to ${touchedPlan}`;
      this.snackBar.open(message, undefined, { duration: 2000 });
    }),
    catchError(error => {
      return of(
        new PlanError({
          message: 'Unable to switch plan',
          duration: 2000,
          error,
        }),
      );
    }),
  );

  @Effect()
  MakePlanPrimary$ = this.actions$.pipe(
    ofType<MakePlanPrimary>(PlanActionTypes.MakePlanPrimary),
    withLatestFrom(this.store$.select(getDegreePlannerState)),
    filter(([_, state]) => state.visibleDegreePlan !== undefined),
    // Get term data for the degree plan specified by the roadmap ID.
    flatMap(([_action, state]) => {
      const { roadmapId, name } = state.visibleDegreePlan as DegreePlan;
      return this.api.updatePlan(roadmapId, name, true);
    }),
    // // Wrap data in an Action for dispatch
    map(response => {
      if (response === 1) {
        return new MakePlanPrimarySuccess();
      } else {
        return new MakePlanPrimaryFailure();
      }
    }),
    tap(state => {
      const message = 'This plan has been set as the primary plan';
      this.snackBar.open(message, undefined, { duration: 2000 });
    }),
    catchError(error => {
      return of(
        new PlanError({
          message: 'Unable to make this plan primary',
          duration: 2000,
          error,
        }),
      );
    }),
  );

  @Effect()
  ChangePlanName$ = this.actions$.pipe(
    ofType<ChangePlanName>(PlanActionTypes.ChangePlanName),
    withLatestFrom(this.store$.select(getDegreePlannerState)),
    flatMap(([action, state]) => {
      const { roadmapId, newName } = action.payload;
      const oldDegreePlan = state.allDegreePlans.find(plan => {
        return plan.roadmapId === roadmapId;
      }) as DegreePlan;
      const oldName = oldDegreePlan.name;

      return this.api
        .updatePlan(roadmapId, newName, oldDegreePlan.primary)
        .pipe(
          map(() => {
            return new ChangePlanNameSuccess({ roadmapId, newName });
          }),
          tap(() => {
            const message = `Plan has been renamed to ${newName}`;
            this.snackBar.open(message, undefined, { duration: 2000 });
          }),
          catchError(() => {
            return of(new ChangePlanNameFailure({ roadmapId, oldName }));
          }),
        );
    }),
  );

  @Effect()
  createPlan$ = this.actions$.pipe(
    ofType<CreatePlan>(PlanActionTypes.CreatePlan),
    flatMap(action => {
      const { name, primary } = action.payload;
      return this.api.createDegreePlan(name, primary).pipe(
        map(newPlan => new CreatePlanSuccess({ newPlan })),
        tap(() => {
          const message = `New plan has been created`;
          this.snackBar.open(message, undefined, { duration: 2000 });
        }),
        catchError(error => {
          return of(
            new PlanError({
              message: 'Unable to create new plan',
              duration: 2000,
              error,
            }),
          );
        }),
      );
    }),
  );

  @Effect()
  deletePlan$ = this.actions$.pipe(
    ofType<DeletePlan>(PlanActionTypes.DeletePlan),
    flatMap(action => {
      const { roadmapId } = action.payload;
      return this.api.deleteDegreePlan(roadmapId).pipe(
        map(() => new DeletePlanSuccess({ roadmapId })),
        catchError(error => {
          return of(
            new PlanError({
              message: 'Unable to delete plan',
              duration: 2000,
              error,
            }),
          );
        }),
      );
    }),
  );

  private loadSavedForLaterCourses(subjects: SubjectMapping) {
    return this.api.getSavedForLaterCourses().pipe(
      map(courseBases => {
        return courseBases.map<SavedForLaterCourse>(base => {
          return {
            ...base,
            subject: subjects[base.subjectCode] as string,
          };
        });
      }),
    );
  }
}

const loadPlanTerms = (
  api: DegreePlannerApiService,
  visibleDegreePlan: DegreePlan,
  subjects: SubjectMapping,
  activeTermCodes: string[],
): Observable<PlannedTerm[]> => {
  const notesAndTerms$ = forkJoinWithKeys({
    notes: api.getAllNotes(visibleDegreePlan.roadmapId),
    terms: api.getAllTermCourses(visibleDegreePlan.roadmapId),
  });

  const uniqueTerms$ = notesAndTerms$.pipe(
    map(({ notes, terms }) => {
      const noteTermCodes = notes.map(note => note.termCode);
      const courseTermCodes = terms.map(term => term.termCode);
      const uniqueTermCodes = unique([
        ...noteTermCodes,
        ...courseTermCodes,
        ...activeTermCodes,
      ]);

      return uniqueTermCodes.sort();
    }),
  );

  const visibleTerms$ = forkJoin(uniqueTerms$, notesAndTerms$).pipe(
    map(([uniqueTerms, { notes, terms }]) => {
      return uniqueTerms.map(termCode => {
        const note = notes.find(matchesTermCode(termCode));
        const term = terms.find(matchesTermCode(termCode));
        const courses = (term ? term.courses : []).map(course => ({
          ...course,
          subject: subjects[course.subjectCode],
        }));

        return { termCode, note, courses } as PlannedTerm;
      });
    }),
  );

  return visibleTerms$;
};

type SimpleMap = { [name: string]: any };
type ObservableMap<T = SimpleMap> = { [K in keyof T]: Observable<T[K]> };

const forkJoinWithKeys = <T = SimpleMap>(pairs: ObservableMap<T>) => {
  const keys = Object.keys(pairs);
  const observables = keys.map(key => pairs[key]);
  return forkJoin(observables).pipe(
    map<any[], T>(values => {
      const valueMapping = {} as T;

      keys.forEach((key, index) => {
        valueMapping[key] = values[index];
      });

      return valueMapping;
    }),
  );
};

const unique = <T>(things: T[]): T[] => {
  return things.filter((thing, index, all) => all.indexOf(thing) === index);
};

const matchesTermCode = (termCode: string) => (thing: { termCode: string }) => {
  return thing.termCode === termCode;
};

const pickPrimaryDegreePlan = (plans: DegreePlan[]): DegreePlan => {
  const primary = plans.find(plan => plan.primary);
  return primary ? primary : plans[0];
};

const checkExpanded = (activeTermCodes, visibleTerms) => {
  console.log(visibleTerms);
};

const hasVisibleDegreePlan = <T extends Action>(
  pair: [T, DegreePlannerState],
): pair is [T, { visibleDegreePlan: DegreePlan } & DegreePlannerState] => {
  return pair[1].visibleDegreePlan !== undefined;
};