# app/blueprints/ui.py
import os
import secrets
import shutil
import time
import uuid
import sqlite3
from datetime import datetime
from flask import (
    Blueprint,
    current_app,
    render_template,
    request,
    redirect,
    url_for,
    flash,
    send_file,
    jsonify,
    session,
)

from db.bulk import bulk_db
from services.auth import require_login_access_token
from services.drafts import save_draft, load_draft
from services.media import safe_filename, video_duration_seconds_ffprobe, build_public_media_url
from services.creator import creator_can_post_now, parse_publish_at_iso_to_ts

from token_store import load_token, save_token
from tiktok_client import query_creator_info, upload_video_direct_post, fetch_post_status

bp = Blueprint("ui", __name__)

ADMIN_SESSION_KEY = "admin_authenticated"
TERMINAL_JOB_STATUSES = {"complete", "failed", "cancelled"}


def _is_connected() -> bool:
    return session.get("tiktok_token") is not None


def _ensure_alias_token_persisted(alias: str) -> None:
    """
    Defensa extra para el caso en que ya existan sesiones antiguas donde
    oauth_alias estaba seteado pero token_store no lo tenía aún.
    """
    a = (alias or "").strip()
    if not a:
        return
    try:
        if not load_token(a):
            tok = session.get("tiktok_token")
            if tok:
                save_token(a, tok)
    except Exception:
        # No bloqueamos UI por fallos de persistencia; el worker te lo dirá si falta alias
        pass


def _table_columns(conn: sqlite3.Connection, table_name: str):
    cur = conn.cursor()
    cur.execute(f"PRAGMA table_info({table_name})")
    cols = []
    for row in cur.fetchall():
        try:
            cols.append(row["name"])
        except Exception:
            cols.append(row[1])
    return cols


def _insert_bulk_job(conn: sqlite3.Connection, job: dict) -> None:
    cols = _table_columns(conn, "bulk_jobs")
    filtered = {k: v for k, v in job.items() if k in set(cols)}
    colnames = ", ".join(filtered.keys())
    placeholders = ", ".join(["?"] * len(filtered))
    conn.execute(f"INSERT INTO bulk_jobs ({colnames}) VALUES ({placeholders})", tuple(filtered.values()))


def _is_admin_authenticated() -> bool:
    return session.get(ADMIN_SESSION_KEY) is True


def _admin_guard():
    if _is_admin_authenticated():
        return None
    next_url = request.full_path if request.query_string else request.path
    return redirect(url_for("ui.admin_login", next=next_url))


def _job_file_path(stored_filename: str):
    filename = (stored_filename or "").strip()
    if not filename or "/" in filename or "\\" in filename or ".." in filename:
        return None
    return os.path.join(current_app.config["UPLOAD_DIR"], filename)


def _job_has_file(stored_filename: str) -> bool:
    path = _job_file_path(stored_filename)
    return bool(path and os.path.isfile(path))


def _format_ts(ts) -> str:
    if ts in (None, ""):
        return "-"
    try:
        return datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M:%S")
    except Exception:
        return str(ts)


def _format_bytes(size) -> str:
    if size in (None, ""):
        return "-"
    units = ["B", "KB", "MB", "GB"]
    value = float(size)
    unit = units[0]
    for unit in units:
        if value < 1024 or unit == units[-1]:
            break
        value /= 1024.0
    return f"{value:.1f} {unit}" if unit != "B" else f"{int(value)} {unit}"


def _status_tone(status: str) -> str:
    st = (status or "").strip().lower()
    if st == "complete":
        return "ok"
    if st == "failed":
        return "err"
    if st == "cancelled":
        return "warn"
    if st in {"submitted", "submitting"}:
        return "warn"
    return "info"


def _job_for_admin(row: sqlite3.Row):
    job = dict(row)
    status = (job.get("status") or "").strip().lower()
    job["is_terminal"] = status in TERMINAL_JOB_STATUSES
    job["status_tone"] = _status_tone(status)
    job["has_file"] = _job_has_file(job.get("stored_filename") or "")
    job["created_at_label"] = _format_ts(job.get("created_at_ts"))
    job["publish_at_label"] = _format_ts(job.get("publish_at_ts"))
    job["next_attempt_label"] = _format_ts(job.get("next_attempt_ts"))
    job["updated_at_label"] = _format_ts(job.get("updated_at_ts"))

    if job["has_file"]:
        file_path = _job_file_path(job.get("stored_filename") or "")
        try:
            job["file_size_bytes"] = os.path.getsize(file_path)
        except OSError:
            job["file_size_bytes"] = None
    else:
        job["file_size_bytes"] = None
    job["file_size_label"] = _format_bytes(job["file_size_bytes"])

    return job


