#!/usr/bin/env python3
"""X Bookmarks Export — 直接呼叫 GraphQL API"""

import csv
import html
import json
import os
import random
import sys
import time
import urllib.parse
from datetime import datetime
from pathlib import Path

import httpx

OUTPUT_DIR = Path(__file__).parent / "output"
OUTPUT_DIR.mkdir(exist_ok=True)

GRAPHQL_URL = "https://x.com/i/api/graphql/tmd4ifV8RHltzn8ymGg1aw/Bookmarks"

FEATURES = {
    "graphql_timeline_v2_bookmark_timeline": True,
    "rweb_tipjar_consumption_enabled": True,
    "responsive_web_graphql_exclude_directive_enabled": True,
    "verified_phone_label_enabled": False,
    "creator_subscriptions_tweet_preview_api_enabled": True,
    "responsive_web_graphql_timeline_navigation_enabled": True,
    "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
    "communities_web_enable_tweet_community_results_fetch": True,
    "c9s_tweet_anatomy_moderator_badge_enabled": True,
    "articles_preview_enabled": True,
    "responsive_web_edit_tweet_api_enabled": True,
    "tweetypie_unmention_optimization_enabled": True,
    "responsive_web_enhance_cards_enabled": False,
    "responsive_web_media_download_video_enabled": False,
    "responsive_web_twitter_article_tweet_consumption_enabled": True,
    "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True,
    "creator_subscriptions_quote_tweet_preview_enabled": False,
    "freedom_of_speech_not_reach_fetch_enabled": True,
    "standardized_nudges_misinfo": True,
    "tweet_awards_web_tipping_enabled": False,
    "longform_notetweets_inline_media_enabled": True,
    "longform_notetweets_rich_text_read_enabled": True,
    "longform_notetweets_consumption_enabled": True,
    "responsive_web_twitter_blue_verified_badge_is_enabled": True,
    "view_counts_everywhere_api_enabled": True,
}


def parse_tweet(tweet_result: dict) -> dict | None:
    if tweet_result.get("__typename") == "TweetWithVisibilityResults":
        tweet_result = tweet_result.get("tweet", {})

    if tweet_result.get("__typename") != "Tweet":
        return None

    core = tweet_result.get("core", {})
    user_results = core.get("user_results", {}).get("result", {})
    legacy_user = user_results.get("legacy", {})
    legacy = tweet_result.get("legacy", {})

    retweeted = legacy.get("retweeted_status_result", {}).get("result", {})
    if retweeted:
        rt_legacy = retweeted.get("legacy", {})
        if retweeted.get("__typename") == "TweetWithVisibilityResults":
            rt_legacy = retweeted.get("tweet", {}).get("legacy", {})
        full_text = rt_legacy.get("full_text", legacy.get("full_text", ""))
    else:
        full_text = legacy.get("full_text", "")

    note_tweet = tweet_result.get("note_tweet", {}).get("note_tweet_results", {}).get("result", {})
    if note_tweet:
        full_text = note_tweet.get("text", full_text)

    full_text = html.unescape(full_text)

    tweet_id = legacy.get("id_str") or tweet_result.get("rest_id", "")
    screen_name = legacy_user.get("screen_name", "")

    media_entities = legacy.get("extended_entities", {}).get("media", [])
    if not media_entities:
        media_entities = legacy.get("entities", {}).get("media", [])
    media_urls = []
    for m in media_entities:
        if m.get("type") in ("video", "animated_gif"):
            variants = m.get("video_info", {}).get("variants", [])
            mp4s = [v for v in variants if v.get("content_type") == "video/mp4"]
            if mp4s:
                best = max(mp4s, key=lambda v: v.get("bitrate", 0))
                media_urls.append(best["url"])
            else:
                media_urls.append(m.get("media_url_https", ""))
        else:
            media_urls.append(m.get("media_url_https", ""))

    created_at = legacy.get("created_at", "")
    if created_at:
        try:
            dt = datetime.strptime(created_at, "%a %b %d %H:%M:%S %z %Y")
            created_at = dt.isoformat()
        except ValueError:
            pass

    return {
        "tweet_id": tweet_id,
        "author_handle": screen_name,
        "author_name": legacy_user.get("name", ""),
        "full_text": full_text,
        "created_at": created_at,
        "tweet_url": f"https://x.com/{screen_name}/status/{tweet_id}" if screen_name and tweet_id else "",
        "likes": legacy.get("favorite_count", 0),
        "retweets": legacy.get("retweet_count", 0),
        "replies": legacy.get("reply_count", 0),
        "views": tweet_result.get("views", {}).get("count", ""),
        "media_urls": media_urls,
        "lang": legacy.get("lang", ""),
        "in_reply_to_handle": legacy.get("in_reply_to_screen_name", ""),
    }


