# app.py (Python 3.8 compatible)
from flask import (
    Flask,
    render_template,
    request,
    redirect,
    url_for,
    session,
    flash,
    jsonify,
    send_file,
    send_from_directory,
    abort,
)
import os
import secrets
import uuid
import subprocess
from typing import Optional, Dict, Any, Tuple
from config import BULK_API_KEY
import time

from config import (
    FLASK_SECRET_KEY,
    # Asegúrate de tener también en config.py:
    # PUBLIC_BASE_URL, MEDIA_SIGNING_SECRET, MEDIA_TOKEN_TTL_SECONDS
    PUBLIC_BASE_URL,
    MEDIA_SIGNING_SECRET,
    MEDIA_TOKEN_TTL_SECONDS,
)

from media_tokens import make_media_token, verify_media_token
from tiktok_client import (
    build_authorize_url,
    exchange_code_for_token,
    query_creator_info,
    upload_video_direct_post,
    fetch_post_status,
)

app = Flask(__name__)
app.secret_key = FLASK_SECRET_KEY

# Config para media tokens
app.config["PUBLIC_BASE_URL"] = PUBLIC_BASE_URL
app.config["MEDIA_SIGNING_SECRET"] = MEDIA_SIGNING_SECRET
app.config["MEDIA_TOKEN_TTL_SECONDS"] = MEDIA_TOKEN_TTL_SECONDS

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
os.makedirs(UPLOAD_DIR, exist_ok=True)

# (añade cerca de BASE_DIR/UPLOAD_DIR)
URLPROP_DIR = os.path.join(BASE_DIR, "url_properties")
os.makedirs(URLPROP_DIR, exist_ok=True)


# -----------------------
# Helpers
# -----------------------
def require_bulk_api_key():
    k = request.headers.get("X-Api-Key", "").strip()
    if not k or k != BULK_API_KEY:
        abort(401)
def require_login() -> Optional[str]:
    token_data = session.get("tiktok_token")
    if not token_data:
        return None
    return token_data.get("access_token")

@app.route("/api/bulk/publish", methods=["POST"])
def api_bulk_publish():
    """
    Server-to-server publish:
    - Auth: X-Api-Key
    - Input: access_token, video(file), title, privacy_level, toggles...
    - Output: {ok, publish_id, video_url, init_resp}
    """
    require_bulk_api_key()

    access_token = (request.form.get("access_token") or "").strip()
    if not access_token:
        return jsonify({"ok": False, "error": "missing_access_token"}), 400

    if "video" not in request.files:
        return jsonify({"ok": False, "error": "missing_video"}), 400

    file = request.files["video"]
    if not file.filename:
        return jsonify({"ok": False, "error": "missing_filename"}), 400

    # Params
    title = (request.form.get("title") or "").strip()
    privacy_level = (request.form.get("privacy_level") or "").strip()
    if not privacy_level:
        return jsonify({"ok": False, "error": "missing_privacy_level"}), 400

    allow_comment = request.form.get("allow_comment", "1") == "1"
    allow_duet = request.form.get("allow_duet", "1") == "1"
    allow_stitch = request.form.get("allow_stitch", "1") == "1"

    commercial_toggle = request.form.get("commercial_toggle", "0") == "1"
    brand_organic_toggle = request.form.get("brand_organic_toggle", "0") == "1"
    brand_content_toggle = request.form.get("brand_content_toggle", "0") == "1"
    is_aigc = request.form.get("is_aigc", "1") == "1"

    # Branded content cannot be SELF_ONLY
    if brand_content_toggle and privacy_level == "SELF_ONLY":
        return jsonify({"ok": False, "error": "branded_cannot_be_self_only"}), 400

    # creator_info (latest)
    try:
        creator_info = query_creator_info(access_token)
    except Exception as e:
        return jsonify({"ok": False, "error": "creator_info_failed", "details": str(e)}), 400

    can_post_now, reason = creator_can_post_now(creator_info)
    if not can_post_now:
        return jsonify({"ok": False, "error": "cannot_post_now", "reason": reason}), 400

    # Validate privacy option is allowed
    options = creator_info.get("privacy_level_options") or []
    if privacy_level not in options:
        return jsonify({"ok": False, "error": "invalid_privacy_level", "options": options}), 400

    # Respect creator settings
    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

    # Save file to uploads/
    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)

    # Duration enforcement (best-effort)
    max_dur = creator_info.get("max_video_post_duration_sec")
    dur = video_duration_seconds_ffprobe(save_path)
    if isinstance(max_dur, int) and max_dur > 0 and dur > 0 and dur > max_dur:
        try:
            os.remove(save_path)
        except Exception:
            pass
        return jsonify({"ok": False, "error": "duration_exceeds_max", "dur": dur, "max": max_dur}), 400

    # Build pull URL (signed)
    video_url = build_public_media_url(stored_filename)

    # Direct Post
    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=is_aigc,
            mode="PULL_FROM_URL",
            video_url=video_url,
        )
        publish_id = (init_resp.get("data") or {}).get("publish_id")
        return jsonify({
            "ok": True,
            "publish_id": publish_id,
            "video_url": video_url,
            "init_resp": init_resp,
            "stored_filename": stored_filename,
        }), 200
    except Exception as e:
        return jsonify({"ok": False, "error": "direct_post_failed", "details": str(e)}), 400