def _load_admin_jobs():
    conn = bulk_db(current_app.config["BULK_DB_PATH"])
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    cur.execute(
        """
        SELECT * FROM bulk_jobs
        ORDER BY
          CASE
            WHEN status IN ('scheduled', 'submitting', 'submitted') THEN 0
            ELSE 1
          END,
          publish_at_ts ASC,
          updated_at_ts DESC
        """
    )
    rows = cur.fetchall()
    conn.close()

    jobs = []
    for row in rows:
        job = _job_for_admin(row)
        if job["is_terminal"] and not job["has_file"]:
            continue
        jobs.append(job)
    return jobs


def _load_upload_inventory(jobs):
    upload_dir = current_app.config["UPLOAD_DIR"]
    job_links = {}
    for job in jobs:
        stored_filename = (job.get("stored_filename") or "").strip()
        if not stored_filename:
            continue
        job_links.setdefault(stored_filename, []).append(job)

    files = []
    for entry in os.scandir(upload_dir):
        if not entry.is_file():
            continue

        stat = entry.stat()
        linked_jobs = job_links.get(entry.name, [])
        files.append(
            {
                "stored_filename": entry.name,
                "size_bytes": stat.st_size,
                "size_label": _format_bytes(stat.st_size),
                "modified_at_label": _format_ts(int(stat.st_mtime)),
                "linked_jobs_count": len(linked_jobs),
                "active_jobs_count": sum(0 if j["is_terminal"] else 1 for j in linked_jobs),
                "terminal_jobs_count": sum(1 if j["is_terminal"] else 0 for j in linked_jobs),
            }
        )

    files.sort(
        key=lambda item: (
            item["active_jobs_count"] == 0,
            -item["linked_jobs_count"],
            item["stored_filename"].lower(),
        )
    )
    return files


def _count_jobs_for_file(stored_filename: str):
    conn = bulk_db(current_app.config["BULK_DB_PATH"])
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    cur.execute(
        """
        SELECT status, COUNT(*) AS total
        FROM bulk_jobs
        WHERE stored_filename=?
        GROUP BY status
        """,
        (stored_filename,),
    )
    rows = cur.fetchall()
    conn.close()

    active = 0
    terminal = 0
    for row in rows:
        total = int(row["total"] or 0)
        if (row["status"] or "").strip().lower() in TERMINAL_JOB_STATUSES:
            terminal += total
        else:
            active += total
    return active, terminal


# ---------------------------------------------------------------------
# Pages
# ---------------------------------------------------------------------
@bp.route("/", endpoint="landing")
def landing():
    return render_template("landing.html", is_connected=_is_connected())


@bp.route("/app", endpoint="app_home")
def app_home():
    return render_template("index.html", is_connected=_is_connected())


@bp.route("/contact", endpoint="contact")
def contact():
    return render_template("contact.html", is_connected=_is_connected())


@bp.route("/tos", endpoint="tos")
def tos():
    return render_template("tos.html")


@bp.route("/privacy", endpoint="privacy")
def privacy():
    return render_template("privacy.html")


# ---------------------------------------------------------------------
# Admin panel
# ---------------------------------------------------------------------
@bp.route("/admin/login", methods=["GET", "POST"], endpoint="admin_login")
def admin_login():
    if _is_admin_authenticated():
        return redirect(url_for("ui.admin_jobs"))

    next_url = (request.args.get("next") or request.form.get("next") or "").strip()

    if request.method == "POST":
        provided = request.form.get("password") or ""
        expected = str(current_app.config.get("ADMIN_PASSWORD") or "")

        if expected and secrets.compare_digest(provided, expected):
            session[ADMIN_SESSION_KEY] = True
            session.modified = True
            flash("Acceso admin concedido.", "success")
            if next_url.startswith("/"):
                return redirect(next_url)
            return redirect(url_for("ui.admin_jobs"))

        flash("Contraseña admin incorrecta.", "error")

    return render_template("admin_login.html", next_url=next_url)


