#!/usr/bin/env python3
from __future__ import annotations
import base64, json, os, platform, secrets, socket, struct, subprocess, sys, threading, time, urllib.parse, urllib.request, webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
BASE=os.environ.get('NORAX_BASE','https://noraxdev.org'); RELAY=BASE+'/remote'
ROOT=Path.home()/'.norax-node'; CFG=ROOT/'node.json'; LOG=ROOT/'norax-lite.log'
STATE={'connected':False,'node_id':'','token':'','last':'starting','chat':[],'stop':False,'started':False,'attempts':0}
DANGER=['mkfs','dd if=/dev',':(){','/boot','format ']
def log(x): ROOT.mkdir(parents=True,exist_ok=True); LOG.open('a').write(time.strftime('%F %T ')+str(x)+'\n')
def req(method,path,payload=None,timeout=30):
 data=None if payload is None else json.dumps(payload).encode(); r=urllib.request.Request(BASE+path,data=data,method=method,headers={'content-type':'application/json','user-agent':'Mozilla/5.0 NoraxConnectLite/1.1'})
 with urllib.request.urlopen(r,timeout=timeout) as f: return json.loads(f.read().decode())
def save(n,t): ROOT.mkdir(parents=True,exist_ok=True); CFG.write_text(json.dumps({'node_id':n,'token':t},indent=2)); os.chmod(CFG,0o600); STATE.update(node_id=n,token=t)
def load():
 if CFG.exists():
  d=json.loads(CFG.read_text()); STATE.update(node_id=d.get('node_id',''),token=d.get('token',''))
 return STATE['node_id'],STATE['token']
def enroll():
 STATE['last']='enrolling with noraxdev.org'
 r=req('POST','/remote/public/enroll',{'name':platform.node() or 'linux-pc','roots':['~']},20); save(r['node_id'],r['token']); STATE['last']='enrolled '+r['node_id']; return r['node_id'],r['token']
def wsurl(n,t):
 p=urllib.parse.urlparse(RELAY); return urllib.parse.urlunparse(('wss',p.netloc,'/remote/node','',urllib.parse.urlencode({'node_id':n,'token':t}),''))
class WS:
 def __init__(self,u): self.u=u; self.s=None
 def connect(self):
  import ssl
  u=urllib.parse.urlparse(self.u); STATE['last']='opening websocket to '+u.hostname; raw=socket.create_connection((u.hostname,u.port or 443),timeout=20); raw=ssl.create_default_context().wrap_socket(raw,server_hostname=u.hostname); raw.settimeout(45)
  key=base64.b64encode(secrets.token_bytes(16)).decode(); path=u.path+'?'+u.query
  raw.sendall((f'GET {path} HTTP/1.1\r\nHost: {u.netloc}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\nUser-Agent: Mozilla/5.0 NoraxConnectLite/1.1\r\n\r\n').encode())
  resp=b''
  while b'\r\n\r\n' not in resp: STATE['last']='waiting for websocket handshake'; resp+=raw.recv(4096)
  if b' 101 ' not in resp.split(b'\r\n',1)[0]: raise RuntimeError(resp[:220].decode(errors='replace'))
  self.s=raw
 def send(self,text):
  data=text.encode(); n=len(data); h=bytearray([0x81])
  if n<126: h.append(0x80|n)
  elif n<65536: h+=bytes([0x80|126])+struct.pack('!H',n)
  else: h+=bytes([0x80|127])+struct.pack('!Q',n)
  m=secrets.token_bytes(4); self.s.sendall(bytes(h)+m+bytes(b^m[i%4] for i,b in enumerate(data)))
 def recvn(self,n):
  b=b''
  while len(b)<n:
   c=self.s.recv(n-len(b))
   if not c: raise EOFError('socket closed')
   b+=c
  return b
 def recv(self):
  h=self.recvn(2); op=h[0]&15; n=h[1]&127
  if n==126: n=struct.unpack('!H',self.recvn(2))[0]
  elif n==127: n=struct.unpack('!Q',self.recvn(8))[0]
  data=self.recvn(n)
  if op==8: raise EOFError('closed')
  if op==9: self.s.sendall(bytes([0x8A,len(data)])+data); return self.recv()
  return data.decode(errors='replace')
