import fs from 'fs';
import path from 'path';
import { query } from './lib/mysql';
import { logger } from './lib/logger';
import { callLMStudio } from './lib/lmstudio';

async function main() {
  await insertGenres();
  await generatePrompts();
  process.exit();
}

async function insertGenres() {
  logger.info('Starting genre insertion...');
  try {
    const dataDir = path.join(__dirname, 'data');
    const files = fs.readdirSync(dataDir).filter((f) => f.endsWith('.json'));

    for (const file of files) {
      // skip the central genres.json (if present) since individual files represent sub-genres
      if (file === 'genres.json') continue;

      const filePath = path.join(dataDir, file);
      let raw: string;
      try {
        raw = fs.readFileSync(filePath, 'utf-8');
      } catch (err) {
        logger.warn(`Could not read file ${file}: ${err}`);
        continue;
      }

      let jsonData: any;
      try {
        jsonData = JSON.parse(raw);
      } catch (err) {
        logger.warn(`Skipping ${file} - invalid JSON: ${err}`);
        continue;
      }

      // Derive a genre from the filename (e.g. abstract_art_3d_renderings.json -> abstract_art)
      const base = path.basename(file, '.json');
      const parts = base.split('_');
      const genre = parts[0] || base;

      // Normalize to an array of items
      let items: any[] = [];
      if (Array.isArray(jsonData)) {
        items = jsonData;
      } else if (typeof jsonData === 'object' && jsonData !== null) {
        // If there is an 'items' property, prefer it
        if (Array.isArray((jsonData as any).items)) {
          items = (jsonData as any).items;
        } else {
          // Otherwise, try to collect object values that look like items (fallback)
          const vals = Object.values(jsonData);
          for (const v of vals) {
            if (Array.isArray(v)) {
              items = items.concat(v);
            } else if (v && typeof v === 'object') {
              items.push(v);
            }
          }
        }
      }

      for (const item of items) {
        if (item && typeof item === 'object') {
          await processItem(genre, item);
        }
      }
    }

    logger.info('Finished processing genres.');
  } catch (error) {
    logger.error('Error processing genres:', error);
  }
}

async function generatePrompts() {
  logger.info('Starting prompt generation...');
  try {
    const videosToUpdate = await query(
      'SELECT * FROM video WHERE image_prompt IS NULL OR video_prompt IS NULL'
    ) as any[];

    for (const video of videosToUpdate) {
      const prompt = `
        Based on the following video details, generate an image prompt and a video prompt.
        Each prompt should be around 100 words.
        The output should be a JSON object in the format: { "imagePrompt": "...", "videoPrompt": "..." }
        For video prompt, it takes  8 seconds. So start with current scene, describe action and camera work.
        Please include lighting details too.
        
        Details:
        - Genre: ${video.genre}
        - Sub-Genre: ${video.sub_genre}
        - Scene: ${video.scene}
        - Action: ${video.action}
        - Camera: ${video.camera}
      `;

      logger.info(`Generating prompt for video ID: ${video.id}`);
      logger.debug(prompt);

      let success = false;
      for (let i = 0; i < 10; i++) {
        try {
          const response = await callLMStudio(prompt);
          if (response && response.imagePrompt && response.videoPrompt) {
            await query(
              'UPDATE video SET image_prompt = ?, video_prompt = ? WHERE id = ?',
              [response.imagePrompt, response.videoPrompt, video.id]
            );
            logger.info(`Successfully updated prompts for video ID: ${video.id}`);
            success = true;
            break;
          }
        } catch (error) {
          logger.warn(`Attempt ${i + 1} failed for video ID ${video.id}:`, error);
        }
      }

      if (!success) {
        logger.error(`Failed to generate and parse prompts for video ID: ${video.id} after 10 attempts.`);
      }
    }
    logger.info('Finished generating prompts.');
  } catch (error) {
    logger.error('Error generating prompts:', error);
  }
}

