import { useQuery } from '@tanstack/react-query';
import { uniqBy } from 'lodash-es';
import { useEffect, useMemo, useState } from 'react';

import { useStores } from '@/app/(root)/AppProviders';
import { useContextSelector } from '@/hooks/useContextSelector';
import { AlignmentSpec } from '@/hooks/useGenerate';
import { ApiClient } from '@/lib/apiClient';
import { Clip } from '@/state/clipStore';

import { BooleanFilter } from '../clipBrowser/types';
import { EditTiming } from '../edit2025/types';
import StudioContext, {
  hasStemConditionTask,
  hasStemTask,
} from './StudioContext';
import { getClipContentSeconds } from './getClipContentSeconds';
import queryClient from './queryClient';
import {
  getDerivedTiming,
  getFocusedTrack,
  getSelectionEndBeats,
  getSelectionStartBeats,
} from './selectors';
import { StudioClip, StudioProjectState } from './types';
import useFocusedClip from './useFocusedClip';

export interface AlignedClip {
  clip: Clip;
  historyClipId?: string;
  historyEndSeconds?: number;
  clipStartSeconds?: number;
  futureClipId?: string;
  clipEndSeconds?: number;
  futureStartSeconds?: number;
}

interface AlignedClipsResponse {
  alignments: AlignedClip[];
}

export const isHistoryOnly = (alignmentSpec: AlignmentSpec) => {
  return (
    alignmentSpec.historyClipId != null &&
    alignmentSpec.historyEndSeconds != null &&
    (alignmentSpec.futureClipId == null ||
      alignmentSpec.futureStartSeconds == null)
  );
};

export const isFutureOnly = (alignmentSpec: AlignmentSpec) => {
  return (
    alignmentSpec.futureClipId != null &&
    alignmentSpec.futureStartSeconds != null &&
    (alignmentSpec.historyClipId == null ||
      alignmentSpec.historyEndSeconds == null)
  );
};

export const isHistoryAndFuture = (alignmentSpec: AlignmentSpec) => {
  return (
    alignmentSpec.historyClipId != null &&
    alignmentSpec.historyEndSeconds != null &&
    alignmentSpec.futureClipId != null &&
    alignmentSpec.futureStartSeconds != null
  );
};

export const getAlignmentSpec = (
  timing: EditTiming,
  clipList: StudioClip[],
  startBeats: number,
  endBeats: number
): AlignmentSpec | undefined => {
  const historyClip = clipList.find(
    (c) => c.startBeats < startBeats && c.endBeats >= startBeats
  );
  const futureClip = clipList.find(
    (c) => c.startBeats <= endBeats && c.endBeats > endBeats
  );

  const alignmentSpec: {
    historyClipId?: string;
    historyEndSeconds?: number;
    futureClipId?: string;
    futureStartSeconds?: number;
  } = {};

  if (historyClip && historyClip.clipId) {
    alignmentSpec.historyClipId = historyClip.clipId;
    alignmentSpec.historyEndSeconds = getClipContentSeconds(
      historyClip,
      timing,
      startBeats
    )[1];
  }

  if (futureClip && futureClip.clipId) {
    alignmentSpec.futureClipId = futureClip.clipId;
    alignmentSpec.futureStartSeconds = getClipContentSeconds(
      futureClip,
      timing,
      endBeats
    )[0];
  }

  if (Object.keys(alignmentSpec).length === 0) {
    return undefined;
  }

  return alignmentSpec;
};

const getAlignmentRange = (
  timing: EditTiming,
  clipList: StudioClip[],
  startBeats: number,
  endBeats: number
):
  | {
      historyClipId?: string;
      historyEndSecondsGTE?: number;
      historyEndSecondsLTE?: number;
      futureClipId?: string;
      futureStartSecondsGTE?: number;
      futureStartSecondsLTE?: number;
    }
  | undefined => {
  const historyClip = clipList.find(
    (c) => c.startBeats < startBeats && c.endBeats >= startBeats
  );
  const futureClip = clipList.find(
    (c) => c.startBeats <= endBeats && c.endBeats > endBeats
  );

  const alignmentRange: {
    historyClipId?: string;
    historyEndSecondsGTE?: number;
    historyEndSecondsLTE?: number;
    futureClipId?: string;
    futureStartSecondsGTE?: number;
    futureStartSecondsLTE?: number;
  } = {};

  if (historyClip && historyClip.clipId) {
    alignmentRange.historyClipId = historyClip.clipId;
    alignmentRange.historyEndSecondsGTE =
      getClipContentSeconds(
        historyClip,
        timing,
        Math.max(historyClip.startBeats, startBeats - 4),
        true
      )[0] - 0.1;
    alignmentRange.historyEndSecondsLTE =
      getClipContentSeconds(
        historyClip,
        timing,
        Math.min(historyClip.endBeats, endBeats),
        true
      )[1] + 0.1;
  }

  if (futureClip && futureClip.clipId) {
    alignmentRange.futureClipId = futureClip.clipId;
    alignmentRange.futureStartSecondsGTE =
      getClipContentSeconds(
        futureClip,
        timing,
        Math.max(futureClip.startBeats, startBeats),
        true
      )[0] - 0.1;
    alignmentRange.futureStartSecondsLTE =
      getClipContentSeconds(
        futureClip,
        timing,
        Math.min(futureClip.endBeats, endBeats + 4),
        true
      )[1] + 0.1;
  }

  if (Object.keys(alignmentRange).length === 0) {
    return undefined;
  }

  return alignmentRange;
};

