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

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

/**
 * Utility: extract JSON substring from a text.
 * Tries to extract from fenced ```json blocks first, otherwise extracts first {...} span.
 */
function extractJsonFromText(text: string): any | null {
    if (!text || typeof text !== 'string') return null;

    // Try fenced code block with optional json language
    const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
    if (fenced && fenced[1]) {
        try {
            return JSON.parse(fenced[1].trim());
        } catch (e) {
            // fall through to brace extraction
        }
    }

    // Try to extract first {...} match (greedy between first { and last })
    const brace = text.match(/\{[\s\S]*\}/);
    if (brace && brace[0]) {
        try {
            return JSON.parse(brace[0]);
        } catch (e) {
            return null;
        }
    }

    return null;
}

/**
 * Wrapper to call LMStudio with an image and prompt, and extract JSON reliably.
 * - Uses callLMStudioWithFile to pass the image.
 * - Tries to parse JSON from response if needed.
 * - Retries up to maxRetries times (default 5) when parsing fails or an error occurs.
 */
async function callLMWithImageAndExtract(imagePath: string, prompt: string, maxRetries = 5): Promise<any | null> {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const res = await callLMStudioWithFile(imagePath, prompt);
            // callLMStudioWithFile attempts to return parsed JSON already. Accept objects directly.
            if (res && typeof res === 'object') {
                return res;
            }

            // If it returned text, try to extract JSON
            if (typeof res === 'string') {
                const parsed = extractJsonFromText(res);
                if (parsed) return parsed;
            }

            logger.warn(`callLMWithImageAndExtract: attempt ${attempt} returned unexpected result. Retrying...`);
        } catch (err) {
            logger.warn(`callLMWithImageAndExtract: attempt ${attempt} failed: ${err}`);
        }
    }

    logger.error(`callLMWithImageAndExtract: failed to get valid JSON after ${maxRetries} attempts`);
    return null;
}

/**
 * Main sweep: find DB records whose video_prompt contains 'cut' or 'zoom' (case-insensitive),
 * run multi-step LMStudio flow (object -> action -> camerawork -> final prompt) using the image,
 * and update the record.
 */
async function updatePromptsFromDB() {
    logger.info("Starting DB sweep for video_prompt containing 'cut' or 'zoom'...");

    // Banned regex per requirement
    const banned = /\b(cut|cuts|cutting|quick cut|insert|macro insert|close-?up|extreme close-?up|zoom|zooming|push-?in|pull-?out|whip|switch angle|change angle|montage|cross-?cut|smash cut|transition|meanwhile|later)\b/i;

    let rows: any[] = [];
    try {
        // Case-insensitive search for 'cut' or 'zoom' anywhere in video_prompt
        rows = (await query(
            "SELECT id, genre, sub_genre, scene, action, camera, video_prompt, image_path FROM video where (video_path = '' or video_path is null) and modified_at < '2025-08-30 09:15:33'",
        )) as any[];
    } catch (err) {
        logger.error('DB query failed while searching for problematic prompts:', err);
        return;
    }

    if (!rows || rows.length === 0) {
        logger.info("No records found with 'cut' or 'zoom' in video_prompt.");
        return;
    }

    logger.info(`Found ${rows.length} record(s) to process.`);

    for (const row of rows) {
        const id = row.id;
        const genre = row.genre || '';
        const subGenre = row.sub_genre || '';
        const scene = row.scene || '';
        const action = row.action || '';
        const camera = row.camera || '';
        const imagePathRaw = row.image_path || row.image || null;

        if (!genre || !subGenre || !scene) {
            logger.info(`Skipping id=${id} due to missing identification fields: genre='${genre}', sub_genre='${subGenre}', scene='${scene}'`);
            continue;
        }

        if (!imagePathRaw) {
            logger.info(`Skipping id=${id} because image_path is empty for this record.`);
            continue;
        }

        // Resolve the image path: if relative, make absolute based on cwd
        let imageFullPath = imagePathRaw;
        if (!path.isAbsolute(imageFullPath)) {
            imageFullPath = path.resolve(process.cwd(), imageFullPath);
        }

        if (!fs.existsSync(imageFullPath)) {
            logger.info(`Skipping id=${id} because image not found at path: ${imageFullPath}`);
            continue;
        }

        logger.info(`Processing id=${id} using image: ${imageFullPath}`);

        // Step 1: Detect main object
        const step1Prompt = `
Return exactly one JSON object and nothing else: { "mainobject": "..." }.
Look at the provided image and determine the single most prominent/main object or subject in the scene.
Answer with a short noun or short phrase (no extra commentary).
If unsure, give the best concise guess.
`;
        const step1Res = await callLMWithImageAndExtract(imageFullPath, step1Prompt, 5);
        const mainobject = (step1Res && (step1Res.mainobject || step1Res.mainObject || step1Res.object)) ? String(step1Res.mainobject || step1Res.mainObject || step1Res.object).trim() : '';

        if (!mainobject) {
            logger.warn(`id=${id} - could not detect main object. Skipping record.`);
            continue;
        }

        logger.info(`id=${id} - detected main object: ${mainobject}`);

        // Step 2: Determine best action for this scene
        const step2Prompt = `
You have access to the image and the detected main object: "${mainobject}".
Decide which single action type best fits this scene from the list:
- no action
- micro animation (animate object but small movement)
- big movement
- impossible movement

Return exactly one JSON object and nothing else: { "actiontype": "...", "action": ""}.
Do not add commentary. Choose the single best option from the list above.
`;
        const step2Res = await callLMWithImageAndExtract(imageFullPath, step2Prompt, 5);
        const actiontype = (step2Res && (step2Res.actiontype || step2Res.actionType)) ? String(step2Res.actiontype || step2Res.actionType).trim() : '';

        if (!actiontype) {
            logger.warn(`id=${id} - could not determine action type. Skipping record.`);
            continue;
        }

        logger.info(`id=${id} - decided action type: ${actiontype}`);

        // Step 3: Ask LMStudio what is the best camera work for the scene
        const step3Prompt = `
Given the image and the following information:
- main object: "${mainobject}"
- chosen action type: "${actiontype}"

From the options below pick the single best camera approach for this scene:
- static camera
- pan
- rotation
- follow the moving object
- zoom to the object
- impossible camera work

Return exactly one JSON object and nothing else: { "cameraworkType": "..." }.
Choose one of the listed options and do not add commentary.
`;
        const step3Res = await callLMWithImageAndExtract(imageFullPath, step3Prompt, 5);
        const cameraworkType = (step3Res && (step3Res.cameraworkType || step3Res.cameraWorkType || step3Res.camera)) ? String(step3Res.cameraworkType || step3Res.cameraWorkType || step3Res.camera).trim() : '';

        if (!cameraworkType) {
            logger.warn(`id=${id} - could not determine camera work. Skipping record.`);
            continue;
        }

        logger.info(`id=${id} - decided camera work: ${cameraworkType}`);

        // Step 4: Generate final video prompt using all gathered info
        const finalPromptInput = buildLMInputFromRecordWithImageInfo(
            genre,
            subGenre,
            scene,
            action,
            camera,
            mainobject,
            actiontype,
            cameraworkType
        );

        // Use wrapper to call LM and extract JSON { videoPrompt: "" }
        const finalRes = await callLMWithImageAndExtract(imageFullPath, finalPromptInput, 5);
        const videoPrompt = (finalRes && (finalRes.videoPrompt || finalRes.video_prompt || finalRes.prompt)) ? String(finalRes.videoPrompt || finalRes.video_prompt || finalRes.prompt).trim() : null;
        logger.info(`id=${id} - videoPrompt: ${videoPrompt}`);
        if (!videoPrompt) {
            logger.warn(`id=${id} - LM did not return a valid videoPrompt. Skipping record.`);
            continue;
        }

        // Check banned regex
        if (banned.test(videoPrompt)) {
            logger.info(`Generated prompt for id=${id} contains banned phrases - skipping update.`);
            logger.info(videoPrompt);
            continue;
        }


        // Update DB
        try {
            await query('UPDATE video SET video_prompt = ? WHERE id = ?', [videoPrompt, id]);
            logger.info(`Updated video_prompt for id=${id}`);
        } catch (err) {
            logger.error(`Failed to update video_prompt for id=${id}: ${err}`);
        }

    }

    logger.info('Finished DB sweep for problematic prompts.');
}

