/**
 * BookDate: Helper Functions for Recommendations
 * Documentation: documentation/features/bookdate-prd.md
 */

import { prisma } from '@/lib/db';
import { getEncryptionService } from '@/lib/services/encryption.service';
import { getConfigService } from '@/lib/services/config.service';
import { AudibleService } from '@/lib/integrations/audible.service';
import { getPlexService } from '@/lib/integrations/plex.service';
import { findPlexMatch } from '@/lib/utils/audiobook-matcher';
import { RMABLogger } from '@/lib/utils/logger';

const logger = RMABLogger.create('BookDate');

export interface LibraryBook {
  title: string;
  author: string;
  narrator?: string | null;
  rating?: number | null;
}

interface CachedLibraryBook {
  title: string;
  author: string;
  narrator: string | null;
  plexGuid: string;
  plexRatingKey: string | null;
  userRating?: any; // Admin's cached rating
}

export interface SwipeHistory {
  title: string;
  author: string;
  action: string;
  markedAsKnown: boolean;
}

export interface AIRecommendation {
  title: string;
  author: string;
  reason: string;
}

/**
 * Enrich cached library books with user's personal ratings from Plex
 * @param userId - User ID (to fetch their Plex token)
 * @param cachedBooks - Books from PlexLibrary table cache
 * @returns Books enriched with user's personal ratings
 */
async function enrichWithUserRatings(
  userId: string,
  cachedBooks: CachedLibraryBook[]
): Promise<LibraryBook[]> {
  try {
    // Get user's Plex token, plexId, and role
    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { authToken: true, plexId: true, role: true },
    });

    if (!user) {
      logger.warn('User not found');
      return cachedBooks.map(book => ({
        title: book.title,
        author: book.author,
        narrator: book.narrator || undefined,
        rating: undefined,
      }));
    }

    // Local admin users: Use cached ratings (from system Plex token)
    // Local admins authenticate with username/password, not Plex OAuth
    if (user.plexId.startsWith('local-')) {
      logger.info('User is local admin, using cached ratings (from system Plex token)');
      return cachedBooks.map(book => ({
        title: book.title,
        author: book.author,
        narrator: book.narrator || undefined,
        rating: book.userRating ? parseFloat(book.userRating.toString()) : undefined,
      }));
    }

    // Plex-authenticated users (including admins): Fetch library with their token to get personal ratings
    // Note: /library/sections/{id}/all returns items with the authenticated user's ratings
    logger.info('User is Plex-authenticated, fetching library with user token to get personal ratings');

    if (!user.authToken) {
      logger.warn('User has no Plex auth token');
      return cachedBooks.map(book => ({
        title: book.title,
        author: book.author,
        narrator: book.narrator || undefined,
        rating: undefined,
      }));
    }

    // Get Plex configuration
    const configService = getConfigService();
    const plexConfig = await configService.getPlexConfig();

    if (!plexConfig.serverUrl || !plexConfig.libraryId) {
      logger.warn('No Plex server URL or library ID configured');
      return cachedBooks.map(book => ({
        title: book.title,
        author: book.author,
        narrator: book.narrator || undefined,
        rating: undefined,
      }));
    }

    // Decrypt user's plex.tv OAuth token
    let userPlexToken: string;
    const encryptionService = getEncryptionService();
    try {
      userPlexToken = encryptionService.decrypt(user.authToken);
    } catch (decryptError) {
      // Token might be stored as plain text (from before encryption or different implementation)
      // Try using it as-is
      logger.warn('Failed to decrypt user Plex token, trying as plain text');
      userPlexToken = user.authToken;
    }

    try {
      // Get server-specific access token
      // Per Plex API: plex.tv OAuth tokens are for plex.tv, but we need
      // server-specific access tokens from /api/v2/resources to talk to PMS
      const plexService = getPlexService();

      // Get server machine ID from stored config (no need to access system token)
      if (!plexConfig.machineIdentifier) {
        logger.error('Server machine identifier not configured');
        return cachedBooks.map(book => ({
          title: book.title,
          author: book.author,
          narrator: book.narrator || undefined,
          rating: undefined,
        }));
      }

      const serverMachineId = plexConfig.machineIdentifier;
      const serverAccessToken = await plexService.getServerAccessToken(
        serverMachineId,
        userPlexToken
      );

      if (!serverAccessToken) {
        logger.warn('Could not get server access token for user (may not have server access)');
        return cachedBooks.map(book => ({
          title: book.title,
          author: book.author,
          narrator: book.narrator || undefined,
          rating: undefined,
        }));
      }

      logger.info('Successfully obtained server access token for user');

      // Fetch library content with user's SERVER access token to get their personal ratings
      const userLibrary = await plexService.getLibraryContent(
        plexConfig.serverUrl,
        serverAccessToken,
        plexConfig.libraryId
      );

      logger.info(`Fetched ${userLibrary.length} items from Plex with user's token`);

      // Create a map of guid/ratingKey -> userRating for quick lookup
      const ratingsMap = new Map<string, number>();
      userLibrary.forEach(item => {
        if (item.userRating) {
          // Try to match by guid first (most reliable)
          if (item.guid) {
            ratingsMap.set(item.guid, item.userRating);
          }
          // Also store by ratingKey as fallback
          if (item.ratingKey) {
            ratingsMap.set(item.ratingKey, item.userRating);
          }
        }
      });

      logger.info(`Found ${ratingsMap.size} rated items for non-admin user`);

      // Enrich cached books with user's ratings from the fetched library
      return cachedBooks.map(book => {
        // Try to find rating by guid first (most reliable), then ratingKey
        let rating: number | undefined;
        if (book.plexGuid) {
          rating = ratingsMap.get(book.plexGuid);
        }
        if (!rating && book.plexRatingKey) {
          rating = ratingsMap.get(book.plexRatingKey);
        }

        return {
          title: book.title,
          author: book.author,
          narrator: book.narrator || undefined,
          rating: rating,
        };
      });

    } catch (fetchError: any) {
      if (fetchError?.response?.status === 401 || fetchError?.message?.includes('401')) {
        logger.warn('User token unauthorized for library access (shared users may not have direct API access)');
        logger.warn('Falling back to recommendations without user ratings');
      } else {
        logger.error('Failed to fetch library with user token', { error: fetchError instanceof Error ? fetchError.message : String(fetchError) });
      }
      // Fallback: return books without ratings
      return cachedBooks.map(book => ({
        title: book.title,
        author: book.author,
        narrator: book.narrator || undefined,
        rating: undefined,
      }));
    }

  } catch (error) {
    logger.error('Error enriching books with user ratings', { error: error instanceof Error ? error.message : String(error) });
    // Fallback: return books without ratings on error
    return cachedBooks.map(book => ({
      title: book.title,
      author: book.author,
      narrator: book.narrator || undefined,
      rating: undefined,
    }));
  }
}