const getStateAlignmentRange = (state: StudioProjectState) => {
  const startBeats = getSelectionStartBeats(state);
  const endBeats = getSelectionEndBeats(state);
  const focusedTrackClips =
    getFocusedTrack(state)?.clips ?? EMPTY_STUDIO_CLIP_ARRAY;
  const timing = getDerivedTiming(state);
  return getAlignmentRange(timing, focusedTrackClips, startBeats, endBeats);
};

const alignmentSpecsAreComparable = (
  specA: AlignmentSpec | undefined,
  specB: AlignmentSpec | undefined
) => {
  if (!specA || !specB) return false;
  if (specA.historyClipId !== specB.historyClipId) return false;
  if (specA.futureClipId !== specB.futureClipId) return false;
  if (specA.historyClipId && specA.historyEndSeconds == null) return false;
  if (specA.futureClipId && specA.futureStartSeconds == null) return false;
  if (specB.historyClipId && specB.historyEndSeconds == null) return false;
  if (specB.futureClipId && specB.futureStartSeconds == null) return false;

  return true;
};

export const alignmentSpecsMatch = (
  specA: AlignmentSpec | undefined,
  specB: AlignmentSpec | undefined,
  windowSize: number = 0.08
) => {
  const halfWindow = windowSize / 2;

  if (!alignmentSpecsAreComparable(specA, specB)) {
    return false;
  }

  if (specA!.historyEndSeconds != null && specB!.historyEndSeconds != null) {
    if (
      Math.abs(specA!.historyEndSeconds - specB!.historyEndSeconds) > halfWindow
    ) {
      return false;
    }
  }

  if (specA!.futureStartSeconds != null && specB!.futureStartSeconds != null) {
    if (
      Math.abs(specA!.futureStartSeconds - specB!.futureStartSeconds) >
      halfWindow
    ) {
      return false;
    }
  }

  return true;
};

export const getClipAlignmentSpecFromHistory = (
  clip: Clip
): AlignmentSpec | undefined => {
  const historyObject = clip.metadata.history?.slice(-1)[0] as any;

  const result: AlignmentSpec = {};

  if (
    clip.metadata.override_history_clip_id &&
    clip.metadata.override_history_end_seconds != null
  ) {
    result.historyClipId = clip.metadata.override_history_clip_id;
    result.historyEndSeconds = clip.metadata.override_history_end_seconds;
  } else if (
    historyObject?.id &&
    (historyObject.infill_start_s != null || historyObject.continue_at != null)
  ) {
    result.historyClipId = historyObject.id;
    result.historyEndSeconds = (historyObject.infill_start_s ??
      historyObject.continue_at)!;
  }

  if (
    clip.metadata.override_future_clip_id &&
    clip.metadata.override_future_start_seconds != null
  ) {
    result.futureClipId = clip.metadata.override_future_clip_id;
    result.futureStartSeconds = clip.metadata.override_future_start_seconds;
  } else if (historyObject?.id && historyObject.infill_end_s != null) {
    result.futureClipId = historyObject.id;
    result.futureStartSeconds = historyObject.infill_end_s;
  }

  if (Object.keys(result).length === 0) {
    return undefined;
  }

  return result;
};

const EMPTY_STUDIO_CLIP_ARRAY: StudioClip[] = [];

