'use client';

import {
  createContext,
  useContext,
  useState,
  ReactNode,
  useCallback,
  useMemo,
  useRef,
  RefObject,
} from 'react';
import { indexedDBService } from '@/utils/indexedDB';
import { useTTS } from '@/contexts/TTSContext';
import { Book, Rendition } from 'epubjs';
import { createRangeCfi } from '@/utils/epub';
import type { NavItem } from 'epubjs';
import { setLastDocumentLocation } from '@/utils/indexedDB';
import { SpineItem } from 'epubjs/types/section';
import { useParams } from 'next/navigation';
import { useConfig } from './ConfigContext';

interface EPUBContextType {
  currDocData: ArrayBuffer | undefined;
  currDocName: string | undefined;
  currDocPages: number | undefined;
  currDocPage: number | string;
  currDocText: string | undefined;
  setCurrentDocument: (id: string) => Promise<void>;
  clearCurrDoc: () => void;
  extractPageText: (book: Book, rendition: Rendition, shouldPause?: boolean) => Promise<string>;
  createFullAudioBook: (onProgress: (progress: number) => void, signal?: AbortSignal, format?: 'mp3' | 'm4b') => Promise<ArrayBuffer>;
  bookRef: RefObject<Book | null>;
  renditionRef: RefObject<Rendition | undefined>;
  tocRef: RefObject<NavItem[]>;
  locationRef: RefObject<string | number>;
  handleLocationChanged: (location: string | number) => void;
  setRendition: (rendition: Rendition) => void;
  isAudioCombining: boolean;
}

const EPUBContext = createContext<EPUBContextType | undefined>(undefined);

/**
 * Provider component for EPUB functionality
 * Manages the state and operations for EPUB document handling
 * @param {Object} props - Component props
 * @param {ReactNode} props.children - Child components to be wrapped by the provider
 */
