from __future__ import annotations
import hashlib,json,os,secrets,sqlite3,time
from datetime import datetime,timezone
from pathlib import Path
from fastapi import FastAPI,Request,HTTPException,Depends
from fastapi.responses import HTMLResponse,JSONResponse,FileResponse
from pydantic import BaseModel

DATA=Path(os.getenv('STREAMFORGE_LICENSE_DATA','./license_server/data')).resolve();DATA.mkdir(parents=True,exist_ok=True);DB=DATA/'licences.db';UPDATES=DATA/'updates';UPDATES.mkdir(exist_ok=True)
ADMIN_USER=os.getenv('STREAMFORGE_LICENSE_ADMIN','admin');ADMIN_PASS=os.getenv('STREAMFORGE_LICENSE_PASSWORD','change-me-now')
app=FastAPI(title='StreamForge Licence Server',docs_url=None,redoc_url=None,openapi_url=None)

def conn():
 c=sqlite3.connect(DB);c.row_factory=sqlite3.Row;return c

def init():
 with conn() as c:c.executescript('''CREATE TABLE IF NOT EXISTS licences(id INTEGER PRIMARY KEY AUTOINCREMENT,identifier TEXT UNIQUE NOT NULL,identifier_type TEXT NOT NULL CHECK(identifier_type IN ('ip','domain')),plan TEXT NOT NULL CHECK(plan IN ('basic','premium','legend')),status TEXT DEFAULT 'active',customer TEXT DEFAULT '',notes TEXT DEFAULT '',created_at TEXT DEFAULT CURRENT_TIMESTAMP,updated_at TEXT DEFAULT CURRENT_TIMESTAMP,last_seen_at TEXT,last_install_id TEXT DEFAULT '');CREATE TABLE IF NOT EXISTS sessions(token_hash TEXT PRIMARY KEY,expires REAL NOT NULL);''')
init()

def auth(req:Request):
 raw=req.cookies.get('sfls');
 if not raw:raise HTTPException(401)
 h=hashlib.sha256(raw.encode()).hexdigest()
 with conn() as c:r=c.execute('SELECT expires FROM sessions WHERE token_hash=?',(h,)).fetchone()
 if not r or r['expires']<time.time():raise HTTPException(401)
 return True

def normalize_domain(d):return d.strip().lower().split(':')[0].rstrip('.')
def client_ip(req):
 # Intentionally use direct peer unless operator explicitly places the server behind a trusted proxy.
 return req.client.host if req.client else ''
class Check(BaseModel):domain:str='';install_id:str=''
@app.get('/health')
def health():return {'ok':True,'service':'streamforge-licence-server','time':datetime.now(timezone.utc).isoformat()}
@app.post('/v1/check')
def check(d:Check,req:Request):
 ip=client_ip(req);domain=normalize_domain(d.domain)
 with conn() as c:
  r=None
  if domain:r=c.execute("SELECT * FROM licences WHERE identifier_type='domain' AND lower(identifier)=? AND status='active'",(domain,)).fetchone()
  if not r and ip:r=c.execute("SELECT * FROM licences WHERE identifier_type='ip' AND identifier=? AND status='active'",(ip,)).fetchone()
  if not r:return {'valid':False,'plan':'none','updates':False,'load_balancing':False,'observed_ip':ip,'domain':domain}
  c.execute('UPDATE licences SET last_seen_at=CURRENT_TIMESTAMP,last_install_id=?,updated_at=CURRENT_TIMESTAMP WHERE id=?',(d.install_id,r['id']));c.commit();plan=r['plan']
  return {'valid':True,'plan':plan,'updates':plan in ('premium','legend'),'load_balancing':plan=='legend','observed_ip':ip,'matched_identifier':r['identifier'],'last_valid_unix':time.time()}
@app.get('/v1/updates/stable.json')
def update_manifest():
 p=UPDATES/'stable.json'
 if not p.exists():return {'version':'','url':'','sha256':''}
 return json.loads(p.read_text())
@app.get('/v1/updates/files/{name}')
def update_file(name:str):
 p=(UPDATES/Path(name).name)
 if not p.exists():raise HTTPException(404)
 return FileResponse(p)
