// src/services/aiApiClient.ts
/**
 * @file This file acts as a client-side API wrapper for all AI-related functionalities.
 * It communicates with the application's own backend endpoints, which then securely
 * call the Google AI services. This ensures no API keys are exposed on the client.
 */
import type {
  FlyerItem,
  Store,
  MasterGroceryItem,
  ProcessingStage,
  GroundedResponse,
} from '../types';
import { logger } from './logger.client';
import { apiFetch, authedGet, authedPost, authedPostForm } from './apiClient';

/**
 * Uploads a flyer file to the backend to be processed asynchronously.
 * This is the first step in the new background processing flow.
 * @param file The flyer file (PDF or image).
 * @param checksum The SHA-256 checksum of the file.
 * @param tokenOverride Optional token for testing.
 * @returns A promise that resolves to the API response, which should contain a `jobId`.
 */
export const uploadAndProcessFlyer = async (
  file: File,
  checksum: string,
  tokenOverride?: string,
): Promise<{ jobId: string }> => {
  const formData = new FormData();
  formData.append('flyerFile', file);
  formData.append('checksum', checksum);

  logger.info(`[aiApiClient] Starting background processing for file: ${file.name}`);

  const response = await authedPostForm('/ai/upload-and-process', formData, { tokenOverride });

  if (!response.ok) {
    let errorBody;
    // Clone the response so we can read the body twice (once as JSON, and as text on failure).
    const clonedResponse = response.clone();
    try {
      errorBody = await response.json();
    } catch (e) {
      errorBody = { message: await clonedResponse.text() };
    }
    // Throw a structured error so the component can inspect the status and body
    throw { status: response.status, body: errorBody };
  }

  return response.json();
};

// Define the expected shape of the job status response
export interface JobStatus {
  id: string;
  state: 'completed' | 'failed' | 'active' | 'waiting' | 'delayed' | 'paused';
  progress: {
    stages?: ProcessingStage[];
    estimatedTimeRemaining?: number;
    // The structured error payload from the backend worker
    errorCode?: string;
    message?: string;
  } | null;
  returnValue: {
    flyerId?: number;
  } | null;
  failedReason: string | null; // The raw error string from BullMQ
}

/**
 * Custom error class for job failures to make `catch` blocks more specific.
 * This allows the UI to easily distinguish between a job failure and a network error.
 */
export class JobFailedError extends Error {
  public errorCode: string;

  constructor(message: string, errorCode: string) {
    super(message);
    this.name = 'JobFailedError';
    this.errorCode = errorCode;
  }
}

/**
 * Fetches the status of a background processing job.
 * This is the second step in the new background processing flow.
 * @param jobId The ID of the job to check.
 * @param tokenOverride Optional token for testing.
 * @returns A promise that resolves to the parsed job status object.
 * @throws A `JobFailedError` if the job has failed, or a generic `Error` for other issues.
 */
export const getJobStatus = async (
  jobId: string,
  tokenOverride?: string,
): Promise<JobStatus> => {
  const response = await authedGet(`/ai/jobs/${jobId}/status`, { tokenOverride });

  // Handle non-OK responses first, as they might not have a JSON body.
  if (!response.ok) {
    let errorMessage = `API Error: ${response.status} ${response.statusText}`;
    try {
      // Try to get a more specific message from the body.
      const errorData = await response.json();
      if (errorData.message) {
        errorMessage = errorData.message;
      }
    } catch (e) {
      // The body was not JSON, which is fine for a server error page.
      // The default message is sufficient.
      logger.warn('getJobStatus received a non-JSON error response.', { status: response.status });
    }
    throw new Error(errorMessage);
  }

  // If we get here, the response is OK (2xx). Now parse the body.
  try {
    const statusData: JobStatus = await response.json();

    // If the job itself has failed, we should treat this as an error condition
    // for the polling logic by rejecting the promise. This will stop the polling loop.
    if (statusData.state === 'failed') {
      // The structured error payload is in the 'progress' object.
      const progress = statusData.progress;
      const userMessage =
        progress?.message || statusData.failedReason || 'Job failed with an unknown error.';
      const errorCode = progress?.errorCode || 'UNKNOWN_ERROR';

      logger.error(`Job ${jobId} failed with code: ${errorCode}, message: ${userMessage}`);

      // Throw a custom, structured error so the frontend can react to the errorCode.
      throw new JobFailedError(userMessage, errorCode);
    }

    return statusData;
  } catch (error) {
    // If it's the specific error we threw, just re-throw it.
    if (error instanceof JobFailedError) {
      throw error;
    }
    // This now primarily catches JSON parsing errors on an OK response, which is unexpected.
    logger.error('getJobStatus failed to parse a successful API response.', { error });
    throw new Error('Failed to parse job status from a successful API response.');
  }
};

export const isImageAFlyer = (
  imageFile: File,
  tokenOverride?: string,
): Promise<Response> => {
  const formData = new FormData();
  formData.append('image', imageFile);

  // Use apiFetchWithAuth for FormData to let the browser set the correct Content-Type.
  // The URL must be relative, as the helper constructs the full path.
  return authedPostForm('/ai/check-flyer', formData, { tokenOverride });
};

