// Licensed to the Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership. The ASF licenses this
// file to you under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//	http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package tracebaseline

import (
	"encoding/json"
	"fmt"
	"io"
	"strings"
)

// RenderHTML writes a standalone, diagram-first baseline report.
func RenderHTML(writer io.Writer, suite SuiteReport) error {
	readiness := EvaluateBaselineReadiness(suite)
	suite.Readiness = &readiness
	encoded, marshalErr := json.Marshal(suite)
	if marshalErr != nil {
		return fmt.Errorf("cannot encode baseline suite for HTML: %w", marshalErr)
	}
	document := strings.Replace(baselineHTML, "__SUITE_JSON__", string(encoded), 1)
	if _, writeErr := io.WriteString(writer, document); writeErr != nil {
		return fmt.Errorf("cannot write baseline HTML report: %w", writeErr)
	}
	return nil
}

//nolint:lll // The embedded standalone report intentionally keeps its HTML, CSS, and JavaScript compact.
const baselineHTML = `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>BanyanDB — One-shard ordinary merge baseline</title>
<style>
:root{--ink:#071016;--panel:#0c1920;--panel2:#10232b;--grid:#17333c;--paper:#dce8df;--muted:#82a19c;--green:#9cff57;--amber:#ffbf45;--red:#ff6262;--cyan:#60d8e8;--line:#2b4b52}
*{box-sizing:border-box}html{background:var(--ink);color:var(--paper);font-family:"IBM Plex Mono","Cascadia Mono",monospace}body{margin:0;background:radial-gradient(circle at 75% 0,#16353a80,transparent 35%),linear-gradient(#ffffff05 1px,transparent 1px),linear-gradient(90deg,#ffffff05 1px,transparent 1px);background-size:auto,28px 28px,28px 28px}.shell{max-width:1460px;margin:auto;padding:28px 32px 72px}.mast{display:grid;grid-template-columns:1fr auto;gap:24px;align-items:end;border-bottom:1px solid var(--line);padding-bottom:22px}.eyebrow,.tag{color:var(--green);letter-spacing:.16em;font-size:11px;text-transform:uppercase}.mast h1{font-family:"Iowan Old Style","Palatino Linotype",serif;font-weight:400;font-size:clamp(42px,6vw,88px);line-height:.86;letter-spacing:-.05em;margin:12px 0 0;max-width:920px}.stamp{width:178px;height:178px;border:1px solid var(--green);border-radius:50%;display:grid;place-content:center;text-align:center;color:var(--green);transform:rotate(5deg);box-shadow:0 0 40px #9cff5718}.stamp b{font-size:38px}.stamp span{font-size:10px;letter-spacing:.12em}.warning{margin:16px 0 26px;border-left:3px solid var(--amber);padding:10px 14px;background:#ffbf450b;color:#d8c699;font-size:12px}.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:14px}.card{background:linear-gradient(145deg,#10232bee,#0a171dee);border:1px solid var(--line);padding:18px;position:relative;overflow:hidden}.card:before{content:attr(data-index);position:absolute;right:11px;top:8px;color:#ffffff1c;font-size:42px;font-family:serif}.span12{grid-column:span 12}.span8{grid-column:span 8}.span7{grid-column:span 7}.span6{grid-column:span 6}.span5{grid-column:span 5}.span4{grid-column:span 4}.section-title{display:flex;align-items:baseline;gap:12px;margin-bottom:16px}.section-title h2{font:400 25px "Iowan Old Style",serif;margin:0}.section-title small{color:var(--muted)}svg{width:100%;display:block;overflow:visible}.kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:1px;background:var(--line);border:1px solid var(--line);margin:18px 0}.kpi{background:var(--panel);padding:16px}.kpi label{display:block;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.1em}.kpi strong{font:400 31px "Iowan Old Style",serif;display:block;margin-top:8px}.kpi em{font-style:normal;font-size:10px;color:var(--green)}.legend{display:flex;gap:18px;font-size:10px;color:var(--muted);margin-top:10px}.dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px}.gate-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.gate{border:1px solid var(--line);padding:11px;min-height:88px}.gate.pass{border-color:#579b43}.gate.fail{border-color:#8e4848}.gate.diagnostic{border-color:#9b7b3f}.gate b{display:block;font-size:11px}.gate span{display:block;color:var(--muted);font-size:9px;margin-top:8px;line-height:1.5}.gate i{float:right;width:9px;height:9px;border-radius:50%;background:var(--red);box-shadow:0 0 10px var(--red)}.gate.pass i{background:var(--green);box-shadow:0 0 10px var(--green)}.gate.diagnostic i{background:var(--amber);box-shadow:0 0 10px var(--amber)}table{width:100%;border-collapse:collapse;font-size:10px}th,td{text-align:left;padding:8px;border-bottom:1px solid var(--line)}th{color:var(--muted);font-weight:400}.foot{display:flex;justify-content:space-between;margin-top:28px;color:var(--muted);font-size:10px}.axis{stroke:#48636a;stroke-width:1}.gridline{stroke:#1c3941;stroke-width:1}.label{fill:#88a7a2;font:9px monospace}.value{fill:#dce8df;font:11px monospace}.green{stroke:var(--green);fill:none}.amber{stroke:var(--amber);fill:none}.cyan{stroke:var(--cyan);fill:none}.area{fill:#60d8e818;stroke:var(--cyan)}@media(max-width:1050px){.kpis{grid-template-columns:repeat(3,1fr)}}@media(max-width:900px){.span8,.span7,.span6,.span5,.span4{grid-column:span 12}.kpis{grid-template-columns:1fr 1fr}.gate-grid{grid-template-columns:1fr 1fr}.stamp{display:none}.shell{padding:20px 14px}.mast h1{font-size:48px}}
</style></head><body><main class="shell">
<header class="mast"><div><div class="eyebrow">Apache SkyWalking BanyanDB · performance dossier 05</div><h1>ONE SHARD /<br>ORDINARY MERGE</h1></div><div class="stamp"><span>BASELINE</span><b id="stamp-result">—</b><span>PIPELINE DISABLED</span></div></header>
<div class="warning" id="boundary">Interpretation boundary — every measurement describes one active trace shard. It is not whole-node or cluster capacity.</div>
<section class="kpis" id="kpis"></section>
<div class="grid">
<article class="card span7" data-index="01"><div class="section-title"><h2>Publication barriers</h2><small>write → epoch-aware merge-idle</small></div><svg id="barrier-diagram" viewBox="0 0 760 270" aria-label="publication barrier diagram"></svg><div class="legend"><span><i class="dot" style="background:var(--cyan)"></i>per-write barrier</span><span><i class="dot" style="background:var(--amber)"></i>p95</span></div></article>
<article class="card span5" data-index="02"><div class="section-title"><h2>Work-flow ledger</h2><small>core + both SIDX children</small></div><svg id="flow-diagram" viewBox="0 0 520 270" aria-label="merge byte flow diagram"></svg></article>
<article class="card span8" data-index="03"><div class="section-title"><h2>Run-to-run signal</h2><small>five fresh processes after warm-up</small></div><svg id="variance-diagram" viewBox="0 0 860 470" aria-label="run variance dot plot"></svg></article>
<article class="card span4" data-index="04"><div class="section-title"><h2>Selection states</h2><small>mixed selections appear in both sets</small></div><svg id="class-diagram" viewBox="0 0 400 330" aria-label="overlapping hot and mature merge diagram"></svg></article>
<article class="card span12" data-index="05"><div class="section-title"><h2>Inventory waveform</h2><small>parts after each serialized merge-idle barrier</small></div><svg id="backlog-diagram" viewBox="0 0 1200 300" aria-label="part inventory area chart"></svg></article>
<article class="card span7" data-index="06"><div class="section-title"><h2>Controlled pipeline stability</h2><small>alternating disabled / retain-all mature merges</small></div><svg id="controlled-variance-diagram" viewBox="0 0 760 300" aria-label="controlled pipeline wall and CPU variance"></svg><div class="legend"><span><i class="dot" style="background:var(--cyan)"></i>disabled</span><span><i class="dot" style="background:var(--amber)"></i>retain-all</span></div></article>
<article class="card span5" data-index="07"><div class="section-title"><h2>Controlled evidence</h2><small>hard ceiling → metadata plan → measured staging</small></div><table><thead><tr><th>RUN</th><th>MODE</th><th>WALL</th><th>CPU</th><th>HARD CAP</th><th>BATCH LIMIT</th><th>ESTIMATE / PEAK</th><th>CALLS</th><th>RESULT</th></tr></thead><tbody id="controlled-run-table"></tbody></table></article>
<article class="card span12" data-index="08"><div class="section-title"><h2>Correctness circuit</h2><small>results are rejected when a required gate opens</small></div><div class="gate-grid" id="gates"></div></article>
<article class="card span12" data-index="09"><div class="section-title"><h2>Measured runs</h2><small>absolute one-shard values</small></div><table><thead><tr><th>RUN</th><th>MODE</th><th>PRIMARY</th><th>COOLDOWN</th><th>CPU</th><th>PEAK RSS</th><th>READ / WRITE</th><th>HOT-CONTAINING / MATURE-CONTAINING</th><th>LEDGER</th></tr></thead><tbody id="run-table"></tbody></table><div class="legend">Mixed selections may appear in both hot and mature counts. Sampling execution is reported independently in merge events.</div></article>
</div><footer class="foot"><span id="identity"></span><span>Standalone HTML · source data embedded below</span></footer>
<script type="application/json" id="suite-data">__SUITE_JSON__</script>
<script>
const d=JSON.parse(document.getElementById('suite-data').textContent),runs=d.serialRuns||[],controlledRuns=d.disabledEnabledAlternating||[],readiness=d.readiness||{};
const ns=n=>n/1e9, mb=n=>n/1048576, fmt=(n,p=1)=>Number(n||0).toFixed(p), med=a=>{a=[...a].sort((x,y)=>x-y);return a.length?a[Math.floor(a.length/2)]:0}, q=(a,p)=>{a=[...a].sort((x,y)=>x-y);return a.length?a[Math.min(a.length-1,Math.ceil(a.length*p)-1)]:0};
const tp=runs,walls=tp.map(r=>ns(r.primary.wallNanos)),cpus=tp.map(r=>ns((r.primary.end.cpuNanos||0)-(r.primary.start.cpuNanos||0))),rss=tp.map(r=>mb(r.primary.end.rssBytes||0)),allocs=tp.map(r=>mb((r.primary.end.allocatedBytes||0)-(r.primary.start.allocatedBytes||0))),writes=tp.map(r=>mb((r.primary.end.writeBytes||0)-(r.primary.start.writeBytes||0))),firstRun=tp[0]||{},mergeAggregates=firstRun.merges?.aggregates||[],mergeInput=mergeAggregates.reduce((sum,aggregate)=>sum+(aggregate.inputBytes||0)+(aggregate.childInputBytes||0),0),mergeOutput=mergeAggregates.reduce((sum,aggregate)=>sum+(aggregate.outputBytes||0)+(aggregate.childOutputBytes||0),0),logicalWA=d.logicalWriteAmplification>0?d.logicalWriteAmplification:mergeOutput/Math.max(1,mergeInput),barriers=firstRun.status?.map(x=>ns(x.barrierNanos||0))||[];
const allCorrect=runs.length>0&&runs.every(r=>r.correct),rowCountsValid=runs.length>0&&runs.every(r=>r.inventory?.coreRows===r.expectedRows&&r.inventory?.indexRows?.latency===r.expectedRows&&r.inventory?.indexRows?.start_time===r.expectedRows),coverageRuns=runs,totalHot=coverageRuns.reduce((s,r)=>s+(r.hotMerges||0),0),totalMature=coverageRuns.reduce((s,r)=>s+(r.matureMerges||0),0),totalMixed=coverageRuns.reduce((sum,run)=>sum+(run.merges?.events||[]).filter(event=>(event.hotInputParts||0)>0&&(event.matureInputParts||0)>0).length,0);
document.getElementById('boundary').textContent='Interpretation boundary — one active trace shard at '+fmt(d.writeIntensity||1,1)+'× production write intensity; writes and merge-idle barriers are serialized across separate processes.';
document.getElementById('stamp-result').textContent='HOLD';
document.getElementById('kpis').innerHTML=[['SERIAL WRITES',fmt(firstRun.published,0),'24h logical schedule'],['PRIMARY MEDIAN',fmt(med(walls),2)+'s','p95 '+fmt(q(walls,.95),2)+'s'],['CPU MEDIAN',fmt(med(cpus),2)+'s',fmt(med(cpus)/Math.max(.001,med(walls)),2)+' effective cores'],['PEAK RSS',fmt(q(rss,.95),1)+' MiB','p95 across runs'],['LOGICAL WA',fmt(logicalWA,2)+'×','core + SIDX merge output'],['MERGE COVERAGE',totalHot+' / '+totalMature,'hot-containing / mature-containing']].map(x=>'<div class="kpi"><label>'+x[0]+'</label><strong>'+x[1]+'</strong><em>'+x[2]+'</em></div>').join('');
function line(x1,y1,x2,y2,c='gridline'){return '<line class="'+c+'" x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'"/>'} function text(x,y,s,c='label',a='start'){return '<text class="'+c+'" x="'+x+'" y="'+y+'" text-anchor="'+a+'">'+s+'</text>'}
{const svg=document.getElementById('barrier-diagram'),mx=Math.max(1,...barriers),step=Math.max(1,Math.ceil(barriers.length/180)),shown=barriers.filter((_,i)=>i%step===0),p95=q(barriers,.95);let h='';for(let i=0;i<5;i++){let y=225-i*45;h+=line(55,y,730,y)+text(45,y+3,fmt(mx*i/4,2)+'s','label','end')}let pts=shown.map((v,i)=>(65+i/Math.max(1,shown.length-1)*640)+','+(225-v/mx*180)).join(' ');h+='<polyline points="'+pts+'" class="cyan" stroke-width="2"/>';let py=225-p95/mx*180;h+=line(55,py,730,py,'amber')+text(725,py-8,'p95 '+fmt(p95,3)+'s','value','end');svg.innerHTML=h}
{const inv=firstRun.inventory||{},input=d.fixtureInputBytes||firstRun.primary?.inputBytes||0,final=(inv.coreBytes||0)+Object.values(inv.indexBytes||{}).reduce((a,b)=>a+b,0),vals=[input,mergeOutput,final],names=['FIXTURE IN','COMPACTION OUT','FINAL LEDGER'],mx=Math.max(1,...vals);let h='';vals.forEach((v,i)=>{let x=30+i*170,w=110,hh=40+170*v/mx,y=235-hh;h+='<rect x="'+x+'" y="'+y+'" width="'+w+'" height="'+hh+'" fill="'+['#60d8e833','#ffbf4533','#9cff5733'][i]+'" stroke="'+['#60d8e8','#ffbf45','#9cff57'][i]+'"/>'+text(x+w/2,255,names[i],'label','middle')+text(x+w/2,y-10,fmt(mb(v),1)+' MiB','value','middle');if(i<2)h+='<path d="M '+(x+w)+' '+(y+hh/2)+' C '+(x+145)+' '+(y+hh/2)+', '+(x+145)+' 135, '+(x+170)+' 135" stroke="#48636a" fill="none"/>'});document.getElementById('flow-diagram').innerHTML=h}
{let metrics=[['WALL s',walls],['CPU s',cpus],['RSS MiB',rss],['ALLOC MiB',allocs],['WRITE MiB',writes]],h='';metrics.forEach((m,row)=>{let y=55+row*88,a=m[1],mx=Math.max(1,...a);h+=text(20,y,m[0],'value');h+=line(145,y,820,y,'axis');a.forEach((v,i)=>{let x=145+v/mx*640;h+='<circle cx="'+x+'" cy="'+(y+(i%3-1)*8)+'" r="6" fill="#60d8e8"/>'});let md=med(a),p=q(a,.95);h+=line(145+md/mx*640,y-22,145+md/mx*640,y+22,'green')+line(145+p/mx*640,y-22,145+p/mx*640,y+22,'amber')+text(820,y-14,'med '+fmt(md,2)+' · p95 '+fmt(p,2),'label','end')});document.getElementById('variance-diagram').innerHTML=h}
{document.getElementById('class-diagram').innerHTML='<circle cx="145" cy="145" r="105" fill="#60d8e822" stroke="#60d8e8" stroke-width="3"/><circle cx="255" cy="145" r="105" fill="#9cff5722" stroke="#9cff57" stroke-width="3"/>'+text(105,135,totalHot,'value','middle')+text(105,153,'HOT','label','middle')+text(295,135,totalMature,'value','middle')+text(295,153,'MATURE','label','middle')+text(200,135,totalMixed,'value','middle')+text(200,153,'MIXED','label','middle')+text(200,295,'counts describe selected inputs, not sampler calls','label','middle')}
{const st=(tp[0]||runs[0]||{}).status||[],mx=Math.max(1,...st.map(x=>x.coreParts||0));let pts=st.map((p,i)=>(45+i/Math.max(1,st.length-1)*1110)+','+(250-(p.coreParts||0)/mx*205)).join(' '),h='<polygon class="area" points="45,250 '+pts+' 1155,250"/>';for(let i=0;i<5;i++){let y=250-i*50;h+=line(45,y,1155,y)+text(35,y+3,fmt(mx*i/4,0),'label','end')}h+=text(45,280,'00:00 LOGICAL','label')+text(1155,280,'24:00 + DRAIN','label','end');document.getElementById('backlog-diagram').innerHTML=h}
{const svg=document.getElementById('controlled-variance-diagram'),metrics=[['WALL s',controlledRuns.map(r=>ns(r.event?.resources?.elapsedNanos||0))],['CPU s',controlledRuns.map(r=>ns(r.event?.resources?.cpuNanos||0))]],colors=controlledRuns.map(r=>r.pipelineMode==='retain-all'?'#ffbf45':'#60d8e8');let h='';metrics.forEach((metric,row)=>{const y=85+row*125,values=metric[1],mx=Math.max(1,...values);h+=text(20,y,metric[0],'value')+line(125,y,720,y,'axis');values.forEach((value,index)=>{const x=125+value/mx*570;h+='<circle cx="'+x+'" cy="'+(y+(index%3-1)*9)+'" r="6" fill="'+colors[index]+'"/>'});h+=text(720,y-20,'median '+fmt(med(values),3)+'s','label','end')});svg.innerHTML=h}
document.getElementById('controlled-run-table').innerHTML=controlledRuns.map(run=>'<tr><td>'+run.runID+'</td><td>'+run.pipelineMode+'</td><td>'+fmt(ns(run.event?.resources?.elapsedNanos||0),3)+'s</td><td>'+fmt(ns(run.event?.resources?.cpuNanos||0),3)+'s</td><td>'+fmt(mb(run.event?.stagingHardLimit||run.stagingLimits?.stageBytes||0),1)+' MiB</td><td>'+fmt(mb(run.event?.decisionBatchLimit||0),1)+' MiB</td><td>'+fmt(mb(run.event?.estimatedStagingBytes||0),1)+' / '+fmt(mb(run.event?.peakStagedBytes||0),1)+' MiB</td><td>'+fmt(run.event?.pluginCalls||0,0)+'</td><td style="color:'+(run.correct?'var(--green)':'var(--red)')+'">'+(run.correct?'PASS':'FAIL')+'</td></tr>').join('');
const gates=readiness.gates||[],overallReady=readiness.ready===true;document.getElementById('stamp-result').textContent=overallReady?'VALID':'HOLD';document.getElementById('gates').innerHTML=gates.map(g=>'<div class="gate '+(g.passed?'pass':'fail')+'"><i></i><b>'+g.name+'</b><span>'+g.detail+'</span></div>').join('');
document.getElementById('run-table').innerHTML=runs.map(r=>{let cpu=ns((r.primary?.end?.cpuNanos||0)-(r.primary?.start?.cpuNanos||0)),rw=fmt(mb((r.primary?.end?.readBytes||0)-(r.primary?.start?.readBytes||0)),1)+' / '+fmt(mb((r.primary?.end?.writeBytes||0)-(r.primary?.start?.writeBytes||0)),1);return '<tr><td>'+r.runID+'</td><td>'+r.mode+'</td><td>'+fmt(ns(r.primary?.wallNanos),2)+'s</td><td>'+fmt(ns(r.cooldown?.wallNanos),3)+'s</td><td>'+fmt(cpu,2)+'s</td><td>'+fmt(mb(r.primary?.end?.rssBytes),1)+' MiB</td><td>'+rw+'</td><td>'+r.hotMerges+' / '+r.matureMerges+'</td><td style="color:'+(r.correct?'var(--green)':'var(--red)')+'">'+(r.correct?'PASS':'FAIL')+'</td></tr>'}).join('');document.getElementById('identity').textContent='commit '+(d.commit||'unknown')+' · binary '+(d.binarySHA256||'unrecorded').slice(0,12)+' · generated '+(d.generatedAt||'');
</script></main></body></html>`