@bp.route("/admin/logout", methods=["POST"], endpoint="admin_logout")
def admin_logout():
    session.pop(ADMIN_SESSION_KEY, None)
    session.modified = True
    flash("Sesión admin cerrada.", "success")
    return redirect(url_for("ui.admin_login"))


@bp.get("/admin", endpoint="admin_jobs")
def admin_jobs():
    guard = _admin_guard()
    if guard:
        return guard

    jobs = _load_admin_jobs()
    in_progress_jobs = [job for job in jobs if not job["is_terminal"]]
    finished_jobs = [job for job in jobs if job["is_terminal"]]
    upload_files = _load_upload_inventory(jobs)

    return render_template(
        "admin_jobs.html",
        in_progress_jobs=in_progress_jobs,
        finished_jobs=finished_jobs,
        upload_files=upload_files,
        total_jobs=len(jobs),
    )


@bp.post("/admin/jobs/<job_id>/cancel", endpoint="admin_cancel_job")
def admin_cancel_job(job_id: str):
    guard = _admin_guard()
    if guard:
        return guard

    conn = bulk_db(current_app.config["BULK_DB_PATH"])
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    cur.execute("SELECT * FROM bulk_jobs WHERE id=?", (job_id,))
    row = cur.fetchone()

    if not row:
        conn.close()
        flash("El trabajo indicado no existe.", "error")
        return redirect(url_for("ui.admin_jobs"))

    status = (row["status"] or "").strip().lower()
    if status in TERMINAL_JOB_STATUSES:
        conn.close()
        flash("Ese trabajo ya estaba finalizado.", "warning")
        return redirect(url_for("ui.admin_jobs"))

    now_ts = int(time.time())
    cur.execute(
        """
        UPDATE bulk_jobs
        SET status='cancelled',
            next_attempt_ts=?,
            last_status='CANCELLED_BY_ADMIN',
            updated_at_ts=?
        WHERE id=? AND status NOT IN ('complete', 'failed', 'cancelled')
        """,
        (now_ts, now_ts, job_id),
    )
    conn.commit()
    conn.close()

    if cur.rowcount != 1:
        flash("No se pudo cancelar el trabajo porque su estado cambió mientras tanto.", "warning")
        return redirect(url_for("ui.admin_jobs"))

    if status in {"submitted", "submitting"}:
        flash(
            "Trabajo cancelado localmente. Si TikTok ya había aceptado el envío, esta acción no puede retirarlo allí.",
            "warning",
        )
    else:
        flash("Trabajo cancelado correctamente.", "success")

    return redirect(url_for("ui.admin_jobs"))


@bp.post("/admin/jobs/<job_id>/relaunch", endpoint="admin_relaunch_job")
def admin_relaunch_job(job_id: str):
    guard = _admin_guard()
    if guard:
        return guard

    conn = bulk_db(current_app.config["BULK_DB_PATH"])
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    cur.execute("SELECT * FROM bulk_jobs WHERE id=?", (job_id,))
    row = cur.fetchone()

    if not row:
        conn.close()
        flash("El trabajo indicado no existe.", "error")
        return redirect(url_for("ui.admin_jobs"))

    status = (row["status"] or "").strip().lower()
    if status not in TERMINAL_JOB_STATUSES:
        conn.close()
        flash("Solo se pueden relanzar trabajos finalizados.", "error")
        return redirect(url_for("ui.admin_jobs"))

    source_filename = row["stored_filename"] or ""
    source_path = _job_file_path(source_filename)
    if not source_path or not os.path.isfile(source_path):
        conn.close()
        flash("No se puede relanzar porque el archivo original ya no existe.", "error")
        return redirect(url_for("ui.admin_jobs"))

    original_filename = safe_filename((row["original_filename"] or "").strip() or source_filename)
    new_job_id = uuid.uuid4().hex
    new_stored_filename = f"{new_job_id}_{original_filename}"
    new_file_path = _job_file_path(new_stored_filename)

    try:
        shutil.copy2(source_path, new_file_path)
    except OSError as exc:
        conn.close()
        flash(f"No se pudo duplicar el archivo para el relanzamiento: {exc}", "error")
        return redirect(url_for("ui.admin_jobs"))

    now_ts = int(time.time())
    job = {
        "id": new_job_id,
        "created_at_ts": now_ts,
        "publish_at_ts": now_ts,
        "next_attempt_ts": now_ts,
        "status": "scheduled",
        "attempts": 0,
        "last_error": None,
        "access_token": row["access_token"],
        "account_alias": row["account_alias"],
        "title": row["title"],
        "privacy_level": row["privacy_level"],
        "allow_comment": row["allow_comment"],
        "allow_duet": row["allow_duet"],
        "allow_stitch": row["allow_stitch"],
        "commercial_toggle": row["commercial_toggle"],
        "brand_organic_toggle": row["brand_organic_toggle"],
        "brand_content_toggle": row["brand_content_toggle"],
        "is_aigc": row["is_aigc"],
        "stored_filename": new_stored_filename,
        "original_filename": original_filename,
        "video_url": "",
        "publish_id": None,
        "last_status": None,
        "last_fail_reason": None,
        "updated_at_ts": now_ts,
    }

    try:
        _insert_bulk_job(conn, job)
        conn.commit()
    except Exception as exc:
        conn.close()
        try:
            os.remove(new_file_path)
        except OSError:
            pass
        flash(f"No se pudo crear el nuevo trabajo: {exc}", "error")
        return redirect(url_for("ui.admin_jobs"))

    conn.close()
    flash(f"Trabajo relanzado. Nuevo job: {new_job_id}", "success")
    return redirect(url_for("ui.admin_jobs"))