/**
 * Get user's Plex library books based on scope
 * @param userId - User ID
 * @param scope - 'full' | 'listened' | 'rated' | 'favorites'
 * @returns Array of library books (max 40)
 */
export async function getUserLibraryBooks(
  userId: string,
  scope: 'full' | 'listened' | 'rated' | 'favorites'
): Promise<LibraryBook[]> {
  try {
    const configService = getConfigService();
    const backendMode = await configService.getBackendMode();

    // Early validation: audiobookshelf doesn't support ratings
    if (backendMode === 'audiobookshelf' && scope === 'rated') {
      logger.warn('Audiobookshelf does not support ratings, falling back to full library');
      scope = 'full';
    }

    // Handle favorites scope
    if (scope === 'favorites') {
      const user = await prisma.user.findUnique({
        where: { id: userId },
        select: { bookDateFavoriteBookIds: true },
      });

      const favoriteIds = user?.bookDateFavoriteBookIds
        ? JSON.parse(user.bookDateFavoriteBookIds)
        : [];

      if (favoriteIds.length === 0) {
        logger.warn('Favorites scope selected but no favorites stored, falling back to full library');
        scope = 'full';
      } else {
        // Get library ID for filtering
        let libraryId: string;
        if (backendMode === 'audiobookshelf') {
          const absLibraryId = await configService.get('audiobookshelf.library_id');
          if (!absLibraryId) {
            logger.warn('No Audiobookshelf library ID configured');
            return [];
          }
          libraryId = absLibraryId;
        } else {
          const plexConfig = await configService.getPlexConfig();
          if (!plexConfig.libraryId) {
            logger.warn('No Plex library ID configured');
            return [];
          }
          libraryId = plexConfig.libraryId;
        }

        // Query favorite books
        const cachedBooks = await prisma.plexLibrary.findMany({
          where: {
            id: { in: favoriteIds },
            plexLibraryId: libraryId, // Ensure books are from current library
          },
          select: {
            title: true,
            author: true,
            narrator: true,
            plexGuid: true,
            plexRatingKey: true,
            userRating: true,
          },
          orderBy: { addedAt: 'desc' },
        });

        logger.info(`Fetched ${cachedBooks.length} favorite books for user ${userId}`);

        // For Plex: Enrich with user's personal ratings
        // For Audiobookshelf: Skip enrichment (no rating support)
        if (backendMode === 'plex') {
          return await enrichWithUserRatings(userId, cachedBooks);
        } else {
          // Audiobookshelf: Map to LibraryBook without ratings
          return cachedBooks.map(book => ({
            title: book.title,
            author: book.author,
            narrator: book.narrator || undefined,
            rating: undefined,
          }));
        }
      }
    }

    // Get library ID based on backend mode
    let libraryId: string;
    if (backendMode === 'audiobookshelf') {
      const absLibraryId = await configService.get('audiobookshelf.library_id');
      if (!absLibraryId) {
        logger.warn('No Audiobookshelf library ID configured');
        return [];
      }
      libraryId = absLibraryId;
    } else {
      // Plex mode
      const plexConfig = await configService.getPlexConfig();
      if (!plexConfig.libraryId) {
        logger.warn('No Plex library ID configured');
        return [];
      }
      libraryId = plexConfig.libraryId;
    }

    // Check user type for local admin detection (Plex-specific logic)
    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { plexId: true },
    });
    const isLocalAdmin = user?.plexId.startsWith('local-') ?? false;

    // Build query filters
    let whereClause: any = { plexLibraryId: libraryId };
    let takeLimit = 40;

    // Apply rating filter only for Plex backend with rated scope
    if (backendMode === 'plex' && scope === 'rated') {
      if (isLocalAdmin) {
        // Local admin: Use cached ratings from system token
        whereClause.userRating = { not: null };
      } else {
        // OAuth user: Fetch more, filter after user rating enrichment
        takeLimit = 100;
      }
    }

    // Query library from database (same table for both backends)
    let cachedBooks = await prisma.plexLibrary.findMany({
      where: whereClause,
      orderBy: {
        addedAt: 'desc',
      },
      take: takeLimit,
      select: {
        title: true,
        author: true,
        narrator: true,
        plexGuid: true,
        plexRatingKey: true,
        userRating: true,
      },
    });

    // For Plex: Enrich with user's personal ratings
    // For Audiobookshelf: Skip enrichment (no rating support)
    if (backendMode === 'plex') {
      const enrichedBooks = await enrichWithUserRatings(userId, cachedBooks);

      // Filter to rated books if scope is 'rated'
      if (scope === 'rated') {
        const ratedBooks = enrichedBooks.filter(book => book.rating != null);
        return isLocalAdmin ? ratedBooks : ratedBooks.slice(0, 40);
      }

      return enrichedBooks;
    } else {
      // Audiobookshelf: Map to LibraryBook without ratings
      return cachedBooks.map(book => ({
        title: book.title,
        author: book.author,
        narrator: book.narrator || undefined,
        rating: undefined, // ABS doesn't support ratings
      }));
    }

  } catch (error) {
    logger.error('Error fetching library books', { error: error instanceof Error ? error.message : String(error) });
    return [];
  }
}

