# -*- coding: utf-8 -*-
"""
uidai_flow.py — FINAL (CurlMime multipart upload)
Captcha: fetch UIDAI → upload img402.dev (no key) → dhboss OCR → send OTP
"""
import sys
try:
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
    sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except Exception:
    pass

import os, json, io, secrets, hashlib, base64, time, uuid as _uuid, glob
from urllib.parse import urlparse, parse_qs, urljoin
import curl_cffi
from curl_cffi import requests

BASE = 'https://tathya.uidai.gov.in'
CLIENT_ID = 'myAadhaar'
IMPERSONATE = 'chrome'
STATE_DIR = os.path.dirname(os.path.abspath(__file__))
UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
      '(KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36')

# ══════════════════════════════════════════════════════════════
IMGBB_API_KEY = ''  # 49-char key skip होगी (32 needed). img402 primary.
CAPTCHA_API_URL     = 'https://api.dhboss.com/apicall/captcha_image_to_text/'
CAPTCHA_API_KEY     = '73a08fZ2o5ddnpYN5FctZ8iMPJBwo2ZM3nGstKkJmnxuovCrMRUxWfjBbiM0zjh3dgfSxZ'
CAPTCHA_MAX_RETRIES = 3
# ══════════════════════════════════════════════════════════════


# ═══════════════ STATE ═══════════════
def state_path(sid):
    safe = ''.join(c for c in sid if c.isalnum() or c in '-_')
    return os.path.join(STATE_DIR, f'uidai_state_{safe}.json')

def load_state(sid):
    p = state_path(sid)
    if not os.path.exists(p): return {}
    try:
        with open(p, encoding='utf-8') as f: return json.load(f)
    except Exception: return {}

def save_state(sid, state):
    try:
        with open(state_path(sid), 'w', encoding='utf-8') as f: json.dump(state, f)
    except Exception: pass

def clear_state(sid):
    try: os.remove(state_path(sid))
    except Exception: pass

def cleanup_old(max_age=3600):
    for f in glob.glob(os.path.join(STATE_DIR, 'uidai_state_*.json')):
        try:
            if time.time() - os.path.getmtime(f) > max_age: os.remove(f)
        except Exception: pass

def get_session(state):
    s = requests.Session(impersonate=IMPERSONATE)
    for c in state.get('cookies', []):
        try:
            s.cookies.set(c['name'], c['value'],
                          domain=c.get('domain', 'tathya.uidai.gov.in'),
                          path=c.get('path', '/'))
        except Exception: pass
    return s

def save_cookies(s, state):
    cookies, seen = [], set()
    try:
        for c in s.cookies.jar:
            key = (c.name, c.domain or '', c.path or '/')
            if key in seen: continue
            seen.add(key)
            cookies.append({'name': c.name, 'value': c.value,
                            'domain': c.domain or 'tathya.uidai.gov.in',
                            'path': c.path or '/'})
    except Exception:
        try:
            for k in s.cookies.keys():
                cookies.append({'name': k, 'value': s.cookies.get(k),
                                'domain': 'tathya.uidai.gov.in', 'path': '/'})
        except Exception: pass
    state['cookies'] = cookies

def gen_pkce():
    v = base64.urlsafe_b64encode(secrets.token_bytes(96)).rstrip(b'=').decode()
    h = hashlib.sha256(v.encode()).hexdigest()
    c = base64.b64encode(h.encode()).decode()
    return v, c

def rid(state, suffix):
    if not state.get('req_uuid'):
        state['req_uuid'] = str(_uuid.uuid4())
    return f"{state['req_uuid']}-{suffix}"

def _extract_code(url):
    if not url: return None
    try:
        qs = parse_qs(urlparse(url).query)
        return (qs.get('code') or [None])[0]
    except Exception: return None

def _normalize_url(url):
    if not url: return url
    if url.startswith('http://'): return 'https://' + url[7:]
    return url

def _follow_to_get_code(s, url, req_dbg, max_hops=4):
    headers = {
        'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'accept-language': 'en-US,en;q=0.9', 'dnt': '1',
        'referer': 'https://myaadhaar.uidai.gov.in/',
        'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate',
        'sec-fetch-site': 'same-site', 'sec-fetch-user': '?1',
        'upgrade-insecure-requests': '1', 'user-agent': UA,
    }
    current = _normalize_url(url)
    for hop in range(max_hops):
        code = _extract_code(current)
        if code: return code
        try:
            r = s.get(current, headers=headers, allow_redirects=False, timeout=20)
            req_dbg.append(f'hop{hop+1}: HTTP {r.status_code}')
            if 300 <= r.status_code < 400:
                loc = r.headers.get('location') or ''
                if not loc: return None
                code = _extract_code(loc)
                if code: return code
                current = _normalize_url(urljoin(current, loc))
            else: return None
        except Exception as e:
            req_dbg.append(f'hop{hop+1}: err={e}')
            return None
    return None


