#!/opt/media-downloader/venv/bin/python3 -u
"""
One-off script to backfill tagged users for existing Instagram posts.

Tries imginn.com first (no rate limits), then falls back to instagram.com
for posts where imginn didn't have tag data.

Tracks checked posts in a local file so re-runs skip already-checked posts
without polluting the database.

Usage:
    python3 /opt/media-downloader/scripts/backfill_tagged_users.py [--creator-id N] [--limit N] [--dry-run]
"""

import argparse
import json
import os
import random
import re
import sys
import time

sys.path.insert(0, '/opt/media-downloader')
import modules.db_bootstrap  # noqa: F401 — triggers pgadapter monkey-patch

import sqlite3

CHECKED_FILE = '/tmp/backfill_checked_posts.txt'
NORDVPN_CREDS = 'Dc9mgrpJnFnkTtc5iQkGNwLM:fKd2ZEjBUJ3YDQ5hhcoTKsnW'
NORDVPN_SERVERS = [
    'us.socks.nordhold.net',
    'nl.socks.nordhold.net',
    'se.socks.nordhold.net',
    'amsterdam.nl.socks.nordhold.net',
    'atlanta.us.socks.nordhold.net',
    'chicago.us.socks.nordhold.net',
    'dallas.us.socks.nordhold.net',
    'los-angeles.us.socks.nordhold.net',
    'new-york.us.socks.nordhold.net',
    'stockholm.se.socks.nordhold.net',
]
_proxy_index = 0


def get_current_proxy():
    """Return the current proxy without rotating."""
    server = NORDVPN_SERVERS[_proxy_index % len(NORDVPN_SERVERS)]
    return {'https': f'socks5://{NORDVPN_CREDS}@{server}:1080'}


def rotate_proxy():
    """Switch to the next server. Call on rate limit or persistent errors."""
    global _proxy_index
    _proxy_index += 1
    server = NORDVPN_SERVERS[_proxy_index % len(NORDVPN_SERVERS)]
    print(f'  Rotating proxy -> {server}', flush=True)
    return {'https': f'socks5://{NORDVPN_CREDS}@{server}:1080'}


def get_next_proxy():
    """Legacy: rotate and return. Only used in retry path."""
    return rotate_proxy()


def load_checked_posts():
    """Load set of already-checked post DB IDs from local file."""
    if not os.path.exists(CHECKED_FILE):
        return set()
    with open(CHECKED_FILE, 'r') as f:
        return {int(line.strip()) for line in f if line.strip().isdigit()}


def save_checked_post(db_id):
    """Append a checked post ID to the local tracking file."""
    with open(CHECKED_FILE, 'a') as f:
        f.write(f'{db_id}\n')


def get_session():
    """Get a curl_cffi session with browser impersonation."""
    from curl_cffi.requests import Session

    # Try multiple browser versions for curl_cffi compatibility
    for _browser in ("chrome131", "chrome136", "chrome"):
        try:
            session = Session(impersonate=_browser)
            break
        except Exception:
            continue
    else:
        session = Session()
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                       '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language': 'en-US,en;q=0.9',
        'Referer': 'https://www.instagram.com/',
    })
    return session


def get_posts_without_tags(conn, creator_id=None, limit=0):
    """Get Instagram posts without tagged users, newest first."""
    query = """
        SELECT p.id, p.post_id, p.creator_id, c.username
        FROM paid_content_posts p
        JOIN paid_content_creators c ON p.creator_id = c.id
        LEFT JOIN paid_content_post_tagged_users tu ON tu.post_id = p.id
        WHERE c.service_id = 'instagram'
          AND p.deleted_at IS NULL
          AND p.post_id NOT LIKE 'story_%%'
          AND p.post_id NOT LIKE 'highlight_%%'
          AND tu.post_id IS NULL
    """
    params = []
    if creator_id:
        query += " AND p.creator_id = ?"
        params.append(creator_id)
    query += " ORDER BY p.id ASC"
    if limit:
        query += f" LIMIT {limit}"

    cursor = conn.cursor()
    cursor.execute(query, params)
    return [(row[0], row[1], row[2], row[3]) for row in cursor.fetchall()]


