from flask import Blueprint, jsonify, send_file, request, Response,session,render_template
import requests
import os
import uuid
from typing import Optional
from Video_generation.pdf_conversion import secure_filename, _allowed_file, _public_dir, _full_url_for, MAX_UPLOAD_MB, ALLOWED_EXTENSIONS, _text_to_pdf_bytes_verbatim, _bytes_to_mb, _read_text_from_txt
from Video_generation.video_main import run_full_pipeline
from celery.result import AsyncResult
from celery_app import celery
from utils.jwt_service import jwt_required
from mongoengine import DoesNotExist, ValidationError
from bson import ObjectId
from models.video_model import Video
from datetime import datetime
from models.avatar_models import Avatar
from Video_generation.helpers import extract_icon
from Video_generation.mcq_main import run_mcq_pipeline
import mimetypes
from utils.jwt_service import generate_token
from models.admin import Admin
import logging
logger = logging.getLogger(__name__)
video_gen_bp = Blueprint("video_gen", __name__)

@video_gen_bp.route("/upload", methods=["POST"])
@jwt_required
def upload_document():
    """Upload a file or text, convert to PDF if needed, and return its path."""
    json_text: Optional[str] = None
    if request.is_json and (request.json or {}).get("text") is not None:
        json_text = str((request.json or {}).get("text"))

    form_text: Optional[str] = None
    if request.form and request.form.get("text") is not None:
        form_text = str(request.form.get("text"))

    session_id = str(uuid.uuid4())
    public_dir = _public_dir()
    os.makedirs(public_dir, exist_ok=True)

    pdf_path = None

    # --- Handle file uploads ---
    if "file" in request.files and request.files["file"]:
        f = request.files["file"]
        if not f.filename:
            return jsonify({"status": False, "message": "File provided without a filename."}), 400

        filename = secure_filename(f.filename)
        ext = os.path.splitext(filename.lower())[1]

        if not _allowed_file(filename):
            return jsonify({
                "status": False,
                "message": f"Unsupported file type. Allowed: {', '.join(sorted(ALLOWED_EXTENSIONS))}"
            }), 400

        content_length = request.content_length or 0
        if content_length and _bytes_to_mb(content_length) > MAX_UPLOAD_MB:
            return jsonify({
                "status": False,
                "message": f"Upload too large. Max {MAX_UPLOAD_MB} MB"
            }), 413

        # --- Save or convert file ---
        if ext == ".pdf":
            pdf_path = os.path.join(public_dir, filename)
            f.save(pdf_path)

        elif ext == ".txt":
            text = _read_text_from_txt(f)
            base = os.path.splitext(filename)[0]
            out_name = f"{base}.pdf"
            pdf_bytes = _text_to_pdf_bytes_verbatim(text)
            pdf_path = os.path.join(public_dir, out_name)
            with open(pdf_path, "wb") as out:
                out.write(pdf_bytes)

        else:
            return jsonify({"status": False, "message": "Unsupported file type."}), 400

    # --- Handle raw text upload ---
    elif json_text or form_text:
        text = json_text or form_text
        out_name = f"text-{uuid.uuid4().hex[:8]}.pdf"
        pdf_bytes = _text_to_pdf_bytes_verbatim(text)
        pdf_path = os.path.join(public_dir, out_name)
        with open(pdf_path, "wb") as out:
            out.write(pdf_bytes)

    else:
        return jsonify({"status": False, "message": "Provide a valid file or text."}), 400

    # --- Success response ---
    return jsonify({
        "status": True,
        "message": "File uploaded and converted successfully.",
        "session_id": session_id,
        "pdf_path": pdf_path
    }), 200