# ══════════════════════════════════════════════════════════════
# IMAGE UPLOAD — 🔑 CurlMime (NOT files=)
# ══════════════════════════════════════════════════════════════
def _upload_multipart(url, field_name, filename, img_bytes, extra_data=None, timeout=30):
    """Upload via curl_cffi CurlMime multipart. Returns (url, error_msg)."""
    mp = curl_cffi.CurlMime()
    try:
        # File part
        mp.addpart(
            name=field_name,
            content_type='image/jpeg',
            filename=filename,
            data=img_bytes,
        )
        # Extra form fields (if any)
        if extra_data:
            for k, v in extra_data.items():
                mp.addpart(name=k, data=str(v).encode('utf-8'))

        r = requests.post(url, multipart=mp, timeout=timeout,
                          impersonate=IMPERSONATE)
        return r, None
    except Exception as e:
        return None, str(e)
    finally:
        try: mp.close()
        except Exception: pass


def _upload_image(image_b64):
    """Try img402 → imgbb → litterbox → uguu → 0x0.st with CurlMime."""
    b64 = image_b64
    if ',' in b64 and b64.startswith('data:'):
        b64 = b64.split(',', 1)[1]
    b64 = ''.join(b64.split())

    try:
        img_bytes = base64.b64decode(b64)
    except Exception as e:
        return None, [f'base64 decode: {e}']

    fname = f'captcha_{secrets.token_hex(4)}.jpg'
    errors = []

    # ─────────────────────────────────────
    # 1) img402.dev — NO KEY NEEDED
    # ─────────────────────────────────────
    try:
        r, err = _upload_multipart(
            'https://img402.dev/api/free',
            'image', fname, img_bytes, timeout=30,
        )
        if r is not None:
            if r.status_code == 200:
                try:
                    j = r.json()
                    u = j.get('url') or (j.get('data') or {}).get('url')
                    if u and u.startswith('http'):
                        return u, []
                except Exception: pass
            errors.append(f'img402: HTTP {r.status_code} {r.text[:100]}')
        else:
            errors.append(f'img402: {err}')
    except Exception as e:
        errors.append(f'img402: {e}')

    # ─────────────────────────────────────
    # 2) imgbb — only if 32-char key
    # ─────────────────────────────────────
    if IMGBB_API_KEY and len(IMGBB_API_KEY) == 32:
        try:
            r, err = _upload_multipart(
                f'https://api.imgbb.com/1/upload?key={IMGBB_API_KEY}',
                'image', fname, img_bytes, timeout=30,
            )
            if r is not None and r.status_code == 200:
                j = r.json()
                if j.get('success') and j.get('data', {}).get('url'):
                    return j['data']['url'], []
            errors.append(f'imgbb: fail')
        except Exception as e:
            errors.append(f'imgbb: {e}')

    # ─────────────────────────────────────
    # 3) Litterbox — no key
    # ─────────────────────────────────────
    try:
        r, err = _upload_multipart(
            'https://litterbox.catbox.moe/resources/internals/api.php',
            'fileToUpload', fname, img_bytes,
            extra_data={'reqtype': 'fileupload', 'time': '1h'},
            timeout=30,
        )
        if r is not None and r.status_code == 200:
            u = r.text.strip()
            if u.startswith('http'): return u, []
        errors.append(f'litterbox: HTTP {r.status_code if r else "?"}')
    except Exception as e:
        errors.append(f'litterbox: {e}')

    # ─────────────────────────────────────
    # 4) uguu.se — no key
    # ─────────────────────────────────────
    try:
        r, err = _upload_multipart(
            'https://uguu.se/upload.php',
            'files[]', fname, img_bytes, timeout=30,
        )
        if r is not None and r.status_code == 200:
            try:
                j = r.json()
                u = (j.get('files') or [{}])[0].get('url', '')
                if u.startswith('http'): return u, []
            except Exception: pass
        errors.append(f'uguu: HTTP {r.status_code if r else "?"}')
    except Exception as e:
        errors.append(f'uguu: {e}')

    # ─────────────────────────────────────
    # 5) 0x0.st
    # ─────────────────────────────────────
    try:
        r, err = _upload_multipart(
            'https://0x0.st',
            'file', fname, img_bytes, timeout=30,
        )
        if r is not None and r.status_code == 200:
            u = r.text.strip()
            if u.startswith('http'): return u, []
        errors.append(f'0x0.st: HTTP {r.status_code if r else "?"}')
    except Exception as e:
        errors.append(f'0x0.st: {e}')

    return None, errors