/**
 * Get user's recent swipes
 * Prioritizes non-dismiss actions (likes/requests/dislikes) over dismissals
 * @param userId - User ID
 * @param limit - Max number of swipes to return
 * @returns Array of recent swipes (prioritized: non-dismiss first, then dismissals)
 */
export async function getUserRecentSwipes(
  userId: string,
  limit: number = 10
): Promise<SwipeHistory[]> {
  try {
    // First, get the most recent non-dismiss swipes (left=reject, right=like/request)
    // These are most informative for AI recommendations
    const nonDismissSwipes = await prisma.bookDateSwipe.findMany({
      where: {
        userId,
        action: { in: ['left', 'right'] },
      },
      orderBy: { createdAt: 'desc' },
      take: limit,
      select: {
        bookTitle: true,
        bookAuthor: true,
        action: true,
        markedAsKnown: true,
        createdAt: true,
      },
    });

    // Calculate remaining slots for dismissals
    const remainingSlots = limit - nonDismissSwipes.length;

    // If we have remaining slots, fill with dismiss swipes (up=dismiss)
    let dismissSwipes: typeof nonDismissSwipes = [];
    if (remainingSlots > 0) {
      dismissSwipes = await prisma.bookDateSwipe.findMany({
        where: {
          userId,
          action: 'up',
        },
        orderBy: { createdAt: 'desc' },
        take: remainingSlots,
        select: {
          bookTitle: true,
          bookAuthor: true,
          action: true,
          markedAsKnown: true,
          createdAt: true,
        },
      });
    }

    // Combine both lists, maintaining chronological order (most recent first)
    const allSwipes = [...nonDismissSwipes, ...dismissSwipes].sort(
      (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
    );

    logger.info(
      `Fetched ${allSwipes.length} swipes: ${nonDismissSwipes.length} non-dismiss, ${dismissSwipes.length} dismiss`
    );

    return allSwipes.map((s) => ({
      title: s.bookTitle,
      author: s.bookAuthor,
      action: s.action,
      markedAsKnown: s.markedAsKnown,
    }));

  } catch (error) {
    logger.error('Error fetching swipe history', { error: error instanceof Error ? error.message : String(error) });
    return [];
  }
}

/**
 * Build AI prompt for recommendations
 * @param userId - User ID
 * @param config - User's BookDate configuration
 * @returns JSON string prompt for AI
 */
export async function buildAIPrompt(
  userId: string,
  config: { libraryScope: string; customPrompt?: string | null }
): Promise<string> {
  const libraryBooks = await getUserLibraryBooks(
    userId,
    config.libraryScope as 'full' | 'listened' | 'rated' | 'favorites'
  );

  const swipeHistory = await getUserRecentSwipes(userId, 10);

  logger.info('Building AI prompt with context:', {
    libraryBooks: libraryBooks.length,
    swipeHistory: swipeHistory.length,
    customPrompt: config.customPrompt ? 'Yes' : 'No',
    libraryScope: config.libraryScope,
  });

  let instructions =
    'Recommend 15-20 audiobooks the user would enjoy based on their library and swipe history. ' +
    'CRITICAL RULES:\n' +
    '1. DO NOT recommend any books already in the user\'s library (check titles carefully)\n' +
    '2. DO NOT recommend any books from the swipe history (whether requested, rejected, dismissed, or marked_as_liked)\n' +
    '3. You must provide 15-20 diverse recommendations, not just 3-5\n' +
    '4. Focus on variety across genres, authors, and styles\n' +
    '5. Consider user ratings if available (0-10 scale, higher = liked more)\n' +
    '6. Learn from rejected books to avoid similar recommendations\n' +
    '7. Learn from requested books to find similar ones\n' +
    '8. Pay special attention to "marked_as_liked" books - these are books the user has already read/listened to elsewhere and enjoyed. Find similar books to these.\n' +
    '9. Each recommendation should be a NEW book not mentioned anywhere in the user context';

  // Add special instruction for favorites scope
  if (config.libraryScope === 'favorites') {
    instructions += '\n\n' +
      'IMPORTANT: The user has specifically handpicked these ' + libraryBooks.length + ' books as their personal favorites. ' +
      'These represent their preferred genres, authors, themes, and styles. Use these as PRIMARY INSPIRATION for your recommendations. ' +
      'Find books that capture the essence of what makes these favorites special to the user.';
  }

  const prompt = {
    task: 'recommend_audiobooks',
    user_context: {
      library_books: libraryBooks.slice(0, 40),
      swipe_history: swipeHistory.map(s => ({
        title: s.title,
        author: s.author,
        user_action: s.action === 'right'
          ? (s.markedAsKnown ? 'marked_as_liked' : 'requested')
          : s.action === 'left' ? 'rejected' : 'dismissed',
      })),
      custom_preferences: config.customPrompt || null,
    },
    instructions,
  };

  const promptString = JSON.stringify(prompt);
  logger.debug('Full AI prompt:', { prompt: promptString });

  return promptString;
}

/**
 * Call AI API to get recommendations
 * @param provider - 'openai' | 'claude' | 'gemini' | 'custom'
 * @param model - Model ID
 * @param encryptedApiKey - Encrypted API key
 * @param prompt - JSON prompt string
 * @returns Parsed AI response with recommendations
 */
export async function callAI(
  provider: string,
  model: string,
  encryptedApiKey: string,
  prompt: string,
  baseUrl?: string | null
): Promise<{ recommendations: AIRecommendation[] }> {
  const encryptionService = getEncryptionService();
  let apiKey = '';
  try {
    apiKey = encryptionService.decrypt(encryptedApiKey);
  } catch (error) {
    // Allow empty API key for custom provider (local models)
    if (provider !== 'custom') {
      throw error;
    }
  }

  logger.info(`Calling AI provider: ${provider}, model: ${model}`);

  // Define JSON schema for structured output
  const responseSchema = {
    type: 'json_schema',
    json_schema: {
      name: 'audiobook_recommendations',
      strict: true,
      schema: {
        type: 'object',
        properties: {
          recommendations: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                title: { type: 'string' },
                author: { type: 'string' },
                reason: { type: 'string' },
              },
              required: ['title', 'author', 'reason'],
              additionalProperties: false,
            },
            minItems: 15,
            maxItems: 20,
          },
        },
        required: ['recommendations'],
        additionalProperties: false,
      },
    },
  };

  const systemMessage = 'You are an expert audiobook recommender. ' +
    'Your task is to recommend 15-20 NEW audiobooks that the user would enjoy. ' +
    'NEVER recommend books that are already in the user\'s library or swipe history. ' +
    'Focus on discovering books they haven\'t seen yet.';

  if (provider === 'openai') {
    const requestBody = {
      model,
      response_format: responseSchema,
      messages: [
        {
          role: 'system',
          content: systemMessage,
        },
        {
          role: 'user',
          content: prompt,
        },
      ],
    };

    logger.debug('OpenAI request body:', { requestBody });

    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(requestBody),
    });

    if (!response.ok) {
      const errorText = await response.text();
      logger.error('OpenAI API error', { status: response.status, error: errorText });
      throw new Error(`OpenAI API error: ${response.status} ${errorText}`);
    }

    const data = await response.json();
    const content = data.choices[0].message.content;
    logger.debug('OpenAI response:', { content });
    return JSON.parse(content);

  } else if (provider === 'claude') {
    const userMessage = `${systemMessage}\n\n${prompt}\n\nIMPORTANT: Provide exactly 15-20 recommendations. Return ONLY valid JSON with no additional text or formatting.`;
    const requestBody = {
      model,
      max_tokens: 8192,
      messages: [
        {
          role: 'user',
          content: userMessage,
        },
      ],
    };

    logger.debug('Claude request body:', { requestBody });

    const response = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'x-api-key': apiKey,
        'anthropic-version': '2023-06-01',
        'content-type': 'application/json',
      },
      body: JSON.stringify(requestBody),
    });

    if (!response.ok) {
      const errorText = await response.text();
      logger.error('Claude API error', { status: response.status, error: errorText });
      throw new Error(`Claude API error: ${response.status} ${errorText}`);
    }

    const data = await response.json();
    const content = data.content[0].text;
    logger.debug('Claude raw response:', { content });

    // Claude sometimes wraps JSON in markdown code blocks, so clean it
    const cleanedContent = content
      .replace(/^```json\s*/i, '')
      .replace(/\s*```$/i, '')
      .trim();

    logger.debug('Claude cleaned response:', { cleanedContent });
    return JSON.parse(cleanedContent);

  } else if (provider === 'gemini') {
    const requestBody = {
      systemInstruction: {
        parts: [{ text: systemMessage }],
      },
      contents: [
        {
          parts: [{ text: prompt }],
        },
      ],
      generationConfig: {
        responseMimeType: "application/json",
        responseSchema: {
          type: "OBJECT",
          properties: {
            recommendations: {
              type: "ARRAY",
              items: {
                type: "OBJECT",
                properties: {
                  title: { type: "STRING" },
                  author: { type: "STRING" },
                  reason: { type: "STRING" },
                },
                required: ["title", "author", "reason"],
              },
            },
          },
          required: ["recommendations"],
        },
      },
    };

    logger.debug('Gemini request body:', { requestBody });

    const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-goog-api-key': apiKey,
      },
      body: JSON.stringify(requestBody),
    });

    if (!response.ok) {
      const errorText = await response.text();
      logger.error('Gemini API error', { status: response.status, error: errorText });
      throw new Error(`Gemini API error: ${response.status} ${errorText}`);
    }

    const data = await response.json();
    const content = data.candidates?.[0]?.content?.parts?.[0]?.text;

    if (!content) {
      throw new Error('Invalid response format from Gemini API');
    }

    logger.debug('Gemini raw response:', { content });

    // Clean potential markdown wrapping
    const cleanedContent = content
      .replace(/^```(?:json)?\s*/i, '')
      .replace(/\s*```$/i, '')
      .trim();

    logger.debug('Gemini cleaned response:', { cleanedContent });
    return JSON.parse(cleanedContent);

  } else if (provider === 'custom') {
    if (!baseUrl) {
      throw new Error('Base URL is required for custom provider');
    }

    // Try with json_schema first
    let requestBody: any = {
      model,
      response_format: responseSchema,
      messages: [
        {
          role: 'system',
          content: systemMessage,
        },
        {
          role: 'user',
          content: prompt,
        },
      ],
    };

    logger.debug('Custom provider request body:', { requestBody, baseUrl });

    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
    };

    // Only add Authorization header if API key provided
    if (apiKey) {
      headers['Authorization'] = `Bearer ${apiKey}`;
    }

    const endpoint = baseUrl.replace(/\/$/, '') + '/chat/completions';

    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers,
        body: JSON.stringify(requestBody),
      });

      if (!response.ok) {
        const errorText = await response.text();
        logger.error('Custom provider API error', { status: response.status, error: errorText });

        // If response_format not supported, retry without it and add instructions to prompt
        if (errorText.includes('response_format') || errorText.includes('json_schema')) {
          logger.info('Retrying without response_format (provider does not support structured outputs)');
          delete requestBody.response_format;
          requestBody.messages[0].content = systemMessage + ' Return ONLY valid JSON with no additional text or formatting.';

          const retryResponse = await fetch(endpoint, {
            method: 'POST',
            headers,
            body: JSON.stringify(requestBody),
          });

          if (!retryResponse.ok) {
            const retryErrorText = await retryResponse.text();
            throw new Error(`Custom provider API error: ${retryResponse.status} ${retryErrorText}`);
          }

          const retryData = await retryResponse.json();
          const retryContent = retryData.choices[0].message.content;

          // Clean markdown code blocks
          const cleanedContent = retryContent
            .replace(/^```json\s*/i, '')
            .replace(/\s*```$/i, '')
            .trim();

          logger.debug('Custom provider cleaned response (fallback):', { cleanedContent });
          return JSON.parse(cleanedContent);
        }

        throw new Error(`Custom provider API error: ${response.status} ${errorText}`);
      }

      const data = await response.json();
      const content = data.choices[0].message.content;
      logger.debug('Custom provider response:', { content });

      // Clean potential markdown wrapping (some providers still wrap even with json_schema)
      const cleanedContent = content
        .replace(/^```json\s*/i, '')
        .replace(/\s*```$/i, '')
        .trim();

      return JSON.parse(cleanedContent);

    } catch (error: any) {
      logger.error('Custom provider error:', error);
      throw new Error(`Custom provider error: ${error.message}`);
    }

  } else {
    throw new Error(`Invalid provider: ${provider}`);
  }
}

