from datetime import datetime
from typing import Optional

from bson import ObjectId
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile, status
from fastapi.responses import StreamingResponse
from motor.motor_asyncio import AsyncIOMotorDatabase

from ...core.config import settings
from ...core.database import get_database
from ...core.dependencies import get_accessible_project_ids, get_current_user, require_roles
from ...core.logging import get_logger
from ...lib.vtt import VTTEditor
from ...models.job import JobStatus, RequestedOutputs
from ...models.user import User, UserRole
from ...schemas.job import (
    ApproveEnglishRequest,
    ApproveSourceRequest,
    AssetValidationResponse,
    BulkApproveRequest,
    BulkApproveResponse,
    BulkDeleteRequest,
    BulkDeleteResponse,
    BulkDownloadRequest,
    BulkReturnToQCRequest,
    BulkReturnToQCResponse,
    CompleteJobRequest,
    JobDeleteResponse,
    JobDownloadsResponse,
    JobListResponse,
    JobResponse,
    JobUpdateRequest,
    RejectJobRequest,
    ReturnToQCRequest,
    UpdateTTSPreferencesRequest,
    VttContentResponse,
    VttTimingAdjustRequest,
    VttUpdateRequest,
)
from ...schemas.accessible_video import (
    AccessibleVideoEditStateResponse,
    PausePointResponse,
    PausePointUpdateRequest,
    RerenderAccessibleVideoRequest,
    TTSRegenerationItem,
    TTSRegenerationQueueRequest,
    TTSRegenerationRemoveRequest,
    VideoSegmentResponse,
)
from ...models.job import TTSRegenerationRequest
from ...services.websocket import connection_manager
from ...services.gcs import (
    gcs_service,
    upload_file_to_gcs,
    upload_vtt_to_gcs,
)
from ...services.zip_download import generate_zip_filename, generate_zip_stream
from ...services.validation import asset_validation_service
from ...tasks import celery_app
from ...tasks.ingest_and_ai import ingest_and_ai_task
from ...tasks.translate_and_synthesize import translate_and_synthesize_task
from ...services.audit_logger import audit_logger, log_job_action
from ...models.audit_log import AuditAction
from ...services import vtt_versioning
from ...services import language_qc as lqc

logger = get_logger(__name__)
router = APIRouter(prefix="/jobs", tags=["jobs"])


@router.post("", response_model=JobResponse, status_code=status.HTTP_201_CREATED)
async def create_job(
    title: str = Form(...),
    requested_outputs: str = Form(...),  # JSON string
    file: UploadFile = File(...),
    brand_context: Optional[str] = Form(None),
    project_id: Optional[str] = Form(None),
    request: Request = None,
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    # Validate file type
    if not file.content_type or not file.content_type.startswith("video/"):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="File must be a video"
        )

    # Parse requested outputs
    import json
    try:
        outputs_data = json.loads(requested_outputs)
        outputs = RequestedOutputs(**outputs_data)
    except Exception:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid requested_outputs format"
        )

    # Generate job ID and upload file
    job_id = str(ObjectId())
    gcs_uri = await upload_file_to_gcs(
        file, f"{job_id}/source.mp4"
    )

    # Create job document
    # Language is always "auto" - will be detected by AI during processing
    job_data = {
        "_id": job_id,
        "client_id": str(current_user.id),
        "title": title,
        "source": {
            "filename": f"{job_id}/source.mp4",
            "original_filename": file.filename,
            "gcs_uri": gcs_uri,
            "language": "auto",
        },
        "requested_outputs": outputs.dict(),
        "status": JobStatus.CREATED.value,
        "review": {
            "notes": "",
            "history": [{
                "at": datetime.utcnow(),
                "status": JobStatus.CREATED.value,
                "by": "system"
            }]
        },
        "brand_context": brand_context or None,
        "project_id": project_id or None,
        "created_at": datetime.utcnow(),
        "updated_at": datetime.utcnow()
    }

    await db.jobs.insert_one(job_data)

    # Enqueue processing task
    logger.info(f"Dispatching ingest_and_ai_task for job {job_id}")
    logger.info(f"Using Celery app: {celery_app}")
    logger.info(f"Task object: {ingest_and_ai_task}")
    logger.info(f"Task routing config: {celery_app.conf.task_routes}")
    try:
        # Use apply_async for more control and debugging
        task = ingest_and_ai_task.apply_async(args=[job_id], queue='ingest')
        logger.info(f"Task dispatched successfully: {task.id} for job {job_id} to queue 'ingest'")
        logger.info(f"Task state: {task.state}")
        logger.info(f"Task backend: {task.backend}")

        # Try to get the task result to see if it was actually queued
        try:
            # This should timeout quickly since task just started
            import time
            time.sleep(1)  # Give it a moment
            task_info = task.state
            logger.info(f"Task state after 1 second: {task_info}")

            # Check celery inspect to see active/scheduled tasks
            from celery import current_app
            i = current_app.control.inspect()
            active_tasks = i.active()
            scheduled_tasks = i.scheduled()
            logger.info(f"Active tasks across all workers: {active_tasks}")
            logger.info(f"Scheduled tasks across all workers: {scheduled_tasks}")

        except Exception as e:
            logger.warning(f"Could not inspect task status: {e}")

        # Store task ID in job document for monitoring
        await db.jobs.update_one(
            {"_id": job_id},
            {"$set": {"task_id": task.id}}
        )

    except Exception as e:
        logger.error(f"Failed to dispatch task for job {job_id}: {e} (type: {e.__class__.__name__})")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to start processing: {e}"
        )

    await log_job_action(
        AuditAction.JOB_CREATE, job_id, current_user, request,
        details={"title": title, "filename": file.filename},
    )
    return JobResponse(
        id=job_id,
        title=title,
        status=JobStatus.CREATED,
        source=job_data["source"],
        requested_outputs=outputs,
        review=job_data["review"],
        created_at=job_data["created_at"].isoformat(),
        updated_at=job_data["updated_at"].isoformat()
    )