def save_tagged_users(conn, post_db_id, usernames):
    """Save tagged users for a post."""
    cursor = conn.cursor()
    now = time.strftime('%Y-%m-%dT%H:%M:%S')
    for username in usernames:
        cursor.execute("""
            INSERT INTO paid_content_post_tagged_users (post_id, username, created_at)
            VALUES (?, ?, ?)
            ON CONFLICT DO NOTHING
        """, (post_db_id, username, now))
    conn.commit()


def extract_usertags_from_item(item):
    """Extract tagged usernames from an Instagram media item dict."""
    tagged = []

    # Main item usertags
    usertags = item.get('usertags') or {}
    for tag in usertags.get('in', []):
        username = (tag.get('user') or {}).get('username')
        if username and username not in tagged:
            tagged.append(username)

    # Carousel item usertags
    for cm in item.get('carousel_media') or []:
        cm_usertags = cm.get('usertags') or {}
        for tag in cm_usertags.get('in', []):
            username = (tag.get('user') or {}).get('username')
            if username and username not in tagged:
                tagged.append(username)

    return tagged


def create_flaresolverr_session():
    """Create a persistent FlareSolverr session for imginn requests."""
    import requests as std_requests

    try:
        resp = std_requests.post('http://localhost:8191/v1', json={
            'cmd': 'sessions.create',
        }, timeout=30)
        session_id = resp.json().get('session')
        if session_id:
            print(f"  FlareSolverr session: {session_id}")
        return session_id
    except Exception as e:
        print(f"  FlareSolverr unavailable: {e}")
        return None


def destroy_flaresolverr_session(session_id):
    """Clean up FlareSolverr session."""
    if not session_id:
        return
    import requests as std_requests
    try:
        std_requests.post('http://localhost:8191/v1', json={
            'cmd': 'sessions.destroy',
            'session': session_id,
        }, timeout=10)
    except Exception:
        pass


def fetch_usertags_imginn(shortcode, flaresolverr_session=None):
    """Try fetching usertags from imginn.com via FlareSolverr."""
    import requests as std_requests

    url = f'https://imginn.com/p/{shortcode}/'

    try:
        payload = {
            'cmd': 'request.get',
            'url': url,
            'maxTimeout': 60000,
        }
        if flaresolverr_session:
            payload['session'] = flaresolverr_session

        resp = std_requests.post('http://localhost:8191/v1', json=payload, timeout=70)

        data = resp.json()
        if data.get('status') != 'ok':
            return None, 'imginn_flaresolverr_fail'

        html = data.get('solution', {}).get('response', '')
        if not html:
            return None, 'imginn_empty'

        # Parse tagged users from imginn HTML (same logic as instagram_adapter)
        tagged = []
        idx = html.find('class="tagged-user-list"')
        if idx < 0:
            return [], 'imginn_no_tags'

        chunk = html[idx:idx + 5000]
        for m in re.finditer(r'class="name">\s*(\S+)\s*</div>', chunk):
            username = m.group(1).strip()
            if re.match(r'^[a-zA-Z0-9_.]{1,30}$', username):
                tagged.append(username)

        return tagged, 'imginn_ok'

    except Exception as e:
        return None, f'imginn_error: {e}'


def fetch_usertags_instagram(session, shortcode, max_retries=3):
    """Fetch usertags from Instagram post page HTML via proxy.
    Sticks with current proxy server; only rotates on connection errors or rate limits."""
    url = f'https://www.instagram.com/p/{shortcode}/'
    last_error = None

    for attempt in range(max_retries):
        try:
            proxy = get_current_proxy() if attempt == 0 else rotate_proxy()
            server = list(proxy.values())[0].split('@')[1].split(':')[0]
            resp = session.get(url, timeout=20, proxies=proxy)

            if resp.status_code == 429:
                rotate_proxy()
                return None, 'rate_limited'
            if resp.status_code == 404:
                return None, 'not_found'
            if resp.status_code != 200:
                last_error = f'http_{resp.status_code}'
                print(f'    retry {attempt+1}/{max_retries}: http {resp.status_code} via {server}', flush=True)
                continue

            html = resp.text

            # Find embedded post JSON in the page
            idx = html.find('xdt_api__v1__media__shortcode__web_info')
            if idx < 0:
                if 'LoginAndSignupPage' in html or '"require_login":true' in html:
                    return None, 'login_required'
                return None, 'no_embedded_data'

            # Find the items JSON object
            items_start = html.find('{"items":[{"code"', idx)
            if items_start < 0:
                return None, 'no_items_data'

            # Find balanced braces to extract the full JSON
            depth = 0
            end = items_start
            for i in range(items_start, min(items_start + 500000, len(html))):
                if html[i] == '{':
                    depth += 1
                elif html[i] == '}':
                    depth -= 1
                    if depth == 0:
                        end = i + 1
                        break

            data = json.loads(html[items_start:end])
            items = data.get('items', [])
            if not items:
                return [], 'ok'

            tagged = extract_usertags_from_item(items[0])
            return tagged, 'ok'

        except json.JSONDecodeError as e:
            return None, f'json_error: {e}'
        except Exception as e:
            last_error = str(e)
            short_err = str(e).split('.')[0][:80]
            print(f'    retry {attempt+1}/{max_retries}: {short_err} via {server}', flush=True)
            # Connection/proxy errors → retry with next server
            continue

    return None, last_error or 'max_retries'


