import { v4 as uuidv4 } from 'uuid';

import { pickRandomColor } from './colorUtils';
import fixClip, { StudioClip } from './fixClip';
import fixClipCreationIntent, {
  ClipCreationIntent,
} from './fixClipCreationIntent';
import fixInputSpec, { InputSpec } from './fixInputSpec';
import fixInstrumentSpec, { InstrumentSpec } from './fixInstrumentSpec';
import fixTrackEQ, { TrackEQ } from './fixTrackEQ';
import {
  fixArrayInPlace,
  isArray,
  isBoolean,
  isNonNegativeFiniteNumber,
  isObject,
  isString,
  isValidFiniteNumber,
  makeFixer,
} from './utils';

export type StudioTrackCore = {
  id: string;
  name: string;
  height: number;
  clips: StudioClip[];
  clipCreationIntents: ClipCreationIntent[];
};

export type StudioTakeLane = StudioTrackCore;

export type StudioTrack = StudioTrackCore & {
  solo: boolean;
  mute: boolean;
  arm: boolean;
  amplitude: number;
  balance: number;
  color: string;
  input: InputSpec | null;
  instrument: InstrumentSpec;
  soloTakeLaneId: string | null;
  takeLanes: StudioTakeLane[];
  takeLanesExpanded: boolean;
  eq: TrackEQ;
};

export type FixTrackConfig = {
  wipeQueuedGenerations?: boolean;
  keepUploads?: string[];
};

const fixName = makeFixer(isString, 'Track');
const fixHeight = makeFixer(isNonNegativeFiniteNumber, 100);
const fixSolo = makeFixer(isBoolean, false);
const fixMute = makeFixer(isBoolean, false);
const fixArm = makeFixer(isBoolean, false);
const fixAmplitude = makeFixer(isNonNegativeFiniteNumber, 1.0);
const fixBalance = makeFixer(isValidFiniteNumber, 0);
const fixTakeLanesExpanded = makeFixer(isBoolean, false);

const fixSoloTakeLaneId = (soloTakeLaneId: unknown): string | null => {
  if (soloTakeLaneId === null) {
    return null;
  }
  if (isString(soloTakeLaneId)) {
    return soloTakeLaneId;
  }
  return null;
};

const EPSILON = 0.00001;