import time
@video_gen_bp.route("/generate", methods=["POST"])
@jwt_required
def generate_video():
    """
    Generates the video using a pre-uploaded PDF.
    Also creates a database record for tracking.
    """
    logger.info("Received /generate request")
    data = request.get_json(force=True)
    pdf_path = data.get("pdf_path")
    admin_id = data.get("admin_id")
    title = data.get("title", "Untitled Video")
    description = data.get("description", "")
    subject = data.get("subject", "")
    avatar_id =  data.get("avatar_id", None)
    avatar_data = Avatar.objects(id=avatar_id).first()
    if not os.path.exists(avatar_data.avatar_video_path):
        logger.error(f"Avatar video path does not exist: {avatar_data.avatar_video_path}")
        return jsonify({"status": False, "message": "Avatar video path does not exist."}), 400

    avatar= avatar_data.avatar_video_path if avatar_data else None
    voice_id = avatar_data.voices if avatar_data and avatar_data.voices else None
    exam_id=data.get("exam_id")
    sub_exam_id=data.get("sub_exam_id")
    chapter_id=data.get("chapter_id")
    topic_id=data.get("topic_id")
    subject_id=data.get("subject_id")

    if not pdf_path or not admin_id:
        return jsonify({"status": False, "message": "pdf_path and admin_id are required."}), 400

    if not os.path.exists(pdf_path):
        return jsonify({"status": False, "message": f"PDF not found at {pdf_path}"}), 404

    session_id = str(uuid.uuid4())

    # Start video generation task
    task = run_full_pipeline(
        pdf_path=pdf_path,
        avatar_path=avatar,
        session_id=session_id,
        voice_id= voice_id
    )
    admin = Admin.objects(id=ObjectId(admin_id)).first()
    # Store process info in MongoDB
    try:
        video_doc = Video(
            admin_id=admin_id,
            author=admin.username,
            title=title,
            description=description,
            subject=subject,
            video_type= "lecture",
            path=f"./public/Video/{session_id}_final_video.mp4",  # Will be updated once video is saved
            task_id=task.id,
            processing_start_datetime=datetime.utcnow(),
            exam_id=ObjectId(exam_id),
            sub_exam_id=ObjectId(sub_exam_id),
            chapter_id=ObjectId(chapter_id),
            topic_id=ObjectId(topic_id),
            subject_id=ObjectId(subject_id)
        )
        video_doc.save()
    except ValidationError as e:
        return jsonify({"status": False, "message": f"Database validation error: {e}"}), 400

    return jsonify({
        "status": True,
        "message": "Video generation started.",
        "session_id": session_id,
        "task_id": task.id,
        "video_id": str(video_doc.id)
    }), 200

@video_gen_bp.route("/generate_mcq_video", methods=["POST"])
@jwt_required
def generate_mcq_video():
    """
    Generates the video using a pre-uploaded PDF.
    Also creates a database record for tracking.
    """
    data = request.get_json(force=True)
    pdf_path = data.get("pdf_path")
    admin_id = data.get("admin_id")
    title = data.get("title", "Untitled Video")
    description = data.get("description", "")
    subject = data.get("subject", "")
    avatar_id =  data.get("avatar_id", None)
    avatar_data = Avatar.objects(id=avatar_id).first()
    avatar= avatar_data.avatar_video_path if avatar_data else None
    voice_id = avatar_data.voices if avatar_data and avatar_data.voices else None
    exam_id=data.get("exam_id")
    sub_exam_id=data.get("sub_exam_id")
    chapter_id=data.get("chapter_id")
    topic_id=data.get("topic_id")
    subject_id=data.get("subject_id")

    if not pdf_path or not admin_id:
        return jsonify({"status": False, "message": "pdf_path and admin_id are required."}), 400

    if not os.path.exists(pdf_path):
        return jsonify({"status": False, "message": f"PDF not found at {pdf_path}"}), 404

    session_id = str(uuid.uuid4())

    # Start video generation task
    task = run_mcq_pipeline(
        pdf_path=pdf_path,
        avatar_path=avatar,
        session_id=session_id,
        voice_id= voice_id
    )
    admin = Admin.objects(id=ObjectId(admin_id)).first()
    # Store process info in MongoDB
    try:
        video_doc = Video(
            admin_id=admin_id,
            author=admin.username,
            title=title,
            description=description,
            subject=subject,
            video_type="mcq",
            path=f"./public/Video/{session_id}_final_video.mp4",  # Will be updated once video is saved
            task_id=task.id,
            processing_start_datetime=datetime.utcnow(),
            exam_id=ObjectId(exam_id),
            sub_exam_id=ObjectId(sub_exam_id),
            chapter_id=ObjectId(chapter_id),
            topic_id=ObjectId(topic_id),
            subject_id=ObjectId(subject_id)
        )
        video_doc.save()
    except ValidationError as e:
        return jsonify({"status": False, "message": f"Database validation error: {e}"}), 400

    return jsonify({
        "status": True,
        "message": "Video generation started.",
        "session_id": session_id,
        "task_id": task.id,
        "video_id": str(video_doc.id)
    }), 200