@router.delete("/bulk", response_model=BulkDeleteResponse)
async def bulk_delete_jobs(
    request: BulkDeleteRequest,
    current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Bulk delete jobs (production/admin only)"""
    # Deduplicate job IDs to avoid processing the same job twice
    unique_job_ids = list(dict.fromkeys(request.job_ids))
    if len(unique_job_ids) != len(request.job_ids):
        logger.warning(f"Removed {len(request.job_ids) - len(unique_job_ids)} duplicate job IDs from bulk delete request")

    logger.info(f"Bulk deleting {len(unique_job_ids)} jobs requested by {current_user.email}")

    deleted_count = 0
    already_deleted = 0
    errors = []

    for job_id in unique_job_ids:
        try:
            job_doc = await db.jobs.find_one({"_id": job_id})
            if not job_doc:
                # Job may have been deleted by a concurrent request
                already_deleted += 1
                logger.debug(f"Job {job_id} not found (may have been deleted by concurrent request)")
                continue

            # Cancel task if exists
            task_id = job_doc.get("task_id")
            if task_id:
                try:
                    celery_app.control.revoke(task_id, terminate=True)
                except Exception as e:
                    logger.warning(f"Could not cancel task {task_id} for job {job_id}: {e}")

            # Delete GCS assets (errors are logged but don't block deletion)
            await _delete_job_gcs_assets(job_id, job_doc)

            # Delete from database
            result = await db.jobs.delete_one({"_id": job_id})
            if result.deleted_count > 0:
                deleted_count += 1
                logger.info(f"Deleted job {job_id}")
            else:
                # Job was deleted between find_one and delete_one (race condition)
                already_deleted += 1
                logger.debug(f"Job {job_id} was already deleted")

        except Exception as e:
            errors.append(f"Job {job_id}: {str(e)}")
            logger.error(f"Failed to delete job {job_id}: {e}")

    # Consider already_deleted as successful (idempotent delete)
    total_successful = deleted_count + already_deleted

    return {
        "deleted_count": total_successful,
        "total_requested": len(unique_job_ids),
        "errors": errors
    }


@router.post("/bulk/approve", response_model=BulkApproveResponse)
async def bulk_approve_jobs(
    request: BulkApproveRequest,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Bulk approve multiple jobs with optional method selection for accessible video"""
    # Deduplicate job IDs
    unique_job_ids = list(dict.fromkeys(request.job_ids))
    if len(unique_job_ids) != len(request.job_ids):
        logger.warning(f"Removed {len(request.job_ids) - len(unique_job_ids)} duplicate job IDs from bulk approve request")

    logger.info(f"Bulk approving {len(unique_job_ids)} jobs requested by {current_user.email}")

    approved_count = 0
    errors = []

    for job_id in unique_job_ids:
        try:
            # Get job to check status and determine source language
            job_doc = await db.jobs.find_one({"_id": job_id})
            if not job_doc:
                errors.append(f"Job {job_id}: not found")
                continue

            if job_doc["status"] != JobStatus.PENDING_QC.value:
                errors.append(f"Job {job_id}: not in pending QC status")
                continue

            # Transition directly to PENDING_FINAL_REVIEW
            # Translation, TTS, and accessible video rendering now happen BEFORE QC
            new_status = JobStatus.PENDING_FINAL_REVIEW

            # Build update operations
            update_set = {
                "status": new_status.value,
                "review.notes": request.notes or "",
                "review.reviewer_id": str(current_user.id),
                "updated_at": datetime.utcnow()
            }

            # If TTS preferences provided, update
            if request.tts_preferences:
                update_set["requested_outputs.tts_preferences"] = request.tts_preferences.model_dump()

            # If accessible video method provided, update
            if request.accessible_video_method:
                update_set["requested_outputs.accessible_video_method"] = request.accessible_video_method.value

            result = await db.jobs.find_one_and_update(
                {"_id": job_id, "status": JobStatus.PENDING_QC.value},
                {
                    "$set": update_set,
                    "$push": {
                        "review.history": {
                            "at": datetime.utcnow(),
                            "status": new_status.value,
                            "by": str(current_user.id),
                            "notes": request.notes or ""
                        }
                    }
                },
                return_document=True
            )

            if result:
                approved_count += 1
                # No need to trigger translate_and_synthesize_task - all processing done before QC
                logger.info(f"Job {job_id} approved, transitioning directly to final review")
            else:
                errors.append(f"Job {job_id}: update failed (may have been modified concurrently)")

        except Exception as e:
            errors.append(f"Job {job_id}: {str(e)}")
            logger.error(f"Failed to approve job {job_id}: {e}")

    return {
        "approved_count": approved_count,
        "total_requested": len(unique_job_ids),
        "errors": errors
    }


@router.post("/bulk/return-to-qc", response_model=BulkReturnToQCResponse)
async def bulk_return_to_qc(
    request: BulkReturnToQCRequest,
    current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Bulk return multiple jobs to QC review status."""
    unique_job_ids = list(dict.fromkeys(request.job_ids))
    if len(unique_job_ids) != len(request.job_ids):
        logger.warning(f"Removed {len(request.job_ids) - len(unique_job_ids)} duplicate job IDs from bulk return-to-qc request")

    logger.info(f"Bulk return-to-qc for {len(unique_job_ids)} jobs requested by {current_user.email}")

    returned_count = 0
    errors = []

    for job_id in unique_job_ids:
        try:
            job_doc = await db.jobs.find_one({"_id": job_id})
            if not job_doc:
                errors.append(f"Job {job_id}: not found")
                continue

            if job_doc["status"] not in RETURN_TO_QC_ELIGIBLE_STATUSES:
                errors.append(f"Job {job_id}: cannot return to QC from status '{job_doc['status']}'")
                continue

            result = await db.jobs.find_one_and_update(
                {"_id": job_id},
                {
                    "$set": {
                        "status": JobStatus.PENDING_QC.value,
                        "error": None,
                        "updated_at": datetime.utcnow()
                    },
                    "$push": {
                        "review.history": {
                            "at": datetime.utcnow(),
                            "status": JobStatus.PENDING_QC.value,
                            "by": str(current_user.id),
                            "notes": request.notes
                        }
                    }
                },
                return_document=True
            )

            if result:
                returned_count += 1
                logger.info(f"Job {job_id} returned to QC")
            else:
                errors.append(f"Job {job_id}: update failed (may have been modified concurrently)")

        except Exception as e:
            errors.append(f"Job {job_id}: {str(e)}")
            logger.error(f"Failed to return job {job_id} to QC: {e}")

    return {
        "returned_count": returned_count,
        "total_requested": len(unique_job_ids),
        "errors": errors
    }


# Downloadable job statuses
DOWNLOADABLE_STATUSES = [
    JobStatus.COMPLETED.value,
    JobStatus.APPROVED_ENGLISH.value,
    JobStatus.APPROVED_SOURCE.value,
]


@router.post("/bulk/download")
async def bulk_download_jobs(
    request: BulkDownloadRequest,
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """
    Generate a zip file containing all downloadable assets from specified jobs.

    Returns a streaming zip file with structure:
    {job_title}/
    +-- source.mp4
    +-- {lang}/
        +-- captions.vtt
        +-- ad.vtt
        +-- ad.mp3
        +-- accessible_video.mp4
        +-- accessible_captions.vtt

    Requirements:
    - Jobs must be in downloadable status (completed, approved_english, approved_source)
    - Client users can only download their own jobs
    - Maximum 50 jobs per request
    """
    # Validate request
    if not request.job_ids:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="No jobs specified"
        )

    # Deduplicate and limit
    unique_job_ids = list(dict.fromkeys(request.job_ids))
    MAX_JOBS = 50
    if len(unique_job_ids) > MAX_JOBS:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Maximum {MAX_JOBS} jobs allowed per download"
        )

    # Fetch jobs
    jobs = await db.jobs.find({"_id": {"$in": unique_job_ids}}).to_list(length=MAX_JOBS)

    if not jobs:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="No jobs found"
        )

    # Filter by permissions
    _accessible_pids = await get_accessible_project_ids(current_user, db)
    valid_jobs = []
    for job_doc in jobs:
        if _accessible_pids is not None:
            _jpid = job_doc.get("project_id")
            _own_legacy = job_doc.get("client_id") == str(current_user.id) and not _jpid
            if not _own_legacy and not (_jpid and _jpid in _accessible_pids):
                continue

        # Check downloadable status
        if job_doc["status"] not in DOWNLOADABLE_STATUSES:
            continue

        # Check has outputs
        if not job_doc.get("outputs"):
            continue

        valid_jobs.append(job_doc)

    if not valid_jobs:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="No eligible jobs to download (jobs must be approved/completed and have outputs)"
        )

    # Log download request
    logger.info(
        f"Bulk download requested by {current_user.email}: "
        f"{len(valid_jobs)} jobs ({len(unique_job_ids)} requested)"
    )

    # Generate zip filename
    filename = generate_zip_filename()

    # Return streaming response
    return StreamingResponse(
        generate_zip_stream(valid_jobs),
        media_type="application/zip",
        headers={
            "Content-Disposition": f'attachment; filename="{filename}"',
            "Cache-Control": "no-cache",
            "X-Job-Count": str(len(valid_jobs)),
        }
    )


@router.get("", response_model=JobListResponse)
async def list_jobs(
    status: Optional[str] = None,
    mine: bool = False,
    page: int = 1,
    size: int = 20,
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    print(f"🔍 DEBUG: list_jobs called by user {current_user.id}, status={status}, mine={mine}")
    try:
        query = {}

        if status:
            if ',' in status:
                # Handle comma-separated status values
                status_list = [s.strip() for s in status.split(',')]
                query["status"] = {"$in": status_list}
            else:
                query["status"] = status

        if mine:
            query["client_id"] = str(current_user.id)
        else:
            project_ids = await get_accessible_project_ids(current_user, db)
            if project_ids is not None:
                # CLIENT / PM: jobs in accessible projects OR legacy jobs owned by this user
                legacy_filter = {"client_id": str(current_user.id), "project_id": None}
                if project_ids:
                    query["$or"] = [{"project_id": {"$in": project_ids}}, legacy_filter]
                else:
                    # No teams / no projects yet — show only own legacy jobs
                    query.update(legacy_filter)

        # Get total count
        total = await db.jobs.count_documents(query)

        # Get paginated results
        skip = (page - 1) * size
        cursor = db.jobs.find(query).sort("created_at", -1).skip(skip).limit(size)
        jobs = await cursor.to_list(length=size)

        # Batch fetch user names for all jobs
        client_ids = list({job_doc.get("client_id") for job_doc in jobs if job_doc.get("client_id")})
        user_names = {}
        if client_ids:
            users_cursor = db.users.find(
                {"_id": {"$in": client_ids}},
                {"_id": 1, "full_name": 1}
            )
            async for user_doc in users_cursor:
                user_names[str(user_doc["_id"])] = user_doc.get("full_name", "Unknown")

        job_responses = []
        print(f"🔍 DEBUG: Found {len(jobs)} jobs, building responses...")
        for i, job_doc in enumerate(jobs):
            try:
                print(f"🔍 DEBUG: Processing job {i}: {job_doc.get('_id')}")
                client_id = job_doc.get("client_id")
                created_by_name = user_names.get(client_id, "Unknown") if client_id else "Unknown"
                job_responses.append(JobResponse(
                    id=str(job_doc["_id"]),
                    client_id=client_id,
                    title=job_doc["title"],
                    status=job_doc["status"],
                    source=job_doc["source"],
                    requested_outputs=RequestedOutputs(**job_doc["requested_outputs"]),
                    review=job_doc.get("review", {"notes": "", "history": []}),
                    outputs=job_doc.get("outputs"),
                    created_at=job_doc["created_at"].isoformat(),
                    updated_at=job_doc["updated_at"].isoformat(),
                    created_by_name=created_by_name
                ))
                print(f"🔍 DEBUG: Job {i} processed successfully")
            except Exception as e:
                print(f"🚨 ERROR processing job {i}: {e}")
                import traceback
                print(traceback.format_exc())
                raise

        print(f"🔍 DEBUG: All jobs processed, returning response")
        return JobListResponse(
            jobs=job_responses,
            total=total,
            page=page,
            size=size
        )
    except Exception as e:
        print(f"🚨 EXCEPTION in list_jobs: {e}")
        import traceback
        print(traceback.format_exc())
        raise


@router.get("/{job_id}", response_model=JobResponse)
async def get_job(
    job_id: str,
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Check access permissions
    if current_user.role in (UserRole.CLIENT, UserRole.PROJECT_MANAGER):
        accessible = await get_accessible_project_ids(current_user, db)
        job_project_id = job_doc.get("project_id")
        is_own_legacy = job_doc.get("client_id") == str(current_user.id) and not job_project_id
        in_project = accessible is None or (job_project_id and job_project_id in (accessible or []))
        if not is_own_legacy and not in_project:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")

    # Check task status if task_id exists
    task_id = job_doc.get("task_id")
    if task_id and job_doc["status"] == JobStatus.CREATED.value:
        try:
            task_result = celery_app.AsyncResult(task_id)
            if task_result.failed():
                logger.error(f"Task {task_id} failed for job {job_id} - State: {task_result.state}, Error: {task_result.result}")
                # Update job status to reflect task failure
                await db.jobs.update_one(
                    {"_id": job_id},
                    {
                        "$set": {
                            "error": {
                                "type": "task_failure",
                                "message": str(task_result.result) if task_result.result else "Unknown task failure",
                                "timestamp": datetime.utcnow().isoformat()
                            },
                            "updated_at": datetime.utcnow()
                        }
                    }
                )
        except Exception as e:
            logger.warning(f"Could not check task status for job {job_id}: {e}")

    return JobResponse(
        id=str(job_doc["_id"]),
        title=job_doc["title"],
        status=job_doc["status"],
        source=job_doc["source"],
        requested_outputs=RequestedOutputs(**job_doc["requested_outputs"]),
        review=job_doc.get("review", {"notes": "", "history": []}),
        outputs=job_doc.get("outputs"),
        created_at=job_doc["created_at"].isoformat(),
        updated_at=job_doc["updated_at"].isoformat(),
        cost_tracker_project_id=job_doc.get("cost_tracker_project_id"),
    )


@router.patch("/{job_id}", response_model=JobResponse)
async def update_job(
    job_id: str,
    request: JobUpdateRequest,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Update mutable job metadata (title, cost_tracker_project_id)."""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found")

    update_set: dict = {"updated_at": datetime.utcnow()}
    if request.title is not None:
        update_set["title"] = request.title
    if request.cost_tracker_project_id is not None:
        update_set["cost_tracker_project_id"] = request.cost_tracker_project_id

    if len(update_set) == 1:
        # Only updated_at — nothing to do
        pass
    else:
        await db.jobs.update_one({"_id": job_id}, {"$set": update_set})

    updated = await db.jobs.find_one({"_id": job_id})
    return JobResponse(
        id=str(updated["_id"]),
        title=updated["title"],
        status=updated["status"],
        source=updated["source"],
        requested_outputs=RequestedOutputs(**updated["requested_outputs"]),
        review=updated.get("review", {"notes": "", "history": []}),
        outputs=updated.get("outputs"),
        created_at=updated["created_at"].isoformat(),
        updated_at=updated["updated_at"].isoformat(),
        cost_tracker_project_id=updated.get("cost_tracker_project_id"),
    )


@router.post("/{job_id}/actions/approve_source", response_model=JobResponse)
async def approve_source(
    job_id: str,
    request: ApproveSourceRequest,
    http_request: Request = None,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Approve the source language version (works for any language)"""
    # First, get the job to determine the source language
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    if job_doc["status"] != JobStatus.PENDING_QC.value:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Job not in pending QC status"
        )

    # Transition directly to PENDING_FINAL_REVIEW
    # Translation, TTS, and accessible video rendering now happen BEFORE QC
    new_status = JobStatus.PENDING_FINAL_REVIEW

    # Build update operations
    update_set = {
        "status": new_status.value,
        "review.notes": request.notes or "",
        "review.reviewer_id": str(current_user.id),
        "updated_at": datetime.utcnow()
    }

    # If TTS preferences override provided, update requested_outputs.tts_preferences
    if request.tts_preferences:
        update_set["requested_outputs.tts_preferences"] = request.tts_preferences.model_dump()
        logger.info(f"Updating TTS preferences for job {job_id}: {request.tts_preferences}")

    # If accessible video method override provided, update requested_outputs.accessible_video_method
    if request.accessible_video_method:
        update_set["requested_outputs.accessible_video_method"] = request.accessible_video_method.value
        logger.info(f"Setting accessible video method for job {job_id}: {request.accessible_video_method}")

    result = await db.jobs.find_one_and_update(
        {"_id": job_id, "status": JobStatus.PENDING_QC.value},
        {
            "$set": update_set,
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": new_status.value,
                    "by": str(current_user.id),
                    "notes": request.notes or ""
                }
            }
        },
        return_document=True
    )

    if not result:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found or not in pending QC status"
        )

    # No need to trigger translate_and_synthesize_task - all processing done before QC
    logger.info(f"Job {job_id} approved, transitioning directly to final review")

    await log_job_action(
        AuditAction.JOB_APPROVE, job_id, current_user, http_request,
        details={"notes": request.notes, "new_status": new_status.value},
    )
    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )


@router.post("/{job_id}/actions/approve_english", response_model=JobResponse)
async def approve_english(
    job_id: str,
    request: ApproveEnglishRequest,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Legacy endpoint - redirects to approve_source for backwards compatibility"""
    return await approve_source(
        job_id,
        ApproveSourceRequest(notes=request.notes),
        current_user,
        db
    )


@router.post("/{job_id}/actions/reject", response_model=JobResponse)
async def reject_job(
    job_id: str,
    request: RejectJobRequest,
    http_request: Request = None,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    result = await db.jobs.find_one_and_update(
        {"_id": job_id, "status": JobStatus.PENDING_QC.value},
        {
            "$set": {
                "status": JobStatus.REJECTED.value,
                "review.notes": request.notes,
                "review.reviewer_id": str(current_user.id),
                "updated_at": datetime.utcnow()
            },
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": JobStatus.REJECTED.value,
                    "by": str(current_user.id),
                    "notes": request.notes
                }
            }
        },
        return_document=True
    )

    if not result:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found or not in pending QC status"
        )

    await log_job_action(
        AuditAction.JOB_REJECT, job_id, current_user, http_request,
        details={"notes": request.notes},
    )
    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )


