import { reaction } from 'mobx';
import { v4 as uuidv4 } from 'uuid';

import { toast } from '@/components/toast/Toast';
import { isProjectsFeatureEnabled } from '@/utils/session';

import { Clip } from './clipStore';
import { RootStore, Substore } from './rootStore';
import { makeAutoObservableSubstore } from './utils';

const EDIT_STATE_STORAGE_KEY = 'edit-state';
const EDIT_STATE_STORAGE_TIMEOUT = 1000 * 60 * 15;

interface SelectionRange {
  start: number;
  end: number;
}

interface PendingScrollPositionChange {
  page: string;
  scrollPosition: number;
}

export const DEFAULT_EDIT_TOOL = 'edit_details';

export class EditStore implements Substore {
  editSessionId: string | null = null;

  isEditorOpen: boolean = false;
  isEditModeEnabled: boolean = false;
  isLoading: boolean = false;
  isScrubbing: boolean = false;
  scrubPosition: number | null = null;
  editingClipId: string | null = null;
  pendingClipData: any[] = [];
  selectionRange: SelectionRange | null = null;

  // Edit Mode
  activeEditTool: string = DEFAULT_EDIT_TOOL;
  selectionBoxDragStart: number | null = null;
  selectionBoxPositionStart: {
    startPosition: number;
    endPosition: number;
  } | null = null;
  exitEditModeCallback: (() => void) | null = null;
  pendingScrollPositionChange: PendingScrollPositionChange | null = null;
  isReviewingClips: boolean = false;
  clipsToReview: Clip[] = [];
  clipsForMerge: Clip[] = [];
  isDefaultMergeOrder: boolean = true;
  cachedLyrics: string = '';
  isLoadingAction: boolean = false;
  actionIndex: number = 0;
  isEditSessionActive: boolean = false;
  isFadeOut: boolean = false;
  fadeOutLengthMs: number = 5000;
  speedFactor: number = 1.0;
  changeTempoOnly: boolean = false;
  isCropRemove: boolean = false;

  isLocalStorageAvailable: boolean = false;

  readonly root: RootStore;
  get apiClient() {
    return this.root.apiClient;
  }
  // DO NOT ADD INIT LOGIC IN constructor. this will run on every page! considering adding any it in the component init logic instead
  constructor(root: RootStore) {
    this.root = root;
    makeAutoObservableSubstore(this);
  }

  // TODO: is this still needed in playbar?
  async cropClip(projectId?: string) {
    const cropDuration =
      (this.selectionRange?.end ||
        this.root.clips.clipById[this.editingClipId || '']?.metadata
          ?.duration ||
        0) - (this.selectionRange?.start || 0);
    this.isLoading = true;
    this.pendingClipData = [
      ...this.pendingClipData,
      {
        ...this.root.clips.clipById[this.editingClipId || ''],
        metadata: {
          ...this.root.clips.clipById[this.editingClipId || ''].metadata,
          duration: this.isCropRemove
            ? Math.max(
                (this.root.clips.clipById[this.editingClipId || '']?.metadata
                  ?.duration || 0) - cropDuration,
                0
              )
            : (this.selectionRange?.end ||
                this.root.clips.clipById[this.editingClipId || '']?.metadata
                  ?.duration ||
                0) - (this.selectionRange?.start || 0),
        },
        type: 'edit_crop',
        id: undefined,
        actionClipId: this.editingClipId,
        projectId: projectId,
      },
    ];
    const { data } = await this.apiClient.POST('/api/edit/crop/{clip_id}/', {
      params: {
        path: {
          clip_id: this.editingClipId || '',
        },
      },
      body: {
        crop_start_s: this.selectionRange?.start || 0,
        crop_end_s:
          this.selectionRange?.end ||
          this.root.clips.clipById[this.editingClipId || '']?.metadata
            ?.duration ||
          0,
        is_crop_remove: this.isCropRemove,
        ...(this.isFadeOut
          ? {
              fade_out_time: Math.max(
                (this.selectionRange?.end ||
                  this.root.clips.clipById[this.editingClipId || '']?.metadata
                    ?.duration ||
                  0) -
                  this.fadeOutLengthMs / 1000,
                0
              ),
            }
          : {}),
        edit_session_id: this.editSessionId,
      },
    });
    if (data?.action_clip_id) {
      this.pollForActionStatus(data?.action_clip_id, async () => {
        this.isLoading = false;
        const clip = await this.root.clips.loadClipById(data?.action_clip_id);
        if (clip) {
          this.root.clips.updateClips([clip as any]);
          this.root.clips.clipIds = [
            ...this.root.clips.clipIds,
            data?.action_clip_id,
          ];
          if (isProjectsFeatureEnabled(this.root.session) && projectId) {
            this.root.project.addClip(clip as any);
            if (isProjectsFeatureEnabled(this.root.session)) {
              this.root.project.addClipsToProject(
                [{ id: data?.action_clip_id } as any],
                projectId
              );
            }
          }
        }
      });
    }
  }