/**
 * Build final LM input for step 4, including HARD RULES and scene info.
 * The LM should return: { "videoPrompt": "..." }
 */
function buildLMInputFromRecordWithImageInfo(
    genre: string,
    subGenre: string,
    finalScene: string,
    chosenAction: string,
    camera: string,
    mainobject: string,
    actiontype: string,
    cameraworkType: string
) {
    const accents = 'none';
    const mood = 'n/a';
    const lighting = 'n/a';
    const style = 'n/a';

    const lmInput = `
Return exactly one JSON object: { "videoPrompt": "..." } and nothing else.

Write "videoPrompt" in 100–150 words, present tense, plain concrete language.

HARD RULES (must comply):
- One continuous shot ("one take", "oner"). Real-time 8 seconds. No edits.
- Fixed location and vantage. Do not change background or angle.
- Lens and focal length locked. No zooms, no close-ups that imply a lens change, no rack zoom.
- Camera motion: at most subtle pan/tilt/dolly within 1 meter while staying in the same spot.
- Keep framing consistent (e.g., medium-wide two-shot). No “another shot/meanwhile.”
- Describe: (1) main action, (2) framing & motion, (3) lighting & mood, (4) style & small accents.
- Use clear simple sentences. No metaphors or poetic language.

PROHIBITED WORDS/PHRASES (case-insensitive): 
cut, cuts, cutting, quick cut, insert, macro insert, close-up, extreme close-up,
zoom, zooms, zooming, push-in, pull-out, whip, switch angle, change angle,
montage, cross-cut, smash cut, transition, meanwhile, later.

If proximity is needed, say: "the camera glides slightly closer while staying in the same position."

Here is information of the scene, please generate prompt for the video based on these information for key "videoPrompt":
Genre: ${genre}
Sub-Genre: ${subGenre}
Scene: ${finalScene}
Existing Action Field: ${chosenAction || 'n/a'}
Existing Camera Field: ${camera || 'static or subtle movement (stay within scene)'}
Detected Main Object: ${mainobject}
Suggested Action Type: ${actiontype}
Suggested Camera Work: ${cameraworkType}
Accents: ${accents}
Mood: ${mood}
Lighting: ${lighting}
Style: ${style}
`;

    return lmInput;
}

main();