@bp.post("/admin/files/delete", endpoint="admin_delete_file")
def admin_delete_file():
    guard = _admin_guard()
    if guard:
        return guard

    stored_filename = (request.form.get("stored_filename") or "").strip()
    file_path = _job_file_path(stored_filename)
    if not file_path:
        flash("Nombre de archivo inválido.", "error")
        return redirect(url_for("ui.admin_jobs"))

    active_jobs, terminal_jobs = _count_jobs_for_file(stored_filename)

    if not os.path.isfile(file_path):
        flash("El archivo ya no existe en el sistema.", "warning")
        return redirect(url_for("ui.admin_jobs"))

    try:
        os.remove(file_path)
    except OSError as exc:
        flash(f"No se pudo borrar el archivo: {exc}", "error")
        return redirect(url_for("ui.admin_jobs"))

    if active_jobs > 0:
        flash(
            f"Archivo borrado. Ojo: había {active_jobs} trabajo(s) en curso usando ese vídeo y ahora quedarán sin fichero.",
            "warning",
        )
    elif terminal_jobs > 0:
        flash("Archivo borrado. Los trabajos finalizados asociados dejarán de mostrarse en el panel.", "success")
    else:
        flash("Archivo borrado del sistema.", "success")

    return redirect(url_for("ui.admin_jobs"))


# ---------------------------------------------------------------------
# UI flow: local upload -> post page
# ---------------------------------------------------------------------
@bp.route("/upload_local", methods=["POST"], endpoint="upload_local")
def upload_local():
    access_token = require_login_access_token()
    if not access_token:
        flash("Please connect with TikTok first.", "error")
        return redirect("/app")

    if "video" not in request.files:
        flash("No video file provided.", "error")
        return redirect("/app")

    file = request.files["video"]
    if not file.filename:
        flash("No file selected.", "error")
        return redirect("/app")

    upload_dir = current_app.config["UPLOAD_DIR"]

    original_fn = safe_filename(file.filename)
    draft_id = uuid.uuid4().hex
    stored_filename = f"{draft_id}_{original_fn}"
    save_path = os.path.join(upload_dir, stored_filename)
    file.save(save_path)

    save_draft(
        draft_id,
        {
            "file_path": save_path,
            "stored_filename": stored_filename,
            "original_filename": original_fn,
            "publish_id": None,
            "last_init_response": None,
            "posting_summary": None,
            "status": "local_uploaded",
            "video_url": None,
            "bulk_job_id": None,
        },
    )

    return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))


@bp.route("/static_preview/<draft_id>", methods=["GET"], endpoint="static_preview")
def static_preview(draft_id: str):
    access_token = require_login_access_token()
    if not access_token:
        return "Not authenticated", 401

    draft = load_draft(draft_id)
    if not draft:
        return "Not found", 404

    path = draft.get("file_path")
    if not path or not os.path.exists(path):
        return "Not found", 404

    return send_file(path, mimetype="video/mp4", as_attachment=False)