def extract_bookmarks(data: dict) -> tuple[list[dict], str | None]:
    tweets = []
    cursor = None

    timeline = (
        data.get("data", {})
        .get("bookmark_timeline_v2", {})
        .get("timeline", {})
    )

    for instruction in timeline.get("instructions", []):
        if instruction.get("type") == "TimelineAddEntries":
            entries = instruction.get("entries", [])
        elif instruction.get("type") == "TimelineAddToModule":
            entries = instruction.get("moduleItems", [])
        else:
            continue

        for entry in entries:
            entry_id = entry.get("entryId", "")

            if entry_id.startswith("cursor-bottom-"):
                cursor = entry.get("content", {}).get("value")
                continue

            item_content = entry.get("content", {})
            if item_content.get("entryType") == "TimelineTimelineItem":
                tweet_result = (
                    item_content.get("itemContent", {})
                    .get("tweet_results", {})
                    .get("result", {})
                )
                parsed = parse_tweet(tweet_result)
                if parsed:
                    tweets.append(parsed)
            elif item_content.get("entryType") == "TimelineTimelineModule":
                for sub_item in item_content.get("items", []):
                    sub_content = sub_item.get("item", {}).get("itemContent", {})
                    tweet_result = sub_content.get("tweet_results", {}).get("result", {})
                    parsed = parse_tweet(tweet_result)
                    if parsed:
                        tweets.append(parsed)
            else:
                item_inner = entry.get("item", {}).get("itemContent", {})
                tweet_result = item_inner.get("tweet_results", {}).get("result", {})
                parsed = parse_tweet(tweet_result)
                if parsed:
                    tweets.append(parsed)

    return tweets, cursor


def export_markdown(tweets: list[dict]):
    path = OUTPUT_DIR / "bookmarks.md"
    if not tweets:
        print("沒有資料可匯出")
        return

    with open(path, "w", encoding="utf-8") as f:
        f.write(f"# X Bookmarks ({len(tweets)} 則)\n\n")
        for tweet in tweets:
            date = tweet["created_at"][:10] if tweet["created_at"] else "unknown"
            f.write(f"## [{tweet['author_name']} (@{tweet['author_handle']})]({tweet['tweet_url']})\n")
            f.write(f"*{date}* | {tweet['likes']} likes | {tweet['retweets']} RT | {tweet['views']} views\n\n")
            f.write(f"{tweet['full_text']}\n\n")
            for url in tweet.get("media_urls", []):
                if url.endswith(".mp4"):
                    f.write(f"[Video]({url})\n")
                else:
                    f.write(f"![media]({url})\n")
            f.write("\n---\n\n")

    print(f"✓ Markdown 匯出: {path} ({len(tweets)} 則)")


def export_json(tweets: list[dict]):
    path = OUTPUT_DIR / "bookmarks.json"
    with open(path, "w", encoding="utf-8") as f:
        json.dump(tweets, f, ensure_ascii=False, indent=2)
    print(f"✓ JSON 匯出: {path} ({len(tweets)} 則)")


