import asyncio, json, os, re, time, random, pathlib, sys, base64
sys.path.insert(0,'/opt/data/scripts')
import requests, websockets
from cbm_page import BASE, PROFILE, HEADERS, ensure_running
ROOT=f'wss://cbm.huper.technology/api/profiles/{PROFILE}/cdp'
LOG_FILE='/opt/data/agent-state/memory/ig-encourage-agent-log.json'
WARN_RE=re.compile(r'try again later|captcha|suspicious|checkpoint|temporarily blocked|restrict certain activity|automated behavior|confirm it was you|unusual activity|we limit how often|action blocked|couldn.t post|could not post|challenge required|login_required', re.I)
CANDIDATES=[
 {'url':'https://www.instagram.com/p/DZkuyT8H9S_/','account':'biblebee','source':'home','postId':'DZkuyT8H9S_','fit':'Psalm 119:46 / Scripture memory encouragement','comment':'Psalm 119:46 is such a needed reminder: confidence grows as the Word becomes familiar in the heart. May Scripture keep making our witness steady and joyful.','distinctive':'confidence grows as the Word becomes familiar'},
 {'url':'https://www.instagram.com/shereadstruth/reel/DZfW-NCzo9i/','account':'shereadstruth','source':'profile:shereadstruth','postId':'DZfW-NCzo9i','fit':'God gave His Word so we can know Him more deeply','comment':'So true. God’s Word does more than inform us. It draws us near to Him. John 17:3 comes to mind: knowing the Father and the Son is life itself.','distinctive':'God’s Word does more than inform us'},
 {'url':'https://www.instagram.com/shereadstruth/reel/DZPz0EcxifN/','account':'shereadstruth','source':'profile:shereadstruth','postId':'DZPz0EcxifN','fit':'Rest in the Lord, prayer, and presence','comment':"This is a gentle invitation to Sabbath rest. Father, help everyone who sees this slow down, receive Your presence, and pray from peace today. In Jesus' name. Amen.",'distinctive':'gentle invitation to Sabbath rest'},
 {'url':'https://www.instagram.com/wellwateredwomen/p/DTOsmVJE0PV/','account':'wellwateredwomen','source':'profile:wellwateredwomen','postId':'DTOsmVJE0PV','fit':'God is able and worry turns into worship','comment':'“God is able” is such a steady anchor. I love the turn from control to worship. Ephesians 3:20 feels close here: He is able beyond what we ask or imagine.','distinctive':'steady anchor. I love the turn from control to worship'},
 {'url':'https://www.instagram.com/wellwateredwomen/p/DZBN6WbIKil/','account':'wellwateredwomen','source':'profile:wellwateredwomen','postId':'DZBN6WbIKil','fit':'This day matters in the kingdom, Psalm 118:24','comment':'Psalm 118:24 makes ordinary hours feel holy again. This day is not filler. It belongs to the Lord, and there is grace to meet Him in it.','distinctive':'ordinary hours feel holy again'},
 {'url':'https://www.instagram.com/thebibleproject/reel/DZXqC1su3b5/','account':'thebibleproject','source':'profile:thebibleproject','postId':'DZXqC1su3b5','fit':'Ninth commandment and truthful witness protects neighbors','comment':'This is a needed word. The ninth commandment protects neighbors, not just reputations. Lord, make us people whose truth-telling shelters others in love.','distinctive':'protects neighbors, not just reputations'},
 {'url':'https://www.instagram.com/crosswaybooks/p/DZayxbGzNPH/','account':'crosswaybooks','source':'profile:crosswaybooks','postId':'DZayxbGzNPH','fit':'Psalm 62:1 encouragement about waiting on God','comment':'Psalm 62:1 is quiet strength. Salvation belongs to God, so the soul can stop striving and wait. That is a mercy.','distinctive':'Salvation belongs to God, so the soul can stop striving'},
 {'url':'https://www.instagram.com/crosswaybooks/p/DY4zlyPOL7z/','account':'crosswaybooks','source':'profile:crosswaybooks','postId':'DY4zlyPOL7z','fit':'Psalm 34:8 invitation to taste the Lord’s goodness','comment':'Psalm 34:8 is such a kind invitation: not just to know about the Lord, but to taste His goodness personally. He really is good.','distinctive':'taste His goodness personally'}]
