#!/usr/bin/env python3
"""fetch.py: pull recent public posts from the creators in your niche, using Instagram's
official Graph API (the `business_discovery` field). No scraping, no logins.

YOUR Instagram professional account can read the public posts of other professional
(Creator or Business) accounts. Personal accounts can't be read, which is normal.

Needs two environment variables (never paste them into a chat, never commit them):
  IG_USER_ID       your Instagram professional account's ID (the 1784... number)
  IG_ACCESS_TOKEN  a Meta access token with instagram_basic + pages_show_list
                   (+ business_management if your Page sits in a Business Manager)

Usage:
  python3 fetch.py                    # every Active creator in creators.md
  python3 fetch.py handle1 handle2    # just these handles (no @ needed)
  python3 fetch.py --posts 100        # recent posts per creator (default 150)
  python3 fetch.py --check            # check your env vars + token, fetch nothing
Output: raw/<handle>.json  (one file per creator, read by rank.py and breakdown.py)
Only uses the Python standard library.
"""
import argparse, json, os, sys, time, urllib.error, urllib.parse, urllib.request

V = "v23.0"
FIELDS = "id,permalink,media_type,media_product_type,caption,comments_count,like_count,timestamp"


def env(name):
    v = os.environ.get(name, "").strip()
    if not v:
        sys.exit(f"Missing {name}. Set it in your terminal first (see 00-README.md, 'The two things you need').")
    return v


def get(url):
    try:
        with urllib.request.urlopen(url, timeout=60) as r:
            return json.load(r), None
    except urllib.error.HTTPError as e:
        try:
            err = json.load(e).get("error", {})
        except Exception:
            err = {"message": f"HTTP {e.code}"}
        # only safe fields; the token is never printed
        return None, {k: err.get(k) for k in ("message", "code", "error_subcode")}
    except urllib.error.URLError as e:
        return None, {"message": f"network error: {e.reason}"}


def call(token, igid, handle, after=None, limit=50):
    m = f"media.limit({limit})" + (f".after({after})" if after else "")
    f = f"business_discovery.username({handle}){{username,name,followers_count,media_count,{m}{{{FIELDS}}}}}"
    return get(f"https://graph.facebook.com/{V}/{igid}?" + urllib.parse.urlencode({"fields": f, "access_token": token}))


def fetch(token, igid, handle, target):
    out, after = {"handle": handle, "media": []}, None
    while len(out["media"]) < target:
        d, err = call(token, igid, handle, after)
        if err:
            out["error" if not out["media"] else "page_error"] = err
            break
        bd = d.get("business_discovery", {})
        for k in ("username", "name", "followers_count", "media_count"):
            if bd.get(k) is not None:  # later pages only carry media; keep page-1 profile fields
                out[k] = bd[k]
        md = bd.get("media", {})
        out["media"] += md.get("data", [])
        after = md.get("paging", {}).get("cursors", {}).get("after")
        if not after or not md.get("data"):
            break
        time.sleep(0.5)  # be gentle with the rate limit
    out["media"] = out["media"][:target]
    return out


def creators_from_md(path="creators.md"):
    """Rows of the creators.md table where the last column starts with 'y'. '@' optional."""
    if not os.path.exists(path):
        sys.exit(f"{path} not found. Fill in the template first (or pass handles on the command line).")
    hs = []
    for line in open(path, encoding="utf-8"):
        if not line.strip().startswith("|"):
            continue
        cells = [c.strip() for c in line.strip().strip("|").split("|")]
        if len(cells) < 2 or set(cells[0]) <= set("-: "):
            continue
        h = cells[0].lstrip("@").strip()
        if h.lower() in ("handle", "username", "") or " " in h:
            continue
        if cells[-1].lower().startswith("y"):
            hs.append(h)
    if not hs:
        sys.exit(f"No rows with Active = yes in {path}.")
    return hs


def main():
    ap = argparse.ArgumentParser(description="Pull creators' recent public posts via the Instagram Graph API.")
    ap.add_argument("handles", nargs="*", help="handles to fetch (default: Active rows in creators.md)")
    ap.add_argument("--posts", type=int, default=150, help="recent posts per creator (default 150)")
    ap.add_argument("--creators", default="creators.md", help="path to your creator list")
    ap.add_argument("--check", action="store_true", help="only check env vars + token, fetch nothing")
    a = ap.parse_args()

    token, igid = env("IG_ACCESS_TOKEN"), env("IG_USER_ID")
    if a.check:
        d, err = get(f"https://graph.facebook.com/{V}/{igid}?" + urllib.parse.urlencode({"fields": "username", "access_token": token}))
        if err:
            sys.exit(f"Token/ID check failed: {err.get('message')}")
        print(f"OK: connected as @{d.get('username')}")
        return

    handles = [h.lstrip("@") for h in a.handles] or creators_from_md(a.creators)
    os.makedirs("raw", exist_ok=True)
    for h in handles:
        o = fetch(token, igid, h, a.posts)
        with open(f"raw/{h}.json", "w", encoding="utf-8") as fh:
            json.dump(o, fh, indent=1)
        if "error" in o:
            print(f"{h}: ERROR {o['error'].get('message')} (personal accounts can't be read; that's normal)")
        else:
            print(f"{h}: ok, {o.get('followers_count')} followers, {len(o['media'])} posts")


if __name__ == "__main__":
    main()