@router.post("/{job_id}/actions/complete", response_model=JobResponse)
async def complete_job(
    job_id: str,
    request: CompleteJobRequest,
    http_request: Request = None,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    # Get job for validation
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    if job_doc["status"] != JobStatus.PENDING_FINAL_REVIEW.value:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Job not ready for completion"
        )

    # Validate all assets before completion
    is_valid, validation_errors = await asset_validation_service.validate_job_assets(job_doc)
    if not is_valid:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Asset validation failed: {'; '.join(validation_errors)}"
        )

    result = await db.jobs.find_one_and_update(
        {"_id": job_id, "status": JobStatus.PENDING_FINAL_REVIEW.value},
        {
            "$set": {
                "status": JobStatus.COMPLETED.value,
                "review.notes": request.notes or "",
                "updated_at": datetime.utcnow()
            },
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": JobStatus.COMPLETED.value,
                    "by": str(current_user.id),
                    "notes": request.notes or ""
                }
            }
        },
        return_document=True
    )

    if not result:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found or not in pending final review status"
        )


    await log_job_action(
        AuditAction.JOB_APPROVE, job_id, current_user, http_request,
        details={"notes": request.notes, "new_status": JobStatus.COMPLETED.value},
    )

    # Trigger client notification task now that job is completed
    try:
        from ...tasks.notify import notify_client_task
        notify_client_task.delay(job_id)
        logger.info(f"Triggered client notification task for completed job {job_id}")
    except Exception as e:
        logger.warning(f"Failed to trigger client notification task for job {job_id}: {e}")

    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )


@router.post("/{job_id}/actions/reject_final", response_model=JobResponse)
async def reject_final_review(
    job_id: str,
    request: RejectJobRequest,
    http_request: Request = None,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    result = await db.jobs.find_one_and_update(
        {"_id": job_id, "status": JobStatus.PENDING_FINAL_REVIEW.value},
        {
            "$set": {
                "status": JobStatus.QC_FEEDBACK.value,
                "review.notes": request.notes,
                "review.reviewer_id": str(current_user.id),
                "updated_at": datetime.utcnow()
            },
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": JobStatus.QC_FEEDBACK.value,
                    "by": str(current_user.id),
                    "notes": request.notes
                }
            }
        },
        return_document=True
    )

    if not result:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found or not in pending final review status"
        )

    await log_job_action(
        AuditAction.JOB_REJECT, job_id, current_user, http_request,
        details={"notes": request.notes, "stage": "final_review"},
    )
    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )


# Statuses eligible for "Return to QC" action
RETURN_TO_QC_ELIGIBLE_STATUSES = [
    JobStatus.COMPLETED.value,
    JobStatus.PENDING_FINAL_REVIEW.value,
    JobStatus.REJECTED.value,
    JobStatus.QC_FEEDBACK.value,
    JobStatus.TTS_FAILED.value,
    JobStatus.RENDER_FAILED.value,
    JobStatus.APPROVED_ENGLISH.value,
    JobStatus.APPROVED_SOURCE.value,
]