  async fadeClip(projectId?: string) {
    this.isLoading = true;
    this.pendingClipData = [
      ...this.pendingClipData,
      {
        ...this.root.clips.clipById[this.editingClipId || ''],
        type: 'edit_fade',
        id: undefined,
        actionClipId: this.editingClipId,
        projectId: projectId,
      },
    ];
    const { data } = await this.apiClient.POST('/api/edit/fade/{clip_id}/', {
      params: {
        path: {
          clip_id: this.editingClipId || '',
        },
      },
      body: {
        /*crop_start_s: this.selectionRange?.start || 0,
        crop_end_s:
          this.selectionRange?.end ||
          this.root.clips.clipById[this.editingClipId || '']?.metadata?.duration ||
          0,*/
        fade_out_time: Math.max(
          (this.selectionRange?.end ||
            this.root.clips.clipById[this.editingClipId || '']?.metadata
              ?.duration ||
            0) -
            this.fadeOutLengthMs / 1000,
          0
        ),
        edit_session_id: this.editSessionId,
      },
    });
    if (data?.action_clip_id) {
      this.pollForActionStatus(data?.action_clip_id, async () => {
        this.isLoading = false;
        const clip = await this.root.clips.loadClipById(data?.action_clip_id);
        if (clip) {
          this.root.clips.updateClips([clip as any]);
          this.root.clips.clipIds = [
            ...this.root.clips.clipIds,
            data?.action_clip_id,
          ];
          if (isProjectsFeatureEnabled(this.root.session)) {
            this.root.project.addClip(clip as any);
            if (projectId) {
              this.root.project.addClipsToProject(
                [{ id: data?.action_clip_id } as any],
                projectId
              );
            }
          }
        }
      });
    }
  }

  async changeClipSpeed(projectId?: string) {
    this.isLoading = true;
    this.pendingClipData = [
      ...this.pendingClipData,
      {
        ...this.root.clips.clipById[this.editingClipId || ''],
        type: 'edit_speed',
        id: undefined,
        actionClipId: this.editingClipId,
        projectId: projectId,
      },
    ];
    const { data } = await this.apiClient.POST('/api/edit/speed/{clip_id}/', {
      params: {
        path: {
          clip_id: this.editingClipId || '',
        },
      },
      body: {
        speed_factor: this.speedFactor,
        tempo_only: this.changeTempoOnly,
        change_speed_start_time: this.selectionRange?.start,
        change_speed_end_time: this.selectionRange?.end,
        edit_session_id: this.editSessionId,
      },
    });
    if (data?.action_clip_id) {
      this.pollForActionStatus(data?.action_clip_id, async () => {
        this.isLoading = false;
        const clip = await this.root.clips.loadClipById(data?.action_clip_id);
        if (clip) {
          this.root.clips.updateClips([clip as any]);
          this.root.clips.clipIds = [
            ...this.root.clips.clipIds,
            data?.action_clip_id,
          ];
          if (isProjectsFeatureEnabled(this.root.session)) {
            this.root.project.addClip(clip as any);
            if (projectId) {
              this.root.project.addClipsToProject(
                [{ id: data?.action_clip_id } as any],
                projectId
              );
            }
          }
        }
      });
    }
  }

  async pollForActionStatus(
    actionClipId: string,
    onSuccess: () => void,
    retries: number = 10
  ) {
    // set isLoading to false once reaching a terminal state
    const { data } = await this.apiClient.GET(
      '/api/edit/action/{action_clip_id}/',
      {
        params: {
          path: {
            action_clip_id: actionClipId,
          },
        },
      }
    );
    if (data?.status === 'complete') {
      onSuccess?.();
      this.pendingClipData = this.pendingClipData.filter(
        (pendingClip: any) => pendingClip.actionClipId !== this.editingClipId
      );
    } else if (data?.status === 'error' || retries <= 0) {
      toast({
        title: 'An error occurred. Please try again.',
        status: 'error',
        duration: 4000,
        isClosable: true,
      });
      this.isLoading = false;
      this.pendingClipData = this.pendingClipData.filter(
        (pendingClip: any) => pendingClip.actionClipId !== this.editingClipId
      );
    } else {
      setTimeout(
        () => this.pollForActionStatus(actionClipId, onSuccess, --retries),
        4000
      );
    }
  }

  initializeSession() {
    this.isEditSessionActive = true;
    this.editSessionId = uuidv4();
    this.actionIndex = 0;
  }

