import { v4 as uuidv4 } from 'uuid';

import { pickRandomColor } from '../edit2025/getSections';
import {
  SPEED_MAXIMUM,
  SPEED_MINIMUM,
  TRANSPOSE_MAXIMUM_SEMITONES,
  TRANSPOSE_MINIMUM_SEMITONES,
} from '../focusedObjectPanel/StudioClipSettingsSubsection';
import {
  DEFAULT_EQ_BANDS,
  DISABLED_EQ_BANDS,
} from '../focusedObjectPanel/TrackEQSubsection/eqPresets';
import cleanState from './cleanState';
import insertClipsToList from './insertClipsToList';
import {
  SerializedProjectState,
  maybeDeserializeProjectStateInPlace,
} from './projectStateSerialization';
import { getDerivedTiming, getTracksAndTakeLanes } from './selectors';
import { StudioClip, StudioProjectState, TrackEQ } from './types';
import { ffmpegBuffersByUploadId } from './uploadedClipCache';
import { deduplicateMarkersArray } from './warpMarkersRegistry';

const EPSILON = 0.00001;

const fixStateInPlace = (
  maybeSerializedState: unknown,
  mode: 'edit' | 'studio',
  wipeQueuedGenerationClipCreationIntents: boolean = false
): StudioProjectState => {
  if (
    typeof maybeSerializedState !== 'object' ||
    maybeSerializedState === null
  ) {
    throw new Error('maybeSerializedState must be an object');
  }
  const state = maybeDeserializeProjectStateInPlace(
    maybeSerializedState as StudioProjectState | SerializedProjectState
  );

  if (state.amplitude === undefined) {
    state.amplitude = 1.0;
  }

  if (!state.lyricsCorrectionsByClipId) {
    state.lyricsCorrectionsByClipId = {};
  }
  if (!state.metronome) {
    state.metronome = {
      enabled: false,
      amplitude: 1.0,
    };
  }
  if (state.selection.anchorBeats === undefined) {
    state.selection.anchorBeats = (state.selection as any).startBeats || 0;
  }
  if (state.selection.focusBeats === undefined) {
    state.selection.focusBeats = (state.selection as any).endBeats || 0;
  }
  const seenClipIds: string[] = [];
  state.tracks.forEach((t) => {
    if (t.takeLanesExpanded === undefined) {
      t.takeLanesExpanded = false;
    }
    if (t.takeLanes === undefined) {
      t.takeLanes = [];
    }
    if (t.soloTakeLaneId === undefined) {
      t.soloTakeLaneId = null;
    }
    if (!(t.instrument as any)) {
      t.instrument = { type: 'song' };
    }
  });

  if (!state.loop) {
    state.loop = {
      enabled: false,
      startBeats: 0,
      endBeats: 32,
    };
  }

  if (mode === 'edit') {
    if (state.tracks.length > 1) {
      state.tracks = state.tracks.slice(0, 1);
    }
    state.tracks.forEach((t) => {
      t.amplitude = 1.0;
      t.balance = 0;
      t.mute = false;
      t.solo = false;
    });
  }

  // First pass: collect all warp markers for bulk deduplication
  const allWarpMarkers: StudioClip['warp']['markers'][] = [];
  const tracksAndTakeLanes = getTracksAndTakeLanes(state);
  tracksAndTakeLanes.forEach((t) => {
    t.clips.sort((a, b) => a.startBeats - b.startBeats);

    t.clips.forEach((c, i) => {
      if (c.streaming === undefined) c.streaming = false;
      if (c.mute === undefined) c.mute = false;
      if (seenClipIds.includes(c.id)) {
        console.error('duplicate clip id', c.id);
        c.id = uuidv4();
      }
      seenClipIds.push(c.id);
      if (c.transposition === undefined) {
        c.transposition = 0;
      }
      c.transposition = Math.max(
        TRANSPOSE_MINIMUM_SEMITONES,
        Math.min(TRANSPOSE_MAXIMUM_SEMITONES, c.transposition)
      );
      if (c.warp.speed === undefined) {
        c.warp.speed = 1.0;
      }
      c.warp.speed = Math.max(
        SPEED_MINIMUM,
        Math.min(SPEED_MAXIMUM, c.warp.speed)
      );
      const nextClip = t.clips[i + 1];
      if (nextClip?.startBeats <= c.endBeats + EPSILON) {
        c.endBeats = nextClip.startBeats;
      }
    });

    // we briefly had a concept of resolved and unresolved clips, but decided it was preferable to
    // only have resolved clips and use ClipCreationIntents in situations where a clip's audio does not yet exist.
    // now, we have clips that reference their content via an `uploadId` while their upload is in flight.
    t.clips = t.clips.filter(
      (c) =>
        (c as any).resolved !== false &&
        (c.clipId || (c.uploadId && ffmpegBuffersByUploadId[c.uploadId]))
    );

    t.clips = t.clips.filter(
      (c) =>
        c.readStartBeats !== undefined &&
        !Number.isNaN(c.readStartBeats) &&
        c.loop.startBeats !== undefined &&
        !Number.isNaN(c.loop.startBeats) &&
        c.loop.endBeats !== undefined &&
        !Number.isNaN(c.loop.endBeats)
    );

    if (!(t as any).clipCreationIntents) {
      (t as any).clipCreationIntents = [];
    }

    t.clipCreationIntents.forEach((c) => {
      if (c.startTrimmedBeats === undefined) {
        c.startTrimmedBeats = 0;
      }
      if (c.endTrimmedBeats === undefined) {
        c.endTrimmedBeats = 0;
      }
      if (c.queuedGenerationIds === undefined) {
        c.queuedGenerationIds = [];
        if ((c as any).queuedGenerationId) {
          c.queuedGenerationIds.push((c as any).queuedGenerationId);
        }
      }
    });

    t.clipCreationIntents = t.clipCreationIntents
      .filter((c) => {
        const hasClipId = c.possibleClipIds.length > 0;
        const hasQueuedGeneration =
          !!(c as any).queuedGenerationId || c.queuedGenerationIds.length > 0;

        if (
          !hasClipId &&
          (!hasQueuedGeneration ||
            (wipeQueuedGenerationClipCreationIntents && hasQueuedGeneration))
        ) {
          return false;
        }

        return true;
      })
      .map((c) => {
        if (c.queuedGenerationIds.length > 0) {
          return {
            ...c,
            queuedGenerationIds: [],
          };
        } else {
          return c;
        }
      });

    t.clips.forEach((c) => {
      if (c.warp && c.warp.markers) {
        allWarpMarkers.push(c.warp.markers);
      }
    });
  });

  // Bulk deduplicate all markers to populate the global registry
  const { mapping: markersMapping } = deduplicateMarkersArray(allWarpMarkers);

  state.tracks.forEach((t) => {
    if ((t as any).icon) {
      delete (t as any).icon;
    }
    if (t.input === undefined) {
      t.input = null;
    }
    if (t.input && t.input.channel === undefined) {
      t.input.channel = 0;
    }
    if (t.arm === undefined) {
      t.arm = false;
    }
    if (!t.color) {
      t.color = t.clips[0]?.color ?? pickRandomColor(t.id);
    }

    // Migrate old EQ structure to new consolidated structure
    const tAny = t as any;

    // Check if we need to migrate from old structure
    if (
      !t.eq ||
      Array.isArray(tAny.eq) ||
      tAny.highPassFilter ||
      tAny.lowPassFilter
    ) {
      // Old format or missing - migrate to new format
      const oldEQ = Array.isArray(tAny.eq) ? (tAny.eq as any[]) : undefined;
      const oldHighPass = tAny.highPassFilter as any;
      const oldLowPass = tAny.lowPassFilter as any;

      const newEQ: TrackEQ = {
        enabled: true,
        // Band 1: Low shelf by default, or high-pass if old filter was enabled
        band1: oldHighPass?.enabled
          ? {
              type: 'highpass',
              enabled: true,
              frequency: oldHighPass.frequency,
              gain: 0,
              q: oldHighPass.q,
            }
          : DISABLED_EQ_BANDS[0],
        // Bands 2-3: Old bands 1-2 converted to peaking
        band2: oldEQ?.[0]
          ? {
              type: 'peaking',
              enabled: true,
              frequency: oldEQ[0].frequency,
              gain: oldEQ[0].gain,
              q: oldEQ[0].q,
            }
          : DEFAULT_EQ_BANDS[1],
        band3: oldEQ?.[1]
          ? {
              type: 'peaking',
              enabled: true,
              frequency: oldEQ[1].frequency,
              gain: oldEQ[1].gain,
              q: oldEQ[1].q,
            }
          : DEFAULT_EQ_BANDS[2],
        // Bands 4-5: Old bands 2-3 converted to peaking
        band4: oldEQ?.[2]
          ? {
              type: 'peaking',
              enabled: true,
              frequency: oldEQ[2].frequency,
              gain: oldEQ[2].gain,
              q: oldEQ[2].q,
            }
          : DEFAULT_EQ_BANDS[3],
        band5: oldEQ?.[3]
          ? {
              type: 'peaking',
              enabled: true,
              frequency: oldEQ[3].frequency,
              gain: oldEQ[3].gain,
              q: oldEQ[3].q,
            }
          : DEFAULT_EQ_BANDS[4],
        // Band 6: High shelf by default, or low-pass if old filter was enabled
        band6: oldLowPass?.enabled
          ? {
              type: 'lowpass',
              enabled: true,
              frequency: oldLowPass.frequency,
              gain: 0,
              q: oldLowPass.q,
            }
          : DISABLED_EQ_BANDS[5],
      };

      t.eq = newEQ;

      // Clean up old properties
      delete tAny.highPassFilter;
      delete tAny.lowPassFilter;
    }

    // Migrate from 4-band to 6-band structure (intermediate migration)
    if (t.eq && !(t.eq as any).band5) {
      const oldEQ = t.eq as any;
      const newEQ: TrackEQ = {
        enabled: oldEQ.enabled ?? true,
        // Keep band 1 (high-pass) if it exists
        band1: oldEQ.band1 ?? DISABLED_EQ_BANDS[0],
        // Add band 2 (low shelf)
        band2: oldEQ.band2 ?? DEFAULT_EQ_BANDS[1],
        // Keep bands 3-4 (peaking)
        band3: oldEQ.band3 ?? DEFAULT_EQ_BANDS[2],
        band4: oldEQ.band4 ?? DEFAULT_EQ_BANDS[3],
        // Add band 5 (high shelf)
        band5: oldEQ.band5 ?? DEFAULT_EQ_BANDS[4],
        // Add band 6 (low-pass)
        band6: oldEQ.band6 ?? DISABLED_EQ_BANDS[5],
      };
      t.eq = newEQ;
    }

    // Ensure all bands have the required type and enabled fields
    if (t.eq) {
      const bandKeys: Array<
        'band1' | 'band2' | 'band3' | 'band4' | 'band5' | 'band6'
      > = ['band1', 'band2', 'band3', 'band4', 'band5', 'band6'];
      bandKeys.forEach((key, index) => {
        const band = t.eq[key] as any;
        if (!band.type) {
          // Assign default type if missing
          band.type = DEFAULT_EQ_BANDS[index].type;
        }
        if (band.enabled === undefined) {
          // Default to enabled
          if (key === 'band1' || key === 'band6') {
            band.enabled = false;
          } else {
            band.enabled = true;
          }
        }
      });
    }
  });

  tracksAndTakeLanes.forEach((t) => {
    let newClips: StudioClip[] = [];

    t.clips.forEach((c) => {
      if (c.warp.awaitingAnalysis === undefined) {
        c.warp.awaitingAnalysis = false;
      }
      if (c.amplitude === undefined) {
        c.amplitude = 1.0;
      }

      // Deduplicate warp markers for existing clips
      if (c.warp && c.warp.markers) {
        const canonicalMarkers = markersMapping.get(c.warp.markers);
        if (canonicalMarkers) {
          c.warp.markers = canonicalMarkers;
        }
      }

      if (c.endBeats <= c.startBeats) {
        console.error('found clip with end beats after start beats', c);
      } else {
        newClips = insertClipsToList(getDerivedTiming(state), [c], newClips);
      }
    });
    t.clips = newClips;
  });

  if (state.songFadeInBeats === undefined) {
    state.songFadeInBeats = 0;
  }
  if (state.songFadeOutBeats === undefined) {
    state.songFadeOutBeats = 0;
  }

  return cleanState(state);
};

export default fixStateInPlace;
