import asyncio
import random
from datetime import datetime
from typing import Any

from celery.exceptions import SoftTimeLimitExceeded
from motor.motor_asyncio import AsyncIOMotorClient

from ..core.config import settings
from ..core.logging import get_logger
from ..models.job import JobStatus
from ..services import cost_tracker
from ..services.gcs import gcs_path, gcs_service, upload_vtt_to_gcs
from ..services.gemini import gemini_service
from ..services.gemini_tts import TTSSynthesisError
from . import celery_app
from ._websocket_bridge import broadcast_status_update

logger = get_logger(__name__)


async def retry_with_backoff(func, max_retries=3, base_delay=1):
    """Retry a function with exponential backoff"""
    last_exception = None

    for attempt in range(max_retries):
        try:
            return await func()
        except Exception as e:
            last_exception = e
            if attempt == max_retries - 1:
                break

            # Exponential backoff with jitter
            delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
            logger.warning(f"Attempt {attempt + 1} failed, retrying in {delay:.2f}s: {e}")
            await asyncio.sleep(delay)

    raise last_exception


async def _mark_task_timed_out(job_id: str) -> None:
    """Mark a job as failed due to task timeout and notify via WebSocket"""
    try:
        client = AsyncIOMotorClient(settings.mongodb_uri)
        db = client[settings.mongodb_db]
        job_doc = await db.jobs.find_one({"_id": job_id}, {"status": 1, "title": 1})
        if not job_doc:
            return
        current_status = job_doc.get("status", "")
        # Map current processing state to appropriate failure status
        if current_status in ("tts_generating", "translating"):
            fail_status = JobStatus.TTS_FAILED.value
        else:
            fail_status = current_status  # Leave as-is for other states
        await db.jobs.update_one(
            {"_id": job_id},
            {"$set": {
                "status": fail_status,
                "updated_at": datetime.utcnow(),
                "error": "Processing timed out. Please retry."
            }}
        )
        broadcast_status_update(
            job_id=job_id,
            status=fail_status,
            job_title=job_doc.get("title"),
            message="Processing timed out. Please retry."
        )
    except Exception as e:
        logger.error(f"Failed to mark job {job_id} as timed out: {e}")


@celery_app.task(bind=True, time_limit=3600, soft_time_limit=3400)
def translate_and_synthesize_task(self, job_id: str, languages: list[str] | None = None, retranslate: bool = False):
    """
    Pipeline 2: Translation & MP3 Generation
    Triggered when job status changes to 'approved_english'.
    When called with languages/retranslate=True, re-translates only those languages
    from the updated source VTT and returns job to PENDING_QC.
    """
    logger.info(f"🚀 CELERY TASK STARTED: translate_and_synthesize_task for job {job_id} (retranslate={retranslate}, languages={languages})")

    try:
        logger.info(f"📝 About to call asyncio.run for job {job_id}")
        result = asyncio.run(_async_translate_and_synthesize(job_id, languages=languages, retranslate=retranslate))
        logger.info(f"✅ CELERY TASK COMPLETED successfully for job {job_id}")
        return result
    except SoftTimeLimitExceeded:
        logger.error(f"⏰ translate_and_synthesize_task soft time limit exceeded for job {job_id}")
        import asyncio as _asyncio
        _asyncio.run(_mark_task_timed_out(job_id))
        raise
    except Exception as e:
        logger.error(f"❌ CELERY TASK FAILED for job {job_id}: {str(e)}")
        logger.error(f"❌ Exception type: {type(e).__name__}")
        logger.error(f"❌ Exception args: {e.args}")
        import traceback
        logger.error(f"❌ Full traceback: {traceback.format_exc()}")
        raise


