import{initialConfiguration,evaluateConfiguration,evaluateNetwork}from'./planning.mjs';
const $=(s,r=document)=>r.querySelector(s),$$=(s,r=document)=>[...r.querySelectorAll(s)],h=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const K={basket:'asterrackit:v26:basket',compare:'asterrackit:v26:compare',plan:'asterrackit:v26:plan'};let tt;
function toast(s){const t=$('.cx-toast');if(!t)return;t.textContent=s;t.hidden=false;clearTimeout(tt);tt=setTimeout(()=>t.hidden=true,5000);}
function read(k,d){try{return JSON.parse(localStorage.getItem(k)||'null')??d;}catch{return d;}}
function store(k,v){try{localStorage.setItem(k,JSON.stringify(v));return true;}catch{toast('Browser storage is unavailable or full. Export your draft to a file.');return false;}}
const validId=id=>typeof id==='string'&&/^[a-z0-9-]{3,150}$/.test(id);
function basket(){let b=read(K.basket,null);if(!b||b.version!==1||!Array.isArray(b.items))b={version:1,items:[]};return{version:1,items:b.items.filter(x=>x&&validId(x.id)&&Number.isInteger(x.quantity)&&x.quantity>0&&x.quantity<=9999).slice(0,100),configurations:Array.isArray(b.configurations)?b.configurations.filter(c=>c&&typeof c.text==='string'&&c.text.length<=30000).slice(0,10):[],notes:typeof b.notes==='string'?b.notes.slice(0,5000):'',destination:typeof b.destination==='string'?b.destination.slice(0,120):'',condition:typeof b.condition==='string'?b.condition.slice(0,100):'Confirm in quotation'};}
function compared(){const a=read(K.compare,[]);return Array.isArray(a)?[...new Set(a.filter(validId))].slice(0,4):[];}
function counts(){const b=basket();$$('[data-basket-count]').forEach(e=>e.textContent=b.items.length+b.configurations.length);}
async function api(p,o){const r=await fetch('/explore/api/'+p,o);if(!r.ok)throw Error('Catalog request failed ('+r.status+'). Try again.');return r.json();}
async function add(id,qty=1){if(!validId(id)||!Number.isInteger(qty)||qty<1||qty>9999)throw Error('Invalid identifier or quantity.');const r=await api('products?ids='+encodeURIComponent(id));if(r.items.length!==1)throw Error('This product is not in the current catalog.');const b=basket(),x=b.items.find(x=>x.id===id);if(x)x.quantity=Math.min(9999,x.quantity+qty);else{if(b.items.length>=100)throw Error('Export or remove lines before exceeding 100 distinct identifiers.');b.items.push({id,quantity:qty});}if(store(K.basket,b)){counts();toast(r.items[0].identifier+' added to your local quotation basket.');}}
function compareAdd(id){const a=compared();if(a.includes(id)){toast('Already selected.');return;}if(a.length>=4){toast('Compare up to four records. Remove one from the comparison page first.');return;}a.push(id);if(store(K.compare,a))toast('Added to comparison. Open Compare in the navigation.');}
function goCompare(a){store(K.compare,a);location.assign('/explore/compare'+(a.length?'?ids='+encodeURIComponent(a.join(',')):''));}
let lastExportUrl;
function download(n,s,type='text/plain;charset=utf-8'){
if(lastExportUrl)URL.revokeObjectURL(lastExportUrl);
lastExportUrl=URL.createObjectURL(new Blob([s],{type}));
let panel=$('#cx-export-preview');
if(!panel){panel=document.createElement('section');panel.id='cx-export-preview';panel.className='cx-panel';panel.setAttribute('aria-label','Exported draft');$('main').append(panel);}
panel.innerHTML=`
Your draft is ready
Save the file or copy the draft below. Nothing has been sent.
Save ${h(n)} ↓`;
const a=$('#cx-export-link',panel);a.href=lastExportUrl;a.download=n;$('#cx-export-text',panel).value=s;a.click();
panel.scrollIntoView({block:'nearest'});toast('Draft ready. Save the file or copy its contents.');
}
function csv(v){v=String(v??'');if(/^[=+\-@\t\r]/.test(v))v="'"+v;return '"'+v.replaceAll('"','""')+'"';}
function attach(text,kind,plan){if(text.length>30000)throw Error('Plan exceeds the draft size limit.');const b=basket();if(b.configurations.length>=10)throw Error('Export or remove a configuration before adding another.');b.configurations.push({id:crypto.randomUUID(),kind,text,plan,createdAt:new Date().toISOString()});if(store(K.basket,b)){counts();toast('Unvalidated planning draft attached to the quote basket.');}}
let d={};try{d=JSON.parse($('#cx-data')?.textContent||'{}');}catch{toast('Page data could not be read.');}counts();
document.addEventListener('click',async e=>{const b=e.target.closest('button,a');if(!b)return;try{if(b.dataset.add){e.preventDefault();b.disabled=true;await add(b.dataset.add);b.disabled=false;}else if(b.dataset.compare){e.preventDefault();compareAdd(b.dataset.compare);}else if(b.hasAttribute('data-clear-compare'))goCompare([]);else if(b.dataset.removeCompare)goCompare(compared().filter(id=>id!==b.dataset.removeCompare));}catch(err){b.disabled=false;toast(err.message||'Action failed.');}});
window.addEventListener('storage',e=>{if([K.basket,K.compare].includes(e.key)){counts();if(d.kind==='basket')renderBasket();}});
const search=$('[data-suggest]'),box=$('#cx-suggestions');let timer,ab;
if(search&&box){search.setAttribute('aria-controls','cx-suggestions');search.setAttribute('aria-expanded','false');search.addEventListener('input',()=>{clearTimeout(timer);ab?.abort();const q=search.value.trim();if(q.length<2){box.hidden=true;search.setAttribute('aria-expanded','false');return;}timer=setTimeout(async()=>{ab=new AbortController();try{const r=await api('search?size=8&q='+encodeURIComponent(q),{signal:ab.signal});box.innerHTML=r.items.map(p=>`${h(p.brand+' · '+p.identifier)}${h(p.name)}`).join('')||'
':''}`;
function planText(p,s,r){return ['ASTER RACKIT — SERVER PLANNING DRAFT','STATUS: '+r.status.toUpperCase(),'Not an OEM-approved BOM. Exact PNs remain pending.','',p.brand+' '+p.model,...r.bom.map(x=>x.quantity+' x '+x.description),'',`Chassis: ${s.chassis}`,`Controller: ${s.controller}`,`Boot: ${s.boot}`,`Network: ${s.network}`,`RAID math: ${s.raid}`,`Memory: ${r.summary.memoryGB} GB`,`Estimated member capacity: ${r.summary.raidCapacityTB} TB`,`Estimated planning load: ${r.summary.plannedLoadW??'unknown'} W`,`Remaining PSU arithmetic: ${r.summary.redundancyCapacityW} W`,'','POWER ASSUMPTIONS',JSON.stringify(r.summary.assumptions),'','WARNINGS',...r.warnings,'','REQUIRED REVIEW',...r.requires,'','No price, inventory allocation, order or entitlement is created.'].join('\n');}
if(d.kind==='configurator'){
const p=d.profile;let s={...d.initial},result;const keys=Object.keys(s),numeric=keys.filter(k=>typeof s[k]==='number');
function importState(x){if(!x||typeof x!=='object'||x.profile!==p.id)throw Error('Load the matching platform before importing a plan.');const out=initialConfiguration(p);for(const k of keys)if(Object.hasOwn(x,k))out[k]=x[k];for(const k of numeric)if(typeof out[k]!=='number'||!Number.isFinite(out[k]))throw Error('Invalid numeric field: '+k);return out;}
try{const raw=new URL(location.href).searchParams.get('state');if(raw){if(raw.length>18000)throw Error('Shared plan too large.');s=importState(JSON.parse(raw));}}catch(e){toast(e.message||'Invalid shared configuration.');}
function form(){const ch=p.chassis.find(c=>c.id===s.chassis)||p.chassis[0];$('#cx-config-form').innerHTML=[
group('01','Processor and memory',[
select('Processor quantity','cpuCount',Array.from({length:p.cpu.max},(_,i)=>i+1),s.cpuCount),p.cpu.options?.length?select('CPU power class','cpuOption',p.cpu.options,s.cpuOption):number('CPU TDP assumption (W); 0 = unknown','cpuTdpW',s.cpuTdpW,0,1500),select('Capacity per DIMM (GB)','dimmGB',p.memory.capacitiesGB,s.dimmGB),number('Installed DIMMs','dimmCount',s.dimmCount,1,p.memory.maxDimms)],'Logical DIMM counts are not a physical slot-order diagram. Exact module PN and memory generation remain subject to review.'),
group('02','Chassis and storage path',[
select('Chassis planning layout','chassis',p.chassis,s.chassis),select('Data-drive media','driveType',ch.driveTypes,s.driveType),number('Data members, excluding spares','driveCount',s.driveCount,0,ch.driveBays),number('Capacity per member (decimal TB)','driveTB',s.driveTB,.01,1000,.01),select('Controller path','controller',p.controllers,s.controller),select('Capacity calculation','raid',[{id:'none',label:'Raw / no RAID math'},{id:'raid0',label:'RAID 0'},{id:'raid1',label:'RAID 1 / two members'},{id:'raid5',label:'RAID 5'},{id:'raid6',label:'RAID 6'},{id:'raid10',label:'RAID 10'}],s.raid)],'RAID arithmetic assumes equal-size members. Spares, filesystem overhead and reserved capacity are excluded. Supported RAID implementation is a separate check.'),
group('03','Accelerators and cooling',[
select('GPU planning option','gpu',p.gpu,s.gpu),number('GPU quantity','gpuCount',s.gpuCount,0,32),p.cooling?.length?select('Cooling profile','cooling',p.cooling,s.cooling):'
Exact cooling kit / PN requires review.
',number('Target ambient temperature (°C)','ambientC',s.ambientC,-20,80)],'A generic accelerator class is not an orderable PN. GPU selection triggers chassis, width, memory and power checks.'),
group('04','Power, boot and network',[
select('PSU rating (W)','psuW',p.psuWatts,s.psuW),select('Installed power supplies','psuCount',Array.from({length:p.psuCount.max-p.psuCount.min+1},(_,i)=>i+p.psuCount.min),s.psuCount),select('Redundancy arithmetic','redundancy',[{id:'none',label:'No redundant-capacity assumption'},{id:'n-plus-one',label:'N+1: remove one supply'},{id:'n-plus-n',label:'N+N: half installed capacity'}],s.redundancy),select('Site input voltage','voltage',[{id:'unconfirmed',label:'Not yet confirmed'},{id:'100-127',label:'100–127 V AC; review required'},{id:'200-240',label:'200–240 V AC; review required'},{id:'dc',label:'DC; exact PSU review required'}],s.voltage),...(p.boot?.length?[select('Dedicated boot','boot',p.boot,s.boot)]:[]),...(p.networking?.length?[select('Adapter planning option','network',p.networking,s.network)]:[])],'Power ratings are not measured wall power. Review voltage derating, input current, PDU, cables and the actual supported redundancy topology.')].join('');
$$('[data-field]',$('#cx-config-form')).forEach(e=>e.addEventListener('change',()=>{const k=e.dataset.field;s[k]=numeric.includes(k)?Number(e.value):e.value;if(k==='cpuOption')s.cpuTdpW=p.cpu.options.find(o=>o.id===s.cpuOption)?.tdpW||0;if(k==='gpu'){s.gpuCount=p.gpu.find(g=>g.id===s.gpu)?.width==='none'?0:Math.max(1,s.gpuCount);$('[data-field="gpuCount"]',$('#cx-config-form')).value=s.gpuCount;}if(k==='chassis'){const c=p.chassis.find(c=>c.id===s.chassis);if(!c.driveTypes.includes(s.driveType)){s.driveType=c.driveTypes[0];toast('Media type reset for the new chassis. Other limits are re-evaluated.');}form();}summary();}));
}
function summary(){const r=result=evaluateConfiguration(p,s),x=r.summary;$('#cx-config-summary').innerHTML=`${r.errors.length?'BLOCKED COMBINATION':'REVIEW REQUIRED'}
${h(p.brand+' '+p.model)}
${r.errors.length?'Resolve blocking checks before exporting an RFQ BOM.':'No modeled limit blocks this selection. This is not compatibility approval.'}
${r.errors.length?`
Blocking checks (${r.errors.length})
${r.errors.map(e=>`
${h(e.code)}: ${h(e.message)}
`).join('')}
`:''}
Live planning summary
${h(x.memoryGB??'—')} GBInstalled memory
${x.raidCapacityTB==null?'—':x.raidCapacityTB.toFixed(2)} TBMathematical data capacity
';
$$('[data-basket-qty]').forEach(e=>e.addEventListener('change',()=>{const n=Number(e.value);if(!Number.isInteger(n)||n<1||n>9999){toast('Quantity must be an integer from 1 to 9999.');renderBasket();return;}const b=basket(),x=b.items.find(x=>x.id===e.dataset.basketQty);if(x)x.quantity=n;store(K.basket,b);counts();}));
$$('[data-basket-remove]').forEach(e=>e.addEventListener('click',()=>{const b=basket();b.items=b.items.filter(x=>x.id!==e.dataset.basketRemove);store(K.basket,b);renderBasket();counts();}));
$('#cx-basket-configs').innerHTML=b.configurations.length?b.configurations.map(c=>`${h(c.kind||'Configuration')} draft · ${h(c.createdAt||'saved')}
${h(c.text)}
`).join(''):'
No attached plans. Build a server or network draft and add it after resolving blocking checks.
';}}
function saveFields(){const b=basket();b.destination=$('#cx-destination')?.value.slice(0,120)||'';b.notes=$('#cx-project-notes')?.value.slice(0,5000)||'';b.condition=$('#cx-condition')?.value||'Confirm in quotation';store(K.basket,b);}
function rfq(){saveFields();const b=basket();const categories=b.items.map(x=>basketMap.get(x.id)?.category);const server=categories.includes('servers')||b.configurations.some(c=>c.kind==='server');const network=categories.some(c=>['switches','routers','security','wireless','network-platforms'].includes(c))||b.configurations.some(c=>c.kind==='network');const scope=server?'SERVER CONFIGURATION — purchasing review':network?'NETWORK CONFIGURATION — purchasing review':'COMPONENTS — purchasing review';return ['ASTER RACKIT — CUSTOMER RFQ DRAFT',scope,'Not an order, offer, stock allocation or OEM-approved BOM.','Created: '+new Date().toISOString(),'Destination: '+b.destination,'Requested condition: '+b.condition,'','PRODUCTS',...b.items.map(x=>{const p=basketMap.get(x.id);return `${x.quantity} x ${p?.brand||''} ${p?.identifier||x.id}\n ${p?'https://www.aster-rackit.com'+p.href:'Unresolved ID — review required'}`;}),'','PROJECT NOTES',b.notes,'','CONFIGURATION DRAFTS',...b.configurations.map(c=>c.text),'','Confirm exact identity, compatibility, accessories, test evidence, price, lead time, warranty and delivery terms.','Send to sales@aster-rackit.com'].join('\n');}
if(d.kind==='basket'){
const b=basket();$('#cx-destination').value=b.destination;$('#cx-project-notes').value=b.notes;if([...$('#cx-condition').options].some(o=>o.value===b.condition))$('#cx-condition').value=b.condition;for(const s of ['#cx-destination','#cx-project-notes','#cx-condition'])$(s).addEventListener('change',saveFields);renderBasket();
$('[data-basket-clear]').addEventListener('click',()=>{if(!confirm('Clear local product lines and configuration drafts?'))return;const b=basket();b.items=[];b.configurations=[];store(K.basket,b);renderBasket();counts();});
$('#cx-export-txt').addEventListener('click',()=>download('ASTER-RACKIT-RFQ.txt',rfq()));
$('#cx-export-csv').addEventListener('click',()=>{saveFields();const b=basket(),rows=[['Manufacturer','Identifier','Quantity','Catalog URL','Review status'],...b.items.map(x=>{const p=basketMap.get(x.id);return[p?.brand||'',p?.identifier||x.id,x.quantity,p?'https://www.aster-rackit.com'+p.href:'','Current technical and commercial review required'];})];download('ASTER-RACKIT-BOM.csv','\uFEFF'+rows.map(r=>r.map(csv).join(',')).join('\r\n'),'text/csv;charset=utf-8');});
$('#cx-export-json').addEventListener('click',()=>{saveFields();download('ASTER-RACKIT-basket.json',JSON.stringify(basket(),null,2),'application/json');});
$('#cx-basket-import').addEventListener('change',async e=>{try{const f=e.target.files[0];if(!f)return;if(f.size>500000)throw Error('Basket file exceeds 500 KB.');const v=JSON.parse(await f.text());if(!v||v.version!==1||!Array.isArray(v.items)||v.items.length>100)throw Error('Expected a version 1 basket with up to 100 lines.');if(v.items.some(x=>!x||!validId(x.id)||!Number.isInteger(x.quantity)||x.quantity<1||x.quantity>9999))throw Error('Invalid identifier or quantity.');if(new Set(v.items.map(x=>x.id)).size!==v.items.length)throw Error('Duplicate product lines in import. Merge quantities first.');const r=v.items.length?await api('products?ids='+encodeURIComponent(v.items.map(x=>x.id).join(','))):{items:[]};if(r.items.length!==v.items.length)throw Error('Some imported identifiers do not exist in the current catalog.');const configs=Array.isArray(v.configurations)?v.configurations:[];if(configs.length>10||configs.some(c=>!c||typeof c.text!=='string'||c.text.length>29900))throw Error('Invalid configuration drafts.');if(!confirm('Replace this browser basket with the imported draft?'))return;const out={version:1,items:v.items,configurations:configs.map(c=>({id:crypto.randomUUID(),kind:'imported / unvalidated',text:'IMPORTED DRAFT — REVALIDATE BEFORE USE\n'+c.text,createdAt:new Date().toISOString()})),destination:String(v.destination||'').slice(0,120),notes:String(v.notes||'').slice(0,5000),condition:String(v.condition||'Confirm in quotation').slice(0,100)};store(K.basket,out);$('#cx-destination').value=out.destination;$('#cx-project-notes').value=out.notes;if([...$('#cx-condition').options].some(o=>o.value===out.condition))$('#cx-condition').value=out.condition;renderBasket();counts();toast('Imported. Configuration text remains unvalidated.');}catch(e){toast(e.message||'Invalid basket file.');}finally{e.target.value='';}});
$('#cx-mail').addEventListener('click',e=>{saveFields();const b=basket();e.currentTarget.href='mailto:sales@aster-rackit.com?subject='+encodeURIComponent('ASTER RACKIT hardware RFQ')+'&body='+encodeURIComponent('Please review my hardware inquiry. I will attach the exported BOM/RFQ file.\nDestination: '+b.destination+'\nRequested condition: '+b.condition+'\nThis is an inquiry, not an order.');});
$('#cx-send-existing').addEventListener('click',()=>{try{const text=rfq();if(text.length>100000)throw Error('Draft exceeds the existing form limit. Export it as a file instead.');const id=crypto.randomUUID();sessionStorage.setItem('asterrack:quote:'+id,JSON.stringify({version:1,text,createdAt:Date.now()}));location.assign('/quote?draft='+id);}catch(e){toast(e.message||'Session storage unavailable. Export the RFQ instead.');}});
$('#cx-bulk-match').addEventListener('click',async e=>{const lines=$('#cx-bulk').value.split(/\r?\n/).map(x=>x.trim()).filter(Boolean);if(lines.length>100){toast('Paste up to 100 lines.');return;}const b=e.currentTarget;b.disabled=true;$('#cx-bulk-results').textContent='Matching identifiers…';const found=[];for(const line of lines){const [id,qty]=line.split('|').map(x=>x.trim()),q=qty===undefined?1:Number(qty);if(!id||id.length>240||!Number.isInteger(q)||q<1||q>9999){found.push({line,error:'Invalid identifier or quantity.'});continue;}try{found.push({line,q,...await api('lookup?q='+encodeURIComponent(id))});}catch(e){found.push({line,error:e.message});}}$('#cx-bulk-results').innerHTML=found.map(r=>`
${h(r.line)}${r.error?'
'+h(r.error)+'
':r.items.length?`
${r.ambiguous?'Multiple exact/alias matches. Select the correct purchasing unit.':'One exact/alias catalog match. Confirm before adding.'}
${r.items.map(p=>``).join('')}`:'
No exact identifier match. Try the model name or ask for sourcing review.