/**
 * Match AI recommendation to Audnexus
 * @param title - Book title
 * @param author - Book author
 * @returns Matched metadata or null
 */
export async function matchToAudnexus(
  title: string,
  author: string
): Promise<{
  asin: string;
  title: string;
  author: string;
  narrator: string | null;
  rating: number | null;
  description: string | null;
  coverUrl: string | null;
} | null> {
  try {
    // Step 1: Search in Audible cache first (fastest)
    const cached = await prisma.audibleCache.findFirst({
      where: {
        OR: [
          {
            title: {
              contains: title,
              mode: 'insensitive',
            },
            author: {
              contains: author,
              mode: 'insensitive',
            },
          },
        ],
      },
      select: {
        asin: true,
        title: true,
        author: true,
        narrator: true,
        rating: true,
        description: true,
        coverArtUrl: true,
      },
    });

    if (cached) {
      logger.info(`Found in cache: "${cached.title}" by ${cached.author}`);
      return {
        asin: cached.asin,
        title: cached.title,
        author: cached.author,
        narrator: cached.narrator,
        rating: cached.rating ? parseFloat(cached.rating.toString()) : null,
        description: cached.description,
        coverUrl: cached.coverArtUrl,
      };
    }

    // Step 2: Search Audible.com for the book
    logger.info(`Not in cache, searching Audible for "${title}" by ${author}...`);
    const audibleService = new AudibleService();
    const searchQuery = `${title} ${author}`;
    const searchResults = await audibleService.search(searchQuery, 1);

    if (!searchResults.results || searchResults.results.length === 0) {
      logger.warn(`No Audible search results for "${title}" by ${author}`);
      return null;
    }

    // Take the first result (best match)
    const firstResult = searchResults.results[0];
    logger.info(`Found on Audible: "${firstResult.title}" (ASIN: ${firstResult.asin})`);

    // Step 3: Use ASIN to fetch full details from Audnexus (or Audible as fallback)
    const details = await audibleService.getAudiobookDetails(firstResult.asin);

    if (!details) {
      logger.warn(`Could not fetch details for ASIN ${firstResult.asin}`);
      return null;
    }

    logger.info(`Successfully matched "${title}" to ASIN ${details.asin}`);

    return {
      asin: details.asin,
      title: details.title,
      author: details.author,
      narrator: details.narrator || null,
      rating: details.rating || null,
      description: details.description || null,
      coverUrl: details.coverArtUrl || null,
    };

  } catch (error) {
    logger.error(`Audnexus matching error for "${title}"`, { error: error instanceof Error ? error.message : String(error) });
    return null;
  }
}