@bp.route("/post/<draft_id>", methods=["GET"], endpoint="post_to_tiktok")
def post_to_tiktok(draft_id: str):
    access_token = require_login_access_token()
    if not access_token:
        flash("Please connect with TikTok first.", "error")
        return redirect("/app")

    draft = load_draft(draft_id)
    if not draft:
        flash("Draft not found.", "error")
        return redirect("/app")

    try:
        creator_info = query_creator_info(access_token)
    except Exception as e:
        flash(f"Cannot post right now. Please try again later. Details: {e}", "error")
        return redirect("/app")

    can_post_now, cannot_post_reason = creator_can_post_now(creator_info)

    max_dur = creator_info.get("max_video_post_duration_sec")
    dur = video_duration_seconds_ffprobe(draft["file_path"])
    duration_ok = True
    duration_msg = None
    if isinstance(max_dur, int) and max_dur > 0 and dur > 0 and dur > max_dur:
        duration_ok = False
        duration_msg = "Video is %.1fs but max allowed is %ss for this creator." % (dur, max_dur)

    # Nota: este pull_url es solo “preview”; caduca según TTL.
    pull_url = None
    stored_filename = draft.get("stored_filename")
    if stored_filename:
        pull_url = build_public_media_url(
            stored_filename=stored_filename,
            public_base_url=current_app.config["PUBLIC_BASE_URL"],
            signing_secret=current_app.config["MEDIA_SIGNING_SECRET"],
            ttl_seconds=current_app.config["MEDIA_TOKEN_TTL_SECONDS"],
        )

    return render_template(
        "post_to_tiktok.html",
        draft_id=draft_id,
        draft=draft,
        creator_info=creator_info,
        duration_ok=duration_ok,
        duration_msg=duration_msg,
        pull_url=pull_url,
        can_post_now=can_post_now,
        cannot_post_reason=cannot_post_reason,
    )