async function processItem(genre: string, item: any) {
  if (item && typeof item === 'object' && !Array.isArray(item)) {
    const {
      subGenre,
      scene,
      sceneVariants,
      actions,
      action,
      camera,
      accents,
      mood,
      lighting,
      style,
    } = item;

    // Determine final scene text
    const finalScene = scene;

    /*(typeof scene === 'string' && scene.trim().length > 0)
      ? scene
      : Array.isArray(sceneVariants) && sceneVariants.length > 0
        ? sceneVariants[0]
        : '';
    */

    // Choose one random action (fall back to legacy 'action' if needed)
    let chosenAction = '';
    if (Array.isArray(actions) && actions.length > 0) {
      chosenAction = actions[0];
    } else if (typeof action === 'string') {
      chosenAction = action;
    }

    /*
    if (Array.isArray(actions) && actions.length > 0) {
      chosenAction = actions[Math.floor(Math.random() * actions.length)];
    } else if (typeof action === 'string') {
      chosenAction = action;
    }
    */

    // Choose random accents (pick 1 or 2 if available)
    let chosenAccents = '';
    if (Array.isArray(accents) && accents.length > 0) {
      const pickCount = Math.min(accents.length, Math.random() < 0.5 ? 1 : 2);
      const shuffled = accents.slice().sort(() => Math.random() - 0.5);
      chosenAccents = shuffled.slice(0, pickCount).join('; ');
    }

    // Build a meta string that includes accents, mood, lighting, and style
    const metaParts: string[] = [];
    if (chosenAccents) metaParts.push(`Accents: ${chosenAccents}`);
    if (mood) metaParts.push(`Mood: ${mood}`);
    if (lighting) metaParts.push(`Lighting: ${lighting}`);
    if (style) metaParts.push(`Style: ${style}`);
    const meta = metaParts.join(' | ');

    // Compose the action text that will be stored in the DB (so later prompt generation can use it)
    const actionText = [chosenAction, meta].filter(Boolean).join(' ').trim();
    //const actionText = [chosenAction, meta].filter(Boolean).join(' ').trim();

    // Generate image_prompt and video_prompt using LM Studio (with retries)
    let imagePrompt: string | null = null;
    let videoPrompt: string | null = null;


    // Check if the item already exists (compare stored action text so duplicates are avoided)
    const existing = await query(
      'SELECT id FROM video WHERE genre = ? AND sub_genre = ? AND scene = ? AND action = ? AND camera = ?',
      [genre, subGenre, finalScene, chosenAction, camera]
    ) as any[];

    if (existing.length > 0) {
      logger.info(`Skipped (exists): ${genre} - ${subGenre} - ${finalScene}`);
      return;
    }

    const lmInput = `
      Generate two prompts in JSON: { "imagePrompt": "...", "videoPrompt": "..." }.
      Image prompt: ~125 words describing a still image representing the scene. Include lighting, mood, style and any accents provided.
      Video prompt: ~125 words describing an 8-second piece starting from the scene, describing the action and camera movement. Include lighting, mood, style and any accents provided.

      Use the following details:
      - Genre: ${genre}
      - Sub-Genre: ${subGenre}
      - Scene: ${finalScene}
      - Action: ${chosenAction}
      - Camera: ${camera || 'neutral camera movement'}
      - Accents: ${chosenAccents || 'none'}
      - Mood: ${mood || 'n/a'}
      - Lighting: ${lighting || 'n/a'}
      - Style: ${style || 'n/a'}
    `;

    let lmResponse: any = null;
    for (let attempt = 0; attempt < 5; attempt++) {
      try {
        lmResponse = await callLMStudio(lmInput);
        if (lmResponse && (lmResponse.imagePrompt || lmResponse.videoPrompt)) {
          imagePrompt = lmResponse.imagePrompt || null;
          videoPrompt = lmResponse.videoPrompt || null;
          break;
        }
      } catch (err) {
        logger.warn(`LMStudio attempt ${attempt + 1} failed for ${genre} - ${subGenre}: ${err}`);
      }
      // small delay between attempts could be added if desired
    }

    // If LM didn't return prompts, still insert with null prompts but log warning
    if (!imagePrompt || !videoPrompt) {
      logger.warn(`LMStudio did not return both prompts for ${genre} - ${subGenre} - ${finalScene}. Inserting without prompts.`);
    }
    // Insert the new item with generated prompts
    await query(
      'INSERT INTO video (genre, sub_genre, scene, action, camera, image_prompt, video_prompt) VALUES (?, ?, ?, ?, ?, ?, ?)',
      [genre, subGenre, finalScene, chosenAction, camera, imagePrompt, videoPrompt]
    );
    logger.info(`Inserted: ${genre} - ${subGenre} - ${finalScene}`);
    logger.info(`Image prompt: ${imagePrompt}`);
    logger.info(`Video prompt: ${videoPrompt}`);

  }
}

main();