class Conn:
 def __init__(self,ws): self.ws=ws; self.i=0; self.pending={}
 async def pump(self):
  async for raw in self.ws:
   msg=json.loads(raw)
   if 'id' in msg and msg['id'] in self.pending:
    fut=self.pending.pop(msg['id'])
    if 'error' in msg: fut.set_exception(RuntimeError(json.dumps(msg['error'])))
    else: fut.set_result(msg.get('result'))
 async def send(self,m,p=None,timeout=30):
  self.i+=1; fut=asyncio.get_event_loop().create_future(); self.pending[self.i]=fut
  await self.ws.send(json.dumps({'id':self.i,'method':m,'params':p or {}}))
  return await asyncio.wait_for(fut,timeout)
 async def eval(self,e,timeout=30):
  res=await self.send('Runtime.evaluate',{'expression':e,'returnByValue':True,'awaitPromise':False},timeout)
  if 'exceptionDetails' in res: raise RuntimeError(str(res['exceptionDetails'])[:500])
  return res.get('result',{}).get('value')
 async def goto(self,u,w=5): await self.send('Page.navigate',{'url':u},20); await asyncio.sleep(w)
 async def screenshot(self,path):
  pathlib.Path(path).parent.mkdir(parents=True,exist_ok=True)
  res=await self.send('Page.captureScreenshot',{'format':'png','captureBeyondViewport':False},20)
  open(path,'wb').write(base64.b64decode(res['data'])); return path
async def make_page():
 ensure_running(); time.sleep(1)
 arr=requests.get(f'{BASE}/api/profiles/{PROFILE}/cdp/json/list',headers=HEADERS,timeout=30).json()
 ig=[x for x in arr if x.get('type')=='page' and 'instagram.com' in x.get('url','')]
 if ig: wsurl=ig[0]['webSocketDebuggerUrl']
 else:
  rws=await websockets.connect(ROOT,additional_headers=HEADERS,ping_interval=None,open_timeout=30,max_size=None)
  root=Conn(rws); pump=asyncio.create_task(root.pump())
  created=await root.send('Target.createTarget',{'url':'https://www.instagram.com/'},60); await asyncio.sleep(4)
  arr=requests.get(f'{BASE}/api/profiles/{PROFILE}/cdp/json/list',headers=HEADERS,timeout=30).json()
  pg=[x for x in arr if x.get('id')==created.get('targetId')][0]
  wsurl=pg['webSocketDebuggerUrl']; pump.cancel(); await rws.close()
 ws=await websockets.connect(wsurl,additional_headers=HEADERS,ping_interval=None,open_timeout=30,max_size=None)
 p=Conn(ws); pump=asyncio.create_task(p.pump()); await p.send('Runtime.enable'); await p.send('Page.enable'); return p,pump

def load_log():
 try: return json.load(open(LOG_FILE))
 except Exception: return {'comments':[],'errors':[]}
def save_log(data):
 pathlib.Path(LOG_FILE).parent.mkdir(parents=True,exist_ok=True)
 tmp=LOG_FILE+'.tmp'; open(tmp,'w').write(json.dumps(data,indent=2,ensure_ascii=False)); os.replace(tmp,LOG_FILE)
def already_logged(pid):
 return any(c.get('postId')==pid and c.get('verified') for c in load_log().get('comments',[]))
def append_log(cand, screenshot):
 data=load_log(); now=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
 data.setdefault('comments',[]).append({'timestamp':now,'startedAt':STARTED,'source':'cbm-agent-cron','postId':cand['postId'],'url':cand['url'],'comment':cand['comment'],'account':cand['account'],'sourceHashtag':cand['source'],'reason':cand['fit'],'verified':True,'screenshot':screenshot or '', 'verificationMethod':'CBM CDP DOM text match'})
 save_log(data)
async def check_state(p):
 st=await p.eval("(() => ({url:location.href, text:document.body.innerText.slice(0,5000), hasLogin:!!document.querySelector('input[name=\"username\"],input[name=\"password\"]')}))()")
 if st.get('hasLogin'): return 'login_form'
 if WARN_RE.search(st.get('text') or ''): return 'warning_or_friction'
 return None