# ---------------------------------------------------------------------
# Publish now (manual)
# ---------------------------------------------------------------------
@bp.route("/post/<draft_id>/publish", methods=["POST"], endpoint="publish")
def publish(draft_id: str):
    access_token = require_login_access_token()
    if not access_token:
        flash("Please connect with TikTok first.", "error")
        return redirect("/app")

    draft = load_draft(draft_id)
    if not draft:
        flash("Draft not found.", "error")
        return redirect("/app")

    try:
        creator_info = query_creator_info(access_token)
    except Exception as e:
        flash(f"Cannot post right now. Please try again later. Details: {e}", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    if request.form.get("music_confirm") != "1":
        flash("You must agree to TikTok’s Music Usage Confirmation before posting.", "error")
        return redirect(request.referrer or url_for("ui.app_home"))

    can_post_now, cannot_post_reason = creator_can_post_now(creator_info)
    if not can_post_now:
        flash(
            cannot_post_reason or "This TikTok account cannot publish at this moment. Please try again later.",
            "error",
        )
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    privacy_level = (request.form.get("privacy_level") or "").strip()
    if not privacy_level:
        flash("Please select a Privacy status before publishing.", "error")
        return redirect(request.referrer or url_for("ui.app_home"))

    title = (request.form.get("title") or "").strip()

    commercial_toggle = request.form.get("commercial_toggle") == "on"
    your_brand = request.form.get("your_brand") == "on"
    branded_content = request.form.get("branded_content") == "on"

    if branded_content and privacy_level == "SELF_ONLY":
        flash("Branded content cannot be posted with privacy set to Only me (SELF_ONLY).", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    options = creator_info.get("privacy_level_options") or []
    if privacy_level not in options:
        flash("You must select a valid privacy option.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    allow_comment = request.form.get("allow_comment") == "on"
    allow_duet = request.form.get("allow_duet") == "on"
    allow_stitch = request.form.get("allow_stitch") == "on"

    if creator_info.get("comment_disabled") is True:
        allow_comment = False
    if creator_info.get("duet_disabled") is True:
        allow_duet = False
    if creator_info.get("stitch_disabled") is True:
        allow_stitch = False

    disable_comment = not allow_comment
    disable_duet = not allow_duet
    disable_stitch = not allow_stitch

    brand_organic_toggle = False
    brand_content_toggle = False

    if commercial_toggle:
        if not (your_brand or branded_content):
            flash(
                "If commercial content disclosure is on, you must select Your brand, Branded content, or both.",
                "error",
            )
            return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

        brand_organic_toggle = your_brand
        brand_content_toggle = branded_content

        if brand_content_toggle and privacy_level == "SELF_ONLY":
            flash("Branded content cannot be posted with privacy set to Only me (SELF_ONLY).", "error")
            return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    max_dur = creator_info.get("max_video_post_duration_sec")
    dur = video_duration_seconds_ffprobe(draft["file_path"])
    if isinstance(max_dur, int) and max_dur > 0 and dur > 0 and dur > max_dur:
        flash("Video duration %.1fs exceeds allowed maximum %ss." % (dur, max_dur), "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    stored_filename = draft.get("stored_filename")
    if not stored_filename:
        flash("Internal error: missing stored_filename for draft.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    upload_dir = current_app.config["UPLOAD_DIR"]
    local_path = os.path.join(upload_dir, stored_filename)
    if not os.path.exists(local_path):
        flash("Video file not found on server.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    video_url = build_public_media_url(
        stored_filename=stored_filename,
        public_base_url=current_app.config["PUBLIC_BASE_URL"],
        signing_secret=current_app.config["MEDIA_SIGNING_SECRET"],
        ttl_seconds=current_app.config["MEDIA_TOKEN_TTL_SECONDS"],
    )

    try:
        init_resp = upload_video_direct_post(
            access_token=access_token,
            caption=title,
            privacy_level=privacy_level,
            disable_comment=disable_comment,
            disable_duet=disable_duet,
            disable_stitch=disable_stitch,
            brand_content_toggle=brand_content_toggle,
            brand_organic_toggle=brand_organic_toggle,
            is_aigc=True,
            mode="PULL_FROM_URL",
            video_url=video_url,
        )

        publish_id = (init_resp.get("data") or {}).get("publish_id")

        save_draft(
            draft_id,
            {
                **draft,
                "last_init_response": init_resp,
                "publish_id": publish_id,
                "posting_summary": {
                    "title": title,
                    "privacy_level": privacy_level,
                    "allow_comment": allow_comment,
                    "allow_duet": allow_duet,
                    "allow_stitch": allow_stitch,
                    "commercial_toggle": commercial_toggle,
                    "your_brand": your_brand,
                    "branded_content": branded_content,
                },
                "status": "posted",
                "video_url": video_url,
            },
        )

        return redirect(url_for("ui.status_page", draft_id=draft_id))

    except Exception as e:
        flash(f"Error posting to TikTok: {e}", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))


# ---------------------------------------------------------------------
# Schedule from UI (creates a bulk job)
# Requires post_to_tiktok.html to include publish_at (ISO with offset).
# ---------------------------------------------------------------------
@bp.route("/post/<draft_id>/schedule", methods=["POST"], endpoint="schedule_post")
def schedule_post(draft_id: str):
    access_token = require_login_access_token()
    if not access_token:
        flash("Please connect with TikTok first.", "error")
        return redirect("/app")

    draft = load_draft(draft_id)
    if not draft:
        flash("Draft not found.", "error")
        return redirect("/app")

    # Alias persistido por OAuth login/callback. El worker lo usará para refresh.
    account_alias = (session.get("oauth_alias") or "").strip()
    if not account_alias:
        # fallback por compat; si no existe, el worker no podrá refrescar.
        account_alias = "default"
        session["oauth_alias"] = account_alias
        session.modified = True

    _ensure_alias_token_persisted(account_alias)

    publish_at = (request.form.get("publish_at") or "").strip()
    if not publish_at:
        flash("Please select a schedule date/time.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    try:
        publish_at_ts = parse_publish_at_iso_to_ts(publish_at)
    except Exception as e:
        flash(f"Invalid publish_at (must include timezone offset). Details: {e}", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    if request.form.get("music_confirm") != "1":
        flash("You must agree to TikTok’s Music Usage Confirmation before scheduling.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    privacy_level = (request.form.get("privacy_level") or "").strip()
    if not privacy_level:
        flash("Please select a Privacy status before scheduling.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    title = (request.form.get("title") or "").strip()

    commercial_toggle = request.form.get("commercial_toggle") == "on"
    your_brand = request.form.get("your_brand") == "on"
    branded_content = request.form.get("branded_content") == "on"

    if commercial_toggle and not (your_brand or branded_content):
        flash("If commercial content disclosure is on, you must select Your brand, Branded content, or both.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    brand_organic_toggle = bool(commercial_toggle and your_brand)
    brand_content_toggle = bool(commercial_toggle and branded_content)

    if brand_content_toggle and privacy_level == "SELF_ONLY":
        flash("Branded content cannot be scheduled with privacy SELF_ONLY.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    # Defaults de interacción (el worker re-validará contra creator settings)
    allow_comment = request.form.get("allow_comment") == "on"
    allow_duet = request.form.get("allow_duet") == "on"
    allow_stitch = request.form.get("allow_stitch") == "on"

    # Duration enforcement (best-effort)
    file_path = draft.get("file_path")
    if not file_path or not os.path.exists(file_path):
        flash("Video file not found on server.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    stored_filename = draft.get("stored_filename")
    original_filename = (draft.get("original_filename") or "").strip() or os.path.basename(file_path)
    if not stored_filename:
        flash("Internal error: missing stored_filename for draft.", "error")
        return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

    # Validación “UX”: si falla TikTok aquí, seguimos y dejamos que el worker lo gestione.
    try:
        creator_info = query_creator_info(access_token)

        options = creator_info.get("privacy_level_options") or []
        if options and privacy_level not in options:
            flash("You must select a valid privacy option.", "error")
            return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

        if creator_info.get("comment_disabled") is True:
            allow_comment = False
        if creator_info.get("duet_disabled") is True:
            allow_duet = False
        if creator_info.get("stitch_disabled") is True:
            allow_stitch = False

        max_dur = creator_info.get("max_video_post_duration_sec")
        dur = video_duration_seconds_ffprobe(file_path)
        if isinstance(max_dur, int) and max_dur > 0 and dur > 0 and dur > max_dur:
            flash("Video duration %.1fs exceeds allowed maximum %ss." % (dur, max_dur), "error")
            return redirect(url_for("ui.post_to_tiktok", draft_id=draft_id))

        can_post_now, cannot_post_reason = creator_can_post_now(creator_info)
        if not can_post_now:
            flash(
                (cannot_post_reason or "This TikTok account cannot publish at this moment.")
                + " Your job was scheduled anyway; the worker will retry automatically.",
                "warning",
            )
    except Exception as e:
        flash(f"Could not validate creator settings right now (scheduled anyway). Details: {e}", "warning")

    now_ts = int(time.time())
    job_id = uuid.uuid4().hex

    # Guardamos video_url vacío para forzar “just-in-time signing” en el worker:
    # en bulk_worker: row['video_url'] or build_public_media_url(...)
    job = {
        "id": job_id,
        "created_at_ts": now_ts,
        "publish_at_ts": publish_at_ts,
        "next_attempt_ts": publish_at_ts,
        "status": "scheduled",
        "attempts": 0,
        "last_error": None,
        "access_token": access_token,          # compat (worker lo usa si no hay alias)
        "account_alias": account_alias,        # clave real para refresh en worker
        "title": title,
        "privacy_level": privacy_level,
        "allow_comment": 1 if allow_comment else 0,
        "allow_duet": 1 if allow_duet else 0,
        "allow_stitch": 1 if allow_stitch else 0,
        "commercial_toggle": 1 if commercial_toggle else 0,
        "brand_organic_toggle": 1 if brand_organic_toggle else 0,
        "brand_content_toggle": 1 if brand_content_toggle else 0,
        "is_aigc": 1,
        "stored_filename": stored_filename,
        "original_filename": original_filename,
        "video_url": "",  # falsy => el worker firmará en el momento
        "publish_id": None,
        "last_status": None,
        "last_fail_reason": None,
        "updated_at_ts": now_ts,
    }

    conn = bulk_db(current_app.config["BULK_DB_PATH"])
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()

    # INSERT robusto (evita “N values for M columns” si tu schema cambia)
    cur.execute("PRAGMA table_info(bulk_jobs)")
    cols = [r["name"] for r in cur.fetchall()]
    colset = set(cols)
    job = {k: v for k, v in job.items() if k in colset}

    colnames = ", ".join(job.keys())
    placeholders = ", ".join(["?"] * len(job))
    cur.execute(f"INSERT INTO bulk_jobs ({colnames}) VALUES ({placeholders})", tuple(job.values()))

    conn.commit()
    conn.close()

    save_draft(
        draft_id,
        {
            **draft,
            "status": "scheduled",
            "bulk_job_id": job_id,
            "posting_summary": {
                "title": title,
                "privacy_level": privacy_level,
                "allow_comment": allow_comment,
                "allow_duet": allow_duet,
                "allow_stitch": allow_stitch,
                "commercial_toggle": commercial_toggle,
                "your_brand": your_brand,
                "branded_content": branded_content,
                "publish_at": publish_at,
                "account_alias": account_alias,
            },
        },
    )

    flash(f"Scheduled job created: {job_id}", "success")
    return redirect(url_for("ui.bulk_job_page", job_id=job_id))


# ---------------------------------------------------------------------
# Draft status page + polling
# ---------------------------------------------------------------------
@bp.route("/status/<draft_id>", methods=["GET"], endpoint="status_page")
def status_page(draft_id: str):
    access_token = require_login_access_token()
    if not access_token:
        flash("Please connect with TikTok first.", "error")
        return redirect("/app")

    draft = load_draft(draft_id)
    if not draft:
        flash("Draft not found.", "error")
        return redirect("/app")

    return render_template("status.html", draft_id=draft_id, draft=draft)


@bp.route("/api/status/<draft_id>", methods=["GET"], endpoint="api_status")
def api_status(draft_id: str):
    access_token = require_login_access_token()
    if not access_token:
        return jsonify({"ok": False, "error": "not_authenticated"}), 401

    draft = load_draft(draft_id)
    if not draft:
        return jsonify({"ok": False, "error": "draft_not_found"}), 404

    publish_id = draft.get("publish_id")
    if not publish_id:
        return jsonify({"ok": True, "state": "no_publish_id_yet"}), 200

    try:
        data = fetch_post_status(access_token, publish_id)
        return jsonify({"ok": True, "data": data}), 200
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 200


@bp.get("/api/media_url/<draft_id>", endpoint="api_media_url")
def api_media_url(draft_id: str):
    access_token = require_login_access_token()
    if not access_token:
        return jsonify({"ok": False, "error": "not_authenticated"}), 401

    draft = load_draft(draft_id)
    if not draft:
        return jsonify({"ok": False, "error": "draft_not_found"}), 404

    stored_filename = draft.get("stored_filename")
    if not stored_filename:
        return jsonify({"ok": False, "error": "missing_stored_filename"}), 500

    upload_dir = current_app.config["UPLOAD_DIR"]
    local_path = os.path.join(upload_dir, stored_filename)
    if not os.path.exists(local_path):
        return jsonify({"ok": False, "error": "file_not_found"}), 404

    url = build_public_media_url(
        stored_filename=stored_filename,
        public_base_url=current_app.config["PUBLIC_BASE_URL"],
        signing_secret=current_app.config["MEDIA_SIGNING_SECRET"],
        ttl_seconds=current_app.config["MEDIA_TOKEN_TTL_SECONDS"],
    )
    return jsonify({"ok": True, "url": url})


# ---------------------------------------------------------------------
# Bulk job UI (recommended to test cron/worker)
# ---------------------------------------------------------------------
@bp.get("/bulk/job/<job_id>", endpoint="bulk_job_page")
def bulk_job_page(job_id: str):
    access_token = require_login_access_token()
    if not access_token:
        flash("Please connect with TikTok first.", "error")
        return redirect("/app")
    return render_template("bulk_job.html", job_id=job_id)


@bp.get("/api/bulk/job/<job_id>", endpoint="api_bulk_job")
def api_bulk_job(job_id: str):
    access_token = require_login_access_token()
    if not access_token:
        return jsonify({"ok": False, "error": "not_authenticated"}), 401

    conn = bulk_db(current_app.config["BULK_DB_PATH"])
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    cur.execute("SELECT * FROM bulk_jobs WHERE id=?", (job_id,))
    row = cur.fetchone()
    conn.close()

    if not row:
        return jsonify({"ok": False, "error": "not_found"}), 404

    return jsonify({"ok": True, "job": dict(row)}), 200