export function fixTrackCore(
  trackCore: unknown,
  config: FixTrackConfig = {}
): StudioTrackCore | null {
  if (!isObject(trackCore)) {
    return null;
  }

  const t = trackCore as {
    id: unknown;
    name: unknown;
    height: unknown;
    clips: unknown;
    clipCreationIntents: unknown;
  };

  // Validate that we have an id
  if (!isString(t.id)) {
    return null;
  }

  t.name = fixName(t.name);
  t.height = fixHeight(t.height);

  // Fix clips array in place
  if (!isArray(t.clips)) {
    t.clips = [];
  } else {
    fixArrayInPlace(t.clips as unknown[], fixClip);
  }

  // Fix clipCreationIntents array in place
  if (!isArray(t.clipCreationIntents)) {
    t.clipCreationIntents = [];
  } else {
    const wipeQueuedGenerations = config.wipeQueuedGenerations ?? false;
    fixArrayInPlace(t.clipCreationIntents as unknown[], (intent) =>
      fixClipCreationIntent(intent, wipeQueuedGenerations)
    );
  }

  // Sort clipCreationIntents by startBeats, then endBeats
  const intents = t.clipCreationIntents as ClipCreationIntent[];
  intents.sort((a, b) => {
    let diff = (a.startBeats ?? -Infinity) - (b.startBeats ?? -Infinity);
    if (diff === 0) {
      diff = (a.endBeats ?? -Infinity) - (b.endBeats ?? -Infinity);
    }
    return diff;
  });

  // Post-process clips: deduplicate IDs, sort, close small gaps, filter by upload validity
  const clips = t.clips as StudioClip[];

  // Deduplicate clip IDs
  const seenClipIds = new Set<string>();
  for (const clip of clips) {
    if (seenClipIds.has(clip.id)) {
      console.error('duplicate clip id', clip.id);
      clip.id = uuidv4();
    }
    seenClipIds.add(clip.id);
  }

  // Sort clips by startBeats
  clips.sort((a, b) => a.startBeats - b.startBeats);

  // Close small gaps between adjacent clips
  for (let i = 0; i < clips.length; i++) {
    const clip = clips[i];
    const nextClip = clips[i + 1];
    if (nextClip && nextClip.startBeats <= clip.endBeats + EPSILON) {
      clip.endBeats = nextClip.startBeats;
    }
  }

  // Filter clips based on upload validity
  if (config.keepUploads) {
    // If keepUploads provided, keep clips with clipId OR uploadId in the list
    const keepUploadsSet = new Set(config.keepUploads);
    fixArrayInPlace(clips as unknown[], (c) => {
      const clip = c as StudioClip;
      if (clip.clipId) {
        return clip;
      }
      if (clip.uploadId && keepUploadsSet.has(clip.uploadId)) {
        return clip;
      }
      return null;
    });
  } else {
    // If keepUploads not provided, remove all clips with uploadIds but no clipId
    fixArrayInPlace(clips as unknown[], (c) => {
      const clip = c as StudioClip;
      if (clip.clipId) {
        return clip;
      }
      return null;
    });
  }

  // Remove any clips with zero, negative, or invalid duration
  fixArrayInPlace(clips as unknown[], (c) => {
    const clip = c as StudioClip;
    if (!(clip.endBeats > clip.startBeats)) {
      return null;
    }
    return clip;
  });

  return trackCore as StudioTrackCore;
}

export function fixTakeLane(
  takeLane: unknown,
  config: FixTrackConfig = {}
): StudioTakeLane | null {
  return fixTrackCore(takeLane, config);
}

export default function fixTrack(
  track: unknown,
  config: FixTrackConfig = {}
): StudioTrack | null {
  if (!isObject(track)) {
    return null;
  }

  const t = track as {
    id: unknown;
    name: unknown;
    height: unknown;
    clips: unknown;
    clipCreationIntents: unknown;
    solo: unknown;
    mute: unknown;
    arm: unknown;
    amplitude: unknown;
    balance: unknown;
    color: unknown;
    input: unknown;
    instrument: unknown;
    soloTakeLaneId: unknown;
    takeLanes: unknown;
    takeLanesExpanded: unknown;
    eq: unknown;
  };

  // Fix the core fields
  const coreFixed = fixTrackCore(track, config);
  if (coreFixed === null) {
    return null;
  }

  // Fix additional track fields
  t.solo = fixSolo(t.solo);
  t.mute = fixMute(t.mute);
  t.arm = fixArm(t.arm);
  t.amplitude = fixAmplitude(t.amplitude);
  t.balance = fixBalance(t.balance);

  t.input = fixInputSpec(t.input);
  t.instrument = fixInstrumentSpec(t.instrument);
  t.soloTakeLaneId = fixSoloTakeLaneId(t.soloTakeLaneId);

  // Fix takeLanes array in place
  if (!isArray(t.takeLanes)) {
    t.takeLanes = [];
  } else {
    fixArrayInPlace(t.takeLanes as unknown[], (lane) =>
      fixTakeLane(lane, config)
    );
  }

  t.takeLanesExpanded = fixTakeLanesExpanded(t.takeLanesExpanded);
  t.eq = fixTrackEQ(t.eq);

  // Fix track color - use first clip color or random color if missing
  const trackTyped = track as StudioTrack;
  if (!isString(t.color)) {
    const firstClip = trackTyped.clips[0];
    trackTyped.color = firstClip?.color ?? pickRandomColor(trackTyped.id);
  }

  return track as StudioTrack;
}