def check_status(task_id):
    """Check the status of a background video generation job."""
    result = AsyncResult(task_id, app=celery)

    # Map Celery states to friendly states
    state_map = {
        "PENDING": "queued",
        "STARTED": "processing",
        "SUCCESS": "completed",
        "FAILURE": "failed",
        "RETRY": "retrying",
    }

    state = state_map.get(result.state, result.state)

    response = {
        "task_id": task_id,
        "state": state,
    }

    if result.state == "FAILURE":
        response["error"] = str(result.info)
    elif result.state == "SUCCESS":
        response["result"] = result.result

    return response

# --- List all videos for an admin ---
@video_gen_bp.route("/videos/<admin_id>", methods=["GET"])
@jwt_required
def list_videos(admin_id):
    """
    Lists all videos generated by a specific admin (teacher).
    Optionally filters by Celery status (?status=PENDING/STARTED/SUCCESS/FAILURE).
    """
    status_filter = request.args.get("status")

    # Fetch all videos for the given admin
    videos = Video.objects(admin_id=admin_id).order_by("-created_at")

    if not videos:
        return jsonify({"message": "No videos found for this admin"}), 404

    videos_data = []
    for v in videos:
        # Check current Celery task status
        task_status = check_status(v.task_id)
        task_id=v.task_id

        # Apply optional ?status filter (based on Celery state)
        if status_filter and task_status["state"] != status_filter.upper():
            continue

        videos_data.append({
            "id": str(v.id),
            "admin_id": str(v.admin_id.id) if v.admin_id else None,
            "title": v.title,
            "description": v.description,
            "subject": v.subject,
            "status": task_status["state"],
            "created_at": v.created_at,
            "job_id":task_id,
            "author":v.author,
            "reviewed_by":v.reviewed_by,
            "review_status":v.review_status,
            "type":v.video_type
        })

    if not videos_data:
        return jsonify({"message": f"No videos found for this admin with status '{status_filter}'"}), 404

    return jsonify(videos_data), 200



# --- Download a specific video ---
@video_gen_bp.route("/download/<video_id>", methods=["GET"])
@jwt_required
def download_video(video_id):
    """
    Allows a teacher to download their processed video.
    Only works for videos with status = 'completed'.
    """
    try:
        video = Video.objects.get(id=ObjectId(video_id))
    except (DoesNotExist, ValidationError):
        return jsonify({"error": "Invalid or missing video"}), 404
    # Optional: prevent download if not completed
    if check_status(video.task_id)["state"] != "completed":
        return jsonify({"error": "Video is not ready for download"}), 403

    file_path = video.path  # Remove leading dot if present

    print("DEBUG: File path =", file_path)
    print("DEBUG: Exists =", os.path.isfile(file_path))

    if not os.path.isfile(file_path):
        return jsonify({"error": "Video file not found on server"}), 404

    try:
        return send_file(file_path, as_attachment=True)
    except Exception as e:
        print("SEND_FILE ERROR:", e)
        return jsonify({"error": "Failed to send file"}), 500
    