def _solve_captcha(image_b64):
    """Upload → URL → dhboss OCR."""
    url, up_errors = _upload_image(image_b64)
    if not url:
        return {'error': 'Upload failed on all hosts', 'upload_errors': up_errors}

    try:
        r = requests.get(
            CAPTCHA_API_URL,
            params={'image_url': url, 'apikeyfill': CAPTCHA_API_KEY},
            timeout=30, impersonate=IMPERSONATE,
        )
        data = r.json()
        if data.get('status') == 'success':
            text = ((data.get('response') or {}).get('text') or '').strip()
            if text:
                return {'text': text, 'score': data.get('score', 1), 'url': url}
        return {'error': f'OCR API: {json.dumps(data)[:200]}', 'url': url}
    except Exception as e:
        return {'error': f'OCR failed: {e}', 'url': url}


# ═══════════════ CAPTCHA FETCH ═══════════════
def _fetch_captcha(sid):
    clear_state(sid)
    state = {}
    verifier, challenge = gen_pkce()
    state['code_verifier'] = verifier
    state['code_challenge'] = challenge
    state['req_uuid'] = str(_uuid.uuid4())
    state['req_counter'] = 0

    s = get_session(state)
    oauth_url = (
        f"{BASE}/access/oauth/authorize?grant_type=authorization_code&response_type=code"
        f"&client_id={CLIENT_ID}&scope=READ&code_challenge={challenge}"
        f"&code_challenge_method=S256&telemetry_session_id={state['req_uuid']}"
    )
    login_headers = {
        'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'accept-language': 'en-US,en;q=0.9', 'dnt': '1',
        'referer': 'https://myaadhaar.uidai.gov.in/',
        'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate',
        'sec-fetch-site': 'same-site', 'sec-fetch-user': '?1',
        'upgrade-insecure-requests': '1', 'user-agent': UA,
    }
    try:
        r1 = s.get(oauth_url, headers=login_headers, allow_redirects=True, timeout=25)
    except Exception as e:
        return {'error': f'OAuth redirect failed: {e}'}

    save_cookies(s, state)
    if not any(c.get('name') == 'SESSION' for c in state.get('cookies', [])):
        save_state(sid, state)
        return {'error': f'No SESSION cookie (HTTP {r1.status_code})'}

    state['req_counter'] = 1
    try:
        r2 = s.get(f'{BASE}/access/generateCaptcha', headers={
            'accept': 'application/json, text/plain, */*',
            'accept-language': 'en-US,en;q=0.9', 'dnt': '1',
            'referer': f'{BASE}/access/login?role=resident',
            'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-origin', 'user-agent': UA,
            'x-request-id': rid(state, 1),
        }, timeout=20)
    except Exception as e:
        save_state(sid, state)
        return {'error': f'captcha request failed: {e}'}

    save_cookies(s, state)
    if r2.status_code != 200:
        save_state(sid, state)
        return {'error': f'generateCaptcha HTTP {r2.status_code}'}
    try: data = r2.json()
    except Exception:
        save_state(sid, state)
        return {'error': f'Not JSON: {r2.text[:200]}'}
    if data.get('sessionActive') is not True:
        save_state(sid, state)
        return {'error': f'sessionActive:{data.get("sessionActive")}'}
    try: inner = json.loads(data['message'])
    except Exception:
        save_state(sid, state)
        return {'error': 'bad message JSON'}

    state['captcha_txn_id'] = inner.get('transactionId', '')
    save_state(sid, state)
    return {
        'imageBase64': inner.get('imageBase64', ''),
        'transactionId': inner.get('transactionId', ''),
    }