def export_csv(tweets: list[dict]):
    path = OUTPUT_DIR / "bookmarks.csv"
    if not tweets:
        print("沒有資料可匯出")
        return

    fieldnames = [
        "tweet_id", "author_handle", "author_name", "full_text",
        "created_at", "tweet_url", "likes", "retweets", "replies",
        "views", "media_urls", "lang", "in_reply_to_handle",
    ]

    with open(path, "w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        for tweet in tweets:
            row = {**tweet, "media_urls": " | ".join(tweet.get("media_urls", []))}
            writer.writerow(row)

    print(f"✓ CSV 匯出: {path} ({len(tweets)} 則)")


def load_env():
    env_path = Path(__file__).parent / ".env"
    if not env_path.exists():
        return
    for line in env_path.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, _, value = line.partition("=")
        os.environ.setdefault(key.strip(), value.strip())


def main():
    load_env()

    auth_token = os.environ.get("AUTH_TOKEN") or input("auth_token: ").strip()
    ct0 = os.environ.get("CT0") or input("ct0: ").strip()

    if not auth_token or not ct0:
        print("ERROR: auth_token 和 ct0 都必須提供")
        sys.exit(1)

    if os.environ.get("AUTH_TOKEN"):
        print("→ 從 .env 讀取 auth credentials")

    headers = {
        "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
        "x-csrf-token": ct0,
        "cookie": f"auth_token={auth_token}; ct0={ct0}",
        "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
        "x-twitter-active-user": "yes",
        "x-twitter-client-language": "en",
    }

    existing_tweets: list[dict] = []
    existing_ids: set[str] = set()
    json_path = OUTPUT_DIR / "bookmarks.json"
    if json_path.exists():
        with open(json_path, encoding="utf-8") as f:
            existing_tweets = json.load(f)
        existing_ids = {t["tweet_id"] for t in existing_tweets if t.get("tweet_id")}
        print(f"→ 載入既有資料: {len(existing_tweets)} 則")

    all_tweets: list[dict] = []
    seen_ids: set[str] = set()
    cursor: str | None = None
    page_num = 0
    hit_existing = False

    print("→ 開始抓取新書籤...\n")

    with httpx.Client(headers=headers, timeout=30) as client:
        while True:
            page_num += 1
            variables: dict = {"count": 100}
            if cursor:
                variables["cursor"] = cursor

            params = {
                "variables": json.dumps(variables),
                "features": json.dumps(FEATURES),
            }

            resp = client.get(GRAPHQL_URL, params=params)

            if resp.status_code == 429:
                print("  ⚠ Rate limited，等待 60 秒...")
                time.sleep(60)
                continue

            if resp.status_code != 200:
                print(f"  ERROR: HTTP {resp.status_code}")
                print(f"  {resp.text[:500]}")
                break

            data = resp.json()

            if "errors" in data:
                print(f"  API 錯誤: {data['errors']}")
                break

            tweets, next_cursor = extract_bookmarks(data)

            new_count = 0
            for tweet in tweets:
                tid = tweet["tweet_id"]
                if tid and tid in existing_ids:
                    hit_existing = True
                    continue
                if tid and tid not in seen_ids:
                    seen_ids.add(tid)
                    all_tweets.append(tweet)
                    new_count += 1

            print(f"  第 {page_num} 頁: 取得 {len(tweets)} 則（新增 {new_count}），累計: {len(all_tweets)}")

            if hit_existing:
                print("  → 遇到已存在的書籤，停止抓取")
                break

            if not next_cursor or new_count == 0:
                break

            cursor = next_cursor
            delay = random.uniform(2.0, 5.0)
            time.sleep(delay)

    if all_tweets:
        print(f"\n→ 新增 {len(all_tweets)} 則書籤")
        all_tweets.extend(existing_tweets)
    else:
        print("\n→ 沒有新書籤")
        all_tweets = existing_tweets

    print(f"→ 總計 {len(all_tweets)} 則書籤")

    all_tweets.sort(key=lambda t: t.get("created_at", ""), reverse=True)

    export_json(all_tweets)
    export_csv(all_tweets)
    export_markdown(all_tweets)

    print("\n完成 ✓")


if __name__ == "__main__":
    main()