export const useAlignmentSpec = () => {
  const startBeats = useContextSelector(StudioContext, (context) =>
    getSelectionStartBeats(context.state)
  );
  const endBeats = useContextSelector(StudioContext, (context) =>
    getSelectionEndBeats(context.state)
  );
  const focusedTrackClips = useContextSelector(
    StudioContext,
    (context) =>
      getFocusedTrack(context.state)?.clips ?? EMPTY_STUDIO_CLIP_ARRAY
  );
  const timing = useContextSelector(StudioContext, (context) =>
    getDerivedTiming(context.state)
  );
  return useMemo(
    () => getAlignmentSpec(timing, focusedTrackClips, startBeats, endBeats),
    [timing, startBeats, endBeats, focusedTrackClips]
  );
};

export const getAlignmentCacheKey = (
  alignmentSpec: AlignmentSpec | undefined
) => {
  return ['aligned-clips-jun-16-2025', JSON.stringify(alignmentSpec)];
};

export const getAlignedSiblingsCacheKey = (clipId: string | null) => {
  if (!clipId) {
    return ['aligned-siblings-jun-16-2025', 'null'];
  }
  return ['aligned-siblings-jun-16-2025', clipId];
};

export const getStemSiblingsCacheKey = (clipId: string | null) => {
  if (!clipId) {
    return ['stem-siblings-jun-16-2025', 'null'];
  }
  return ['stem-siblings-jun-16-2025', clipId];
};

export const bustAlignmentCaches = (
  alignmentSpec: AlignmentSpec | undefined,
  focusedClipId: string | null
) => {
  const queryKeysToInvalidate: any[] = [];

  if (alignmentSpec) {
    queryKeysToInvalidate.push(getAlignmentCacheKey(alignmentSpec));
  }

  if (focusedClipId) {
    const alignedSiblingsKey = getAlignedSiblingsCacheKey(focusedClipId);
    const cachedData = queryClient.getQueryData(alignedSiblingsKey);
    if (cachedData && Array.isArray(cachedData)) {
      cachedData.forEach((clipId) => {
        queryKeysToInvalidate.push(getAlignedSiblingsCacheKey(clipId));
      });
    }
  }

  if (queryKeysToInvalidate.length > 0) {
    queryClient.invalidateQueries({ queryKey: queryKeysToInvalidate });
    queryClient.refetchQueries({ queryKey: queryKeysToInvalidate });
  }
};

const useAlignedClipsQuery = (
  state: StudioProjectState,
  refetchInterval: number | false = false
) => {
  const alignmentRange = useMemo(() => {
    return getStateAlignmentRange(state);
  }, [state]);

  const { clips: clipsStore } = useStores();

  return useQuery<AlignedClipsResponse>({
    queryKey: getAlignmentCacheKey(alignmentRange),
    staleTime: 1000,
    gcTime: 1000,
    refetchInterval,
    queryFn: async () => {
      if (!alignmentRange) {
        return { alignments: [] };
      }

      const { data, error } = await clipsStore.apiClient.GET(
        '/api/clips/aligned_clips',
        {
          params: {
            query: {
              history_clip_id: alignmentRange.historyClipId,
              history_end_seconds_gte: alignmentRange.historyEndSecondsGTE,
              history_end_seconds_lte: alignmentRange.historyEndSecondsLTE,
              future_clip_id: alignmentRange.futureClipId,
              future_start_seconds_gte: alignmentRange.futureStartSecondsGTE,
              future_start_seconds_lte: alignmentRange.futureStartSecondsLTE,
            },
          },
        }
      );

      if (error) {
        throw new Error((error as any).detail);
      }

      // Transform the response to use clipId and camelCase
      const response = data as { alignments: any[] };

      clipsStore.updateClips(response.alignments.map((a) => a.clip));

      return {
        alignments: response.alignments.map((alignment) => ({
          clip: alignment.clip,
          historyClipId: alignment.history_clip_id,
          historyEndSeconds: alignment.history_end_seconds,
          clipStartSeconds: alignment.clip_start_seconds,
          futureClipId: alignment.future_clip_id,
          clipEndSeconds: alignment.clip_end_seconds,
          futureStartSeconds: alignment.future_start_seconds,
        })),
      };
    },
  });
};

export const fetchAlignedSiblings = async (
  apiClient: ApiClient,
  clipId: string
) => {
  const { data, error } = await apiClient.GET(
    '/api/clips/aligned_clip_siblings',
    {
      params: {
        query: {
          clip_id: clipId,
        },
      },
    }
  );

  if (error) {
    throw new Error((error as any).detail);
  }

  // Transform the response to use clipId and camelCase
  const response = data as { alignments: any[] };
  const transformedResponse = {
    alignments: response.alignments.map((alignment) => ({
      clip: alignment.clip,
      historyClipId: alignment.history_clip_id,
      historyEndSeconds: alignment.history_end_seconds,
      clipStartSeconds: alignment.clip_start_seconds,
      futureClipId: alignment.future_clip_id,
      clipEndSeconds: alignment.clip_end_seconds,
      futureStartSeconds: alignment.future_start_seconds,
    })),
  };

  // Cache the response for each clip in the alignments
  transformedResponse.alignments.forEach((alignment) => {
    queryClient.setQueryData(
      getAlignedSiblingsCacheKey(alignment.clip.id),
      transformedResponse
    );
  });

  return transformedResponse;
};

