# app.py
from flask import Flask, render_template, request, send_file, redirect, url_for, abort
import yt_dlp
import uuid
import os
import re
import json
from datetime import datetime
from typing import Tuple, Dict, Any, Optional
from urllib.parse import urlparse

app = Flask(__name__, template_folder="templates", static_folder="static")

# ========== CONFIG ==========
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DOWNLOAD_DIR = os.path.join(BASE_DIR, "downloads")
os.makedirs(DOWNLOAD_DIR, exist_ok=True)

# FFmpeg path (use env or auto-detect /usr/bin on servers)
FFMPEG_DIR = os.environ.get("FFMPEG_DIR") or ("/usr/bin" if os.path.exists("/usr/bin/ffmpeg") else None)

# Auth for Instagram Stories
COOKIES_TXT = os.environ.get("COOKIES_TXT") or (
    os.path.join(BASE_DIR, "cookies.txt") if os.path.isfile(os.path.join(BASE_DIR, "cookies.txt")) else None
)
COOKIES_FROM_BROWSER = os.environ.get("COOKIES_FROM_BROWSER")

DEFAULT_UA = os.environ.get("IG_UA") or (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/122.0 Safari/537.36"
)

print("=== InstaSave startup ===")
print(f"COOKIES_TXT: {COOKIES_TXT!r} (exists={os.path.isfile(COOKIES_TXT) if COOKIES_TXT else False})")
print(f"COOKIES_FROM_BROWSER: {COOKIES_FROM_BROWSER!r}")
print(f"FFMPEG_DIR: {FFMPEG_DIR!r}")
print("=========================")


# ========== HELPERS ==========
def _file_path(file_id: str) -> str:
    return os.path.join(DOWNLOAD_DIR, f"{file_id}.mp4")

def _meta_path(file_id: str) -> str:
    return os.path.join(DOWNLOAD_DIR, f"{file_id}.json")

def _save_meta(file_id: str, meta: Dict[str, Any]) -> None:
    try:
        with open(_meta_path(file_id), "w", encoding="utf-8") as f:
            json.dump(meta, f, ensure_ascii=False, indent=2)
    except Exception:
        pass

def _load_meta(file_id: str) -> Dict[str, Any]:
    p = _meta_path(file_id)
    if os.path.isfile(p):
        try:
            with open(p, "r", encoding="utf-8") as f:
                return json.load(f)
        except Exception:
            return {}
    return {}

def _fmt_duration(seconds: Optional[float]) -> Optional[str]:
    if seconds is None:
        return None
    try:
        seconds = int(round(seconds))
        m, s = divmod(seconds, 60)
        h, m = divmod(m, 60)
        return f"{h:d}:{m:02d}:{s:02d}" if h else f"{m:d}:{s:02d}"
    except Exception:
        return None

def _infer_service(url: str) -> str:
    u = url.lower()
    if "tiktok.com" in u: return "tiktok"
    if re.search(r"(instagram\.com|instagr\.am)", u): return "instagram"
    raise ValueError("Only TikTok and Instagram links are supported.")

def _normalize_handle(s: str) -> str:
    """
    Accepts:
      @username
      https://www.instagram.com/<username>/
      https://www.instagram.com/stories/<username>/?r=1
    Returns plain username or "".
    """
    s = (s or "").strip().strip('\'"')
    if not s: return ""
    if s.startswith("@"): s = s[1:]
    # URL forms
    try:
        p = urlparse(s)
        if p.netloc and "instagram.com" in p.netloc:
            parts = [seg for seg in p.path.split("/") if seg]
            if not parts:  # root domain
                return ""
            head = parts[0].lower()
            if head in {"p", "reel", "tv"}:
                return ""  # media urls are not handles
            if head == "stories":
                s = parts[1] if len(parts) >= 2 else ""
            else:
                s = parts[0]
    except Exception:
        pass
    m = re.match(r"^[A-Za-z0-9._]{1,30}$", s)
    return m.group(0) if m else ""

def _is_instagram_handle_or_profile(s: str) -> bool:
    s = (s or "").strip()
    if not s: return False
    if s.startswith("@"): return True
    try:
        p = urlparse(s)
        if p.netloc and "instagram.com" in p.netloc:
            parts = [seg for seg in p.path.split("/") if seg]
            if not parts: return False  # root
            if parts[0].lower() in {"p", "reel", "tv"}: return False
            return True  # /stories/<user> or /<user>
    except Exception:
        pass
    return bool(re.match(r"^[A-Za-z0-9._@/:-]+$", s))

