import {env,readJson} from './server';

// Imported only by server routes. Keys stay in the custody service, never this app.
if(typeof window!=='undefined')throw new Error('Server-only wallet address adapter');

export class WalletAddressError extends Error {
 code:string;status:number;
 constructor(code='wallet_address_unavailable',status=503){super(code);this.code=code;this.status=status}
}
export type WalletAddressConfig={url:string;token:string;provider:string;chainId:number;network:string};
export type RegisteredWalletAddress={id:string;network:string;address:string;label:string;wallet_type:'internal';verified_at:string;chain_id:number};
const uuid=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const nonzeroAddress=(v:unknown):v is string=>typeof v==='string'&&/^0x[0-9a-f]{40}$/i.test(v)&&!/^0x0{40}$/i.test(v);
const record=(v:unknown):v is Record<string,unknown>=>!!v&&typeof v==='object'&&!Array.isArray(v);

export function walletAddressConfig():WalletAddressConfig|null{
 const url=env('PLANCK_WALLET_PROVISION_URL'),token=env('PLANCK_WALLET_PROVISION_TOKEN');
 const provider=env('PLANCK_WALLET_PROVIDER'),chainId=Number(env('PLANCK_WALLET_CHAIN_ID')),network=env('PLANCK_WALLET_NETWORK');
 if(!url&&!token&&!provider&&!chainId&&!network)return null;
 let parsed:URL;try{parsed=new URL(url)}catch{return null}
 if(parsed.protocol!=='https:'||parsed.username||parsed.password||parsed.hash||parsed.search||!parsed.hostname.includes('.')||parsed.hostname==='localhost'||/\.(localhost|local|internal)$/i.test(parsed.hostname)||/^[\d.]+$/.test(parsed.hostname)||parsed.hostname.includes(':')||token.length<32||token.length>4096||/[\r\n]/.test(token)||!(/^[a-z0-9][a-z0-9_-]{1,59}$/).test(provider)||!Number.isSafeInteger(chainId)||chainId<1||network.length<1||network.length>60||network.trim()!==network||/[\x00-\x1f]/.test(network)||!env('SUPABASE_SERVICE_ROLE_KEY')||!env('SUPABASE_URL'))return null;
 return {url:parsed.toString(),token,provider,chainId,network};
}

async function responseJson(response:Response):Promise<unknown>{
 // The same streaming limit used for incoming API bodies also bounds provider replies.
 if(!response.body)throw new WalletAddressError();
 try{return await readJson(response as unknown as Request,16384)}catch{throw new WalletAddressError()}
}

const rpcErrors:Record<string,number>={wallet_address_busy:429,wallet_address_rate_limit:429,wallet_address_conflict:409,wallet_address_provider_changed:409,account_suspended:403,account_closed:403,not_authenticated:401};
async function walletAddressRpc(name:string,body:Record<string,unknown>,fetcher:typeof fetch):Promise<unknown>{
 const key=env('SUPABASE_SERVICE_ROLE_KEY');
 const response=await fetcher(`${env('SUPABASE_URL').replace(/\/$/,'')}/rest/v1/rpc/${name}`,{method:'POST',cache:'no-store',redirect:'error',headers:{apikey:key,Authorization:`Bearer ${key}`,'Content-Type':'application/json'},body:JSON.stringify(body),signal:AbortSignal.timeout(10000)});
 const data=await responseJson(response);
 if(!response.ok){
  const code=record(data)&&typeof data.message==='string'?data.message:'';
  if(Object.hasOwn(rpcErrors,code))throw new WalletAddressError(code,rpcErrors[code]);
  throw new WalletAddressError();
 }
 return data;
}

function registeredWallet(value:unknown,config:WalletAddressConfig):RegisteredWalletAddress{
 if(!record(value)||typeof value.id!=='string'||!uuid.test(value.id)||!nonzeroAddress(value.address)||value.chain_id!==config.chainId||value.network!==config.network||value.wallet_type!=='internal'||typeof value.verified_at!=='string'||!Number.isFinite(Date.parse(value.verified_at)))throw new WalletAddressError();
 // Explicit output allowlist. Never return arbitrary upstream fields to the browser.
 return {id:value.id,network:value.network,address:value.address,label:'Planck',wallet_type:'internal',verified_at:value.verified_at,chain_id:value.chain_id};
}

export async function provisionWalletAddress(userId:string,config:WalletAddressConfig,fetcher:typeof fetch=fetch):Promise<RegisteredWalletAddress>{
 try{
  if(!uuid.test(userId))throw new WalletAddressError('not_authenticated',401);
  const reservation=await walletAddressRpc('marketplace_wallet_address_reserve',{p_user:userId,p_provider:config.provider,p_chain_id:config.chainId,p_network:config.network},fetcher);
  if(!record(reservation)||typeof reservation.id!=='string'||!uuid.test(reservation.id))throw new WalletAddressError();
  if(reservation.wallet)return registeredWallet(reservation.wallet,config);
  // The database owns this opaque id and preserves it after timeouts. The custody
  // service MUST replay the same public address for this idempotency key forever.
  const response=await fetcher(config.url,{method:'POST',cache:'no-store',redirect:'error',headers:{Authorization:`Bearer ${config.token}`,'Content-Type':'application/json','Idempotency-Key':reservation.id},body:JSON.stringify({requestId:reservation.id,chainId:config.chainId}),signal:AbortSignal.timeout(15000)});
  const data=await responseJson(response);
  if(!response.ok||!record(data)||data.status!=='ready'||data.requestId!==reservation.id||data.chainId!==config.chainId||!nonzeroAddress(data.address))throw new WalletAddressError();
  if(['privateKey','private_key','mnemonic','seed','secret','keyMaterial'].some(k=>k in data))throw new WalletAddressError();
  const wallet=await walletAddressRpc('marketplace_wallet_address_complete',{p_user:userId,p_request:reservation.id,p_provider:config.provider,p_chain_id:config.chainId,p_address:data.address.toLowerCase()},fetcher);
  return registeredWallet(wallet,config);
 }catch(error){if(error instanceof WalletAddressError)throw error;throw new WalletAddressError()}
}