# ═══════════════ OTP SEND ═══════════════
def _send_otp(sid, uid, captcha_text, captcha_txn):
    state = load_state(sid)
    s = get_session(state)
    state['req_counter'] = state.get('req_counter', 0) + 1

    body = json.dumps({'uid': uid, 'captcha': captcha_text, 'captchaTxnId': captcha_txn},
                      separators=(',', ':')).encode()
    try:
        r = s.post(f'{BASE}/access/generateOTPForOAuth', headers={
            'accept': 'application/json, text/plain, */*',
            'accept-language': 'en-US,en;q=0.9', 'content-type': 'application/json',
            'dnt': '1', 'origin': BASE,
            'referer': f'{BASE}/access/login?role=resident',
            'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-origin', 'user-agent': UA,
            'x-request-id': rid(state, state['req_counter']),
        }, data=body, timeout=25, allow_redirects=False)
    except Exception as e:
        return {'error': f'OTP failed: {e}'}

    save_cookies(s, state)
    try: data = r.json()
    except Exception:
        save_state(sid, state)
        return {'error': f'OTP not JSON (HTTP {r.status_code})', 'debug': r.text[:300]}

    if data.get('status') not in (True, 'Y', 'true'):
        msg = (data.get('message') or data.get('errorMessage') or 'OTP failed').lower()
        save_state(sid, state)
        return {'error': data.get('message') or data.get('errorMessage') or 'OTP failed',
                'is_captcha_error': 'captcha' in msg or 'text' in msg,
                'debug': data}

    state['uid'] = uid
    state['otp_txn_id'] = data.get('txnId', '')
    save_state(sid, state)
    return {'success': True, 'txnId': state['otp_txn_id'],
            'message': data.get('message', 'OTP sent')}


# ═══════════════ AUTO-OTP ═══════════════
def action_auto_otp(sid, uid):
    cleanup_old()
    req_dbg = []
    for attempt in range(1, CAPTCHA_MAX_RETRIES + 1):
        req_dbg.append(f'--- attempt {attempt}/{CAPTCHA_MAX_RETRIES} ---')

        cap = _fetch_captcha(sid)
        if 'error' in cap:
            req_dbg.append(f'captcha: {cap["error"]}')
            if attempt < CAPTCHA_MAX_RETRIES: time.sleep(1.5)
            continue

        req_dbg.append(f'captcha fetched txn={cap["transactionId"]}')

        solve = _solve_captcha(cap['imageBase64'])
        if 'error' in solve:
            req_dbg.append(f'solve: {solve["error"]}')
            for e in (solve.get('upload_errors') or []): req_dbg.append(f'  {e}')
            if attempt < CAPTCHA_MAX_RETRIES: time.sleep(1.5)
            continue

        req_dbg.append(f'✅ solved "{solve["text"]}" via {solve.get("url", "")[:80]}')

        otp = _send_otp(sid, uid, solve['text'], cap['transactionId'])
        if 'error' in otp:
            req_dbg.append(f'OTP: {otp["error"]}')
            if otp.get('is_captcha_error'):
                req_dbg.append('→ wrong captcha, retry')
                if attempt < CAPTCHA_MAX_RETRIES: time.sleep(1.5)
                continue
            return {'error': otp['error'], 'attempts': req_dbg}

        return {
            'success': True,
            'message': otp.get('message', 'OTP sent'),
            'txnId': otp.get('txnId', ''),
            'solved_captcha': solve['text'],
            'attempts': req_dbg,
        }

    return {'error': 'Auto-solve failed after retries', 'attempts': req_dbg}