@app.get('/',response_class=HTMLResponse)
def home(req:Request):
 try:auth(req);logged=True
 except:logged=False
 if not logged:return HTMLResponse('''<!doctype html><html><head><meta name=viewport content="width=device-width"><title>StreamForge Licence Server</title><style>body{font-family:system-ui;background:#080914;color:#eee;display:grid;place-items:center;height:100vh;margin:0}.c{width:380px;background:#101322;border:1px solid #34364b;padding:30px;border-radius:12px}input,button{width:100%;box-sizing:border-box;padding:12px;margin:7px 0;background:#171a2b;color:white;border:1px solid #383b55;border-radius:7px}button{background:#6519c9}</style></head><body><div class=c><h2>StreamForge Licence Server</h2><form id=f><input id=u placeholder=Username><input id=p type=password placeholder=Password><button>Sign in</button></form><div id=e></div></div><script>f.onsubmit=async x=>{x.preventDefault();let r=await fetch('/login',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({username:u.value,password:p.value})});if(r.ok)location.reload();else e.textContent='Invalid login'}</script></body></html>''')
 with conn() as c:rows=[dict(x) for x in c.execute('SELECT * FROM licences ORDER BY id DESC')]
 data=json.dumps(rows).replace('</','<\\/')
 return HTMLResponse('''<!doctype html><html><head><meta name=viewport content="width=device-width"><title>StreamForge Licence Server</title><style>body{font-family:system-ui;background:#070812;color:#eee;margin:0;padding:28px}h1{margin-top:0}.top{display:flex;justify-content:space-between}.card{background:#0f1220;border:1px solid #2d3042;border-radius:10px;padding:20px;margin:18px 0}input,select,button{padding:10px;background:#171a2a;color:white;border:1px solid #3b3e52;border-radius:6px}button{cursor:pointer;background:#6017c5}table{width:100%;border-collapse:collapse}td,th{padding:11px;border-bottom:1px solid #242738;text-align:left}.good{color:#50e37b}</style></head><body><div class=top><div><h1>StreamForge Licence Server</h1><div class=good>● Online</div></div><button onclick="fetch('/logout',{method:'POST'}).then(()=>location.reload())">Logout</button></div><div class=card><h3>Register IP / Domain</h3><input id=i placeholder="203.0.113.10 or panel.example.com"><select id=t><option value=ip>Public IP</option><option value=domain>Domain</option></select><select id=p><option>basic</option><option>premium</option><option>legend</option></select><input id=c placeholder="Customer"><input id=n placeholder="Notes"><button onclick=add()>Register</button></div><div class=card><table><thead><tr><th>Identifier</th><th>Type</th><th>Plan</th><th>Status</th><th>Customer</th><th>Last seen</th><th></th></tr></thead><tbody id=rows></tbody></table></div><script>let D='''+data+''';function draw(){rows.innerHTML=D.map(x=>`<tr><td>${x.identifier}</td><td>${x.identifier_type}</td><td><select onchange="edit(${x.id},'plan',this.value)"><option ${x.plan=='basic'?'selected':''}>basic</option><option ${x.plan=='premium'?'selected':''}>premium</option><option ${x.plan=='legend'?'selected':''}>legend</option></select></td><td><select onchange="edit(${x.id},'status',this.value)"><option ${x.status=='active'?'selected':''}>active</option><option ${x.status=='suspended'?'selected':''}>suspended</option></select></td><td>${x.customer||''}</td><td>${x.last_seen_at||'Never'}</td><td><button onclick="del(${x.id})">Delete</button></td></tr>`).join('')}draw();async function add(){let r=await fetch('/admin/licences',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({identifier:i.value,identifier_type:t.value,plan:p.value,customer:c.value,notes:n.value})});if(r.ok)location.reload();else alert((await r.json()).detail)}async function edit(id,k,v){await fetch('/admin/licences/'+id,{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({[k]:v})})}async function del(id){if(confirm('Delete licence?')){await fetch('/admin/licences/'+id,{method:'DELETE'});location.reload()}}</script></body></html>''')
@app.post('/login')
async def login(req:Request):
 d=await req.json();
 if not secrets.compare_digest(str(d.get('username','')),ADMIN_USER) or not secrets.compare_digest(str(d.get('password','')),ADMIN_PASS):raise HTTPException(401)
 raw=secrets.token_urlsafe(32);h=hashlib.sha256(raw.encode()).hexdigest()
 with conn() as c:c.execute('INSERT INTO sessions(token_hash,expires) VALUES(?,?)',(h,time.time()+86400));c.commit()
 r=JSONResponse({'ok':True});r.set_cookie('sfls',raw,httponly=True,samesite='strict',max_age=86400);return r
@app.post('/logout')
def logout():r=JSONResponse({'ok':True});r.delete_cookie('sfls');return r
class LicIn(BaseModel):identifier:str;identifier_type:str;plan:str;customer:str='';notes:str=''
@app.post('/admin/licences')
def add_lic(x:LicIn,req:Request,_=Depends(auth)):
 ident=normalize_domain(x.identifier) if x.identifier_type=='domain' else x.identifier.strip()
 if x.identifier_type not in ('ip','domain') or x.plan not in ('basic','premium','legend'):raise HTTPException(400,'Invalid licence fields')
 try:
  with conn() as c:cur=c.execute('INSERT INTO licences(identifier,identifier_type,plan,customer,notes) VALUES(?,?,?,?,?)',(ident,x.identifier_type,x.plan,x.customer,x.notes));c.commit();return {'id':cur.lastrowid}
 except Exception as e:raise HTTPException(400,str(e))
@app.patch('/admin/licences/{lid}')
async def edit(lid:int,req:Request,_=Depends(auth)):
 d=await req.json();allowed={'plan','status','customer','notes'};sets=[];args=[]
 for k,v in d.items():
  if k in allowed:sets.append(k+'=?');args.append(v)
 if sets:
  args.append(lid)
  with conn() as c:c.execute('UPDATE licences SET '+','.join(sets)+',updated_at=CURRENT_TIMESTAMP WHERE id=?',args);c.commit()
 return {'ok':True}
@app.delete('/admin/licences/{lid}')
def delete(lid:int,req:Request,_=Depends(auth)):
 with conn() as c:c.execute('DELETE FROM licences WHERE id=?',(lid,));c.commit()
 return {'ok':True}