async def post_one(p,cand):
 await p.goto(cand['url'],6+random.random()*2)
 stop=await check_state(p)
 if stop: return {'status':'stop','step':'initial_state','reason':stop,'candidate':cand}
 body=await p.eval('document.body.innerText') or ''
 if re.search(r'vincent\.emmanuel\.lee|Vincent Emmanuel Lee',body): return {'status':'skip','reason':'Vincent already visible before posting','candidate':cand}
 focus=await p.eval("""(() => { const ta=document.querySelector('textarea[aria-label*="comment" i]'); if(!ta) return {found:false}; ta.scrollIntoView({behavior:'instant',block:'center'}); ta.click(); ta.focus(); return {found:true}; })()""")
 if not focus or not focus.get('found'):
  await p.eval('window.scrollBy(0,500)'); await asyncio.sleep(1)
  focus=await p.eval("""(() => { const ta=document.querySelector('textarea[aria-label*="comment" i]'); if(!ta) return {found:false}; ta.scrollIntoView({behavior:'instant',block:'center'}); ta.click(); ta.focus(); return {found:true}; })()""")
 if not focus or not focus.get('found'): return {'status':'skip','reason':'comment textarea not found','candidate':cand}
 await asyncio.sleep(1)
 await p.send('Input.insertText',{'text':cand['comment']},20)
 await asyncio.sleep(1.5)
 stop=await check_state(p)
 if stop: return {'status':'stop','step':'after_text_insert','reason':stop,'candidate':cand}
 clicked=await p.eval("""(() => { const btns=[...document.querySelectorAll('[role="button"],button')]; for(const btn of btns){ if(btn.textContent.trim()==='Post'){ btn.scrollIntoView({behavior:'instant',block:'center'}); btn.click(); return {clicked:true,text:btn.textContent.trim()}; }} return {clicked:false, texts:btns.slice(-10).map(b=>b.textContent.trim())}; })()""")
 if not clicked or not clicked.get('clicked'): return {'status':'stop','step':'click_post','reason':'Post button not found','clicked':clicked,'candidate':cand}
 await asyncio.sleep(5)
 stop=await check_state(p)
 if stop: return {'status':'stop','step':'after_post_click','reason':stop,'candidate':cand}
 # verify, refresh DOM several times
 verified=False; text=''
 for _ in range(5):
  text=await p.eval('document.body.innerText') or ''
  if re.search(r'vincent\.emmanuel\.lee|Vincent Emmanuel Lee',text) and cand['distinctive'] in text:
   verified=True; break
  await asyncio.sleep(3)
 if not verified:
  # reload once and check
  await p.goto(cand['url'],5)
  text=await p.eval('document.body.innerText') or ''
  verified= bool(re.search(r'vincent\.emmanuel\.lee|Vincent Emmanuel Lee',text) and cand['distinctive'] in text)
 if not verified: return {'status':'stop','step':'verification','reason':'DOM verification failed','candidate':cand,'bodyPreview':text[:1000]}
 shot=''
 try: shot=await asyncio.wait_for(p.screenshot(f"/tmp/ig-cbm-{cand['postId']}-result.png"),timeout=25)
 except Exception as e: shot=''
 append_log(cand,shot)
 return {'status':'posted','candidate':cand,'screenshot':shot,'verification':'vincent username + distinctive substring DOM match'}
STARTED=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
async def main():
 p,pump=await make_page()
 if 'instagram.com' not in await p.eval('location.href'):
  await p.goto('https://www.instagram.com/',6)
 posted=[]; skipped=[]
 for i,c in enumerate(CANDIDATES):
  if already_logged(c['postId']):
   skipped.append({'candidate':c,'reason':'already in local verified log'}); continue
  res=await post_one(p,c)
  if res['status']=='posted':
   posted.append(res); print('POSTED',len(posted),c['postId'],flush=True)
   if len(posted)>=8: break
   pause=random.randint(22,43); print('PAUSE',pause,flush=True); await asyncio.sleep(pause)
  elif res['status']=='skip':
   skipped.append(res); print('SKIP',c['postId'],res.get('reason'),flush=True); await asyncio.sleep(random.randint(5,10))
  else:
   print(json.dumps({'ok':False,'stopped':res,'posted':posted,'skipped':skipped},indent=2,ensure_ascii=False)); pump.cancel(); return
 print(json.dumps({'ok':True,'posted':posted,'skipped':skipped,'target':8,'count':len(posted)},indent=2,ensure_ascii=False))
 pump.cancel()
asyncio.run(main())