async def _async_translate_and_synthesize(job_id: str, languages: list[str] | None = None, retranslate: bool = False):
    """Async implementation of translation and synthesis"""
    logger.info(f"🔄 ASYNC FUNCTION STARTED: _async_translate_and_synthesize for job {job_id}")

    # Connect to MongoDB
    logger.info(f"📡 Connecting to MongoDB for job {job_id}")
    client = AsyncIOMotorClient(settings.mongodb_uri)
    db = client[settings.mongodb_db]
    logger.info(f"📡 MongoDB connection established for job {job_id}")

    try:
        # Get job details
        logger.info(f"🔍 Looking up job document for job {job_id}")
        job_doc = await db.jobs.find_one({"_id": job_id})
        if not job_doc:
            logger.error(f"❌ Job {job_id} not found in database!")
            raise ValueError(f"Job {job_id} not found")

        job_title = job_doc.get("title", "Untitled Job")
        logger.info(f"✅ Found job document for {job_id} ({job_title}), status: {job_doc.get('status', 'UNKNOWN')}")
        _cost_ctx = {
            "user_id": job_doc.get("client_id", "system"),
            "job_id": job_id,
            "project_id": job_doc.get("cost_tracker_project_id"),
        }

        # Check for valid status to process translation
        # Valid statuses: approved_english, approved_source (legacy), translating, or pending_qc (retranslation)
        current_status = job_doc["status"]
        valid_statuses = [
            JobStatus.APPROVED_ENGLISH.value,
            JobStatus.APPROVED_SOURCE.value,
            JobStatus.TRANSLATING.value,
            JobStatus.PENDING_QC.value,  # allowed when retranslate=True
        ]
        if current_status not in valid_statuses:
            logger.warning(f"⚠️ Job {job_id} not in valid status for translation (current: {current_status}), skipping")
            return

        # Get source language from job
        source_language = job_doc["source"].get("language", "en")
        logger.info(f"✅ Job {job_id} is in correct status, proceeding with translation (source: {source_language})")

        # Update status to translating
        await db.jobs.update_one(
            {"_id": job_id},
            {
                "$set": {
                    "status": JobStatus.TRANSLATING.value,
                    "updated_at": datetime.utcnow()
                },
                "$push": {
                    "review.history": {
                        "at": datetime.utcnow(),
                        "status": JobStatus.TRANSLATING.value,
                        "by": "system"
                    }
                }
            }
        )

        # Glossary: lazy-loaded per target language during the loop
        from ..services.glossary_service import get_glossary_block_for_job

        sdh_requested = job_doc["requested_outputs"].get("sdh_vtt", False)

        # Source language VTT is the EN master for all translations
        source_outputs = job_doc["outputs"].get(source_language)
        if not source_outputs:
            raise ValueError(f"No outputs found for source language {source_language}")

        captions_blob_path = source_outputs["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
        ad_blob_path = source_outputs["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
        source_captions_vtt = gcs_service.bucket.blob(captions_blob_path).download_as_text()
        source_ad_vtt = gcs_service.bucket.blob(ad_blob_path).download_as_text()

        source_sdh_vtt = None
        if sdh_requested and source_outputs.get("sdh_captions_vtt_gcs"):
            sdh_blob_path = source_outputs["sdh_captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            source_sdh_vtt = gcs_service.bucket.blob(sdh_blob_path).download_as_text()

        # Process each requested language (filtered when retranslating a subset)
        requested_languages = job_doc["requested_outputs"]["languages"]
        if languages is not None:
            requested_languages = [lg for lg in requested_languages if lg in languages]
        transcreation_languages = job_doc["requested_outputs"]["transcreation"]

        updated_outputs = job_doc.get("outputs", {})
        _source_text_for_glossary = " ".join(filter(None, [source_captions_vtt, source_ad_vtt]))
        _failed_languages: list[str] = []

        try:
            target_languages = [lang for lang in requested_languages if lang != source_language]

            for language in target_languages:
                _style = "transcreate" if language in transcreation_languages else "literal"
                logger.info(f"Processing language: {language} (source: {source_language}, style: {_style})")

                await cost_tracker.aio_preflight(
                    model=gemini_service.model_name,
                    user_external_id=_cost_ctx["user_id"],
                    project_id=_cost_ctx["project_id"],
                )

                _job_for_glossary = {**job_doc, "_glossary_source_text": _source_text_for_glossary}
                _glossary = await get_glossary_block_for_job(_job_for_glossary, language, db)

                try:
                    async def translate_captions(_lang=language, _gloss=_glossary, _s=_style):
                        return await gemini_service.translate_vtt(
                            source_captions_vtt, _lang, source_language=source_language,
                            glossary_block=_gloss, style=_s, _cost_ctx=_cost_ctx,
                        )

                    async def translate_ad(_lang=language, _gloss=_glossary, _s=_style):
                        return await gemini_service.translate_vtt(
                            source_ad_vtt, _lang, source_language=source_language,
                            glossary_block=_gloss, style=_s, _cost_ctx=_cost_ctx,
                        )

                    translated_captions = await retry_with_backoff(translate_captions, max_retries=3)
                    translated_ad = await retry_with_backoff(translate_ad, max_retries=3)

                    # Validate cue alignment against EN master
                    from ..lib.vtt import VTTEditor
                    VTTEditor.assert_cue_alignment(source_captions_vtt, translated_captions, language)
                    VTTEditor.assert_cue_alignment(source_ad_vtt, translated_ad, language)

                    captions_gcs_uri = await upload_vtt_to_gcs(
                        translated_captions, gcs_path(job_doc, language, "captions.vtt")
                    )
                    ad_gcs_uri = await upload_vtt_to_gcs(
                        translated_ad, gcs_path(job_doc, language, "ad.vtt")
                    )

                    origin = "transcreate" if _style == "transcreate" else "gemini_translate"
                    lang_out: dict = {
                        "captions_vtt_gcs": captions_gcs_uri,
                        "ad_vtt_gcs": ad_gcs_uri,
                        "origin": origin,
                    }

                    if sdh_requested and source_sdh_vtt:
                        async def translate_sdh(_lang=language, _gloss=_glossary, _s=_style):
                            return await gemini_service.translate_vtt(
                                source_sdh_vtt, _lang, source_language=source_language,
                                glossary_block=_gloss, style=_s, _cost_ctx=_cost_ctx,
                            )
                        translated_sdh = await retry_with_backoff(translate_sdh, max_retries=3)
                        sdh_gcs_uri = await upload_vtt_to_gcs(
                            translated_sdh, gcs_path(job_doc, language, "sdh_captions.vtt")
                        )
                        lang_out["sdh_captions_vtt_gcs"] = sdh_gcs_uri

                    try:
                        from ..services.descriptive_transcript import (
                            generate_descriptive_transcript,
                        )
                        transcript_text = generate_descriptive_transcript(translated_captions, translated_ad)
                        if transcript_text:
                            transcript_gcs_uri = await upload_vtt_to_gcs(
                                transcript_text, gcs_path(job_doc, language, "descriptive_transcript.txt")
                            )
                            lang_out["descriptive_transcript_gcs"] = transcript_gcs_uri
                    except Exception as transcript_err:
                        logger.warning(f"Failed to generate descriptive transcript for {language}: {transcript_err}")

                    updated_outputs[language] = lang_out
                    logger.info(f"Processed language: {language} (origin: {origin})")

                except Exception as e:
                    logger.error(f"Failed to process language {language}: {e}", exc_info=True)
                    _failed_languages.append(language)
                    # Preserve existing GCS URIs and origin so retranslation failure
                    # doesn't destroy captions the user can still view
                    existing = updated_outputs.get(language, {})
                    updated_outputs[language] = {
                        **existing,
                        "qa_notes": f"Translation failed: {str(e)[:200]}",
                    }

        finally:
            pass

        # Update status to TTS generating.
        # Use per-language dot-notation so concurrent single-language retranslation tasks
        # don't overwrite each other's results (concurrency:2 race condition).
        per_lang_updates = {
            f"outputs.{lang}": updated_outputs[lang]
            for lang in target_languages
            if lang in updated_outputs
        }
        _status_update: dict = {
            "status": JobStatus.TTS_GENERATING.value,
            "updated_at": datetime.utcnow(),
            **per_lang_updates,
        }
        if _failed_languages:
            _status_update["translation_errors"] = _failed_languages
            logger.warning(f"Job {job_id}: translation failed for languages: {_failed_languages}")
        await db.jobs.update_one(
            {"_id": job_id},
            {
                "$set": _status_update,
                "$push": {
                    "review.history": {
                        "at": datetime.utcnow(),
                        "status": JobStatus.TTS_GENERATING.value,
                        "by": "system"
                    }
                }
            }
        )

        # Generate TTS when MP3 output or accessible video is requested.
        # accessible_video rendering requires per-cue MP3s (ad_cue_manifest) produced by TTS,
        # so TTS must run even when the final assembled ad.mp3 is not requested as a download.
        accessible_video_requested = job_doc["requested_outputs"].get("accessible_video_mp4", False)

        if job_doc["requested_outputs"]["audio_description_mp3"] or accessible_video_requested:
            # Get TTS preferences from job
            tts_preferences = job_doc["requested_outputs"].get("tts_preferences", {})
            # For retranslation, only regenerate TTS for the specific target languages —
            # not all languages. Regenerating all is wasteful and triggers unwanted renders.
            tts_outputs = (
                {lang: updated_outputs[lang] for lang in target_languages if lang in updated_outputs}
                if retranslate
                else updated_outputs
            )
            await _generate_tts_for_languages(
                job_id, tts_outputs, db, source_language, tts_preferences, accessible_video_requested,
                user_id=_cost_ctx["user_id"], cost_project_id=_cost_ctx["project_id"],
            )

        # Update final status
        # When retranslating from QC, always return straight to PENDING_QC (skip video render)
        if retranslate:
            await db.jobs.update_one(
                {"_id": job_id},
                {
                    "$set": {
                        "status": JobStatus.PENDING_QC.value,
                        "updated_at": datetime.utcnow()
                    },
                    "$push": {
                        "review.history": {
                            "at": datetime.utcnow(),
                            "status": JobStatus.PENDING_QC.value,
                            "by": "system"
                        }
                    }
                }
            )
            broadcast_status_update(
                job_id,
                JobStatus.PENDING_QC.value,
                job_title=job_title,
                message=f"{job_title} re-translation complete — ready for QC Review"
            )
            return

        # NEW WORKFLOW: Translation pipeline now ends at PENDING_QC for QC review
        # If accessible video is requested, the render task will handle the transition
        # to PENDING_QC when all videos are complete
        if not accessible_video_requested:
            await db.jobs.update_one(
                {"_id": job_id},
                {
                    "$set": {
                        "status": JobStatus.PENDING_QC.value,
                        "updated_at": datetime.utcnow()
                    },
                    "$push": {
                        "review.history": {
                            "at": datetime.utcnow(),
                            "status": JobStatus.PENDING_QC.value,
                            "by": "system"
                        }
                    }
                }
            )

            # Broadcast status update
            broadcast_status_update(
                job_id,
                JobStatus.PENDING_QC.value,
                job_title=job_title,
                message=f"{job_title} has finished translation and audio generation - ready for QC Review"
            )
        else:
            # accessible_video_mp4=True: render tasks were dispatched from within
            # _generate_language_tts for each language. Stay in TTS_GENERATING;
            # render_accessible_video_task will transition to PENDING_QC when all videos complete.
            logger.info(
                f"Accessible video rendering dispatched for job {job_id}. "
                f"Staying in TTS_GENERATING until all videos are complete."
            )

        logger.info(f"Successfully completed translation and synthesis for job {job_id}")

    except TTSSynthesisError:
        # TTSSynthesisError is already handled in _generate_tts_for_languages
        # which sets status to TTS_FAILED - just re-raise
        raise

    except Exception as e:
        logger.error(f"Translation and synthesis failed for job {job_id}: {e}")

        # Determine appropriate failure step based on current job state
        current_job = await db.jobs.find_one({"_id": job_id}, {"status": 1})
        current_status = current_job.get("status") if current_job else None

        if current_status == JobStatus.TTS_GENERATING.value:
            failure_step = "tts"
        elif current_status == JobStatus.TRANSLATING.value:
            failure_step = "translation"
        else:
            failure_step = "tts"

        now = datetime.utcnow()
        await db.jobs.update_one(
            {"_id": job_id},
            {
                "$set": {
                    "status": JobStatus.PROCESSING_FAILED.value,
                    "failure": {
                        "step": failure_step,
                        "type": type(e).__name__,
                        "message": str(e)[:500],
                        "retriable": True,
                        "occurred_at": now,
                        "retry_count": 0,
                    },
                    "error": {
                        "type": f"{failure_step}_failure",
                        "message": str(e),
                        "timestamp": now.isoformat(),
                    },
                    "updated_at": now,
                },
                "$push": {
                    "review.history": {
                        "at": now,
                        "status": JobStatus.PROCESSING_FAILED.value,
                        "by": "system",
                        "notes": f"Pipeline failed ({failure_step}): {str(e)[:200]}",
                    }
                },
            }
        )

        # Broadcast failure status
        broadcast_status_update(
            job_id,
            JobStatus.PROCESSING_FAILED.value,
            job_title=job_title,
            message=f"Processing failed: {str(e)[:100]}"
        )

        raise

    finally:
        client.close()


async def _generate_tts_for_languages(
    job_id: str,
    outputs: dict[str, Any],
    db,
    source_language: str = "en",
    tts_preferences: dict = None,
    accessible_video_requested: bool = False,
    user_id: str = "system",
    cost_project_id: str = None,
):
    """Generate TTS audio for each language's audio description.

    Raises TTSSynthesisError if any cue fails after retries, transitioning job to tts_failed status.
    """
    if tts_preferences is None:
        tts_preferences = {}

    try:
        # Always generate source language MP3 first
        if source_language in outputs and "ad_vtt_gcs" in outputs[source_language]:
            await _generate_language_tts(
                job_id, source_language, outputs[source_language], db, tts_preferences, accessible_video_requested,
                user_id=user_id, cost_project_id=cost_project_id,
            )

        # Generate for other languages
        for language, lang_output in outputs.items():
            if language != source_language and "ad_vtt_gcs" in lang_output:
                await _generate_language_tts(
                    job_id, language, lang_output, db, tts_preferences, accessible_video_requested,
                    user_id=user_id, cost_project_id=cost_project_id,
                )

    except TTSSynthesisError as e:
        logger.error(
            f"TTS synthesis hard failure for job {job_id}: cue {e.cue_index}, "
            f"text: {e.cue_text[:50]}{'...' if len(e.cue_text) > 50 else ''}, error: {e}"
        )

        # Update job status to tts_failed
        await db.jobs.update_one(
            {"_id": job_id},
            {
                "$set": {
                    "status": JobStatus.TTS_FAILED.value,
                    "error": {
                        "type": "tts_synthesis_failure",
                        "cue_index": e.cue_index,
                        "cue_text": e.cue_text[:200],  # Truncate for storage
                        "message": str(e),
                        "api_response_info": e.api_response_info,
                        "timestamp": datetime.utcnow().isoformat()
                    },
                    "updated_at": datetime.utcnow()
                },
                "$push": {
                    "review.history": {
                        "at": datetime.utcnow(),
                        "status": JobStatus.TTS_FAILED.value,
                        "by": "system",
                        "notes": f"TTS failed for cue {e.cue_index}: {str(e)[:200]}"
                    }
                }
            }
        )

        # Broadcast failure status
        job_doc = await db.jobs.find_one({"_id": job_id})
        broadcast_status_update(
            job_id,
            JobStatus.TTS_FAILED.value,
            job_title=job_doc.get("title") if job_doc else None,
            message=f"TTS generation failed for cue {e.cue_index}. Manual reprocessing required."
        )

        # Re-raise to stop the pipeline
        raise


async def _generate_language_tts(job_id: str, language: str, lang_output: dict, db, tts_preferences: dict = None, accessible_video_requested: bool = False, user_id: str = "system", cost_project_id: str = None):
    """
    Generate TTS for a specific language using parallel cue synthesis.

    Dispatches per-cue tasks to the dedicated TTS worker queue (concurrency=8),
    then assembles the results into a combined MP3.
    """
    import io

    from celery.result import allow_join_result
    from pydub import AudioSegment

    from .tts_synthesis import (
        dispatch_language_tts,
        parse_ad_cues,
        synthesize_cue_task,
        update_vtt_in_gcs,
    )

    if tts_preferences is None:
        tts_preferences = {}

    try:
        # Download AD VTT content
        ad_blob_path = lang_output["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
        ad_blob = gcs_service.bucket.blob(ad_blob_path)
        ad_vtt_content = ad_blob.download_as_text()

        # Parse VTT to extract cues
        cues = parse_ad_cues(ad_vtt_content)
        if not cues:
            logger.warning(f"No AD cues found for job {job_id}, language {language}")
            return

        logger.info(
            f"Generating TTS for {language} with {len(cues)} cues using parallel dispatch. "
            f"Provider={tts_preferences.get('provider', 'gemini')}, "
            f"accessible_video={accessible_video_requested}"
        )

        # Preflight budget check before dispatching TTS
        from .tts_synthesis import _TTS_MODEL_STRINGS
        tts_model_key = tts_preferences.get("model", "flash")
        await cost_tracker.aio_preflight(
            model=_TTS_MODEL_STRINGS.get(tts_model_key, tts_model_key),
            user_external_id=user_id,
            project_id=cost_project_id,
        )

        # Dispatch parallel cue synthesis tasks to TTS worker
        group_result = dispatch_language_tts(
            job_id=job_id,
            language=language,
            cues=cues,
            tts_preferences=tts_preferences,
            user_id=user_id,
            cost_project_id=cost_project_id,
        )

        if group_result is None:
            logger.warning(f"No cues to synthesize for job {job_id}, language {language}")
            return

        # Poll for group completion with async sleep (max 15 minutes per language)
        TTS_POLL_TIMEOUT_SECONDS = 900
        poll_count = 0
        while not group_result.ready():
            await asyncio.sleep(1.0)
            poll_count += 1
            if poll_count % 30 == 0:
                logger.info(
                    f"Waiting for TTS group ({language}): {poll_count}s elapsed, "
                    f"completed={group_result.completed_count()}/{len(cues)}"
                )
            if poll_count >= TTS_POLL_TIMEOUT_SECONDS:
                logger.error(
                    f"TTS group timed out for job {job_id}, language {language} "
                    f"after {TTS_POLL_TIMEOUT_SECONDS}s — "
                    f"{group_result.completed_count()}/{len(cues)} cues completed"
                )
                raise TTSSynthesisError(
                    message=(
                        f"TTS synthesis timed out after {TTS_POLL_TIMEOUT_SECONDS}s "
                        f"({group_result.completed_count()}/{len(cues)} cues completed)"
                    ),
                    cue_index=group_result.completed_count(),
                    cue_text="",
                    api_response_info="timeout"
                )

        # Get results from all cue tasks
        # Use propagate=False to get results even if some tasks failed
        try:
            with allow_join_result():
                cue_results = group_result.get(timeout=60, propagate=False)
        except Exception as e:
            # Catch any unexpected errors from group.get() (connection issues, timeouts, etc.)
            logger.error(f"Failed to get TTS group results for {language}: {e}")
            raise TTSSynthesisError(
                message=f"Failed to retrieve TTS results: {e}",
                cue_index=0,
                cue_text="",
                api_response_info=str(e)
            ) from e

        # Handle case where results contain exceptions (shouldn't happen with new task design, but safety net)
        processed_results = []
        for i, result in enumerate(cue_results):
            if isinstance(result, Exception):
                # Convert exception to failure dict
                logger.error(f"TTS cue {i} returned exception: {result}")
                processed_results.append({
                    "cue_index": i,
                    "success": False,
                    "error_message": str(result),
                    "text": cues[i].get("text", "") if i < len(cues) else ""
                })
            else:
                processed_results.append(result)

        cue_results = processed_results

        # Check for failures and attempt automatic rewrite+retry
        failed_cues = [r for r in cue_results if not r.get("success", False)]
        if failed_cues:
            logger.info(f"TTS failed for {len(failed_cues)} cue(s), attempting automatic rewrite")

            # Extract TTS settings for retry (same as dispatch_language_tts)
            voices_per_language = tts_preferences.get("voices_per_language", {})
            voice_name = voices_per_language.get(language, tts_preferences.get("default_voice"))
            provider = tts_preferences.get("provider", "gemini")
            model = tts_preferences.get("model", "flash")
            speed = tts_preferences.get("speed", 1.0)
            style_preset = tts_preferences.get("style_preset", "neutral")
            custom_style_prompt = tts_preferences.get("custom_style_prompt")

            if style_preset == "custom" and custom_style_prompt:
                style_prompt = custom_style_prompt
            else:
                style_prompt = settings.gemini_tts_style_prompts.get(style_preset, "")

            rewrites_made = []
            still_failed = []

            for failure in failed_cues:
                cue_idx = failure["cue_index"]
                original_text = failure.get("text", "")

                try:
                    # Step 1: Get Gemini rewrite
                    logger.info(f"Requesting Gemini rewrite for cue {cue_idx}: '{original_text[:50]}...'")
                    rewritten_text = await gemini_service.rewrite_tts_cue(original_text, language)

                    # Step 2: Update VTT in GCS
                    await update_vtt_in_gcs(job_id, language, cue_idx, rewritten_text)

                    # Step 3: Retry TTS with rewritten text
                    logger.info(f"Retrying TTS for cue {cue_idx} with rewritten text")
                    retry_result = synthesize_cue_task.apply_async(
                        kwargs={
                            "job_id": job_id,
                            "language": language,
                            "cue_index": cue_idx,
                            "text": rewritten_text,
                            "start_time": failure["start_time"],
                            "end_time": failure["end_time"],
                            "voice_name": voice_name,
                            "provider": provider,
                            "model": model,
                            "speed": speed,
                            "style_prompt": style_prompt
                        },
                        queue="tts"
                    )

                    # Wait for retry result
                    with allow_join_result():
                        retry_cue_result = retry_result.get(timeout=120)

                    if retry_cue_result.get("success"):
                        # Success! Record rewrite
                        logger.info(f"Rewrite+retry succeeded for cue {cue_idx}")
                        rewrites_made.append({
                            "language": language,
                            "cue_index": cue_idx,
                            "original_text": original_text,
                            "rewritten_text": rewritten_text,
                            "timestamp": datetime.utcnow().isoformat()
                        })
                        # Update cue_results with successful result
                        for i, r in enumerate(cue_results):
                            if r["cue_index"] == cue_idx:
                                cue_results[i] = retry_cue_result
                                break
                    else:
                        logger.warning(f"TTS still failed after rewrite for cue {cue_idx}")
                        still_failed.append({**failure, "rewritten_text": rewritten_text})

                except Exception as e:
                    logger.error(f"Rewrite attempt failed for cue {cue_idx}: {e}")
                    still_failed.append(failure)

            # Store rewrite history in job for UI caution display
            if rewrites_made:
                await db.jobs.update_one(
                    {"_id": job_id},
                    {
                        "$push": {"tts_rewrites": {"$each": rewrites_made}},
                        "$set": {"updated_at": datetime.utcnow()}
                    }
                )
                logger.info(f"Recorded {len(rewrites_made)} TTS rewrite(s) for job {job_id}")

            # If any cues still failed after rewrite, raise error
            if still_failed:
                first_failure = still_failed[0]
                raise TTSSynthesisError(
                    message=f"{len(still_failed)} cue(s) failed even after rewrite: {first_failure.get('error_message', 'Unknown error')}",
                    cue_index=first_failure["cue_index"],
                    cue_text=first_failure.get("rewritten_text", first_failure.get("text", "")),
                    api_response_info=first_failure.get("error_message")
                )

        logger.info(f"All {len(cue_results)} TTS cues synthesized for {language}, assembling combined MP3")

        # Download cue segments from GCS and assemble combined MP3
        audio_segments = []
        current_audio_position = 0.0

        # Sort results by cue index to ensure correct ordering
        sorted_results = sorted(cue_results, key=lambda r: r["cue_index"])

        for result in sorted_results:
            target_start_time = result["start_time"]

            # Add silence to reach the exact VTT start time
            if target_start_time > current_audio_position:
                silence_duration = target_start_time - current_audio_position
                silence = AudioSegment.silent(duration=int(silence_duration * 1000))
                audio_segments.append(silence)
                current_audio_position = target_start_time

            # Download cue audio from GCS
            gcs_uri = result["gcs_uri"]
            blob_path = gcs_uri.replace(f"gs://{settings.gcs_bucket}/", "")
            blob = gcs_service.bucket.blob(blob_path)
            audio_bytes = blob.download_as_bytes()

            # Add to combined audio
            audio_segment = AudioSegment.from_file(io.BytesIO(audio_bytes), format="mp3")
            audio_segments.append(audio_segment)

            # Update position based on actual audio duration
            current_audio_position += result["duration"]

        # Combine all segments
        if audio_segments:
            final_audio = sum(audio_segments, AudioSegment.empty())
        else:
            final_audio = AudioSegment.silent(duration=1000)

        # Export combined to MP3
        output_buffer = io.BytesIO()
        final_audio.export(output_buffer, format="mp3", bitrate="128k")
        mp3_data = output_buffer.getvalue()

        # Fetch job_doc for gcs_path resolution (needed for gcs_prefix on newer jobs)
        job_doc = await db.jobs.find_one({"_id": job_id})
        if not job_doc:
            raise ValueError(f"Job {job_id} not found when assembling TTS MP3")

        # Upload combined MP3 to GCS
        mp3_blob_path = gcs_path(job_doc, language, "ad.mp3")
        mp3_blob = gcs_service.bucket.blob(mp3_blob_path)
        mp3_blob.content_type = "audio/mpeg"
        mp3_blob.upload_from_string(mp3_data, content_type="audio/mpeg")

        mp3_gcs_uri = f"gs://{settings.gcs_bucket}/{mp3_blob_path}"

        # Build per-cue manifest for safe re-rendering (prevents MP3/VTT desync on cue insert/delete)
        ad_cue_manifest = [
            {
                "cue_index": r["cue_index"],
                "gcs_uri": r["gcs_uri"],
                "text": r.get("text", "")[:80],
                "duration_s": r.get("duration", 0.0)
            }
            for r in sorted_results
            if r.get("success") and r.get("gcs_uri")
        ]

        # Update job outputs
        ad_cues_gcs_prefix = f"gs://{settings.gcs_bucket}/{job_id}/{language}/ad_cues/"
        update_fields = {
            f"outputs.{language}.ad_mp3_gcs": mp3_gcs_uri,
            f"outputs.{language}.ad_cues_gcs_prefix": ad_cues_gcs_prefix,
            f"outputs.{language}.ad_cue_manifest": ad_cue_manifest,
            "updated_at": datetime.utcnow()
        }

        await db.jobs.update_one(
            {"_id": job_id},
            {"$set": update_fields}
        )

        logger.info(f"Successfully generated TTS for {language} ({len(cue_results)} cues)")

        # Broadcast asset ready notification immediately
        # This allows frontend to update in real-time as each MP3 becomes available
        broadcast_status_update(
            job_id,
            "asset_ready",
            job_title=job_doc.get("title"),
            message=f"Audio description MP3 ready for {language.upper()}"
        )

        # Trigger accessible video rendering if requested
        if accessible_video_requested:

            # Initialize progress tracking for this language
            await db.jobs.update_one(
                {"_id": job_id},
                {
                    "$set": {
                        f"accessible_video_progress.{language}": {
                            "status": "pending"
                        }
                    }
                }
            )

            from ..services.cloud_run_dispatch import dispatch as _cr_dispatch
            await _cr_dispatch("render", job_id, language=language)
            logger.info(f"Triggered accessible video rendering for job {job_id}/{language}")

    except TTSSynthesisError:
        # Let TTSSynthesisError propagate up to _generate_tts_for_languages
        # where it will be caught and handled (job status set to tts_failed)
        raise
    except Exception as e:
        # For non-TTS errors (e.g., GCS failures), log and re-raise
        logger.error(f"TTS generation failed for {language}: {e}")

        # Update with error note
        await db.jobs.update_one(
            {"_id": job_id},
            {
                "$set": {
                    f"outputs.{language}.qa_notes": f"TTS generation failed: {str(e)}",
                    "updated_at": datetime.utcnow()
                }
            }
        )
        raise