@router.post("/{job_id}/actions/return_to_qc", response_model=JobResponse)
async def return_to_qc(
    job_id: str,
    request: ReturnToQCRequest,
    http_request: Request = None,
    current_user: User = Depends(require_roles(UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Return a job to QC review status for re-editing."""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    if job_doc["status"] not in RETURN_TO_QC_ELIGIBLE_STATUSES:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Job cannot be returned to QC from status '{job_doc['status']}'"
        )

    result = await db.jobs.find_one_and_update(
        {"_id": job_id},
        {
            "$set": {
                "status": JobStatus.PENDING_QC.value,
                "error": None,
                "updated_at": datetime.utcnow()
            },
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": JobStatus.PENDING_QC.value,
                    "by": str(current_user.id),
                    "notes": request.notes
                }
            }
        },
        return_document=True
    )

    if not result:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found or status changed"
        )

    # Reset per-language QC statuses while preserving assignments
    await lqc.reset_all_for_return_to_qc(db, job_id)

    logger.info(f"Job {job_id} returned to QC by {current_user.email}: {request.notes}")

    await log_job_action(
        AuditAction.JOB_STATUS_CHANGE, job_id, current_user, http_request,
        details={"new_status": JobStatus.PENDING_QC.value, "notes": request.notes},
    )
    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )


@router.get("/{job_id}/downloads", response_model=JobDownloadsResponse)
async def get_job_downloads(
    job_id: str,
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    from ...services.gcs import get_signed_download_url

    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Check access permissions
    if current_user.role in (UserRole.CLIENT, UserRole.PROJECT_MANAGER):
        _ap = await get_accessible_project_ids(current_user, db)
        _jpid = job_doc.get("project_id")
        _own = job_doc.get("client_id") == str(current_user.id) and not _jpid
        if _ap is not None and not _own and not (_jpid and _jpid in (_ap or [])):
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")

    # Allow downloads for jobs that have outputs available
    # (PENDING_QC, APPROVED_ENGLISH, TRANSLATING, COMPLETED, etc.)
    if job_doc["status"] in [JobStatus.CREATED.value, JobStatus.INGESTING.value, JobStatus.AI_PROCESSING.value]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Job is still being processed"
        )

    # Check if job has outputs
    if not job_doc.get("outputs"):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="No outputs available for this job"
        )

    # Generate signed URLs for all outputs
    downloads = {}

    # Add source video URL
    if job_doc.get("source", {}).get("gcs_uri"):
        source_blob_path = job_doc["source"]["gcs_uri"].replace(f"gs://{settings.gcs_bucket}/", "")
        try:
            source_signed_url = await get_signed_download_url(source_blob_path, 24)
            downloads["source_video"] = source_signed_url
        except Exception as e:
            logger.warning(f"Failed to generate signed URL for source video: {e}")

    outputs = job_doc.get("outputs", {})

    for language, lang_output in outputs.items():
        if not isinstance(lang_output, dict):
            continue

        lang_downloads = {}

        # Captions VTT
        if "captions_vtt_gcs" in lang_output:
            blob_path = lang_output["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            try:
                signed_url = await get_signed_download_url(blob_path, 24)
                lang_downloads["captions_vtt"] = signed_url
            except Exception as e:
                logger.warning(f"Failed to generate signed URL for captions {language}: {e}")

        # Audio Description VTT
        if "ad_vtt_gcs" in lang_output:
            blob_path = lang_output["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            try:
                signed_url = await get_signed_download_url(blob_path, 24)
                lang_downloads["audio_description_vtt"] = signed_url
            except Exception as e:
                logger.warning(f"Failed to generate signed URL for AD VTT {language}: {e}")

        # Audio Description MP3
        if "ad_mp3_gcs" in lang_output:
            blob_path = lang_output["ad_mp3_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            try:
                signed_url = await get_signed_download_url(blob_path, 24)
                lang_downloads["audio_description_mp3"] = signed_url
            except Exception as e:
                logger.warning(f"Failed to generate signed URL for AD MP3 {language}: {e}")

        # Accessible Video MP4
        if "accessible_video_gcs" in lang_output:
            blob_path = lang_output["accessible_video_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            try:
                signed_url = await get_signed_download_url(blob_path, 24)
                lang_downloads["accessible_video_mp4"] = signed_url
            except Exception as e:
                logger.warning(f"Failed to generate signed URL for accessible video {language}: {e}")

        # SDH Captions VTT
        if "sdh_captions_vtt_gcs" in lang_output:
            blob_path = lang_output["sdh_captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            try:
                signed_url = await get_signed_download_url(blob_path, 24)
                lang_downloads["sdh_captions_vtt"] = signed_url
            except Exception as e:
                logger.warning(f"Failed to generate signed URL for SDH captions {language}: {e}")

        # Re-timed Captions VTT (for pause-insert accessible videos)
        if "retimed_captions_vtt_gcs" in lang_output:
            blob_path = lang_output["retimed_captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            try:
                signed_url = await get_signed_download_url(blob_path, 24)
                lang_downloads["accessible_captions_vtt"] = signed_url
            except Exception as e:
                logger.warning(f"Failed to generate signed URL for retimed captions {language}: {e}")

        # Descriptive Transcript TXT (WCAG 2.1 1.2.1)
        if "descriptive_transcript_gcs" in lang_output:
            blob_path = lang_output["descriptive_transcript_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            try:
                signed_url = await get_signed_download_url(blob_path, 24)
                lang_downloads["descriptive_transcript"] = signed_url
            except Exception as e:
                logger.warning(f"Failed to generate signed URL for descriptive transcript {language}: {e}")

        if lang_downloads:
            downloads[language] = lang_downloads

    return JobDownloadsResponse(downloads=downloads)


@router.get("/{job_id}/vtt", response_model=VttContentResponse)
async def get_job_vtt_content(
    job_id: str,
    language: Optional[str] = None,  # If None, defaults to source language
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Get VTT content for editing. If language is not specified, returns source language content."""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Check access permissions
    if current_user.role in (UserRole.CLIENT, UserRole.PROJECT_MANAGER):
        _ap = await get_accessible_project_ids(current_user, db)
        _jpid = job_doc.get("project_id")
        _own = job_doc.get("client_id") == str(current_user.id) and not _jpid
        if _ap is not None and not _own and not (_jpid and _jpid in (_ap or [])):
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")

    # Default to source language if not specified
    target_language = language or job_doc["source"].get("language", "en")

    outputs = job_doc.get("outputs", {})
    lang_output = outputs.get(target_language, {})

    response = VttContentResponse()

    # Fetch captions VTT if available
    if "captions_vtt_gcs" in lang_output:
        blob_path = lang_output["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
        try:
            blob = gcs_service.bucket.blob(blob_path)
            response.captions_vtt = blob.download_as_text()
        except Exception as e:
            logger.warning(f"Failed to fetch captions VTT: {e}")

    # Fetch audio description VTT if available
    if "ad_vtt_gcs" in lang_output:
        blob_path = lang_output["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
        try:
            blob = gcs_service.bucket.blob(blob_path)
            response.audio_description_vtt = blob.download_as_text()
        except Exception as e:
            logger.warning(f"Failed to fetch AD VTT: {e}")

    # Fetch retimed captions VTT if available (for accessible videos)
    if "retimed_captions_vtt_gcs" in lang_output:
        blob_path = lang_output["retimed_captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
        try:
            blob = gcs_service.bucket.blob(blob_path)
            response.retimed_captions_vtt = blob.download_as_text()
        except Exception as e:
            logger.warning(f"Failed to fetch retimed captions VTT: {e}")

    return response


@router.patch("/{job_id}/vtt", response_model=JobResponse)
async def update_job_vtt_content(
    job_id: str,
    request: VttUpdateRequest,
    http_request: Request = None,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Update VTT content for a job. If language is not specified, updates source language content."""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Only allow editing during QC phase
    if job_doc["status"] not in [JobStatus.PENDING_QC.value, JobStatus.QC_FEEDBACK.value]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="VTT content can only be edited during QC phase"
        )

    # Default to source language if not specified
    target_language = request.language or job_doc["source"].get("language", "en")

    # Linguists can only edit their assigned language
    if current_user.role == UserRole.LINGUIST:
        try:
            lqc._assert_can_act(job_doc, target_language, current_user)
        except HTTPException:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"You are not assigned to language '{target_language}'"
            )

    outputs = job_doc.get("outputs", {})
    lang_output = outputs.get(target_language, {})

    # Validate and update captions VTT
    if request.captions_vtt is not None:
        # Validate VTT format
        is_valid, errors = VTTEditor.validate_vtt(request.captions_vtt)
        if not is_valid:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"Invalid captions VTT: {'; '.join(errors)}"
            )

        # Snapshot before overwriting live file
        await vtt_versioning.create_version(
            db, job_id, target_language, "captions", request.captions_vtt, current_user
        )

        # Upload updated VTT
        new_captions_uri = await upload_vtt_to_gcs(
            request.captions_vtt,
            f"{job_id}/{target_language}/captions.vtt"
        )
        lang_output["captions_vtt_gcs"] = new_captions_uri

    # Validate and update audio description VTT
    if request.audio_description_vtt is not None:
        # Validate VTT format
        is_valid, errors = VTTEditor.validate_vtt(request.audio_description_vtt)
        if not is_valid:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"Invalid audio description VTT: {'; '.join(errors)}"
            )

        # Diff old vs new VTT to auto-queue TTS for changed/added cues
        from ...lib.vtt import VTTParser
        from ...tasks.tts_synthesis import parse_ad_cues as _parse_ad_cues_for_diff

        old_cues: list[str] = []
        old_ad_vtt_gcs = lang_output.get("ad_vtt_gcs")
        if old_ad_vtt_gcs:
            try:
                import asyncio as _asyncio
                old_blob_path = old_ad_vtt_gcs.replace(f"gs://{settings.gcs_bucket}/", "")
                old_blob = gcs_service.bucket.blob(old_blob_path)
                loop = _asyncio.get_event_loop()
                old_vtt_text = await loop.run_in_executor(
                    gcs_service.executor, old_blob.download_as_text
                )
                old_cues = [c["text"] for c in _parse_ad_cues_for_diff(old_vtt_text)]
            except Exception as _e:
                logger.warning(f"Could not read old AD VTT for diff: {_e}")

        new_cues = [c["text"] for c in _parse_ad_cues_for_diff(request.audio_description_vtt)]

        # Queue TTS regeneration for any cue whose text changed or that is newly added
        edit_state = lang_output.get("accessible_video_edit_state") or {}
        tts_queue = edit_state.get("tts_regeneration_queue") or []
        queued_indices = {q["cue_index"] for q in tts_queue if q.get("status") == "pending"}

        for idx, new_text in enumerate(new_cues):
            old_text = old_cues[idx] if idx < len(old_cues) else None
            if old_text != new_text and idx not in queued_indices:
                tts_queue.append({
                    "cue_index": idx,
                    "requested_at": datetime.utcnow().isoformat(),
                    "new_text": new_text,
                    "status": "pending",
                    "error_message": None
                })
                queued_indices.add(idx)

        # Update manifest indices for inserted/deleted cues so re-render uses correct MP3s.
        # Any cue that exists in both old and new with the same text keeps its old gcs_uri.
        current_manifest: list[dict] = lang_output.get("ad_cue_manifest") or []
        if current_manifest and len(new_cues) != len(old_cues):
            # Build a text -> manifest entry lookup from old manifest
            old_text_to_entry = {
                e.get("text", "")[:80]: e
                for e in current_manifest
            }
            new_manifest = []
            for idx, new_text in enumerate(new_cues):
                snippet = new_text[:80]
                if snippet in old_text_to_entry:
                    entry = dict(old_text_to_entry[snippet])
                    entry["cue_index"] = idx  # re-index
                    new_manifest.append(entry)
                # New/changed cues will be filled in after TTS regen; skip for now
            lang_output["ad_cue_manifest"] = new_manifest

        edit_state["tts_regeneration_queue"] = tts_queue
        # Clear stale pause points — old positions are invalid with new VTT timings.
        # Re-render will fall back to VTT cue start_time for placement.
        edit_state["pause_points"] = []
        edit_state["video_segments"] = []
        lang_output["accessible_video_edit_state"] = edit_state

        # Snapshot before overwriting live file
        await vtt_versioning.create_version(
            db, job_id, target_language, "ad", request.audio_description_vtt, current_user
        )

        # Upload updated VTT
        new_ad_uri = await upload_vtt_to_gcs(
            request.audio_description_vtt,
            f"{job_id}/{target_language}/ad.vtt"
        )
        lang_output["ad_vtt_gcs"] = new_ad_uri

    # Regenerate descriptive transcript whenever captions or AD VTT changes
    if request.captions_vtt is not None or request.audio_description_vtt is not None:
        from ...services.descriptive_transcript import generate_descriptive_transcript
        import asyncio as _asyncio

        current_captions_vtt = request.captions_vtt
        if current_captions_vtt is None and lang_output.get("captions_vtt_gcs"):
            try:
                _blob_path = lang_output["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
                _blob = gcs_service.bucket.blob(_blob_path)
                _loop = _asyncio.get_event_loop()
                current_captions_vtt = await _loop.run_in_executor(gcs_service.executor, _blob.download_as_text)
            except Exception as _e:
                logger.warning(f"Could not fetch captions VTT for transcript regeneration: {_e}")

        current_ad_vtt = request.audio_description_vtt
        if current_ad_vtt is None and lang_output.get("ad_vtt_gcs"):
            try:
                _blob_path = lang_output["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
                _blob = gcs_service.bucket.blob(_blob_path)
                _loop = _asyncio.get_event_loop()
                current_ad_vtt = await _loop.run_in_executor(gcs_service.executor, _blob.download_as_text)
            except Exception as _e:
                logger.warning(f"Could not fetch AD VTT for transcript regeneration: {_e}")

        if current_captions_vtt or current_ad_vtt:
            try:
                transcript_text = generate_descriptive_transcript(current_captions_vtt or "", current_ad_vtt or "")
                transcript_gcs_uri = await gcs_service.upload_text_to_gcs(
                    transcript_text,
                    f"{job_id}/{target_language}/descriptive_transcript.txt",
                    "text/plain; charset=utf-8"
                )
                lang_output["descriptive_transcript_gcs"] = transcript_gcs_uri
            except Exception as _e:
                logger.warning(f"Failed to regenerate descriptive transcript: {_e}")

    # Update job with new VTT content
    outputs[target_language] = lang_output

    result = await db.jobs.find_one_and_update(
        {"_id": job_id},
        {
            "$set": {
                "outputs": outputs,
                "updated_at": datetime.utcnow()
            },
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": "vtt_updated",
                    "by": str(current_user.id),
                    "notes": f"Updated VTT content for {target_language}"
                }
            }
        },
        return_document=True
    )

    await audit_logger.log_action(
        action=AuditAction.VTT_EDIT,
        description=f"VTT content updated for job {job_id} lang={target_language}",
        user=current_user,
        request=http_request,
        resource_type="job",
        resource_id=job_id,
        details={
            "lang": target_language,
            "captions_updated": request.captions_vtt is not None,
            "ad_updated": request.audio_description_vtt is not None,
        },
    )
    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )


@router.post("/{job_id}/vtt/adjust-timing", response_model=JobResponse)
async def adjust_vtt_timing(
    job_id: str,
    request: VttTimingAdjustRequest,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Adjust timing of VTT content by a specified offset"""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Only allow timing adjustment during QC phase
    if job_doc["status"] not in [JobStatus.PENDING_QC.value, JobStatus.QC_FEEDBACK.value]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="VTT timing can only be adjusted during QC phase"
        )

    # Get current VTT content from GCS
    lang_key = request.language
    outputs = job_doc.get("outputs", {}).get(lang_key, {})

    if not outputs:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No VTT content found for language: {request.language}"
        )

    update_operations = {}
    adjusted_content = {}

    # Adjust captions VTT if requested and exists
    if request.adjust_captions and "captions_vtt_gcs" in outputs:
        try:
            # Download current captions VTT
            captions_blob = gcs_service.bucket.blob(
                outputs["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            )
            current_captions_vtt = captions_blob.download_as_text()

            # Apply timing adjustment
            adjusted_captions_vtt = VTTEditor.adjust_timing_offset(
                current_captions_vtt,
                request.offset_seconds
            )

            # Upload adjusted content back to GCS
            new_captions_gcs_uri = await upload_vtt_to_gcs(
                adjusted_captions_vtt,
                f"{job_id}/{request.language}/captions.vtt"
            )

            update_operations[f"outputs.{lang_key}.captions_vtt_gcs"] = new_captions_gcs_uri
            adjusted_content["captions"] = True

        except Exception as e:
            logger.error(f"Failed to adjust captions timing: {e}")
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Failed to adjust captions timing"
            )

    # Adjust audio description VTT if requested and exists
    if request.adjust_audio_description and "ad_vtt_gcs" in outputs:
        try:
            # Download current AD VTT
            ad_blob = gcs_service.bucket.blob(
                outputs["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "")
            )
            current_ad_vtt = ad_blob.download_as_text()

            # Apply timing adjustment
            adjusted_ad_vtt = VTTEditor.adjust_timing_offset(
                current_ad_vtt,
                request.offset_seconds
            )

            # Upload adjusted content back to GCS
            new_ad_gcs_uri = await upload_vtt_to_gcs(
                adjusted_ad_vtt,
                f"{job_id}/{request.language}/ad.vtt"
            )

            update_operations[f"outputs.{lang_key}.ad_vtt_gcs"] = new_ad_gcs_uri
            adjusted_content["audio_description"] = True

        except Exception as e:
            logger.error(f"Failed to adjust audio description timing: {e}")
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Failed to adjust audio description timing"
            )

    if not update_operations:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="No VTT content to adjust"
        )

    # Update job document
    result = await db.jobs.find_one_and_update(
        {"_id": job_id},
        {
            "$set": {
                **update_operations,
                "updated_at": datetime.utcnow()
            },
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": "timing_adjusted",
                    "by": str(current_user.id),
                    "notes": f"Adjusted timing by {request.offset_seconds:+.1f}s for {', '.join(adjusted_content.keys())}"
                }
            }
        },
        return_document=True
    )

    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )


@router.delete("/{job_id}", response_model=JobDeleteResponse)
async def delete_job(
    job_id: str,
    http_request: Request = None,
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Delete a job and all associated assets"""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Check permissions: clients and PMs can only delete accessible jobs
    if current_user.role in (UserRole.CLIENT, UserRole.PROJECT_MANAGER):
        _ap = await get_accessible_project_ids(current_user, db)
        _jpid = job_doc.get("project_id")
        _own = job_doc.get("client_id") == str(current_user.id) and not _jpid
        if _ap is not None and not _own and not (_jpid and _jpid in (_ap or [])):
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")

    logger.info(f"Deleting job {job_id} requested by {current_user.email}")

    try:
        # 1. Cancel running Celery task if exists
        task_id = job_doc.get("task_id")
        if task_id:
            try:
                celery_app.control.revoke(task_id, terminate=True)
                logger.info(f"Cancelled Celery task {task_id} for job {job_id}")
            except Exception as e:
                logger.warning(f"Could not cancel task {task_id}: {e}")

        # 2. Delete GCS assets
        await _delete_job_gcs_assets(job_id, job_doc)

        # 3. Delete from database
        result = await db.jobs.delete_one({"_id": job_id})

        if result.deleted_count == 0:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="Job not found"
            )

        logger.info(f"Successfully deleted job {job_id}")
        await log_job_action(
            AuditAction.JOB_DELETE, job_id, current_user, http_request,
            details={"title": job_doc.get("title")},
        )
        return {"message": f"Job {job_id} deleted successfully"}

    except Exception as e:
        logger.error(f"Failed to delete job {job_id}: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to delete job: {str(e)}"
        )


async def _delete_job_gcs_assets(job_id: str, job_doc: dict):
    """Delete all GCS assets for a job using prefix-based cleanup.

    All job files are stored under the {job_id}/ prefix in GCS, including:
    - source.mp4 (original video)
    - {lang}/captions.vtt (translated captions)
    - {lang}/ad.vtt (audio description cues)
    - {lang}/ad.mp3 (full AD audio)
    - {lang}/ad_cues/cue_*.mp3 (per-cue TTS segments)
    - {lang}/accessible_video.mp4 (rendered video)
    - {lang}/accessible_captions.vtt (re-timed captions for pause-insert)
    """
    from google.api_core.exceptions import NotFound

    try:
        deleted_count = 0
        not_found_count = 0

        # Convert lazy iterator to list upfront to avoid issues with
        # concurrent deletions causing stale generation numbers
        blobs = list(gcs_service.bucket.list_blobs(prefix=f"{job_id}/"))

        for blob in blobs:
            try:
                # Delete by name to avoid generation mismatch errors when
                # another process already deleted the blob
                gcs_service.bucket.delete_blob(blob.name)
                deleted_count += 1
                logger.debug(f"Deleted GCS file: {blob.name}")
            except NotFound:
                # Blob was already deleted (likely by concurrent request)
                not_found_count += 1
                logger.debug(f"Blob already deleted: {blob.name}")
            except Exception as e:
                logger.warning(f"Could not delete {blob.name}: {e}")

        if not_found_count > 0:
            logger.info(f"Deleted {deleted_count} GCS files for job {job_id} ({not_found_count} already deleted)")
        else:
            logger.info(f"Deleted {deleted_count} GCS files for job {job_id}")
    except Exception as e:
        logger.error(f"Error deleting GCS assets for job {job_id}: {e}")
        # Don't re-raise - GCS cleanup failures shouldn't block job deletion
        # The job record will be deleted and files will eventually be cleaned up


@router.post("/{job_id}/actions/retry_tts", response_model=JobResponse)
async def retry_tts(
    job_id: str,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Retry TTS generation for a job that failed during TTS synthesis.

    This is a lightweight retry that:
    1. Resets the job status from tts_failed back to the approved state
    2. Clears the error field
    3. Re-triggers the translation/TTS pipeline

    Use this when TTS failed due to transient issues or after editing the AD VTT
    to fix content that triggered safety filters.
    """
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Only allow retry from tts_failed status
    if job_doc["status"] != JobStatus.TTS_FAILED.value:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Job is not in TTS failed status (current: {job_doc['status']})"
        )

    # Determine the appropriate approved status based on source language
    source_language = job_doc["source"].get("language", "en")
    approved_status = JobStatus.APPROVED_ENGLISH if source_language == "en" else JobStatus.APPROVED_SOURCE

    # Reset job status and clear error
    result = await db.jobs.find_one_and_update(
        {"_id": job_id, "status": JobStatus.TTS_FAILED.value},
        {
            "$set": {
                "status": approved_status.value,
                "error": None,
                "updated_at": datetime.utcnow()
            },
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": "retry_tts",
                    "by": str(current_user.id),
                    "notes": "TTS retry requested"
                }
            }
        },
        return_document=True
    )

    if not result:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found or status changed"
        )

    # Broadcast status update
    try:
        await connection_manager.broadcast_job_status_update(
            job_id=job_id,
            status=approved_status.value,
            job_title=result.get("title"),
            message=f"{result.get('title', 'Job')} TTS retry initiated"
        )
    except Exception as e:
        logger.warning(f"Failed to broadcast status update for TTS retry {job_id}: {e}")

    # Re-trigger the translation/TTS pipeline
    try:
        translate_and_synthesize_task.delay(job_id)
        logger.info(f"Triggered TTS retry for job {job_id} by {current_user.email}")
    except Exception as e:
        logger.error(f"Failed to trigger TTS retry task for job {job_id}: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Failed to start TTS retry"
        )

    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )


