import argparse
import csv
import os
import tempfile
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any

from tweetapi import TweetAPI, TweetAPIError


FIELDS = [
    "post_id", "created_at", "collected_at", "text", "likes", "reposts",
    "replies", "quotes", "bookmarks", "views", "interaction_total",
]
FORMULA_PREFIXES = ("=", "+", "-", "@", "\t", "\r", "\n", "＝", "＋", "－", "＠")
METRICS = {
    "likes": "likeCount", "reposts": "retweetCount", "replies": "replyCount",
    "quotes": "quoteCount", "bookmarks": "bookmarkCount", "views": "viewCount",
}


def csv_text(value: Any) -> str:
    text = "" if value is None else str(value).replace("\x00", "")
    return "'" + text if text.startswith(FORMULA_PREFIXES) else text


def metric(post: dict[str, Any], field: str) -> int | None:
    value = post.get(field)
    return value if type(value) is int and value >= 0 else None


def post_time(post: dict[str, Any]) -> datetime | None:
    value = post.get("createdAt")
    if not isinstance(value, str):
        return None
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
        return parsed.astimezone(timezone.utc) if parsed.tzinfo else None
    except ValueError:
        return None


def collect(client: TweetAPI, username: str, since: date, until: date,
            max_pages: int) -> tuple[list[dict[str, Any]], bool, int]:
    profile = client.user.get_by_username(username=username)["data"]
    user_id = str(profile["id"])
    rows: list[dict[str, Any]] = []
    seen_ids: set[str] = set()
    missing_dates = 0
    cursor = None
    seen_cursors: set[str] = set()
    for _ in range(max_pages):
        page = client.user.get_tweets(user_id=user_id, cursor=cursor)
        collected_at = datetime.now(timezone.utc).isoformat()
        for post in page.get("data", []):
            if not isinstance(post, dict):
                continue
            post_id = str(post.get("id") or "")
            if not post_id or post_id in seen_ids:
                continue
            seen_ids.add(post_id)
            created_at = post_time(post)
            if created_at is None:
                missing_dates += 1
                continue
            if not since <= created_at.date() < until:
                continue
            values = {column: metric(post, field) for column, field in METRICS.items()}
            counts = [values[key] for key in ("likes", "reposts", "replies", "quotes", "bookmarks")]
            rows.append({
                "post_id": post_id,
                "created_at": created_at.isoformat(),
                "collected_at": collected_at,
                "text": csv_text(post.get("text")),
                **values,
                "interaction_total": sum(counts) if all(count is not None for count in counts) else None,
            })
        cursor = page.get("pagination", {}).get("nextCursor")
        if not cursor:
            return rows, True, missing_dates
        if cursor in seen_cursors:
            raise ValueError("Pagination cursor repeated")
        seen_cursors.add(cursor)
    return rows, False, missing_dates


def write_csv(output: Path, rows: list[dict[str, Any]]) -> None:
    output.parent.mkdir(parents=True, exist_ok=True)
    temporary_path = None
    try:
        with tempfile.NamedTemporaryFile(
            mode="w", encoding="utf-8", newline="", dir=output.parent,
            prefix=f".{output.name}.", suffix=".tmp", delete=False,
        ) as handle:
            temporary_path = Path(handle.name)
            writer = csv.DictWriter(handle, fieldnames=FIELDS)
            writer.writeheader()
            writer.writerows(rows)
        os.replace(temporary_path, output)
    finally:
        if temporary_path is not None:
            temporary_path.unlink(missing_ok=True)


def main() -> None:
    parser = argparse.ArgumentParser(description="Export current public post metrics for a post-date window.")
    parser.add_argument("username", help="Public username, with or without @")
    parser.add_argument("--since", type=date.fromisoformat, required=True, help="First post date, UTC (YYYY-MM-DD)")
    parser.add_argument("--until", type=date.fromisoformat, required=True, help="Exclusive end date, UTC (YYYY-MM-DD)")
    parser.add_argument("--max-pages", type=int, default=5, help="Maximum timeline pages (default: 5)")
    parser.add_argument("--output", type=Path, default=Path("post_metrics.csv"))
    args = parser.parse_args()
    username = args.username.lstrip("@").strip()
    if not username or args.since >= args.until or args.max_pages < 1:
        parser.error("supply a username, an increasing date window, and --max-pages >= 1")
    key = os.environ.get("TWEETAPI_KEY", "").strip()
    if not key:
        parser.error("set TWEETAPI_KEY in your environment")
    try:
        rows, complete, missing_dates = collect(
            TweetAPI(api_key=key), username, args.since, args.until, args.max_pages
        )
        write_csv(args.output, rows)
    except (TweetAPIError, OSError, KeyError, TypeError, ValueError) as error:
        raise SystemExit(f"Report failed without writing a new CSV: {error}")
    print(f"Wrote {len(rows)} posts to {args.output}")
    if not complete:
        print("More pages exist. This CSV covers only the pages fetched; raise --max-pages to extend it.")
    if missing_dates:
        print(f"Skipped {missing_dates} posts without a usable createdAt date.")


if __name__ == "__main__":
    main()