  incrementActionIndex() {
    this.actionIndex++;
  }

  resetEditState() {
    this.activeEditTool = DEFAULT_EDIT_TOOL;
    this.selectionRange = null;
    this.selectionBoxDragStart = null;
    this.selectionBoxPositionStart = null;
    this.pendingScrollPositionChange = null;
    this.isReviewingClips = false;
    this.clipsToReview = [];
    this.cachedLyrics = '';
    this.speedFactor = 1.0;
    this.changeTempoOnly = false;
  }

  checkActionButtonVisibility() {
    const selectionLengthSecs =
      (this.selectionRange?.end || 0) - (this.selectionRange?.start || 0);
    if (this.activeEditTool === 'infill' && selectionLengthSecs < 5) {
      return false;
    }
    return true;
  }

  setSelectionRange(start: number, end: number) {
    this.selectionRange = {
      start: start,
      end: end,
    };
  }

  setSelectionBoxDragStart(start: number | null) {
    this.selectionBoxDragStart = start;
  }

  setSelectionBoxPositionStart(
    start: { startPosition: number; endPosition: number } | null
  ) {
    this.selectionBoxPositionStart = start;
  }

  setIsScrubbing(isScrubbing: boolean) {
    this.isScrubbing = isScrubbing;
    if (!this.isScrubbing) {
      this.scrubPosition = null;
    }
  }

  setScrubPosition(scrubPosition: number | null) {
    this.scrubPosition = scrubPosition;
  }

  clearSelectionRange() {
    this.selectionRange = null;
  }

  setEditingClipId(clipId: string | null) {
    this.editingClipId = clipId;
  }

  setIsEditorOpen(isOpen: boolean) {
    this.isEditorOpen = isOpen;
  }

  enableEditMode(isEnabled: boolean) {
    this.isEditModeEnabled = isEnabled;
  }

  isSelectionRangeActive(): boolean {
    return (
      this.selectionRange?.start !== undefined &&
      this.selectionRange?.end !== undefined
    );
  }

  isOutsideOfSelectionRange(time: number): boolean {
    if (!this.isSelectionRangeActive) {
      return false;
    }
    return (
      time < (this.selectionRange?.start || 0) ||
      time > (this.selectionRange?.end || 0)
    );
  }

  setEditTool(toolId: string) {
    this.activeEditTool = toolId;
  }

  setExitEditModeCallback(callback: () => void) {
    this.exitEditModeCallback = callback;
  }

  setPendingScrollPositionChange(
    scrollPositionChange: PendingScrollPositionChange | null
  ) {
    this.pendingScrollPositionChange = scrollPositionChange;
  }

  setClipsToReview(clipsToReview: Clip[]) {
    this.clipsToReview = clipsToReview;
  }

  setCachedLyrics(lyrics: string) {
    this.cachedLyrics = lyrics;
  }

  setIsFadeOut(isFadeOut: boolean) {
    this.isFadeOut = isFadeOut;
  }

  storageObject = () => {
    return {
      selectionRange: this.selectionRange,
      editingClipId: this.editingClipId,
      editSessionId: this.editSessionId,
      pendingScrollPositionChange: this.pendingScrollPositionChange,
      isReviewingClips: this.isReviewingClips,
      clipsToReview: this.clipsToReview,
      cachedLyrics: this.cachedLyrics,
      activeEditTool: this.activeEditTool,
    };
  };

  initLocalStorageUpdate = () => {
    reaction(this.storageObject, () => {
      if (this.root.isLocalStorageAvailable) {
        const objToStore = {
          ...this.storageObject(),
          lastUpdatedAt: Date.now(),
        };
        const storageStr = JSON.stringify(objToStore);
        localStorage.setItem(EDIT_STATE_STORAGE_KEY, storageStr);
      }
    });
  };

  loadFromLocalStorage = () => {
    if (this.root.isLocalStorageAvailable) {
      const storageStr = localStorage.getItem(EDIT_STATE_STORAGE_KEY);
      try {
        const stateJson = JSON.parse(storageStr || '');
        if (
          stateJson &&
          Date.now() - stateJson.lastUpdatedAt < EDIT_STATE_STORAGE_TIMEOUT
        ) {
          this.selectionRange = stateJson.selectionRange;
          this.editingClipId = stateJson.editingClipId;
          this.editSessionId = stateJson.editSessionId;
          this.pendingScrollPositionChange =
            stateJson.pendingScrollPositionChange;
          this.isReviewingClips = stateJson.isReviewingClips;
          this.clipsToReview = stateJson.clipsToReview;
          this.cachedLyrics = stateJson.cachedLyrics;
          this.activeEditTool = stateJson.activeEditTool;
        }
      } catch (e) {
        console.log(e);
      }
    }
  };
}