def fetch_post_usertags(session, shortcode, flaresolverr_session=None):
    """Fetch usertags from Instagram via proxy."""
    return fetch_usertags_instagram(session, shortcode)


def main():
    parser = argparse.ArgumentParser(description='Backfill tagged users for Instagram posts')
    parser.add_argument('--dry-run', action='store_true', help='Show what would be done without saving')
    parser.add_argument('--creator-id', type=int, help='Only process a specific creator')
    parser.add_argument('--limit', type=int, default=0, help='Max posts to process (0 = all)')
    args = parser.parse_args()

    conn = sqlite3.connect('media_downloader')
    posts = get_posts_without_tags(conn, args.creator_id, args.limit)

    if not posts:
        print("No posts need tagged user backfill.")
        return

    # Filter out already-checked posts (from previous runs)
    checked = load_checked_posts()
    if checked:
        before = len(posts)
        posts = [(db_id, sc, cid, u) for db_id, sc, cid, u in posts if db_id not in checked]
        if before != len(posts):
            print(f"Skipping {before - len(posts)} already-checked posts from previous runs")

    if not posts:
        print("All posts already checked.")
        return

    # Group by creator for display
    creators = {}
    for db_id, shortcode, creator_id, username in posts:
        if creator_id not in creators:
            creators[creator_id] = {'username': username, 'count': 0}
        creators[creator_id]['count'] += 1

    print(f"Found {len(posts)} Instagram posts to check across {len(creators)} creators")
    for cid, info in creators.items():
        print(f"  @{info['username']}: {info['count']} posts")
    if args.dry_run:
        print("DRY RUN - no changes will be saved\n")

    session = get_session()

    total_tagged = 0
    total_no_tags = 0
    total_errors = 0
    rate_limit_wait = 60

    try:
        for i, (db_id, shortcode, creator_id, username) in enumerate(posts):
            if i > 0:
                time.sleep(2)
            print(f"  [{i+1}/{len(posts)}] @{username} {shortcode}: ", end='', flush=True)
            tagged, status = fetch_post_usertags(session, shortcode)

            if status == 'rate_limited':
                print(f"rate limited, waiting {rate_limit_wait}s...", flush=True)
                time.sleep(rate_limit_wait)
                rate_limit_wait = min(rate_limit_wait * 2, 600)
                print(f"  [{i+1}/{len(posts)}] @{username} {shortcode}: ", end='', flush=True)
                tagged, status = fetch_post_usertags(session, shortcode)

            if tagged is None:
                print(f"ERROR {status}", flush=True)
                total_errors += 1
                if status == 'login_required':
                    print("  Instagram is requiring login. Stopping.")
                    break
                continue

            # Reset rate limit backoff on success
            rate_limit_wait = 60

            if tagged:
                if args.dry_run:
                    print(f"would tag {tagged}", flush=True)
                else:
                    save_tagged_users(conn, db_id, tagged)
                    print(f"tagged {tagged}", flush=True)
                total_tagged += 1
            else:
                if not args.dry_run:
                    save_checked_post(db_id)
                print(f"no tags", flush=True)
                total_no_tags += 1

            # Polite delay
            time.sleep(random.uniform(0.5, 1.5))
    except KeyboardInterrupt:
        print("\nInterrupted!")

    conn.close()
    print(f"\nDone! {total_tagged} posts had tagged users, {total_no_tags} had none, {total_errors} errors (out of {len(posts)} total)")


if __name__ == '__main__':
    main()