# ═══════════════ LOGIN + PROFILE + PDF ═══════════════
def action_login(sid, otp):
    state = load_state(sid)
    if not state.get('uid'): return {'error': 'No UID in state'}
    uid = state['uid']
    if not any(c.get('name') == 'SESSION' for c in state.get('cookies', [])):
        return {'error': 'SESSION missing'}

    s = get_session(state)
    state['req_counter'] = state.get('req_counter', 0) + 1
    req_dbg = []

    login_headers = {
        'accept': 'application/json, text/plain, */*',
        'accept-language': 'en-US,en;q=0.9', 'content-type': 'application/json',
        'dnt': '1', 'origin': BASE,
        'referer': f'{BASE}/access/login?role=resident',
        'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors',
        'sec-fetch-site': 'same-origin', 'user-agent': UA,
        'x-request-id': rid(state, state['req_counter']),
    }
    body = json.dumps({'uid': uid, 'otp': otp}, separators=(',', ':')).encode()

    login_data = None
    for attempt in range(1, 5):
        req_dbg.append(f'login {attempt}/4')
        try:
            r = s.post(f'{BASE}/access/login', headers=login_headers,
                       data=body, timeout=25, allow_redirects=False)
            req_dbg.append(f'  HTTP {r.status_code} bytes={len(r.content)}')
        except Exception as e:
            req_dbg.append(f'  ex: {e}')
            if attempt < 4: time.sleep(2.5); continue
            return {'error': str(e), 'attempts': req_dbg}

        save_cookies(s, state); save_state(sid, state)
        if r.status_code != 200:
            return {'error': f'HTTP {r.status_code}', 'attempts': req_dbg}

        if len(r.content) == 0:
            try:
                r = s.post(f'{BASE}/access/login', headers=login_headers,
                           data=body, timeout=25, allow_redirects=False, http_version='http1.1')
                save_cookies(s, state)
            except Exception: pass

        if len(r.content) == 0:
            if attempt < 4: time.sleep(2.5); continue
            return {'error': 'empty login', 'attempts': req_dbg}

        try: login_data = r.json()
        except Exception:
            return {'error': f'JSON fail: {r.text[:200]}', 'attempts': req_dbg}

        status = login_data.get('status'); err = login_data.get('errorMessage') or ''
        req_dbg.append(f'  status={status} err={err}')

        if status == 'Y':
            req_dbg.append(f'  ✅ OK attempt {attempt}')
            break
        if 'already active' in err.lower() or 'already exist' in err.lower():
            if attempt < 4: time.sleep(2.5); continue
            return {'error': 'SESSION_ACTIVE',
                    'message': 'UIDAI session active है। 2-5 मिनट रुकें या browser logout करें।',
                    'action_required': 'wait_or_logout', 'attempts': req_dbg}
        return {'error': err or 'Login failed', 'attempts': req_dbg}

    if not login_data or login_data.get('status') != 'Y':
        return {'error': 'Login failed', 'attempts': req_dbg}

    redirect_url = login_data.get('redirectURL') or ''
    code = _extract_code(redirect_url)
    if not code: code = _follow_to_get_code(s, redirect_url, req_dbg)
    if not code:
        challenge = state.get('code_challenge', '')
        auth_url = (
            f"{BASE}/access/oauth/authorize?grant_type=authorization_code&response_type=code"
            f"&client_id={CLIENT_ID}&scope=READ&code_challenge={challenge}"
            f"&code_challenge_method=S256&telemetry_session_id={state.get('req_uuid', '')}"
        )
        code = _follow_to_get_code(s, auth_url, req_dbg)
    if not code:
        return {'error': 'No code obtained', 'attempts': req_dbg}

    verifier = state.get('code_verifier', '')
    state['req_counter'] += 1
    token_url = (
        f'{BASE}/access/oauth/token?client_id={CLIENT_ID}&grant_type=authorization_code'
        f'&code_verifier={verifier}&code={code}'
    )
    try:
        r3 = s.post(token_url, headers={
            'accept': 'application/json, text/plain, */*',
            'accept-language': 'en-US,en;q=0.9', 'dnt': '1',
            'origin': 'https://myaadhaar.uidai.gov.in',
            'referer': 'https://myaadhaar.uidai.gov.in/',
            'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-site', 'user-agent': UA,
            'x-request-id': rid(state, state['req_counter']),
        }, timeout=25, allow_redirects=False)
    except Exception as e:
        return {'error': f'Token failed: {e}', 'attempts': req_dbg}

    try: token_data = r3.json()
    except Exception:
        save_state(sid, state)
        return {'error': 'Token JSON fail', 'attempts': req_dbg}

    if not token_data.get('access_token'):
        save_state(sid, state)
        return {'error': token_data.get('error_description') or 'No token',
                'debug': token_data, 'attempts': req_dbg}

    state['access_token'] = token_data['access_token']
    state['refresh_token'] = token_data.get('refresh_token', '')
    state['expires_at'] = int(time.time() + int(token_data.get('expires_in', 299)))
    state['user_name'] = token_data.get('user_name', '')
    save_state(sid, state); save_cookies(s, state)

    time.sleep(0.5)

    profile, profile_err, profile_raw = None, None, ''
    xrid = state['req_uuid']
    for pa in range(1, 4):
        try:
            pr = s.post(f'{BASE}/profileService/api/v1/oauth/profile', headers={
                'accept': 'application/json, text/plain, */*',
                'accept-language': 'en_IN', 'appid': 'MYAADHAAR',
                'authorization': f'Bearer {state["access_token"]}',
                'content-type': 'application/json', 'dnt': '1',
                'origin': 'https://myaadhaar.uidai.gov.in',
                'referer': 'https://myaadhaar.uidai.gov.in/',
                'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors',
                'sec-fetch-site': 'same-site', 'user-agent': UA,
                'x-request-id': xrid,
            }, data=json.dumps({'uidNumber': uid}, separators=(',', ':')).encode(),
               timeout=25, allow_redirects=False)
            profile_raw = pr.text[:2000]
            if pr.status_code == 200:
                try:
                    parsed = pr.json()
                    if parsed.get('status') == 'Success' and parsed.get('responseData'):
                        profile = parsed; break
                    profile_err = parsed.get('errorCode') or 'unknown'
                except Exception as pe: profile_err = f'parse: {pe}'
            else: profile_err = f'HTTP {pr.status_code}'
        except Exception as e: profile_err = str(e)
        if pa < 3: time.sleep(1.5)

    state['req_counter'] += 1
    pdf_b64, pdf_err = None, None
    try:
        pdfr = s.post(f'{BASE}/downloadAadhaarService/api/ext/oauth/aadhaar/download', headers={
            'accept': 'application/json, text/plain, */*',
            'accept-language': 'en_IN', 'appid': 'MYAADHAAR',
            'authorization': f'Bearer {state["access_token"]}',
            'content-type': 'application/json', 'dnt': '1',
            'origin': 'https://myaadhaar.uidai.gov.in',
            'referer': 'https://myaadhaar.uidai.gov.in/',
            'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-site',
            'transactionid': str(_uuid.uuid4()).upper(),
            'user-agent': UA, 'x-request-id': xrid,
        }, data=json.dumps({'uid': uid, 'mask': False}, separators=(',', ':')).encode(),
           timeout=45, allow_redirects=False)
        if pdfr.status_code == 200:
            try:
                pd = pdfr.json()
                pdf_b64 = (pd.get('data') or {}).get('aadhaarPdf') or pd.get('aadhaarPdf')
            except Exception: pdf_err = 'parse fail'
        else: pdf_err = f'HTTP {pdfr.status_code}'
    except Exception as e: pdf_err = str(e)

    size_bytes = 0
    if pdf_b64:
        try: size_bytes = len(base64.b64decode(pdf_b64))
        except Exception: pass

    save_state(sid, state)

    return {
        'success': True,
        'user_name': state.get('user_name', ''),
        'expires_in': int(token_data.get('expires_in', 0)),
        'expires_at': state['expires_at'],
        'access_token': state['access_token'],
        'profile': profile,
        'profile_error': profile_err,
        'profile_raw': profile_raw,
        'aadhaarPdf': pdf_b64,
        'aadhaarPdfInfo': {'sizeBytes': size_bytes,
                           'sizeKB': round(size_bytes / 1024, 1)} if pdf_b64 else None,
        'pdf_error': pdf_err,
        'attempts': req_dbg,
    }


