import { getWarpBeatsFromSeconds } from '@suno/studiokit/warpUtils';
import { setIn } from 'lodash-redux-immutability';
import { v4 as uuidv4 } from 'uuid';

import { Clip } from '@/state/clipStore';

import { EditTiming } from '../edit2025/types';
import clearTimeFromTrack from './actionHelpers/clearTimeFromTrack';
import moveTrackContentAfter from './actionHelpers/moveTrackContentAfter';
import { updateTrackOrTakeLane } from './actions/updateTracksAndTakeLanes';
import createTrack from './createTrack';
import { describeClipReplacement } from './describeClipAdjustments';
import findAlignmentMatches, {
  AlignmentMatch,
  pruneAlignmentMatches,
} from './findAlignmentMatches';
import getAbsolutePoint from './getAbsolutePoint';
import { alignEndGridOffset, alignStartGridOffset } from './getGridOffsetBeats';
import getProjectTimingForClip from './getProjectTimingForClip';
import insertClipsToList from './insertClipsToList';
import {
  getDerivedTiming,
  getFocusedTrackId,
  getStudioClips,
  getTakeLaneParentsByTrackId,
} from './selectors';
import { AFTER_LAST_TRACK, ClipArrangementPackage } from './types';
import { StudioClip, StudioProjectState } from './types';
import { alignmentSpecsMatch } from './useAlignedClips';

export type ClipInsertionSpec = {
  replacementStartBeats: number;
  replacementEndBeats: number;
  trackId: string | null;
  studioClip: StudioClip | null;
  clip: Clip;
};

export const getAlternateInsertionSpec = (
  timing: EditTiming,
  arrangementPackage: ClipArrangementPackage,
  focusedClipArrangementPackage: ClipArrangementPackage | null,
  focusedTrackId: string | null,
  canMoveOtherClips: boolean = true
): ClipInsertionSpec | null => {
  if (
    focusedTrackId &&
    focusedClipArrangementPackage &&
    alignmentSpecsMatch(
      focusedClipArrangementPackage.alignmentSpec,
      arrangementPackage.alignmentSpec
    )
  ) {
    const { resultingStudioClip } = describeClipReplacement(
      timing,
      focusedClipArrangementPackage.studioClip,
      focusedClipArrangementPackage.clip,
      arrangementPackage.studioClip,
      arrangementPackage.clip
    );

    if (!canMoveOtherClips) {
      const originalStartBeats = resultingStudioClip.startBeats;
      resultingStudioClip.startBeats = Math.max(
        getAbsolutePoint(
          timing,
          resultingStudioClip,
          getWarpBeatsFromSeconds(resultingStudioClip.warp, 0),
          true
        ),
        focusedClipArrangementPackage.studioClip.startBeats
      );
      resultingStudioClip.readStartBeats +=
        resultingStudioClip.startBeats - originalStartBeats;
      resultingStudioClip.endBeats = Math.min(
        getAbsolutePoint(
          timing,
          resultingStudioClip,
          getWarpBeatsFromSeconds(
            resultingStudioClip.warp,
            arrangementPackage.clip.metadata.duration ?? Infinity // fall back to current clip duration. this shouldn't happen tho.
          ),
          true
        ),
        focusedClipArrangementPackage.studioClip.endBeats
      );
    }

    return {
      replacementStartBeats: resultingStudioClip.startBeats,
      replacementEndBeats: resultingStudioClip.endBeats,
      trackId: focusedTrackId,
      studioClip: {
        ...arrangementPackage.studioClip,
        transposition: focusedClipArrangementPackage.studioClip.transposition,
        startBeats: resultingStudioClip.startBeats,
        endBeats: resultingStudioClip.endBeats,
        readStartBeats: resultingStudioClip.readStartBeats,
      },
      clip: focusedClipArrangementPackage.clip,
    };
  }
  return null;
};