@video_gen_bp.route("/avatar_list", methods=["GET"])
@jwt_required
def get_avatar_list():
    """Return list of avatars from the database with name, description and video path."""
    try:
        avatars = Avatar.objects().order_by("-created_at")
    except Exception as e:
        return jsonify({"status": False, "message": f"Database error: {e}"}), 500

    avatars_out = []
    for a in avatars:
        created_at = getattr(a, "created_at", None)
        avatars_out.append({
            "id": str(a.id),
            "avatar_name": getattr(a, "avatar_name", "") or "",
            "avatar_description": getattr(a, "avatar_description", "") or "",
            "avatar_video_path": getattr(a, "avatar_video_path", "") or "",
            "avatar_icon": getattr(a, "avatar_icon", "") or "",
            "voices": getattr(a, "voices", None),
            "gender": getattr(a, "gender", None),
            "avatar_status": getattr(a, "avatar_status", None),
            "created_at": created_at.isoformat() if created_at else None,
        })

    return jsonify({"status": True, "avatars": avatars_out}), 200




@video_gen_bp.route("/stream/<video_id>", methods=["GET"])
@jwt_required
def stream_video(video_id):
    """
    Stream a video file by video_id.
    Supports byte-range requests for smooth playback.
    If file not found but DB record exists, it's still processing or queued.
    """
    try:
        video = Video.objects.get(task_id=video_id)
    except (DoesNotExist, ValidationError):
        return jsonify({
            "status": False,
            "message": "Invalid or missing video ID."
        }), 404

    video_path = video.path  # e.g., public/videos/sample2.mp4

    # ✅ Check if file exists on disk
    if not os.path.isfile(video_path):
        # Check if processing or queued
        task_status = "unknown"
        if video.task_id:
            result = AsyncResult(video.task_id, app=celery)
            task_state = result.state.upper()
            if task_state in ["PENDING", "STARTED"]:
                task_status = "processing"
            elif task_state == "FAILURE":
                task_status = "failed"

        # Return a friendly message based on task state
        if task_status == "processing":
            msg = "Video is still processing or in queue. Please try again later."
        elif task_status == "failed":
            msg = "Video generation failed. Please re-generate the video."
        else:
            msg = "Video file not found on server."

        return jsonify({
            "status": False,
            "message": msg,
            "video_id": str(video.id),
            "current_status": task_status
        }), 200 # 200 Accepted = still in progress

    # ✅ Proceed with streaming if file exists
    file_size = os.path.getsize(video_path)
    range_header = request.headers.get("Range", None)
    mime_type = mimetypes.guess_type(video_path)[0] or "video/mp4"

    if range_header:
        byte1, byte2 = 0, None
        match = range_header.replace("bytes=", "").split("-")
        if match[0]:
            byte1 = int(match[0])
        if len(match) > 1 and match[1]:
            byte2 = int(match[1])
        length = (byte2 + 1 if byte2 else file_size) - byte1

        def generate():
            with open(video_path, "rb") as f:
                f.seek(byte1)
                yield f.read(length)

        rv = Response(generate(), status=206, mimetype=mime_type)
        rv.headers.add("Content-Range", f"bytes {byte1}-{byte1 + length - 1}/{file_size}")
        rv.headers.add("Accept-Ranges", "bytes")
        rv.headers.add("Content-Length", str(length))
        return rv

    # No range header → stream entire file
    def generate_full():
        with open(video_path, "rb") as f:
            while True:
                chunk = f.read(8192)
                if not chunk:
                    break
                yield chunk

    return Response(generate_full(), mimetype=mime_type)