const useAlignedSiblingsQuery = (
  state: StudioProjectState,
  refetchInterval: number | false = false
) => {
  const { clips: clipsStore } = useStores();
  const focusedClip = useFocusedClip(state);
  return useQuery<AlignedClipsResponse>({
    queryKey: getAlignedSiblingsCacheKey(focusedClip?.id ?? null),
    staleTime: 1000,
    gcTime: 1000,
    refetchInterval,
    queryFn: async () => {
      if (!focusedClip) {
        return { alignments: [] };
      }
      return await fetchAlignedSiblings(clipsStore.apiClient, focusedClip.id);
    },
  });
};

export const fetchAlignedClip = async (apiClient: ApiClient, clip: Clip) => {
  return (await fetchAlignedSiblings(apiClient, clip.id)).alignments.find(
    (a) => a.clip.id === clip.id
  );
};

export const fetchClipAlignmentSpec = async (
  apiClient: ApiClient,
  clip: Clip
): Promise<AlignmentSpec | undefined> => {
  const alignedClip = await fetchAlignedClip(apiClient, clip);
  if (alignedClip) {
    return {
      historyClipId: alignedClip.historyClipId,
      historyEndSeconds: alignedClip.historyEndSeconds,
      futureClipId: alignedClip.futureClipId,
      futureStartSeconds: alignedClip.futureStartSeconds,
    };
  }
  return getClipAlignmentSpecFromHistory(clip);
};

export const fetchStemSiblings = async (
  apiClient: ApiClient,
  clipId: string
) => {
  const response = await apiClient.POST('/api/feed/v3', {
    body: {
      cursor: null,
      limit: 64,
      filters: {
        stem: {
          presence: BooleanFilter.True,
          fromClipId: clipId,
        },
      },
    },
  });

  if (response.error) {
    throw new Error((response as any).error);
  }

  return response.data?.clips || [];
};

export const useStemSiblingsQuery = (
  state: StudioProjectState,
  refetchInterval: number | false = false
) => {
  const { clips: clipsStore } = useStores();
  const focusedClip = useFocusedClip(state);
  const parentClipId =
    focusedClip &&
    (hasStemTask(focusedClip) || hasStemConditionTask(focusedClip)) &&
    focusedClip?.metadata?.stem_from_id;
  return useQuery<AlignedClipsResponse>({
    queryKey: getStemSiblingsCacheKey(parentClipId || null),
    staleTime: 1000,
    gcTime: 1000,
    refetchInterval,
    queryFn: async () => {
      if (!parentClipId) {
        return { alignments: [] };
      }
      const clips = await fetchStemSiblings(clipsStore.apiClient, parentClipId);
      clipsStore.updateClips(clips);
      return {
        alignments: clips.map((c) => ({
          clip: c,
        })),
      };
    },
  });
};

export default function useAlignedClips(
  state: StudioProjectState,
  expectedIds: string[] = []
) {
  const [refetchInterval, setRefetchInterval] = useState<number | false>(false);
  const alignedClipsQuery = useAlignedClipsQuery(state, refetchInterval);
  const alignedSiblingsQuery = useAlignedSiblingsQuery(state, refetchInterval);
  const stemSiblingsQuery = useStemSiblingsQuery(state, refetchInterval);

  const result = useMemo(() => {
    return uniqBy(
      [
        ...(alignedClipsQuery.data?.alignments || []),
        ...(alignedSiblingsQuery.data?.alignments || []),
        ...(stemSiblingsQuery.data?.alignments || []),
      ],
      (a) => a.clip.id
    );
  }, [
    alignedClipsQuery.data?.alignments,
    alignedSiblingsQuery.data?.alignments,
    stemSiblingsQuery.data?.alignments,
  ]);

  // Check if all expected IDs are present in the result
  useEffect(() => {
    const resultIds = new Set(result.map((alignment) => alignment.clip.id));
    const allExpectedIdsPresent = expectedIds.every((id) => resultIds.has(id));
    setRefetchInterval(allExpectedIdsPresent ? false : 2500);
  }, [result, expectedIds]);

  return result;
}