// Given a clip arrangement package, where can the clip be inserted to align with any associated clips?
export const getTimelineInsertionSpecs = (
  arrangementPackage: ClipArrangementPackage,
  focusedClipArrangementPackage: ClipArrangementPackage | null,
  state: StudioProjectState,
  keepLocalGridOffset: boolean = true,
  canMoveOtherClips: boolean = true
): ClipInsertionSpec[] => {
  let result: ClipInsertionSpec[] = [];

  if (
    !arrangementPackage.alignmentSpec ||
    !arrangementPackage.clip.metadata.duration
  ) {
    return result;
  }

  const timing = getDerivedTiming(state);

  const alternateInsertionSpec = getAlternateInsertionSpec(
    timing,
    arrangementPackage,
    focusedClipArrangementPackage,
    getFocusedTrackId(state),
    canMoveOtherClips
  );

  if (alternateInsertionSpec) {
    result.push(alternateInsertionSpec);
  }

  state.tracks.forEach((track) => {
    const rawMatches = findAlignmentMatches(
      timing,
      arrangementPackage.alignmentSpec!,
      track.clips
    );

    const prunedMatches = pruneAlignmentMatches(rawMatches);

    result.push(
      ...prunedMatches.map((m) =>
        createInsertionSpec(
          m,
          arrangementPackage,
          track.id,
          keepLocalGridOffset
        )
      )
    );
  });

  if (!canMoveOtherClips) {
    result = result.map((s) => getUnchangedDurationInsertionSpec(s));
  } else if (!arrangementPackage.alignmentSpec.futureClipId) {
    result = result.map((s) => ({
      ...s,
      replacementEndBeats: Infinity,
    }));
  }

  // TODO: stem clips that go to new tracks.
  return result;
};

export const getClosestInsertionSpec = (
  insertionSpecs: ClipInsertionSpec[],
  trackId: string | null,
  beats: number
): ClipInsertionSpec | null => {
  const specsOnTrack = insertionSpecs.filter((s) => s.trackId === trackId);

  const specsContainingBeats = specsOnTrack.filter((s) => {
    return s.replacementStartBeats <= beats && s.replacementEndBeats >= beats;
  });

  const specsNotContainingBeats = specsOnTrack.filter((s) => {
    return s.replacementStartBeats > beats || s.replacementEndBeats < beats;
  });

  const sortSpecs = (a: ClipInsertionSpec, b: ClipInsertionSpec) => {
    const aCenter = (a.replacementStartBeats + a.replacementEndBeats) / 2;
    const bCenter = (b.replacementStartBeats + b.replacementEndBeats) / 2;
    const aDistance = Math.abs(aCenter - beats);
    const bDistance = Math.abs(bCenter - beats);
    return aDistance - bDistance;
  };

  const insertionSpecMatch = [
    ...specsContainingBeats.sort(sortSpecs),
    ...specsNotContainingBeats.sort(sortSpecs),
  ][0];

  return insertionSpecMatch || null;
};

const createInsertionSpec = (
  match: AlignmentMatch,
  arrangementPackage: ClipArrangementPackage,
  trackId: string,
  keepLocalGridOffset: boolean = true
): ClipInsertionSpec => {
  const clipDurationBeats =
    arrangementPackage.studioClip.endBeats -
    arrangementPackage.studioClip.startBeats;

  const replacementStartBeats =
    match.historyClipGridOffset === null
      ? match.replacementEndBeats - clipDurationBeats
      : match.replacementStartBeats;

  const replacementEndBeats =
    match.futureClipGridOffset === null
      ? match.replacementStartBeats + clipDurationBeats
      : match.replacementEndBeats;

  let studioClip = {
    ...arrangementPackage.studioClip,
    startBeats: match.replacementStartBeats,
    endBeats:
      match.replacementStartBeats +
      (arrangementPackage.studioClip.endBeats -
        arrangementPackage.studioClip.startBeats),
  };

  if (keepLocalGridOffset) {
    if (match.historyClipGridOffset !== null) {
      studioClip = alignStartGridOffset(
        match.historyClipGridOffset,
        studioClip,
        arrangementPackage.clip
      );
    }

    if (match.futureClipGridOffset !== null) {
      studioClip = alignEndGridOffset(
        match,
        studioClip,
        arrangementPackage.clip
      );
    }
  }

  return {
    replacementStartBeats,
    replacementEndBeats,
    trackId,
    clip: arrangementPackage.clip,
    studioClip,
  };
};