@video_gen_bp.route("/statuses/<admin_id>", methods=["GET"])
@jwt_required
def list_statuses(admin_id):
    """
    Lists all videos generated by a specific admin (teacher).
    Optionally filters by Celery status (?status=PENDING/STARTED/SUCCESS/FAILURE).
    """
    status_filter = request.args.get("status")

    # Fetch all videos for the given admin
    videos = Video.objects(admin_id=admin_id).order_by("-created_at")

    if not videos:
        return jsonify({"message": "No videos found for this admin"}), 404

    videos_data = []
    for v in videos:
        # Check current Celery task status
        videos_data.append({
            "id": str(v.id),
            "admin_id": str(v.admin_id.id) if v.admin_id else None,
            "title": v.title,
            "description": v.description,
            "subject": v.subject,
            "status": "completed" if v.execution_completed_datetime else "queued",
            "created_at": v.created_at,
            "job_id":v.task_id
        })

    if not videos_data:
        return jsonify({"message": f"No videos found for this admin with status '{status_filter}'"}), 404

    return jsonify(videos_data), 200



@video_gen_bp.route("/video_delete/<video_id>", methods=["DELETE"])
@jwt_required
def delete_video(video_id):
    """
    Delete a video record, remove file from disk (if exists), and try to revoke the Celery task.
    Optional query param: ?admin_id=<admin_id> — when provided, the route will verify ownership.
    """
    admin_id_q = request.args.get("admin_id")

    if not video_id:
        return jsonify({"status": False, "message": "Missing video id"}), 400

    # Try multiple lookup strategies for robustness
    video = None
    lookup_errors = []
    try:
        # Preferred: let MongoEngine coerce the string id
        video = Video.objects.get(id=video_id)
    except (DoesNotExist, ValidationError) as e:
        lookup_errors.append(f"get(id=video_id) failed: {e}")
        try:
            # Try explicit ObjectId conversion (in case id string needs it)
            video = Video.objects.get(id=ObjectId(video_id))
        except Exception as e2:
            lookup_errors.append(f"get(id=ObjectId(...)) failed: {e2}")
            video = None

    if not video:
        # helpful debugging payload
        return jsonify({
            "status": False,
            "message": "Video not found",
            "video_id": video_id,
            "lookup_attempts": lookup_errors
        }), 404

    # optional ownership check
    if admin_id_q:
        try:
            owner_id = str(video.admin_id.id) if video.admin_id else None
        except Exception:
            owner_id = None
        if owner_id != admin_id_q:
            return jsonify({"status": False, "message": "Forbidden: admin_id does not own this video"}), 403

    # try to revoke celery task (best-effort)
    task_id = getattr(video, "task_id", None)
    revoke_info = {"revoked": False, "task_id": task_id}
    if task_id:
        try:
            res = AsyncResult(task_id, app=celery)
            res.revoke(terminate=True, signal="SIGKILL")
            revoke_info["revoked"] = True
        except Exception as e:
            revoke_info["error"] = str(e)

    # try to remove video file from disk (best-effort)
    file_path = getattr(video, "path", None) or ""
    file_removed = False
    file_exists = False
    file_error = None
    if file_path:
        try:
            # normalize path (relative -> absolute)
            p = file_path.lstrip(".")
            p = p if os.path.isabs(p) else os.path.join(os.getcwd(), p.lstrip("/"))
            if os.path.isfile(p):
                file_exists = True
                try:
                    os.remove(p)
                    file_removed = True
                except Exception as e:
                    file_error = str(e)
            else:
                file_exists = False
        except Exception as e:
            file_error = str(e)

    # remove DB record
    try:
        video.delete()
    except Exception as e:
        return jsonify({
            "status": False,
            "message": "Failed to delete video record from database",
            "db_error": str(e)
        }), 500

    resp = {
        "status": True,
        "message": "Video deleted",
        "video_id": video_id,
        "file": {
            "original_path": file_path,
            "exists": file_exists,
            "removed": file_removed,
            "error": file_error
        },
        "task": revoke_info
    }
    return jsonify(resp), 200