def job(j):
 try:
  a=j.get('action')
  if a=='hello': return {'ok':True,'platform':platform.platform(),'hostname':platform.node(),'cwd':os.getcwd(),'pid':os.getpid()}
  if a=='exec':
   cmd=str(j.get('command',''))
   if any(x in cmd.lower() for x in DANGER): return {'ok':False,'error':'blocked command'}
   cp=subprocess.run(cmd,shell=True,cwd=os.path.expanduser(j.get('cwd') or '~'),capture_output=True,text=True,timeout=min(float(j.get('timeout',30)),120))
   return {'ok':cp.returncode==0,'exit_code':cp.returncode,'stdout':cp.stdout[:12000],'stderr':cp.stderr[:4000]}
  if a=='read': return {'ok':True,'content':Path(os.path.expanduser(str(j['path']))).read_text(errors='replace')[:int(j.get('limit',20000))]}
  if a=='list': return {'ok':True,'items':[x.name for x in Path(os.path.expanduser(str(j.get('path','~')))).iterdir()][:500]}
  if a=='write':
   p=Path(os.path.expanduser(str(j['path']))); p.parent.mkdir(parents=True,exist_ok=True); p.write_text(str(j.get('content',''))); return {'ok':True,'path':str(p)}
  return {'ok':False,'error':'unknown action'}
 except Exception as e: return {'ok':False,'error':str(e),'type':type(e).__name__}
def loop():
 while not STATE['stop']:
  try:
   STATE['attempts']=int(STATE.get('attempts') or 0)+1
   n,t=load()
   if not n or not t:
    STATE['last']='no saved node; enrolling via public HTTPS'
    n,t=enroll()
   STATE.update(connected=True,last='connected via HTTPS polling as '+n)
   msg=req('POST','/remote/node/poll',{'node_id':n,'token':t,'timeout':25},35)
   if msg.get('type')=='job':
    jid=msg.get('job_id',''); j=msg.get('job') if isinstance(msg.get('job'),dict) else {}
    res=job(j)
    req('POST','/remote/node/result',{'node_id':n,'token':t,'job_id':jid,'result':res},30)
   elif msg.get('type')=='noop':
    STATE['last']='connected via HTTPS polling as '+n+' (idle)'
   time.sleep(0.2)
  except Exception as e:
   STATE.update(connected=False,last='HTTPS polling reconnecting: '+str(e)); log('poll loop '+repr(e)); time.sleep(3)

HTML='''<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Norax Connect</title>
<style>
body{font-family:system-ui;background:#05070b;color:#f3f7ff;margin:0}.wrap{max-width:900px;margin:24px auto;padding:18px}.card{background:#0c111a;border:1px solid #1f2937;border-radius:16px;padding:18px;margin:12px 0}input{width:100%;box-sizing:border-box;background:#070b12;color:#fff;border:1px solid #263244;border-radius:10px;padding:12px}button{background:#00e87b;color:#001b0e;border:0;border-radius:10px;padding:12px 16px;font-weight:800}.muted{color:#9aa8bd}.chat{height:320px;overflow:auto;background:#070b12;border:1px solid #263244;border-radius:10px;padding:12px;white-space:pre-wrap}.row{display:flex;gap:8px}.row input{flex:1}.ok{color:#00e87b}.bad{color:#ff477e}
</style>
</head>
<body>
<div class="wrap">
  <h1>Norax Connect</h1>
  <div class="card">
    <b>Status:</b> <span id="st">auto-connecting</span>
    <div class="muted" id="detail"></div>
    <p class="muted">This PC connects with outbound HTTPS polling. No browser button or WebSocket dependency.</p>
    <input id="node" placeholder="node id optional">
    <br><br>
    <input id="tok" placeholder="token optional">
    <br><br>
    <button id="connectBtn" type="button">Connect</button>
  </div>
  <div class="card">
    <h2>Chat with Norax</h2>
    <div class="chat" id="chat"></div>
    <br>
    <div class="row">
      <input id="msg" placeholder="Message Norax...">
      <button id="sendBtn" type="button">Send</button>
    </div>
  </div>
</div>
<script>
(function(){
  'use strict';
  console.log('Norax Connect UI loaded v1.3');
  function el(id){ return document.getElementById(id); }
  function setStatus(text, cls){ var st = el('st'); st.textContent = text; st.className = cls || ''; }
  function setDetail(text){ el('detail').textContent = text || ''; }
  async function api(path, opts){
    var res = await fetch(path, opts || {});
    var text = await res.text();
    if(!res.ok){ throw new Error(text || String(res.status)); }
    try { return JSON.parse(text); }
    catch(e){ throw new Error('bad json: ' + text.slice(0, 200)); }
  }
  async function poll(){
    try{
      var s = await api('/status');
      if(s.connected){ setStatus('connected', 'ok'); } else { setStatus('not connected', 'bad'); }
      setDetail('node: ' + (s.node_id || 'none') + ' | ' + (s.last || ''));
      var chat = el('chat');
      chat.textContent = (s.chat || []).map(function(x){ return x.role + ': ' + x.text; }).join('\n\n');
      chat.scrollTop = chat.scrollHeight;
    }catch(e){ setStatus('local GUI error', 'bad'); setDetail(String(e)); }
  }
  async function connectNow(){
    try{
      setStatus('connecting...', ''); setDetail('button clicked');
      var payload = { node_id: el('node').value, token: el('tok').value };
      var r = await api('/connect', { method:'POST', headers:{'content-type':'application/json'}, body:JSON.stringify(payload) });
      if(r && r.ok === false){ throw new Error(r.error || 'connect failed'); }
      await poll();
    }catch(e){ setStatus('connect failed', 'bad'); setDetail(String(e)); }
  }
  async function sendNow(){
    var msg = el('msg').value.trim(); if(!msg){ return; }
    el('msg').value = '';
    await api('/chat', { method:'POST', headers:{'content-type':'application/json'}, body:JSON.stringify({message:msg}) });
    await poll();
  }
  window.addEventListener('error', function(e){ try { setStatus('ui javascript error', 'bad'); setDetail(e.message + ' @ ' + e.filename + ':' + e.lineno); } catch(_) {} });
  document.addEventListener('DOMContentLoaded', function(){
    el('connectBtn').addEventListener('click', connectNow);
    el('sendBtn').addEventListener('click', sendNow);
    el('msg').addEventListener('keydown', function(e){ if(e.key === 'Enter'){ sendNow(); } });
    setInterval(poll, 1500); poll();
  });
})();
</script>
</body>
</html>'''