export const extractAddressFromImage = (
  imageFile: File,
  tokenOverride?: string,
): Promise<Response> => {
  const formData = new FormData();
  formData.append('image', imageFile);

  return authedPostForm('/ai/extract-address', formData, { tokenOverride });
};

export const extractLogoFromImage = (
  imageFiles: File[],
  tokenOverride?: string,
): Promise<Response> => {
  const formData = new FormData();
  imageFiles.forEach((file) => {
    formData.append('images', file);
  });

  return authedPostForm('/ai/extract-logo', formData, { tokenOverride });
};

export const getQuickInsights = (
  items: Partial<FlyerItem>[],
  signal?: AbortSignal,
  tokenOverride?: string,
): Promise<Response> => {
  return authedPost('/ai/quick-insights', { items }, { tokenOverride, signal });
};

export const getDeepDiveAnalysis = (
  items: Partial<FlyerItem>[],
  signal?: AbortSignal,
  tokenOverride?: string,
): Promise<Response> => {
  return authedPost('/ai/deep-dive', { items }, { tokenOverride, signal });
};

export const searchWeb = (
  query: string,
  signal?: AbortSignal,
  tokenOverride?: string,
): Promise<Response> => {
  return authedPost('/ai/search-web', { query }, { tokenOverride, signal });
};

// ============================================================================
// STUBS FOR FUTURE AI FEATURES
// ============================================================================

export const planTripWithMaps = async (
  items: FlyerItem[],
  store: Store | undefined,
  userLocation: GeolocationCoordinates,
  signal?: AbortSignal,
  tokenOverride?: string,
): Promise<Response> => {
  logger.debug('Stub: planTripWithMaps called with location:', { userLocation });
  return authedPost('/ai/plan-trip', { items, store, userLocation }, { signal, tokenOverride });
};

/**
 * [STUB] Generates an image based on a text prompt using the Imagen model.
 * @param prompt A description of the image to generate (e.g., a meal plan).
 * @returns A base64-encoded string of the generated PNG image.
 */
export const generateImageFromText = (
  prompt: string,
  signal?: AbortSignal,
  tokenOverride?: string,
): Promise<Response> => {
  logger.debug('Stub: generateImageFromText called with prompt:', { prompt });
  return authedPost('/ai/generate-image', { prompt }, { tokenOverride, signal });
};

/**
 * [STUB] Converts a string of text into speech audio data.
 * @param text The text to be spoken.
 * @returns A base64-encoded string of the raw audio data.
 */
export const generateSpeechFromText = (
  text: string,
  signal?: AbortSignal,
  tokenOverride?: string,
): Promise<Response> => {
  logger.debug('Stub: generateSpeechFromText called with text:', { text });
  return authedPost('/ai/generate-speech', { text }, { tokenOverride, signal });
};

/**
 * [STUB] Initiates a real-time voice conversation session using the Live API.
 * This function is more complex and would require a WebSocket connection proxied
 * through the backend. For now, this remains a conceptual client-side function.
 * A full implementation would involve a separate WebSocket client.
 * @param callbacks An object containing onopen, onmessage, onerror, and onclose handlers.
 * @returns A promise that resolves to the live session object.
 */
export const startVoiceSession = (callbacks: {
  onopen?: () => void;
  onmessage: (message: import('@google/genai').LiveServerMessage) => void;
  onerror?: (error: ErrorEvent) => void;
  onclose?: () => void;
}): Promise<unknown> => {
  logger.debug('Stub: startVoiceSession called.', { callbacks });
  // In a real implementation, this would connect to a WebSocket endpoint on your server,
  // which would then proxy the connection to the Google AI Live API.
  // This is a placeholder and will not function.
  throw new Error(
    'Voice session feature is not fully implemented and requires a backend WebSocket proxy.',
  );
};

/*
 The following functions are server-side only and have been moved to `aiService.server.ts`.
 This file should not contain any server-side logic or direct use of `fs` or `process.env`.

 - extractItemsFromReceiptImage
 - extractCoreDataFromFlyerImage
*/

/**
 * Sends a cropped area of an image to the backend for targeted text extraction.
 * @param imageFile The original image file.
 * @param cropArea The { x, y, width, height } of the area to scan.
 * @param extractionType The type of data to look for ('store_name', 'dates', etc.).
 * @param tokenOverride Optional token for testing.
 * @returns A promise that resolves to the API response containing the extracted text.
 */
export const rescanImageArea = (
  imageFile: File,
  cropArea: { x: number; y: number; width: number; height: number },
  extractionType: 'store_name' | 'dates' | 'item_details',
  tokenOverride?: string,
): Promise<Response> => {
  const formData = new FormData();
  formData.append('image', imageFile);
  formData.append('cropArea', JSON.stringify(cropArea));
  formData.append('extractionType', extractionType);

  return authedPostForm('/ai/rescan-area', formData, { tokenOverride });
};

/**
 * Sends a user's watched items to the AI backend for price comparison.
 * @param watchedItems An array of the user's watched master grocery items.
 * @returns A promise that resolves to the raw `Response` object from the API.
 */
export const compareWatchedItemPrices = (
  watchedItems: MasterGroceryItem[],
  signal?: AbortSignal,
): Promise<Response> => {
  // Use the apiFetch wrapper for consistency with other API calls in this file.
  // This centralizes token handling and base URL logic.
  return authedPost('/ai/compare-prices', { items: watchedItems }, { signal });
};