ELEVEN_VOICES_URL = "https://api.elevenlabs.io/v1/voices"


@video_gen_bp.route("/voices_list", methods=["GET"])
@jwt_required
def get_voice_list():
    """
    Fetch the list of available voices from ElevenLabs API
    and return as { description_or_name: voice_id } dictionary.
    """
    api_key = os.getenv("ELEVENLABS_API_KEY")
    if not api_key:
        return jsonify({"status": False, "message": "ELEVENLABS_API_KEY not set"}), 400

    headers = {
        "xi-api-key": api_key,
        "Content-Type": "application/json",
    }

    try:
        resp = requests.get(ELEVEN_VOICES_URL, headers=headers)
        if resp.status_code != 200:
            return jsonify({
                "status": False,
                "message": f"Failed to fetch voices: {resp.status_code}",
                "error": resp.text
            }), 500

        data = resp.json()
        voices = data.get("voices", [])

        voice_map = {}
        for v in voices:
            voice_id = v.get("voice_id") or v.get("id")
            if not voice_id:
                continue

            # Pick the most descriptive text available
            label = v.get("description") or v.get("name") or voice_id
            voice_map[label] = voice_id

        return jsonify({"status": True, "voices": voice_map}), 200

    except Exception as e:
        return jsonify({"status": False, "message": str(e)}), 500

@video_gen_bp.route("/create_avatar", methods=["POST"])
@jwt_required
def create_avatar():
    """Creates a new avatar entry in the database.
    Accepts multipart/form-data:
      - avatar_video (mp4)
      - avatar_icon (png/jpg/jpeg)
      - avatar_name (string)
      - avatar_description (optional)
      - gender (optional)
    """
    # files
    video_file = request.files.get("avatar_video") or request.files.get("video")

    # fields
    avatar_name = (request.form.get("avatar_name") or request.values.get("avatar_name"))
    avatar_description = request.form.get("avatar_description", "")
    gender = request.form.get("gender", "Not Specified")
    voice_id = request.form.get("voice_id", None)  # Optional voice ID

    # basic validation
    if not video_file or not avatar_name:
        return jsonify({"status": False, "message": "avatar_video, avatar_icon and avatar_name are required."}), 400

    # allowed extensions
    vid_exts = {".mp4"}

    # secure filenames and extensions
    v_name = secure_filename(video_file.filename or "")
    v_ext = os.path.splitext(v_name.lower())[1]

    if v_ext not in vid_exts:
        return jsonify({"status": False, "message": "avatar_video must be an mp4 file."}), 400
    # prepare directories
    video_dir = os.getenv("AVATAR_CLIP_DIR", "./public/avatar_clips")
    os.makedirs(video_dir, exist_ok=True)

    # unique filenames
    v_out_name = f"{uuid.uuid4().hex[:8]}_{v_name}"

    v_path = os.path.join(video_dir, v_out_name)

    # save files
    try:
        video_file.save(v_path)
    except Exception as e:
        return jsonify({"status": False, "message": f"Failed saving files: {e}"}), 500
    icon_path= extract_icon(v_path)
    # store paths as saved on disk
    try:
        new_avatar = Avatar(
            avatar_icon=icon_path,
            avatar_name=avatar_name,
            avatar_description=avatar_description,
            avatar_video_path=v_path,
            voices= voice_id,  # default voice
            gender= gender,
            avatar_status=1  # Active by default
        )
        new_avatar.save()
    except ValidationError as e:
        return jsonify({"status": False, "message": f"Database validation error: {e}"}), 400

    return jsonify({
        "status": True,
        "message": "Avatar created successfully.",
        "avatar_id": str(new_avatar.id),
        "avatar_icon_path": icon_path,
        "avatar_video_path": v_path
    }), 201