class H(BaseHTTPRequestHandler):
 def j(self,o):
  b=json.dumps(o).encode(); self.send_response(200); self.send_header('content-type','application/json'); self.send_header('content-length',str(len(b))); self.end_headers(); self.wfile.write(b)
 def body(self): return json.loads(self.rfile.read(int(self.headers.get('content-length','0') or 0)) or b'{}')
 def do_GET(self):
  if self.path.startswith('/status'): return self.j({k:STATE.get(k) for k in ['connected','node_id','last','chat','attempts']})
  b=HTML.encode(); self.send_response(200); self.send_header('content-type','text/html'); self.send_header('content-length',str(len(b))); self.end_headers(); self.wfile.write(b)
 def do_POST(self):
  if self.path.startswith('/connect'):
   d=self.body()
   try:
    if d.get('node_id') and d.get('token'): save(d['node_id'],d['token'])
    else: enroll()
    if not STATE.get('started'):
     STATE['started']=True; threading.Thread(target=loop,daemon=True).start()
    STATE['last']='connecting'
    return self.j({'ok':True,'node_id':STATE.get('node_id')})
   except Exception as e:
    STATE['last']='connect failed: '+str(e); return self.j({'ok':False,'error':str(e)})
  if self.path.startswith('/chat'):
   m=str(self.body().get('message','')).strip(); STATE['chat'].append({'role':'you','text':m})
   try:
    r=req('POST','/api/public-chat',{'message':m,'node_id':STATE.get('node_id')},120); STATE['chat'].append({'role':'Norax','text':r.get('reply') or r.get('error') or str(r)})
   except Exception as e: STATE['chat'].append({'role':'Norax','text':'chat error: '+str(e)})
   return self.j({'ok':True})
def main():
 ROOT.mkdir(parents=True,exist_ok=True); load(); STATE['last']='connecting via HTTPS polling'; STATE['started']=True; threading.Thread(target=loop,daemon=True).start(); srv=ThreadingHTTPServer(('127.0.0.1',0),H); url=f'http://127.0.0.1:{srv.server_port}/'; print('Norax Connect GUI:',url,flush=True); print('Norax Connect: auto-connecting this PC now.',flush=True);
 try:
  webbrowser.open(url)
 except Exception as e:
  print('Browser open skipped: '+str(e),flush=True)
 srv.serve_forever()
if __name__=='__main__': main()
