import {isIP} from 'node:net';

export type LoginContext={device_type:'desktop'|'mobile'|'tablet'|'bot'|'unknown';operating_system:string;browser:string;user_agent:string;ip_address:string|null;ip_source:'cloudflare'|'local_socket'|'unavailable';location:{country_code:string|null;region:string|null;city:string|null;timezone:string|null;source:'cloudflare'|'local'|'unavailable'}};
export function boundedUserAgent(value:string|null|undefined){return (value||'').replace(/[\u0000-\u001f\u007f]/g,'').trim().slice(0,512)}
const clean=(value:unknown,limit:number)=>typeof value==='string'?value.replace(/[\u0000-\u001f\u007f]/g,'').trim().slice(0,limit)||null:null;
export function clientSoftware(value:string|null|undefined):Pick<LoginContext,'device_type'|'operating_system'|'browser'|'user_agent'>{
 const ua=boundedUserAgent(value);let device:LoginContext['device_type']='unknown',os='نامشخص',browser='نامشخص';
 if(/bot\b|crawler|spider|headless/i.test(ua))device='bot';
 else if(/iPad|Tablet|Android(?!.*Mobile)/i.test(ua))device='tablet';
 else if(/iPhone|iPod|Android.*Mobile|Windows Phone|Mobile/i.test(ua))device='mobile';
 else if(/Windows NT|Macintosh|X11|Linux|CrOS/i.test(ua))device='desktop';
 const version=(pattern:RegExp)=>ua.match(pattern)?.[1]?.replace(/_/g,'.');
 if(/iPhone|iPad|iPod/.test(ua))os=(/iPad/.test(ua)?'iPadOS':'iOS')+(version(/(?:CPU (?:iPhone )?OS|iPhone OS) ([\d_]+)/)?' '+version(/(?:CPU (?:iPhone )?OS|iPhone OS) ([\d_]+)/):'');
 else if(/Windows Phone/.test(ua))os='Windows Phone';
 else if(/Android/.test(ua))os='Android'+(version(/Android ([\d.]+)/)?' '+version(/Android ([\d.]+)/):'');
 else if(/Windows NT/.test(ua))os='Windows'; // Modern UA strings cannot reliably distinguish Windows 10 and 11.
 else if(/CrOS/.test(ua))os='ChromeOS';
 else if(/Macintosh|Mac OS X/.test(ua))os='macOS'; // Modern browsers often freeze the advertised macOS version.
 else if(/Linux/.test(ua))os='Linux';
 for(const [pattern,name] of [[/(?:Edg|EdgiOS|EdgA)\/(\d+)/,'Microsoft Edge'],[/(?:OPR|OPiOS)\/(\d+)/,'Opera'],[/SamsungBrowser\/(\d+)/,'Samsung Internet'],[/(?:Firefox|FxiOS)\/(\d+)/,'Firefox'],[/(?:Chrome|CriOS)\/(\d+)/,'Chrome'],[/Version\/(\d+).*Safari/,'Safari']] as const){const match=ua.match(pattern);if(match){browser=name+' '+match[1];break}}
 return {device_type:device,operating_system:os.slice(0,80),browser:browser.slice(0,80),user_agent:ua};
}
export function validClientIp(value:string|null|undefined){const ip=(value||'').trim();if(ip.length>45||!isIP(ip)||ip.includes('%'))return null;return ip.startsWith('::ffff:')&&isIP(ip.slice(7))===4?ip.slice(7):ip}
export function localNetworkIp(ip:string){
 if(ip==='::1'||ip==='::'||/^(?:fc|fd|fe[89ab])/i.test(ip))return true;
 if(isIP(ip)!==4)return false;const [a,b]=ip.split('.').map(Number);
 return a===10||a===127||a===0||a===169&&b===254||a===172&&b>=16&&b<=31||a===192&&b===168||a===100&&b>=64&&b<=127;
}
export const localLoginProofPayload=(ip:string,ua:string,time:string)=>`planck-login-context-v1\n${ip}\n${boundedUserAgent(ua)}\n${time}`;
async function validLocalProof(request:Request,key:string,now:number){
 const ip=validClientIp(request.headers.get('x-planck-login-ip')),time=request.headers.get('x-planck-login-time')||'',proof=request.headers.get('x-planck-login-proof')||'';
 if(!ip||!/^\d{13}$/.test(time)||Math.abs(now-Number(time))>60_000||!/^[a-f0-9]{64}$/.test(proof)||key.length<32)return null;
 const secret=await crypto.subtle.importKey('raw',new TextEncoder().encode(key),{name:'HMAC',hash:'SHA-256'},false,['verify']);
 const bytes=new Uint8Array(proof.match(/../g)!.map(x=>parseInt(x,16)));
 return await crypto.subtle.verify('HMAC',secret,bytes,new TextEncoder().encode(localLoginProofPayload(ip,request.headers.get('user-agent')||'',time)))?ip:null;
}
export async function loginContext(request:Request,{platform='',key='',development=false,now=Date.now()}:{platform?:string;key?:string;development?:boolean;now?:number}={}):Promise<LoginContext>{
 const context:LoginContext={...clientSoftware(request.headers.get('user-agent')),ip_address:null,ip_source:'unavailable',location:{country_code:null,region:null,city:null,timezone:null,source:'unavailable'}};
 if(platform==='local'&&development){
  const ip=await validLocalProof(request,key,now);if(ip){context.ip_address=ip;context.ip_source='local_socket';if(localNetworkIp(ip))context.location.source='local'}
 }else if(platform==='cloudflare'){
  // Request.cf is runtime-owned. Forwarded headers alone never establish trust.
  const cf=(request as Request&{cf?:Record<string,unknown>}).cf;
  if(cf&&typeof cf.colo==='string'&&/^[A-Z0-9]{3}$/.test(cf.colo)){
   const ip=validClientIp(request.headers.get('cf-connecting-ip'));
   if(ip){context.ip_address=ip;context.ip_source='cloudflare';if(localNetworkIp(ip))context.location.source='local';else{const country=clean(cf.country,2);context.location={country_code:country&&/^[A-Z]{2}$/.test(country)&&!['XX','T1'].includes(country)?country:null,region:clean(cf.region,100),city:clean(cf.city,100),timezone:clean(cf.timezone,80),source:'cloudflare'}}}
  }
 }
 return context;
}