/**
 * Check if book is already in user's library
 * Uses the same matching algorithm as homepage (audiobook-matcher.ts)
 * @param userId - User ID
 * @param title - Book title
 * @param author - Book author
 * @param asin - Optional ASIN for exact matching
 * @returns true if book is in library
 */
export async function isInLibrary(
  userId: string,
  title: string,
  author: string,
  asin?: string
): Promise<boolean> {
  try {
    // Use the centralized matching algorithm from audiobook-matcher.ts
    // This ensures consistent matching behavior across the application
    const match = await findPlexMatch({
      asin: asin || '', // Empty ASIN will skip exact ASIN matching but still do fuzzy matching
      title,
      author,
    });

    if (match) {
      logger.info(`Book "${title}" by ${author} found in library (matched to: "${match.title}")`);
    }

    return !!match;
  } catch (error) {
    logger.error(`Error checking library for "${title}"`, { error: error instanceof Error ? error.message : String(error) });
    return false;
  }
}

/**
 * Check if book has already been requested (audiobook request)
 * @param userId - User ID
 * @param asin - Audible ASIN
 * @returns true if book is already requested
 */
export async function isAlreadyRequested(
  userId: string,
  asin: string
): Promise<boolean> {
  const request = await prisma.request.findFirst({
    where: {
      userId,
      type: 'audiobook', // Only check audiobook requests (ebook requests are separate)
      deletedAt: null, // Only check active requests
      audiobook: {
        audibleAsin: asin,
      },
    },
  });

  return !!request;
}

/**
 * Check if book has already been swiped
 * @param userId - User ID
 * @param title - Book title
 * @param author - Book author
 * @returns true if book has been swiped
 */
export async function isAlreadySwiped(
  userId: string,
  title: string,
  author: string
): Promise<boolean> {
  const swipe = await prisma.bookDateSwipe.findFirst({
    where: {
      userId,
      bookTitle: title,
      bookAuthor: author,
    },
  });

  return !!swipe;
}