@router.get("/{job_id}/validate", response_model=AssetValidationResponse)
async def validate_job_assets(
    job_id: str,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Validate job assets before completion"""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    is_valid, errors = await asset_validation_service.validate_job_assets(job_doc)

    return AssetValidationResponse(
        is_valid=is_valid,
        errors=errors,
        warnings=[]  # Can be extended for non-blocking warnings
    )


# ==============================================================================
# Accessible Video QC Editing Endpoints
# ==============================================================================


@router.get("/{job_id}/accessible-video/{language}/edit-state", response_model=AccessibleVideoEditStateResponse)
async def get_accessible_video_edit_state(
    job_id: str,
    language: str,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Get current pause points, segment metadata, and TTS regeneration queue for QC editing."""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Check job is in QC status
    if job_doc["status"] not in [JobStatus.PENDING_QC.value, JobStatus.RENDERING_QC.value]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Job must be in pending_qc or rendering_qc status (current: {job_doc['status']})"
        )

    # Get language outputs
    lang_output = job_doc.get("outputs", {}).get(language)
    if not lang_output:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No outputs found for language {language}"
        )

    # Get edit state
    edit_state = lang_output.get("accessible_video_edit_state")
    if not edit_state:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No accessible video edit state found for language {language}"
        )

    # Calculate total duration from segments
    segments = edit_state.get("video_segments", [])
    total_duration_ms = sum(s.get("duration_ms", 0) for s in segments)

    # Get signed URL for accessible video
    accessible_video_gcs = lang_output.get("accessible_video_gcs")
    accessible_video_url = None
    if accessible_video_gcs:
        blob_path = accessible_video_gcs.replace(f"gs://{settings.gcs_bucket}/", "")
        try:
            accessible_video_url = await gcs_service.get_signed_url(blob_path, expiration_hours=1)
        except HTTPException:
            # File may not exist yet
            accessible_video_url = None

    # Convert to response format
    return AccessibleVideoEditStateResponse(
        pause_points=[
            PausePointResponse(
                cue_index=pp.get("cue_index"),
                original_ms=pp.get("original_ms"),
                source_ms=pp.get("source_ms", pp.get("original_ms")),  # Fallback for old data
                adjusted_ms=pp.get("adjusted_ms"),
                min_bound_ms=pp.get("min_bound_ms"),
                max_bound_ms=pp.get("max_bound_ms")
            )
            for pp in edit_state.get("pause_points", [])
        ],
        video_segments=[
            VideoSegmentResponse(
                segment_index=seg.get("segment_index"),
                start_ms=seg.get("start_ms"),
                end_ms=seg.get("end_ms"),
                gcs_uri=seg.get("gcs_uri"),
                duration_ms=seg.get("duration_ms"),
                is_freeze_frame=seg.get("is_freeze_frame", False),
                cue_index=seg.get("cue_index")
            )
            for seg in segments
        ],
        tts_regeneration_queue=[
            TTSRegenerationItem(
                cue_index=req.get("cue_index"),
                requested_at=req.get("requested_at").isoformat() if hasattr(req.get("requested_at"), "isoformat") else req.get("requested_at"),
                new_text=req.get("new_text"),
                status=req.get("status", "pending"),
                error_message=req.get("error_message")
            )
            for req in edit_state.get("tts_regeneration_queue", [])
        ],
        last_render_at=edit_state.get("last_render_at").isoformat() if edit_state.get("last_render_at") else None,
        total_duration_ms=total_duration_ms,
        accessible_video_url=accessible_video_url
    )