def _ydl_options_for(service: str, out_pattern: str, allow_playlist: bool = False) -> Dict[str, Any]:
    base: Dict[str, Any] = {
        "outtmpl": out_pattern,
        "merge_output_format": "mp4",
        "ffmpeg_location": FFMPEG_DIR,
        "noplaylist": not allow_playlist,
        "quiet": True,
        "headers": {
            "User-Agent": DEFAULT_UA,
            "Referer": "https://www.instagram.com/",
            "Accept-Language": "en-US,en;q=0.9",
        },
    }
    if COOKIES_TXT:
        if not os.path.isfile(COOKIES_TXT):
            raise FileNotFoundError(f"COOKIES_TXT points to a file that does not exist: {COOKIES_TXT}")
        base["cookiefile"] = COOKIES_TXT
    elif COOKIES_FROM_BROWSER:
        base["cookiesfrombrowser"] = (COOKIES_FROM_BROWSER,)

    if service == "tiktok":
        base.update({"format": "best[ext=mp4][acodec!=none]/bestvideo[ext=mp4]+bestaudio/best",
                     "extractor_args": {"tiktok": {"app_info": "1"}}})
    elif service == "instagram":
        base.update({"format": "best[ext=mp4]/best", "extract_flat": False, "extractor_args": {"instagram": {}}})
    return base

def _extract_common_meta(info: Dict[str, Any]) -> Dict[str, Any]:
    author_name = info.get("uploader") or info.get("uploader_id") or info.get("channel") or ""
    handle = info.get("uploader_id") or info.get("channel_id") or ""
    author_handle = f"@{handle}" if handle and not handle.startswith("@") else handle
    return {
        "author_name": author_name,
        "author_handle": author_handle,
        "author_avatar": info.get("uploader_url"),
        "thumb_url": info.get("thumbnail"),
        "description": info.get("description") or "",
        "duration": _fmt_duration(info.get("duration")),
    }

def _download_with_yt_dlp(url: str, service: str, file_id: str) -> Tuple[str, str, Dict[str, Any]]:
    out_pattern = os.path.join(DOWNLOAD_DIR, f"{file_id}.%(ext)s")
    ydl_opts = _ydl_options_for(service, out_pattern)
    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(url, download=True)
    final_path = _file_path(file_id)
    if not os.path.isfile(final_path):
        raise FileNotFoundError("Output MP4 not found. Set FFMPEG_DIR to your ffmpeg/bin.")
    title = info.get("title") or (info.get("id") or "Video")
    return final_path, title, _extract_common_meta(info)

def _download_instagram_story_latest(handle: str, file_id: str) -> Tuple[str, str, Dict[str, Any]]:
    handle = _normalize_handle(handle)
    if not handle:
        raise ValueError("Missing or invalid Instagram username.")
    story_url = f"https://www.instagram.com/stories/{handle}/"
    out_pattern = os.path.join(DOWNLOAD_DIR, f"{file_id}.%(ext)s")
    ydl_opts = _ydl_options_for("instagram", out_pattern, allow_playlist=True)
    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        ie = ydl.extract_info(story_url, download=False)
        entries = (ie or {}).get("entries") or []
        if not entries:
            raise yt_dlp.utils.DownloadError(
                "No story items found (account has no active stories or login is required)."
            )
        latest = sorted(entries, key=lambda e: e.get("timestamp") or 0, reverse=True)[0]
        ydl.process_ie_result(latest, download=True)
        info = latest
    final_path = _file_path(file_id)
    if not os.path.isfile(final_path):
        raise FileNotFoundError("Story MP4 not found after download. Check FFmpeg and cookies.")
    ts = info.get("timestamp")
    when = datetime.utcfromtimestamp(ts).strftime("%Y-%m-%d %H:%M UTC") if ts else "Story"
    title = f"[Instagram Story] {handle} — {when}"
    meta = _extract_common_meta(info)
    if not meta.get("author_name"):
        meta["author_name"] = handle
        meta["author_handle"] = f"@{handle}"
    return final_path, title, meta