@app.route("/api/bulk/status", methods=["POST"])
def api_bulk_status():
    """
    Server-to-server status:
    - Auth: X-Api-Key
    - Input: access_token, publish_id
    """
    require_bulk_api_key()

    access_token = (request.form.get("access_token") or "").strip()
    publish_id = (request.form.get("publish_id") or "").strip()
    if not access_token or not publish_id:
        return jsonify({"ok": False, "error": "missing_access_token_or_publish_id"}), 400

    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
def safe_filename(name: str) -> str:
    name = (name or "").replace("\\", "_").replace("/", "_")
    return name[:200] if name else "upload.mp4"


def video_duration_seconds_ffprobe(path: str) -> float:
    """
    Best-effort duration check using ffprobe. If ffprobe isn't installed,
    returns -1 and duration checks are skipped.
    """
    try:
        cmd = [
            "ffprobe",
            "-v",
            "error",
            "-show_entries",
            "format=duration",
            "-of",
            "default=noprint_wrappers=1:nokey=1",
            path,
        ]
        out = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode("utf-8").strip()
        return float(out)
    except Exception:
        return -1.0


def get_draft_store() -> Dict[str, Any]:
    """
    Minimal in-session storage for drafts.
    For production, move this to a DB/Redis + server-side sessions.
    """
    if "drafts" not in session:
        session["drafts"] = {}
    return session["drafts"]


def save_draft(draft_id: str, data: Dict[str, Any]) -> None:
    drafts = get_draft_store()
    drafts[draft_id] = data
    session["drafts"] = drafts
    session.modified = True


def load_draft(draft_id: str) -> Optional[Dict[str, Any]]:
    return get_draft_store().get(draft_id)


def build_public_media_url(stored_filename: str) -> str:
    """
    Create a signed URL that TikTok can pull from.
    stored_filename must be a filename inside UPLOAD_DIR.
    """
    token = make_media_token(
        filename=stored_filename,
        secret=app.config["MEDIA_SIGNING_SECRET"],
        ttl_seconds=app.config["MEDIA_TOKEN_TTL_SECONDS"],
    )
    return f'{app.config["PUBLIC_BASE_URL"].rstrip("/")}/media/{token}'