@router.patch("/{job_id}/accessible-video/{language}/pause-points/{cue_index}", response_model=PausePointResponse)
async def update_pause_point(
    job_id: str,
    language: str,
    cue_index: int,
    request: PausePointUpdateRequest,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Update a single pause point timing with millisecond precision."""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Check job is in QC status
    if job_doc["status"] not in [JobStatus.PENDING_QC.value]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Job must be in pending_qc status for editing (current: {job_doc['status']})"
        )

    # Get edit state
    lang_output = job_doc.get("outputs", {}).get(language)
    if not lang_output:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No outputs found for language {language}"
        )

    edit_state = lang_output.get("accessible_video_edit_state")
    if not edit_state:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No edit state found for language {language}"
        )

    # Find the pause point
    pause_points = edit_state.get("pause_points", [])
    pause_point = next((pp for pp in pause_points if pp.get("cue_index") == cue_index), None)
    if not pause_point:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Pause point for cue {cue_index} not found"
        )

    # Validate bounds
    min_bound = pause_point.get("min_bound_ms", 0)
    max_bound = pause_point.get("max_bound_ms", float("inf"))

    if request.adjusted_ms < min_bound:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Adjusted value {request.adjusted_ms}ms is below minimum bound {min_bound}ms"
        )
    if request.adjusted_ms > max_bound:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Adjusted value {request.adjusted_ms}ms is above maximum bound {max_bound}ms"
        )

    # Update the pause point
    pause_point["adjusted_ms"] = request.adjusted_ms

    # Save back to database
    await db.jobs.update_one(
        {"_id": job_id},
        {
            "$set": {
                f"outputs.{language}.accessible_video_edit_state.pause_points": pause_points,
                "updated_at": datetime.utcnow()
            }
        }
    )

    logger.info(f"Updated pause point for cue {cue_index} in job {job_id}/{language}: {request.adjusted_ms}ms")

    return PausePointResponse(
        cue_index=pause_point["cue_index"],
        original_ms=pause_point["original_ms"],
        source_ms=pause_point.get("source_ms", pause_point["original_ms"]),  # Fallback for old data
        adjusted_ms=pause_point["adjusted_ms"],
        min_bound_ms=pause_point["min_bound_ms"],
        max_bound_ms=pause_point["max_bound_ms"]
    )


@router.post("/{job_id}/accessible-video/{language}/tts-regeneration")
async def queue_tts_regeneration(
    job_id: str,
    language: str,
    request: TTSRegenerationQueueRequest,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Queue TTS regeneration for specific cues (uses current AD VTT text)."""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Check job is in QC status
    if job_doc["status"] not in [JobStatus.PENDING_QC.value]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Job must be in pending_qc status for editing (current: {job_doc['status']})"
        )

    # Get edit state
    lang_output = job_doc.get("outputs", {}).get(language)
    if not lang_output:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No outputs found for language {language}"
        )

    edit_state = lang_output.get("accessible_video_edit_state")
    if not edit_state:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No edit state found for language {language}"
        )

    # Get current queue
    current_queue = edit_state.get("tts_regeneration_queue", [])
    existing_cues = {req.get("cue_index") for req in current_queue}

    # Add new requests (avoid duplicates)
    added = []
    for cue_idx in request.cue_indices:
        if cue_idx not in existing_cues:
            new_request = {
                "cue_index": cue_idx,
                "requested_at": datetime.utcnow().isoformat(),
                "new_text": None,  # Will use current VTT text
                "status": "pending",
                "error_message": None
            }
            current_queue.append(new_request)
            added.append(cue_idx)

    # Save back to database
    await db.jobs.update_one(
        {"_id": job_id},
        {
            "$set": {
                f"outputs.{language}.accessible_video_edit_state.tts_regeneration_queue": current_queue,
                "updated_at": datetime.utcnow()
            }
        }
    )

    logger.info(f"Queued TTS regeneration for cues {added} in job {job_id}/{language}")

    return {"message": f"Queued {len(added)} cue(s) for regeneration", "queued_cues": added}


