import {
  createContext,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { v4 as uuidv4 } from 'uuid';

import { useStores } from '@/app/(root)/AppProviders';
import { toast } from '@/components/toast/Toast';
import useClip from '@/hooks/useClip';
import useConcat from '@/hooks/useConcat';
import { useApiClient } from '@/lib/apiClient';
import { Clip } from '@/state/clipStore';
import { DEFAULT_PROJECT_ID } from '@/utils/constants';

import { getEditClipTitle } from './EditList';
import { useEditModeContext } from './EditModeContext';
import { precomputeWaveformAggregates } from './canvasRenderer/renderWaveform';
import { getEditType } from './getEditType';
import { ArrangedClip, Edit } from './types';

export const SERVER_AUTO_INFILL_PADDING_SECONDS = 2;
const INSET_SECONDS = SERVER_AUTO_INFILL_PADDING_SECONDS / 2;

export const getInfillClipBounds = (
  clip: Clip,
  previewClip: Clip
): {
  infillStartSeconds: number;
  infillEndSeconds: number;
  durationSeconds: number;
  startInsetSeconds: number;
  endInsetSeconds: number;
  durationSecondsWithInset: number;
  infillStartSecondsWithInset: number;
  infillEndSecondsWithInset: number;
  addedTimeSeconds: number;
} => {
  if (previewClip.metadata.task === 'fixed_infill') {
    const historyEntry = previewClip.metadata.history?.slice(-1)[0] as any;
    const infillStartSeconds = historyEntry.infill_context_start_s;
    const infillEndSeconds = historyEntry.infill_context_end_s;

    const durationSeconds = previewClip.metadata.duration as number;

    const startInsetSeconds =
      historyEntry.infill_start_s - historyEntry.infill_context_start_s;
    const infillStartSecondsWithInset = infillStartSeconds + startInsetSeconds;

    const endInsetSeconds =
      historyEntry.infill_context_end_s - historyEntry.infill_end_s;
    const infillEndSecondsWithInset = infillEndSeconds - endInsetSeconds;

    const totalInsetTime = startInsetSeconds + endInsetSeconds;
    const durationSecondsWithInset = durationSeconds - totalInsetTime;

    return {
      infillStartSeconds,
      startInsetSeconds,
      infillStartSecondsWithInset,
      infillEndSeconds,
      endInsetSeconds,
      infillEndSecondsWithInset,
      durationSeconds,
      durationSecondsWithInset,
      addedTimeSeconds: 0,
    };
  } else {
    const infillStartSeconds = Math.max(
      0,
      (previewClip.metadata.history?.slice(-1)[0] as any).infill_start_s -
        SERVER_AUTO_INFILL_PADDING_SECONDS
    );
    const infillEndSeconds = Math.min(
      (previewClip.metadata.history?.slice(-1)[0] as any).infill_end_s +
        SERVER_AUTO_INFILL_PADDING_SECONDS,
      clip.metadata.duration as number
    );

    const durationSeconds = previewClip.metadata.duration as number;

    const startInsetSeconds = infillStartSeconds === 0 ? 0 : INSET_SECONDS;
    const infillStartSecondsWithInset = infillStartSeconds + startInsetSeconds;

    const endInsetSeconds =
      infillEndSeconds === clip.metadata.duration ? 0 : INSET_SECONDS;
    const infillEndSecondsWithInset = infillEndSeconds - endInsetSeconds;

    const totalInsetTime = startInsetSeconds + endInsetSeconds;
    const durationSecondsWithInset = durationSeconds - totalInsetTime;

    const addedTimeSeconds =
      durationSeconds - (infillEndSeconds - infillStartSeconds);

    return {
      infillStartSeconds,
      startInsetSeconds,
      infillStartSecondsWithInset,
      infillEndSeconds,
      endInsetSeconds,
      infillEndSecondsWithInset,
      durationSeconds,
      durationSecondsWithInset,
      addedTimeSeconds,
    };
  }
};

export const getExtendClipBounds = (
  clip: Clip,
  previewClip: Clip
): {
  extendStartSeconds: number;
  durationSeconds: number;
} => {
  const extendStartSeconds = (previewClip.metadata.history?.slice(-1)[0] as any)
    .continue_at;
  const durationSeconds = previewClip.metadata.duration as number;

  return {
    extendStartSeconds,
    durationSeconds,
  };
};

const getInfillArrangedClips = (clip: Clip, previewClip: Clip) => {
  const clipDuration = clip.metadata.duration as number;
  const {
    infillStartSecondsWithInset,
    infillEndSecondsWithInset,
    durationSecondsWithInset,
    startInsetSeconds,
    addedTimeSeconds,
  } = getInfillClipBounds(clip, previewClip);

  if (infillStartSecondsWithInset + durationSecondsWithInset >= clipDuration) {
    return [
      {
        startSeconds: 0,
        endSeconds: infillStartSecondsWithInset,
        readStartSeconds: 0,
        id: uuidv4(),
        clipId: clip.id,
        title: clip.title || '',
        contentSeconds: clip.metadata.duration as number,
      },
      {
        startSeconds: infillStartSecondsWithInset,
        endSeconds: clipDuration + addedTimeSeconds,
        readStartSeconds: startInsetSeconds,
        id: uuidv4(),
        clipId: previewClip.id,
        title: getEditClipTitle(previewClip),
        contentSeconds: previewClip.metadata.duration as number,
      },
    ];
  }

  if (infillStartSecondsWithInset <= 0) {
    return [
      {
        startSeconds: 0,
        endSeconds: durationSecondsWithInset,
        readStartSeconds: 0,
        id: uuidv4(),
        clipId: previewClip.id,
        title: getEditClipTitle(previewClip),
        contentSeconds: previewClip.metadata.duration as number,
      },
      {
        startSeconds: durationSecondsWithInset,
        endSeconds: clipDuration + addedTimeSeconds,
        readStartSeconds: infillEndSecondsWithInset,
        id: uuidv4(),
        clipId: clip.id,
        title: clip.title || '',
        contentSeconds: clip.metadata.duration as number,
      },
    ];
  }

  return [
    {
      startSeconds: 0,
      endSeconds: infillStartSecondsWithInset,
      readStartSeconds: 0,
      id: uuidv4(),
      clipId: clip.id,
      title: clip.title || '',
      contentSeconds: clip.metadata.duration as number,
    },
    {
      startSeconds: infillStartSecondsWithInset,
      endSeconds: infillStartSecondsWithInset + durationSecondsWithInset,
      readStartSeconds: startInsetSeconds,
      id: uuidv4(),
      clipId: previewClip.id,
      title: getEditClipTitle(previewClip),
      contentSeconds: previewClip.metadata.duration as number,
    },
    {
      startSeconds: infillStartSecondsWithInset + durationSecondsWithInset,
      endSeconds: clipDuration + addedTimeSeconds,
      readStartSeconds: infillEndSecondsWithInset,
      id: uuidv4(),
      clipId: clip.id,
      title: clip.title || '',
      contentSeconds: clip.metadata.duration as number,
    },
  ];
};

const getExtendArrangedClips = (clip: Clip, previewClip: Clip) => {
  const { extendStartSeconds, durationSeconds } = getExtendClipBounds(
    clip,
    previewClip
  );

  if (extendStartSeconds <= 0) {
    return [
      {
        startSeconds: 0,
        endSeconds: durationSeconds,
        readStartSeconds: 0,
        id: uuidv4(),
        clipId: previewClip.id,
        title: previewClip.title || '',
        contentSeconds: previewClip.metadata.duration as number,
      },
    ];
  }

  return [
    {
      startSeconds: 0,
      endSeconds: extendStartSeconds,
      readStartSeconds: 0,
      id: uuidv4(),
      clipId: clip.id,
      title: clip.title || '',
      contentSeconds: clip.metadata.duration as number,
    },
    {
      startSeconds: extendStartSeconds,
      endSeconds: extendStartSeconds + durationSeconds,
      readStartSeconds: 0,
      id: uuidv4(),
      clipId: previewClip.id,
      title: getEditClipTitle(previewClip),
      contentSeconds: previewClip.metadata.duration as number,
    },
  ];
};

export const usePreviewClipContext = (
  clipId: string,
  editModeContext: ReturnType<typeof useEditModeContext>
) => {
  const [previewEdit, setPreviewEdit] = useState<Edit | null>(null);
  const { project } = useStores();
  const apiClient = useApiClient();
  const previewClipId = previewEdit?.clipId || null;
  const { clip } = useClip(clipId);
  const { clip: previewClip } = useClip(previewClipId || undefined);

  // Precompute waveform aggregates for preview clips when they are loaded
  const precomputedPreviewClipRef = useRef<string | null>(null);
  useEffect(() => {
    if (
      previewClip &&
      previewClipId &&
      precomputedPreviewClipRef.current !== previewClipId
    ) {
      precomputedPreviewClipRef.current = previewClipId;
      precomputeWaveformAggregates(apiClient, previewClipId).catch((error) => {
        console.error(
          `Failed to precompute waveform aggregates for preview clip ${previewClipId}:`,
          error
        );
      });
    }
  }, [previewClip, previewClipId, apiClient]);

  const arrangedClips = useMemo<ArrangedClip[]>(() => {
    if (!clip?.metadata?.duration) return [];
    if (!previewClip?.metadata?.duration)
      return [
        {
          id: uuidv4(),
          clipId: clip.id,
          startSeconds: 0,
          endSeconds: clip.metadata.duration,
          readStartSeconds: 0,
          title: clip.title || '',
          contentSeconds: clip.metadata.duration as number,
        },
      ];

    if (previewClip?.metadata?.infill) {
      return getInfillArrangedClips(clip, previewClip);
    }

    if (previewClip?.metadata?.task === 'extend') {
      return getExtendArrangedClips(clip, previewClip);
    }

    return [];
  }, [clip, previewClip]);

  const songEndSeconds = useMemo(() => {
    return Math.max(...arrangedClips.map((clip) => clip.endSeconds), 0);
  }, [arrangedClips]);

  const precomputedArrangedClipIds = useRef<Set<string>>(new Set());
  useEffect(() => {
    const uniqueClipIds = new Set(
      arrangedClips.map((arrangedClip) => arrangedClip.clipId)
    );
    const newClipIds = [...uniqueClipIds].filter(
      (id) => !precomputedArrangedClipIds.current.has(id)
    );

    if (newClipIds.length > 0) {
      newClipIds.forEach((clipId) => {
        precomputedArrangedClipIds.current.add(clipId);
        precomputeWaveformAggregates(apiClient, clipId).catch((error) => {
          console.error(
            `Failed to precompute waveform aggregates for arranged clip ${clipId}:`,
            error
          );
          // Remove from set so we can retry later if needed
          precomputedArrangedClipIds.current.delete(clipId);
        });
      });
    }
  }, [arrangedClips, apiClient]);

  const concat = useConcat();
  const editType = useMemo(
    () => previewClip && getEditType(previewClip),
    [previewClip]
  );

  const [applying, setApplying] = useState(false);
  const applyPreviewEdit = useCallback(async () => {
    if (!previewEdit?.clipId || !previewClip) return;
    setApplying(true);
    editModeContext.setLastAppliedEditType(editType || 'none');
    if (previewEdit?.songId) {
      window.history.pushState({}, '', `/edit-legacy/${previewEdit?.songId}`);
      return;
    }
    try {
      const concatResult = await concat({
        projectId:
          project.userSelectedProjectId === DEFAULT_PROJECT_ID
            ? null
            : project.userSelectedProjectId,
        clipId: previewEdit.clipId,
        isInfill: editType === 'replace',
      });
      if (concatResult?.id) {
        window.history.pushState({}, '', `/edit-legacy/${concatResult.id}`);
      } else {
        throw new Error('No concat result id returned');
      }
    } catch (e) {
      toast({
        title: 'Error Applying Edit',
        description: `Please try again later, or contact us if this problem persists.`,
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
      setApplying(false);
    }
    // no `finally` because we are going to navigate to another page if this succeeds.
  }, [previewClip, previewEdit?.clipId, previewEdit?.songId, concat, editType]);

  useEffect(() => {
    setApplying(false);
    setPreviewEdit(null);
    // Reset precomputation refs when clipId changes
    precomputedPreviewClipRef.current = null;
    precomputedArrangedClipIds.current.clear();
  }, [clipId]);

  return useMemo(
    () => ({
      applying,
      setApplying,
      previewClipId,
      previewEdit,
      setPreviewEdit,
      previewClip,
      arrangedClips,
      songEndSeconds,
      applyPreviewEdit,
    }),
    [
      applying,
      setApplying,
      previewClipId,
      previewEdit,
      setPreviewEdit,
      previewClip,
      arrangedClips,
      songEndSeconds,
      applyPreviewEdit,
    ]
  );
};

const PreviewClipContext = createContext<
  ReturnType<typeof usePreviewClipContext>
>(undefined as never);

export default PreviewClipContext;