@video_gen_bp.route("/avatar/<filename>", methods=["GET"])
@jwt_required
def get_avatar(filename):
    """Serves a specific avatar image file."""
    avatar_path = os.getenv("AVATAR_DIR", "avatars")
    safe_filename = secure_filename(filename)
    full_path = os.path.join(avatar_path, safe_filename)

    if not os.path.isfile(full_path):
        return jsonify({"status": False, "message": "Avatar not found."}), 404

    try:
        return send_file(full_path, as_attachment=False)
    except Exception as e:
        print("SEND_FILE ERROR:", e)
        return jsonify({"status": False, "message": "Failed to send avatar file."}), 500



@video_gen_bp.route('/avatar/last-upload')
@video_gen_bp.route('/avatar/last-upload/<user_id>')
def last_upload(user_id=None):
    print("user_id:",user_id)
    print("session:",session)
    user = session.get("user") if "user" in session else None
    token = generate_token({"id": user}) if user else None
    return render_template('user/generator-current.html', user=user, token=token)


@video_gen_bp.route('/avatar/video-generator')
def video_generator():
    print("session:",session)
    user = session.get("user") if "user" in session else None
    token = generate_token({"id": user}) if user else None
    return render_template('user/video-generator-dashboard.html', user=user, token=token)

ALLOWED_REVIEW_STATUSES = ['not_reviewed', 'completed', 'rejected']

@video_gen_bp.route('/update/<video_id>', methods=['PATCH'])
@jwt_required
def update_video_metadata(video_id):
    """
    PATCH /user/video_generation/update/<video_id>
    JWT required. The token's identity is expected to be the user id string.
    Body: { "author": "...", "review_status": "completed", "reviewed_by": "admin" }
    """
    try:
        current_user_id = session.get("user")
        if not video_id:
            return jsonify({"error": "missing id"}), 400

        payload = request.get_json(force=True, silent=True) or {}
        author = payload.get('author')
        review_status = payload.get('review_status')
        reviewed_by = payload.get('reviewed_by')  # <-- new

        # validate review_status if present
        if review_status is not None and review_status not in ALLOWED_REVIEW_STATUSES:
            return jsonify({"error": "invalid review_status", "allowed": ALLOWED_REVIEW_STATUSES}), 400

        # optional: basic validation for reviewed_by (if you want)
        if reviewed_by is not None:
            if not isinstance(reviewed_by, (str, int)):
                return jsonify({"error": "invalid reviewed_by, must be string or numeric"}), 400
            reviewed_by = str(reviewed_by).strip()
            # you can enforce non-empty if needed:
            # if reviewed_by == "":
            #     return jsonify({"error": "reviewed_by cannot be empty"}), 400

        video = Video.objects(id=video_id).first()
        if not video:
            return jsonify({"error": "video not found"}), 404

        # permission check: allow if current_user is the admin/owner.
        try:
            owner_id = str(video.admin_id.id) if hasattr(video.admin_id, 'id') else str(video.admin_id)
        except Exception:
            owner_id = str(video.admin_id)

        if current_user_id != owner_id:
            # Optionally: allow admins/roles — add extra logic here
            return jsonify({"error": "forbidden", "message": "you do not have permission to update this video"}), 403

        changed = False
        if author is not None:
            video.author = author
            changed = True
        if review_status is not None:
            video.review_status = review_status
            changed = True
        if reviewed_by is not None:
            # save null/empty string as-is (your normalize code expects DB values possibly null)
            video.reviewed_by = reviewed_by
            changed = True

        if changed:
            video.save()
            # return the important fields so the client can re-render without an extra fetch
            return jsonify({
                "ok": True,
                "id": str(video.id),
                "author": video.author,
                "review_status": video.review_status,
                "reviewed_by": video.reviewed_by
            }), 200
        else:
            return jsonify({"ok": False, "message": "nothing to update"}), 400

    except ValidationError as e:
        return jsonify({"error": "validation error", "details": str(e)}), 400
    except Exception as e:
        import traceback; traceback.print_exc()
        return jsonify({"error": "server error", "details": str(e)}), 500