# ========== ROUTES ==========
@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        value = (request.form.get("url") or "").strip()
        if not value:
            return "<h3>Error</h3><p>Please paste a TikTok/Instagram link or type an Instagram @username.</p>", 400
        # block the bare instagram homepage (always unsupported)
        if re.match(r"^https?://(www\.)?instagram\.com/?$", value, re.I):
            return "<h3>Unsupported URL</h3><p>Enter an @username for Stories or a specific post/reel URL.</p>", 400
        # handle-like? send to /stories
        if _is_instagram_handle_or_profile(value):
            return redirect(url_for("stories"), code=307)
        # media URL
        try:
            service = _infer_service(value)
        except ValueError as e:
            return f"<h3>Unsupported URL</h3><p>{e}</p>", 400
        file_id = uuid.uuid4().hex
        try:
            _, title, meta = _download_with_yt_dlp(value, service, file_id)
            label = "TikTok" if service == "tiktok" else "Instagram"
            _save_meta(file_id, {"title": f"[{label}] {title}", **meta})
            return redirect(url_for("preview", file_id=file_id))
        except yt_dlp.utils.DownloadError as derr:
            ig_tip = ""
            if service == "instagram":
                ig_tip = ("<br><br><b>Tip for Instagram:</b> Public posts usually work. "
                          "Stories/private/age/region-gated content require a valid cookies.txt (set COOKIES_TXT).")
            return f"<h3>Download error</h3><pre>{derr}</pre>{ig_tip}", 500
        except Exception as err:
            return f"<h3>Download error</h3><pre>{err}</pre>", 500
    return render_template("index.html")

@app.route("/stories", methods=["POST"])
def stories():
    raw = request.form.get("handle") or request.form.get("username") or request.form.get("url") or ""
    handle = _normalize_handle(raw)
    if not handle:
        return "<h3>Error</h3><p>Please enter a valid Instagram username (e.g., @nasa).</p>", 400
    if not (COOKIES_TXT or COOKIES_FROM_BROWSER):
        return (
            "<h3>Stories need login</h3>"
            "<p>Set a valid <code>COOKIES_TXT</code> (Netscape cookies.txt with a <code>sessionid</code>) "
            "or set <code>COOKIES_FROM_BROWSER</code> for local dev.</p>",
            401,
        )
    file_id = uuid.uuid4().hex
    try:
        _, title, meta = _download_instagram_story_latest(handle, file_id)
        _save_meta(file_id, {"title": title, **meta})
        return redirect(url_for("preview", file_id=file_id))
    except yt_dlp.utils.DownloadError as derr:
        hint = (
            "<br><br><b>Instagram Stories need login:</b> "
            "Make sure your <code>cookies.txt</code> exists, contains a <code>sessionid</code> line, "
            "and that the app can see it (check startup logs)."
        )
        return f"<h3>Stories download error</h3><pre>{derr}</pre>{hint}", 500
    except Exception as err:
        return f"<h3>Stories download error</h3><pre>{err}</pre>", 500

@app.route("/preview/<file_id>")
def preview(file_id):
    path = _file_path(file_id)
    if not os.path.isfile(path): abort(404)
    meta = _load_meta(file_id)
    title = meta.get("title") or request.args.get("title") or "Video"
    file_size_mb = round(os.path.getsize(path) / (1024 * 1024), 2)
    return render_template("preview.html",
        file_id=file_id, title=title, file_size_mb=file_size_mb,
        author_name=meta.get("author_name"), author_handle=meta.get("author_handle"),
        author_avatar=meta.get("author_avatar"), description=meta.get("description"),
        duration=meta.get("duration"), thumb_url=meta.get("thumb_url"), nowm_url=None)

@app.route("/video/<file_id>")
def serve_video(file_id):
    path = _file_path(file_id)
    if not os.path.isfile(path): abort(404)
    return send_file(path, mimetype="video/mp4")

@app.route("/download/<file_id>")
def download_video(file_id):
    path = _file_path(file_id)
    if not os.path.isfile(path): abort(404)
    return send_file(path, as_attachment=True, download_name=f"{file_id}.mp4", mimetype="video/mp4")

# Optional pages (must exist under /templates)
@app.route("/downloader")
def tiktok_downloader():
    return render_template("TiktokVideoDownloader.html")

@app.route("/mp3")
def download_tiktok_mp3():
    return render_template("DownloadTikTokMP3.html")

# Health check
@app.route("/healthz")
def healthz():
    return "ok", 200

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "8000")),
            debug=os.environ.get("FLASK_DEBUG") == "1") 