def creator_can_post_now(creator_info: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
    """
    Interpreta creator_info para determinar si el creador puede publicar ahora.
    Devuelve (can_post_now, reason).

    Importante UX (TikTok): si creator_info indica que NO puede publicar, la app debe
    detener el intento de publicación y pedir que se intente más tarde.
    """
    if not creator_info:
        return False, "Unable to verify TikTok creator status."

    # Flags habituales que TikTok puede devolver (nombres pueden variar)
    if creator_info.get("posting_disabled") is True:
        return False, "Posting is currently disabled for this TikTok account."

    if creator_info.get("publish_disabled") is True:
        return False, "Publishing is temporarily disabled for this TikTok account."

    if creator_info.get("can_post") is False:
        return False, "This TikTok account cannot publish at this moment."

    if creator_info.get("can_publish") is False:
        return False, "This TikTok account cannot publish at this moment."

    if creator_info.get("can_make_more_posts") is False:
        return False, "Daily posting limit reached. Please try again later."

    # Algunas integraciones devuelven razón textual
    reason = creator_info.get("posting_disabled_reason") or creator_info.get("publish_disabled_reason")
    if reason:
        # Si viene reason pero no viene un flag booleano, no bloqueamos por defecto.
        # (Dejarlo solo como informativo, si quieres mostrarlo.)
        pass

    return True, None


# -----------------------
# Routes
# -----------------------
@app.route("/app")
def app_home():
    token_data = session.get("tiktok_token")
    is_connected = token_data is not None
    return render_template("index.html", is_connected=is_connected)


@app.route("/")
def landing():
    token_data = session.get("tiktok_token")
    is_connected = token_data is not None
    return render_template("landing.html", is_connected=is_connected)


# Reemplaza tu ruta actual /media/<token> por esta variante
@app.get("/media/<path:token_or_file>")
def media(token_or_file: str):
    # 1) Permitir archivos .txt de verificación de TikTok bajo el mismo prefix /media/
    if token_or_file.lower().endswith(".txt"):
        full = os.path.join(URLPROP_DIR, token_or_file)
        if not os.path.exists(full):
            abort(404)
        # TikTok espera 200 directo, sin auth y sin redirects
        return send_from_directory(
            URLPROP_DIR, token_or_file, as_attachment=False, mimetype="text/plain"
        )

    # 2) Flujo normal: token HMAC para servir el mp4
    token = token_or_file
    payload = verify_media_token(token, app.config["MEDIA_SIGNING_SECRET"])
    if not payload:
        abort(403)

    filename = payload.get("f")
    if not filename:
        abort(400)

    # prevent path traversal
    if "/" in filename or "\\" in filename or ".." in filename:
        abort(400)

    full_path = os.path.join(UPLOAD_DIR, filename)
    if not os.path.exists(full_path):
        abort(404)

    return send_from_directory(UPLOAD_DIR, filename, as_attachment=False, mimetype="video/mp4")


@app.route("/contact")
def contact():
    token_data = session.get("tiktok_token")
    is_connected = token_data is not None
    return render_template("contact.html", is_connected=is_connected)


@app.route("/tos")
def tos():
    return render_template("tos.html")


@app.route("/privacy")
def privacy():
    return render_template("privacy.html")


@app.route("/login")
def login():
    state = secrets.token_hex(16)
    session["oauth_state"] = state
    return redirect(build_authorize_url(state))


@app.route("/tiktok/callback")
def tiktok_callback():
    error = request.args.get("error")
    if error:
        flash(f"TikTok error: {error}", "error")
        return redirect("/app")

    code = request.args.get("code")
    state = request.args.get("state")

    if not code or not state or state != session.get("oauth_state"):
        flash("Invalid OAuth state or missing code.", "error")
        return redirect("/app")

    try:
        token_data = exchange_code_for_token(code)
        session["tiktok_token"] = token_data
        flash("Successfully connected with TikTok.", "success")
    except Exception as e:
        flash(f"Error exchanging code: {e}", "error")

    return redirect("/app")


@app.route("/logout", methods=["POST"])
def logout():
    session.pop("tiktok_token", None)
    session.pop("drafts", None)
    flash("Disconnected.", "success")
    return redirect("/app")


# Step 1: local upload (NO TikTok upload yet)
@app.route("/upload_local", methods=["POST"])
def upload_local():
    access_token = require_login()
    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")

    original_fn = safe_filename(file.filename)
    draft_id = uuid.uuid4().hex

    # stored filename is what lives in UPLOAD_DIR (used by /media/<token>)
    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,  # absolute path for local operations (ffprobe/preview)
            "stored_filename": stored_filename,  # relative filename for /media pull
            "original_filename": original_fn,
            "publish_id": None,
            "last_init_response": None,
            "posting_summary": None,
            "status": "local_uploaded",
        },
    )

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


