#!/usr/bin/env python3
"""
LAPU SIM agent: runs next to a hosted SIM (PC + USB phone via ADB, or port the loop into the Android app).
Long-polls the server, so a job arrives within milliseconds of being placed.

  SERVER=https://your.host TOKEN=<sim token> MODE=simulate python3 sim_agent.py
  SIM_PIN=1234 is read from the environment on the device only; the server never sees it.

MODE=simulate  fake results, for end-to-end testing (no real recharge).
MODE=adb       dials via ADB; implement read_reply() for your device (see android/UssdRunner.kt).

Reporting rules (protect against double recharges):
  - success / failed with a clear reply  -> report it.
  - failed BEFORE anything was sent (e.g. USSD busy, no signal) -> report failed + retryable=true (server fails over to another SIM).
  - outcome unknown (timeout after dialing) -> report NOTHING. The server sends it to manual review after 2 minutes.
"""
import os, sys, time, json, random, urllib.request, urllib.error, urllib.parse, subprocess

SERVER = os.environ.get("SERVER", "http://localhost:3000").rstrip("/")
TOKEN = os.environ.get("TOKEN") or sys.exit("set TOKEN")
MODE = os.environ.get("MODE", "simulate")
PIN = os.environ.get("SIM_PIN", "")

class NotSent(Exception): pass   # nothing reached the operator -> safe to retry elsewhere

def call(path, method="GET", body=None, timeout=35):
    req = urllib.request.Request(SERVER + "/api/device" + path, method=method,
        data=json.dumps(body).encode() if body is not None else None,
        headers={"x-device-token": TOKEN, "content-type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            raw = r.read()
            return json.loads(raw) if raw else None
    except urllib.error.HTTPError as e:
        if e.code == 204: return None
        raise

def adb(*a): subprocess.run(["adb", "shell", *a], check=True, timeout=20)

def dial(code):
    enc = urllib.parse.quote(code, safe="*")  # '#' must be sent as %23
    try: adb("am", "start", "-a", "android.intent.action.CALL", "-d", "tel:" + enc)
    except Exception as e: raise NotSent(str(e))

def read_reply():
    raise NotImplementedError("capture the USSD dialog text (Android app / AccessibilityService / uiautomator dump)")

def parse(text):
    t = text.lower()
    ok = any(k in t for k in ("success", "recharged", "completed", "done"))
    bad = any(k in t for k in ("fail", "insufficient", "invalid", "error", "unable", "not allowed"))
    return ("success" if ok and not bad else "failed"), text[:180]

def balance_from(text):
    import re
    m = re.search(r"(?:bal(?:ance)?)[^\d]{0,12}(\d+(?:\.\d+)?)", text, re.I)
    return float(m.group(1)) if m else None

def execute(job):
    if MODE == "simulate":
        time.sleep(random.uniform(0.5, 1.5))
        return {"status": "success", "message": "Recharge successful (simulated)", "txn_ref": "SIM%09d" % random.randint(0, 10**9)}
    reply = ""
    for i, step in enumerate(job["steps"]):
        step = step.replace("{pin}", PIN)
        if i == 0: dial(step)
        else: adb("input", "text", step.replace(" ", "%s")); adb("input", "keyevent", "66")
        time.sleep(2)
        reply = read_reply()
    status, msg = parse(reply)
    return {"status": status, "message": msg, "sim_balance": balance_from(reply)}

def main():
    print("agent up | mode:", MODE, "| server:", SERVER, flush=True)
    last_hb = 0
    while True:
        try:
            if time.time() - last_hb > 60:
                call("/heartbeat", "POST", {}); last_hb = time.time()
            job = call("/jobs/next?wait=20")
            if not job: continue
            print("job", job["order_id"], job["operator"], job["number"], job["amount"], flush=True)
            try:
                res = execute(job)
            except NotSent as e:
                res = {"status": "failed", "retryable": True, "message": "not sent: " + str(e)[:100]}
            except Exception as e:
                print("  outcome unknown, leaving for review:", e, flush=True); continue
            call(f"/jobs/{job['order_id']}/result", "POST", res)
            print("  ->", res["status"], flush=True)
        except Exception as e:
            print("loop error:", e, flush=True); time.sleep(3)

if __name__ == "__main__":
    main()