export const applyKnownTrackInsertionSpec = (
  spec: ClipInsertionSpec & { studioClip: StudioClip; trackId: string },
  exactPlacement: boolean = false
) => {
  const replacedDuration =
    spec.replacementEndBeats - spec.replacementStartBeats;
  const arrangedDuration =
    spec.studioClip.endBeats - spec.studioClip.startBeats;
  const durationChangeBeats = arrangedDuration - replacedDuration;

  return (state: StudioProjectState): StudioProjectState => {
    const timing = getDerivedTiming(state);
    let movedSpec = spec;
    if (getStudioClips(state).length === 0 && !exactPlacement) {
      const offset = getAbsolutePoint(timing, spec.studioClip, 0, true);
      movedSpec = {
        ...spec,
        studioClip: {
          ...spec.studioClip,
          startBeats: spec.studioClip.startBeats - offset,
          endBeats: spec.studioClip.endBeats - offset,
        },
      };
    }
    return updateTrackOrTakeLane(movedSpec.trackId, (track) => {
      track = clearTimeFromTrack(
        timing,
        movedSpec.replacementStartBeats,
        movedSpec.replacementEndBeats,
        track
      );

      if (
        durationChangeBeats !== 0 &&
        Number.isFinite(movedSpec.replacementEndBeats)
      ) {
        track = moveTrackContentAfter(
          timing,
          track,
          movedSpec.replacementEndBeats,
          durationChangeBeats
        );
      }

      const parentTrack = getTakeLaneParentsByTrackId(state)[movedSpec.trackId];

      track = {
        ...track,
        clips: insertClipsToList(
          timing,
          [{ ...movedSpec.studioClip, color: parentTrack.color }],
          track.clips
        ),
      };

      return track;
    })({
      ...state,
      selection: {
        ...state.selection,
        focusedTrackId: movedSpec.trackId,
        focusedArea: 'timeline',
        focusBeats: movedSpec.studioClip.startBeats,
        anchorBeats: movedSpec.studioClip.endBeats,
        trackIds: [spec.trackId],
      },
    });
  };
};

export const applyNewTrackInsertionSpec = (
  spec: ClipInsertionSpec & { studioClip: StudioClip },
  exactPlacement: boolean = false
) => {
  const newTrack = createTrack();
  return (state: StudioProjectState): StudioProjectState => {
    let movedSpec = spec;
    if (getStudioClips(state).length === 0 && !exactPlacement) {
      const timing = getDerivedTiming(state);
      const offset = getAbsolutePoint(timing, spec.studioClip, 0, true);
      movedSpec = {
        ...spec,
        studioClip: {
          ...spec.studioClip,
          startBeats: spec.studioClip.startBeats - offset,
          endBeats: spec.studioClip.endBeats - offset,
        },
      };
    }
    const newState: StudioProjectState = {
      ...state,
      tracks: [
        ...state.tracks,
        {
          ...newTrack,
          name:
            movedSpec.studioClip.name === 'New Clip'
              ? 'New Track'
              : movedSpec.studioClip.name,
          clips: [{ ...movedSpec.studioClip, color: newTrack.color }],
        },
      ],
      selection: {
        ...state.selection,
        focusedTrackId: newTrack.id,
        focusedArea: 'timeline',
        focusBeats: movedSpec.studioClip.startBeats,
        anchorBeats: movedSpec.studioClip.endBeats,
        trackIds: [newTrack.id],
      },
    };

    if (newState.tracks.length === 1) {
      newState.timing = getProjectTimingForClip(
        movedSpec.studioClip,
        newTrack.id
      );
    }

    return newState;
  };
};

export const getUnchangedDurationInsertionSpec = (
  insertionSpec: ClipInsertionSpec
): ClipInsertionSpec => {
  return {
    ...insertionSpec,
    studioClip: insertionSpec.studioClip
      ? {
          ...insertionSpec.studioClip,
          endBeats:
            insertionSpec.studioClip.startBeats +
            (insertionSpec.replacementEndBeats -
              insertionSpec.replacementStartBeats),
        }
      : insertionSpec.studioClip,
  };
};

export const applyInsertionSpec = (
  spec: ClipInsertionSpec,
  exactPlacement: boolean = false
) => {
  if (!spec.studioClip) {
    return (state: StudioProjectState) => state;
  }

  if (!spec.trackId || spec.trackId === AFTER_LAST_TRACK) {
    return applyNewTrackInsertionSpec(
      setIn(
        spec as ClipInsertionSpec & { studioClip: StudioClip },
        ['studioClip', 'id'],
        uuidv4()
      ),
      exactPlacement
    );
  }

  return applyKnownTrackInsertionSpec(
    setIn(
      spec as ClipInsertionSpec & { studioClip: StudioClip; trackId: string },
      ['studioClip', 'id'],
      uuidv4()
    ),
    exactPlacement
  );
};