export function EPUBProvider({ children }: { children: ReactNode }) {
  const { setText: setTTSText, currDocPage, currDocPages, setCurrDocPages, stop, skipToLocation, setIsEPUB } = useTTS();
  const { id } = useParams();
  // Configuration context to get TTS settings
  const {
    apiKey,
    baseUrl,
    voiceSpeed,
    voice,
  } = useConfig();
  // Current document state
  const [currDocData, setCurrDocData] = useState<ArrayBuffer>();
  const [currDocName, setCurrDocName] = useState<string>();
  const [currDocText, setCurrDocText] = useState<string>();
  const [isAudioCombining, setIsAudioCombining] = useState(false);

  // Add new refs
  const bookRef = useRef<Book | null>(null);
  const renditionRef = useRef<Rendition | undefined>(undefined);
  const tocRef = useRef<NavItem[]>([]);
  const locationRef = useRef<string | number>(currDocPage);
  const isEPUBSetOnce = useRef(false);

  /**
   * Clears all current document state and stops any active TTS
   */
  const clearCurrDoc = useCallback(() => {
    setCurrDocData(undefined);
    setCurrDocName(undefined);
    setCurrDocText(undefined);
    setCurrDocPages(undefined);
    stop();
  }, [setCurrDocPages, stop]);

  /**
   * Sets the current document based on its ID by fetching from IndexedDB
   * @param {string} id - The unique identifier of the document
   * @throws {Error} When document data is empty or retrieval fails
   */
  const setCurrentDocument = useCallback(async (id: string): Promise<void> => {
    try {
      const doc = await indexedDBService.getEPUBDocument(id);
      if (doc) {
        console.log('Retrieved document size:', doc.size);
        console.log('Retrieved ArrayBuffer size:', doc.data.byteLength);

        if (doc.data.byteLength === 0) {
          console.error('Retrieved ArrayBuffer is empty');
          throw new Error('Empty document data');
        }

        setCurrDocName(doc.name);
        setCurrDocData(doc.data);  // Store ArrayBuffer directly
      } else {
        console.error('Document not found in IndexedDB');
      }
    } catch (error) {
      console.error('Failed to get EPUB document:', error);
      clearCurrDoc(); // Clean up on error
    }
  }, [clearCurrDoc]);

  /**
   * Extracts text content from the current EPUB page/location
   * @param {Book} book - The EPUB.js Book instance
   * @param {Rendition} rendition - The EPUB.js Rendition instance
   * @param {boolean} shouldPause - Whether to pause TTS
   * @returns {Promise<string>} The extracted text content
   */
  const extractPageText = useCallback(async (book: Book, rendition: Rendition, shouldPause = false): Promise<string> => {
    try {
      const { start, end } = rendition?.location;
      if (!start?.cfi || !end?.cfi || !book || !book.isOpen || !rendition) return '';

      const rangeCfi = createRangeCfi(start.cfi, end.cfi);

      const range = await book.getRange(rangeCfi);
      if (!range) {
        console.warn('Failed to get range from CFI:', rangeCfi);
        return '';
      }
      const textContent = range.toString().trim();

      setTTSText(textContent, shouldPause);
      setCurrDocText(textContent);

      return textContent;
    } catch (error) {
      console.error('Error extracting EPUB text:', error);
      return '';
    }
  }, [setTTSText]);

  /**
   * Extracts text content from the entire EPUB book
   * @returns {Promise<string[]>} Array of text content from each section
   */
  const extractBookText = useCallback(async (): Promise<string[]> => {
    try {
      if (!bookRef.current || !bookRef.current.isOpen) return [''];

      const book = bookRef.current;
      const spine = book.spine;
      const promises: Promise<string>[] = [];

      spine.each((item: SpineItem) => {
        const url = item.href || '';
        if (!url) return;

        const promise = book.load(url)
          .then((section) => (section as Document))
          .then((section) => {
            const textContent = section.body.textContent || '';
            return textContent;
          })
          .catch((err) => {
            console.error(`Error loading section ${url}:`, err);
            return '';
          });

        promises.push(promise);
      });

      const textArray = await Promise.all(promises);
      const filteredArray = textArray.filter(text => text.trim() !== '');
      console.log('Extracted entire EPUB text array:', filteredArray);
      return filteredArray;
    } catch (error) {
      console.error('Error extracting EPUB text:', error);
      return [''];
    }
  }, []);

  /**
   * Creates a complete audiobook by processing all text through NLP and TTS
   */
  const createFullAudioBook = useCallback(async (
    onProgress: (progress: number) => void,
    signal?: AbortSignal,
    format: 'mp3' | 'm4b' = 'mp3'
  ): Promise<ArrayBuffer> => {
    try {
      const textArray = await extractBookText();
      if (!textArray.length) throw new Error('No text content found in book');

      // Calculate total text length for accurate progress tracking
      const totalLength = textArray.reduce((sum, text) => sum + text.trim().length, 0);
      const audioChunks: { buffer: ArrayBuffer; title?: string; startTime: number }[] = [];
      let processedLength = 0;
      let currentTime = 0;

      // Get TOC for chapter titles if available
      const chapters = tocRef.current || [];
      const spine = bookRef.current?.spine;
      
      for (const text of textArray) {
        if (signal?.aborted) {
          const partialBuffer = await combineAudioChunks(audioChunks, format);
          return partialBuffer;
        }

        try {
          const trimmedText = text.trim();
          if (!trimmedText) continue;

          const ttsResponse = await fetch('/api/tts', {
            method: 'POST',
            headers: {
              'x-openai-key': apiKey,
              'x-openai-base-url': baseUrl,
            },
            body: JSON.stringify({
              text: trimmedText,
              voice: voice,
              speed: voiceSpeed,
              format: 'audiobook'
            }),
            signal
          });

          if (!ttsResponse.ok) {
            throw new Error(`TTS processing failed with status ${ttsResponse.status}`);
          }

          const audioBuffer = await ttsResponse.arrayBuffer();
          if (audioBuffer.byteLength === 0) {
            throw new Error('Received empty audio buffer from TTS');
          }

          // Find matching chapter title from TOC if available
          let chapterTitle;
          if (spine && chapters.length > 0) {
            let spineIndex = processedLength;
            let currentSpineHref: string | undefined;
            
            spine.each((item: SpineItem) => {
              if (spineIndex === 0) {
                currentSpineHref = item.href;
              }
              spineIndex--;
            });

            const matchingChapter = chapters.find(chapter => 
              chapter.href && currentSpineHref?.includes(chapter.href)
            );
            chapterTitle = matchingChapter?.label || `Section ${processedLength + 1}`;
          } else {
            chapterTitle = `Section ${processedLength + 1}`;
          }

          audioChunks.push({
            buffer: audioBuffer,
            title: chapterTitle,
            startTime: currentTime
          });

          // Add silence between sections
          const silenceBuffer = new ArrayBuffer(48000);
          audioChunks.push({
            buffer: silenceBuffer,
            startTime: currentTime + (audioBuffer.byteLength / 48000)
          });

          currentTime += (audioBuffer.byteLength + 48000) / 48000;

          // Update progress based on processed text length
          processedLength += trimmedText.length;
          onProgress((processedLength / totalLength) * 100);

        } catch (error) {
          if (error instanceof Error && error.name === 'AbortError') {
            console.log('TTS request aborted');
            const partialBuffer = await combineAudioChunks(audioChunks, format);
            return partialBuffer;
          }
          console.error('Error processing section:', error);
        }
      }

      if (audioChunks.length === 0) {
        throw new Error('No audio was generated from the book content');
      }

      return combineAudioChunks(audioChunks, format);
    } catch (error) {
      console.error('Error creating audiobook:', error);
      throw error;
    }
  }, [extractBookText, apiKey, baseUrl, voice, voiceSpeed]);

  const combineAudioChunks = async (
    audioChunks: { buffer: ArrayBuffer; title?: string; startTime: number }[],
    format: 'mp3' | 'm4b'
  ): Promise<ArrayBuffer> => {
    setIsAudioCombining(true);
    try {
      if (format === 'm4b') {
        // Convert to M4B format using the audio conversion API
        const response = await fetch('/api/audio/convert', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            chapters: audioChunks
              .filter(chunk => chunk.title) // Only include chunks with titles
              .map(chunk => ({
                title: chunk.title,
                buffer: Array.from(new Uint8Array(chunk.buffer))
              }))
          }),
        });

        if (!response.ok) {
          throw new Error('Failed to convert audio to M4B format');
        }

        return response.arrayBuffer();
      }

      // For MP3, just concatenate the buffers
      const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.buffer.byteLength, 0);
      const combinedBuffer = new Uint8Array(totalLength);

      let offset = 0;
      for (const chunk of audioChunks) {
        combinedBuffer.set(new Uint8Array(chunk.buffer), offset);
        offset += chunk.buffer.byteLength;
      }

      return combinedBuffer.buffer;
    } finally {
      setIsAudioCombining(false);
    }
  }

  const setRendition = useCallback((rendition: Rendition) => {
    bookRef.current = rendition.book;
    renditionRef.current = rendition;
  }, []);

  const handleLocationChanged = useCallback((location: string | number) => {
    // Set the EPUB flag once the location changes
    if (!isEPUBSetOnce.current) {
      setIsEPUB(true);
      isEPUBSetOnce.current = true;

      renditionRef.current?.display(location.toString());
      return;
    }

    if (!bookRef.current?.isOpen || !renditionRef.current) return;

    // Handle special 'next' and 'prev' cases
    if (location === 'next' && renditionRef.current) {
      renditionRef.current.next();
      return;
    }
    if (location === 'prev' && renditionRef.current) {
      renditionRef.current.prev();
      return;
    }

    // Save the location to IndexedDB if not initial
    if (id && locationRef.current !== 1) {
      console.log('Saving location:', location);
      setLastDocumentLocation(id as string, location.toString());
    }

    skipToLocation(location);

    locationRef.current = location;
    if (bookRef.current && renditionRef.current) {
      extractPageText(bookRef.current, renditionRef.current);
    }
  }, [id, skipToLocation, extractPageText, setIsEPUB]);

  // Context value memoization
  const contextValue = useMemo(
    () => ({
      setCurrentDocument,
      currDocData,
      currDocName,
      currDocPages,
      currDocPage,
      currDocText,
      clearCurrDoc,
      extractPageText,
      createFullAudioBook,
      bookRef,
      renditionRef,
      tocRef,
      locationRef,
      handleLocationChanged,
      setRendition,
      isAudioCombining,
    }),
    [
      setCurrentDocument,
      currDocData,
      currDocName,
      currDocPages,
      currDocPage,
      currDocText,
      clearCurrDoc,
      extractPageText,
      createFullAudioBook,
      handleLocationChanged,
      setRendition,
      isAudioCombining,
    ]
  );

  return (
    <EPUBContext.Provider value={contextValue}>
      {children}
    </EPUBContext.Provider>
  );
}

/**
 * Custom hook to consume the EPUB context
 * @returns {EPUBContextType} The EPUB context value
 * @throws {Error} When used outside of EPUBProvider
 */
export function useEPUB() {
  const context = useContext(EPUBContext);
  if (context === undefined) {
    throw new Error('useEPUB must be used within an EPUBProvider');
  }
  return context;
}