# ═══════════════ DIAGNOSTIC ═══════════════
def action_diag(sid):
    result = {}
    tiny_b64 = ('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a'
                'HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAA'
                'AQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIh'
                'MUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpT'
                'VFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5'
                'usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAA'
                'AAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEI'
                'FEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVm'
                'Z2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK'
                '0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q==')

    url, errors = _upload_image(tiny_b64)
    result['upload_ok'] = url is not None
    result['upload_url'] = url
    result['upload_errors'] = errors
    return result


# ═══════════════ CLI ═══════════════
def main():
    if len(sys.argv) < 3:
        print(json.dumps({'error': 'Usage: uidai_flow.py <sid> <action> [args]'}))
        return
    sid, action = sys.argv[1], sys.argv[2]
    try:
        if action == 'auto-otp' and len(sys.argv) >= 4:
            result = action_auto_otp(sid, sys.argv[3])
        elif action == 'login' and len(sys.argv) >= 4:
            result = action_login(sid, sys.argv[3])
        elif action == 'diag':
            result = action_diag(sid)
        else:
            result = {'error': f'Unknown action: {action}'}
    except Exception as e:
        import traceback
        result = {'error': f'{type(e).__name__}: {e}',
                  'trace': traceback.format_exc()[-800:]}
    print(json.dumps(result))


if __name__ == '__main__':
    main()