@router.delete("/{job_id}/accessible-video/{language}/tts-regeneration/{cue_index}")
async def remove_tts_regeneration(
    job_id: str,
    language: str,
    cue_index: int,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Remove a cue from the TTS regeneration queue."""
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Get edit state
    lang_output = job_doc.get("outputs", {}).get(language)
    if not lang_output or not lang_output.get("accessible_video_edit_state"):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No edit state found for language {language}"
        )

    edit_state = lang_output["accessible_video_edit_state"]
    current_queue = edit_state.get("tts_regeneration_queue", [])

    # Remove the cue from queue
    new_queue = [req for req in current_queue if req.get("cue_index") != cue_index]

    if len(new_queue) == len(current_queue):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Cue {cue_index} not in regeneration queue"
        )

    # Save back to database
    await db.jobs.update_one(
        {"_id": job_id},
        {
            "$set": {
                f"outputs.{language}.accessible_video_edit_state.tts_regeneration_queue": new_queue,
                "updated_at": datetime.utcnow()
            }
        }
    )

    logger.info(f"Removed cue {cue_index} from TTS regeneration queue for job {job_id}/{language}")

    return {"message": f"Removed cue {cue_index} from regeneration queue"}


@router.post("/{job_id}/accessible-video/{language}/re-render", response_model=JobResponse)
async def trigger_accessible_video_rerender(
    job_id: str,
    language: str,
    request: RerenderAccessibleVideoRequest,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """
    Trigger re-synthesis of accessible video with QC changes.
    - Regenerates only queued TTS segments (others reuse existing MP3s)
    - Optionally runs Whisper pause point refinement
    """
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Check job is in QC status
    if job_doc["status"] not in [JobStatus.PENDING_QC.value]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Job must be in pending_qc status to re-render (current: {job_doc['status']})"
        )

    # Linguists can only re-render their assigned language
    if current_user.role == UserRole.LINGUIST:
        try:
            lqc._assert_can_act(job_doc, language, current_user)
        except HTTPException:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"You are not assigned to language '{language}'"
            )

    # Get edit state
    lang_output = job_doc.get("outputs", {}).get(language)
    if not lang_output or not lang_output.get("accessible_video_edit_state"):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No edit state found for language {language}"
        )

    edit_state = lang_output["accessible_video_edit_state"]

    # Get cues to regenerate
    regenerate_cues = [
        req.get("cue_index")
        for req in edit_state.get("tts_regeneration_queue", [])
        if req.get("status") == "pending"
    ]

    # Update job status to RENDERING_QC — conditional to prevent concurrent render races
    job_title = job_doc.get("title", "Untitled Job")
    transition_result = await db.jobs.update_one(
        {"_id": job_id, "status": JobStatus.PENDING_QC.value},  # Only transition from PENDING_QC
        {
            "$set": {
                "status": JobStatus.RENDERING_QC.value,
                "updated_at": datetime.utcnow()
            },
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": JobStatus.RENDERING_QC.value,
                    "by": str(current_user.id),
                    "notes": f"Re-rendering {language} with {len(regenerate_cues)} TTS regeneration(s), whisper_refine={request.whisper_refine}"
                }
            }
        }
    )
    if transition_result.modified_count == 0:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="A re-render is already in progress for this job. Please wait for it to complete."
        )

    # Trigger re-render task
    from ...tasks.rerender_accessible_video import rerender_accessible_video_task
    rerender_accessible_video_task.delay(
        job_id=job_id,
        language=language,
        regenerate_cue_indices=regenerate_cues,
        whisper_refine=request.whisper_refine
    )

    logger.info(
        f"Triggered accessible video re-render for job {job_id}/{language}: "
        f"regenerate_cues={regenerate_cues}, whisper_refine={request.whisper_refine}"
    )

    # Get updated job
    result = await db.jobs.find_one({"_id": job_id})

    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )


@router.put("/{job_id}/tts-preferences", response_model=JobResponse)
async def update_tts_preferences(
    job_id: str,
    request: UpdateTTSPreferencesRequest,
    current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION, UserRole.ADMIN)),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """
    Update TTS preferences and regenerate all TTS segments for all languages.

    This endpoint:
    1. Updates the TTS preferences in the job
    2. Queues ALL cues for TTS regeneration for all languages with AD outputs
    3. Triggers re-render tasks for each language
    """
    job_doc = await db.jobs.find_one({"_id": job_id})
    if not job_doc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Job not found"
        )

    # Check job is in QC status
    if job_doc["status"] not in [JobStatus.PENDING_QC.value]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Job must be in pending_qc status to update TTS preferences (current: {job_doc['status']})"
        )

    # Check if audio description MP3 is requested
    if not job_doc.get("requested_outputs", {}).get("audio_description_mp3"):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="TTS preferences can only be updated for jobs with audio description MP3 requested"
        )

    # Get languages with AD outputs
    outputs = job_doc.get("outputs", {})
    languages_to_regenerate = []

    for lang, lang_output in outputs.items():
        if lang_output.get("ad_vtt_gcs") and lang_output.get("ad_cues_gcs_prefix"):
            languages_to_regenerate.append(lang)

    if not languages_to_regenerate:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="No languages with audio description outputs found to regenerate"
        )

    # Update TTS preferences in the job
    await db.jobs.update_one(
        {"_id": job_id},
        {
            "$set": {
                "requested_outputs.tts_preferences": request.tts_preferences.model_dump(),
                "status": JobStatus.TTS_GENERATING.value,
                "updated_at": datetime.utcnow()
            },
            "$push": {
                "review.history": {
                    "at": datetime.utcnow(),
                    "status": JobStatus.TTS_GENERATING.value,
                    "by": str(current_user.id),
                    "notes": f"Updating TTS preferences and regenerating for {len(languages_to_regenerate)} language(s)"
                }
            }
        }
    )

    # Import tts_synthesis for parsing AD cues
    from ...tasks.tts_synthesis import parse_ad_cues
    from ...tasks.rerender_accessible_video import rerender_accessible_video_task

    # For each language, get cue count and queue all cues, then trigger re-render
    for lang in languages_to_regenerate:
        lang_output = outputs[lang]

        # Download AD VTT to count cues
        ad_vtt_gcs = lang_output.get("ad_vtt_gcs")
        ad_blob_path = 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 cues to get count
        cues = parse_ad_cues(ad_vtt_content)
        all_cue_indices = list(range(len(cues)))

        logger.info(
            f"Queuing {len(all_cue_indices)} cues for TTS regeneration in language {lang} for job {job_id}"
        )

        # Update TTS regeneration queue for this language
        regeneration_queue = [
            {"cue_index": idx, "status": "pending", "queued_at": datetime.utcnow().isoformat()}
            for idx in all_cue_indices
        ]

        await db.jobs.update_one(
            {"_id": job_id},
            {
                "$set": {
                    f"outputs.{lang}.accessible_video_edit_state.tts_regeneration_queue": regeneration_queue,
                    "updated_at": datetime.utcnow()
                }
            }
        )

        # Trigger re-render task for this language
        rerender_accessible_video_task.delay(
            job_id=job_id,
            language=lang,
            regenerate_cue_indices=all_cue_indices,
            whisper_refine=False
        )

        logger.info(
            f"Triggered TTS regeneration re-render for job {job_id}/{lang} with {len(all_cue_indices)} cues"
        )

    # Broadcast status update
    job_title = job_doc.get("title", "Untitled Job")
    await connection_manager.broadcast_job_update(
        job_id,
        {
            "type": "status_update",
            "job_id": job_id,
            "status": JobStatus.TTS_GENERATING.value,
            "job_title": job_title,
            "message": f"Regenerating TTS for {len(languages_to_regenerate)} language(s) with new voice settings"
        }
    )

    # Get updated job
    result = await db.jobs.find_one({"_id": job_id})

    return JobResponse(
        id=str(result["_id"]),
        title=result["title"],
        status=result["status"],
        source=result["source"],
        requested_outputs=RequestedOutputs(**result["requested_outputs"]),
        review=result.get("review", {"notes": "", "history": []}),
        outputs=result.get("outputs"),
        created_at=result["created_at"].isoformat(),
        updated_at=result["updated_at"].isoformat()
    )