@app.route("/static_preview/<draft_id>", methods=["GET"])
def static_preview(draft_id: str):
    access_token = require_login()
    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)


# Step 2: compliant Post-to-TikTok page (must query creator info here)
@app.route("/post/<draft_id>", methods=["GET"])
def post_to_tiktok(draft_id: str):
    access_token = require_login()
    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")

    # UX Point 1b: if creator cannot post now, stop attempt and prompt try later
    can_post_now, cannot_post_reason = creator_can_post_now(creator_info)

    # Best-effort duration check
    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:
        if dur > max_dur:
            duration_ok = False
            duration_msg = "Video is %.1fs but max allowed is %ss for this creator." % (dur, max_dur)

    # Optional: compute the pull URL to show to the user (not required, but useful for debugging)
    pull_url = None
    stored_filename = draft.get("stored_filename")
    if stored_filename:
        pull_url = build_public_media_url(stored_filename)

    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,
    )


# Step 3: Direct Post — now publish to TikTok using PULL_FROM_URL
@app.route("/post/<draft_id>/publish", methods=["POST"])
def publish(draft_id: str):
    access_token = require_login()
    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")

    # Must re-fetch creator_info right before publishing (latest info)
    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("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("index"))
    # UX Point 1b: stop attempt if creator cannot post now
    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("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("app"))
    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"

    # Si branded content está marcado, NUNCA permitas SELF_ONLY
    # (y especialmente si tu app está en modo solo SELF_ONLY)
    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("post_to_tiktok", draft_id=draft_id))


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

    # Interactions in UI are ALLOW_*; API needs disable_*
    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 has disabled these in TikTok settings, force them disabled
    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("post_to_tiktok", draft_id=draft_id))

        brand_organic_toggle = your_brand
        brand_content_toggle = branded_content

        # Branded content cannot be private; in SELF_ONLY mode we must block it.
        if branded_content and privacy_level == "SELF_ONLY":
            flash("Branded content visibility cannot be set to private/only me.", "error")
            return redirect(url_for("post_to_tiktok", draft_id=draft_id))

    # Duration enforcement (server-side, best-effort)
    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("post_to_tiktok", draft_id=draft_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,
    }

    # Build signed pull URL (TikTok pulls from your server)
    stored_filename = draft.get("stored_filename")
    if not stored_filename:
        flash("Internal error: missing stored_filename for draft.", "error")
        return redirect(url_for("post_to_tiktok", draft_id=draft_id))

    # Ensure file exists
    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("post_to_tiktok", draft_id=draft_id))

    video_url = build_public_media_url(stored_filename)

    try:
        app.logger.warning("TikTok PULL video_url=%s", video_url)
        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": posting_summary,
                "status": "posted_self_only",
                "video_url": video_url,
            },
        )

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

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


@app.route("/status/<draft_id>", methods=["GET"])
def status_page(draft_id: str):
    access_token = require_login()
    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)


@app.route("/api/status/<draft_id>", methods=["GET"])
def api_status(draft_id: str):
    access_token = require_login()
    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


# Optional: debug helper (not required by UX)
# If you keep it, at least require login + file existence.
@app.get("/api/media_url/<draft_id>")
def api_media_url(draft_id: str):
    access_token = require_login()
    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

    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)
    return jsonify({"ok": True, "url": url})


@app.get("/healthz")
def healthz():
    return jsonify({"ok": True}), 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8777, debug=False)
