#!/usr/bin/env python3
"""Pipeline Lab server — serves notebook UI on :3500, proxies API to sidecar :3200."""

import http.server
import json
import urllib.request
import urllib.error

PORT = 3500
SIDECAR = "http://localhost:3200"


class LabHandler(http.server.BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        pass  # quiet

    def do_GET(self):
        if self.path == "/" or self.path == "":
            self._serve_ui()
        elif self.path.startswith("/lab/"):
            self._proxy("GET")
        else:
            self.send_error(404)

    def do_POST(self):
        if self.path.startswith("/lab/"):
            self._proxy("POST")
        else:
            self.send_error(404)

    def do_DELETE(self):
        if self.path.startswith("/lab/"):
            self._proxy("DELETE")
        else:
            self.send_error(404)

    def _proxy(self, method):
        """Proxy request to sidecar."""
        url = SIDECAR + self.path
        body = None
        if method in ("POST", "PUT"):
            length = int(self.headers.get("Content-Length", 0))
            body = self.rfile.read(length) if length else None

        req = urllib.request.Request(url, data=body, method=method)
        req.add_header("Content-Type", self.headers.get("Content-Type", "application/json"))

        try:
            with urllib.request.urlopen(req, timeout=120) as resp:
                data = resp.read()
                self.send_response(resp.status)
                self.send_header("Content-Type", resp.headers.get("Content-Type", "application/json"))
                self.send_header("Access-Control-Allow-Origin", "*")
                self.end_headers()
                self.wfile.write(data)
        except urllib.error.HTTPError as e:
            self.send_response(e.code)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(e.read())
        except Exception as e:
            self.send_response(502)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps({"error": str(e)}).encode())

    def _serve_ui(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(HTML.encode())


HTML = r"""<!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Pipeline Lab &#x2014; Lakehouse</title>
<style>
:root{--bg:#08090c;--surface:rgba(14,16,22,0.9);--border:#2a2d35;--text:#e8e6e3;--text2:#7a7872;--accent:#4ade80;--gold:#e2b55a;--red:#e05252;--blue:#5b9cf5;--purple:#c084fc}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'SF Mono','Menlo','Consolas',monospace;background:var(--bg);color:var(--text);min-height:100vh;padding:20px 28px;font-size:13px}
h1{font-size:18px;font-weight:700;margin-bottom:4px}h1 span{color:var(--accent)}
.subtitle{color:var(--text2);font-size:11px;margin-bottom:20px}
.nav{display:flex;gap:8px;margin-bottom:16px;font-size:10px}
.nav a{color:var(--text2);text-decoration:none;padding:4px 10px;border:1px solid var(--border);border-radius:3px}
.nav a:hover{border-color:var(--accent);color:var(--accent)}
.cells{display:flex;flex-direction:column;gap:12px;max-width:1100px}
.cell{background:var(--surface);border:1px solid var(--border);border-radius:4px;overflow:hidden}
.cell.running{border-color:var(--gold);animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{border-color:var(--gold)}50%{border-color:var(--border)}}
.cell-header{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--border);font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--text2)}
.cell-type{font-weight:700}
.cell-time{margin-left:auto}
.cell-input{padding:12px;background:rgba(0,0,0,0.3)}
.cell-input textarea{width:100%;min-height:60px;background:transparent;border:none;color:var(--text);font-family:inherit;font-size:13px;resize:vertical;outline:none;line-height:1.6}
.cell-output{padding:12px;font-size:12px;line-height:1.6;white-space:pre-wrap;max-height:500px;overflow-y:auto;display:none}
.cell-output.has-data{display:block;border-top:1px solid var(--border)}
.toolbar{display:flex;gap:6px;padding:8px 12px;border-top:1px solid var(--border);flex-wrap:wrap}
.btn{font-family:inherit;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;padding:5px 12px;border:1px solid var(--border);border-radius:3px;background:transparent;color:var(--text2);cursor:pointer;transition:all 0.15s}
.btn:hover{border-color:var(--accent);color:var(--accent)}
.btn.primary{border-color:var(--accent);color:var(--accent);background:rgba(74,222,128,0.06)}
.btn.gold{border-color:var(--gold);color:var(--gold)}
.btn.blue{border-color:var(--blue);color:var(--blue)}
.btn.purple{border-color:var(--purple);color:var(--purple)}
.btn.red{border-color:var(--red);color:var(--red)}
.top-bar{display:flex;gap:8px;margin-bottom:16px;align-items:center;flex-wrap:wrap}
.status-bar{display:flex;gap:12px;padding:8px 12px;background:var(--surface);border:1px solid var(--border);border-radius:4px;margin-bottom:16px;font-size:10px;color:var(--text2)}
.stat{display:flex;align-items:center;gap:4px}.stat b{color:var(--text)}
.result-row{display:flex;gap:8px;padding:6px 8px;border-bottom:1px solid rgba(42,45,53,0.3);align-items:center;font-size:11px}
.result-row:last-child{border-bottom:none}
.score-bar{width:60px;height:5px;background:rgba(0,0,0,0.2);border-radius:3px;overflow:hidden}
.score-fill{height:100%;border-radius:3px}
.threshold-slider{display:flex;align-items:center;gap:8px;padding:0 12px;margin:4px 0}
.threshold-slider input[type=range]{flex:1;accent-color:var(--accent)}
.threshold-slider .val{font-weight:700;min-width:36px;text-align:right}
</style></head><body>
<h1><span>Pipeline Lab</span> // Lakehouse</h1>
<div class="subtitle">Embedding-based screening vs LLM classification &#x2014; iterative experimentation</div>
<div class="nav">
  <a href="http://192.168.1.177:3100">Gateway :3100</a>
  <a href="http://192.168.1.177:3300">UI :3300</a>
  <a href="http://192.168.1.177:3400">Observer :3400</a>
</div>

<div class="status-bar" id="status-bar">
  <div class="stat"><span>Exemplars:</span> <b id="st-exemplars">0</b></div>
  <div class="stat"><span>Categories:</span> <b id="st-categories">0</b></div>
  <div class="stat"><span>Pipelines:</span> <b id="st-pipelines">0</b></div>
  <div class="stat" style="margin-left:auto"><span>Sidecar:</span> <b id="st-health" style="color:var(--text2)">...</b></div>
</div>

<div class="top-bar">
  <button class="btn primary" onclick="addCell('exemplars')">+ Exemplars</button>
  <button class="btn gold" onclick="addCell('screen')">+ Screen</button>
  <button class="btn blue" onclick="addCell('classify')">+ Classify</button>
  <button class="btn purple" onclick="addCell('benchmark')">+ Benchmark</button>
  <button class="btn" onclick="addCell('similarity')">+ Similarity</button>
  <button class="btn" onclick="addCell('generate')">+ Generate</button>
  <button class="btn" onclick="addCell('pipeline')">+ Pipeline</button>
  <span style="flex:1"></span>
  <button class="btn red" onclick="clearCells()">Clear All</button>
</div>

<div class="cells" id="cells"></div>

<script>
var cellCounter = 0;

function esc(t){var d=document.createElement('span');d.textContent=String(t);return d.innerHTML}

async function api(path, body) {
  var opts = body ? {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)} : {};
  var r = await fetch('/lab' + path, opts);
  if (!r.ok) { var e = await r.json().catch(function(){return {error:'request failed'}}); throw new Error(e.error || r.statusText); }
  return r.json();
}

async function refreshStatus() {
  try {
    var ex = await api('/exemplars');
    var pl = await api('/pipelines');
    document.getElementById('st-exemplars').textContent = ex.total || 0;
    document.getElementById('st-categories').textContent = Object.keys(ex.categories || {}).length;
    document.getElementById('st-pipelines').textContent = (pl.pipelines || []).length;
    document.getElementById('st-health').textContent = 'connected';
    document.getElementById('st-health').style.color = 'var(--accent)';
  } catch(e) {
    document.getElementById('st-health').textContent = 'error: ' + e.message;
    document.getElementById('st-health').style.color = 'var(--red)';
  }
}

var placeholders = {
  exemplars:'Category: decision\n---\nWe decided to use Parquet for all storage\nThe team chose React over Vue\nArchitecture decision: microservices',
  screen:'We decided to migrate to PostgreSQL\nThe weather is nice today\nArchitecture: chose event sourcing over CRUD\nLunch was great',
  classify:'We decided to migrate to PostgreSQL\nThe weather is nice today\nArchitecture: chose event sourcing',
  benchmark:'We decided to use Kubernetes for orchestration\nThe new hire starts Monday\nTechnical debt: refactor the auth module\nLunch menu looks good today',
  similarity:'We chose React for the frontend\nReact was selected as our UI framework\nThe database uses PostgreSQL',
  generate:'Enter a prompt for the LLM...',
  pipeline:'Pipeline name: my-extraction\n---\nscreen | threshold=0.6\nclassify\nextract | prompt=Extract the key decision and rationale\nvalidate | dedup_threshold=0.9'
};
var colors = {exemplars:'var(--accent)',screen:'var(--gold)',classify:'var(--blue)',benchmark:'var(--purple)',similarity:'var(--text2)',generate:'var(--text2)',pipeline:'var(--accent)'};
var labels = {exemplars:'EXEMPLARS',screen:'SCREEN',classify:'CLASSIFY (LLM)',benchmark:'BENCHMARK A/B',similarity:'SIMILARITY',generate:'GENERATE',pipeline:'PIPELINE'};

function addCell(type) {
  var id = 'cell-' + (++cellCounter);
  var cells = document.getElementById('cells');
  var cell = document.createElement('div'); cell.className = 'cell'; cell.id = id;

  var header = document.createElement('div'); header.className = 'cell-header';
  var typeSpan = document.createElement('span'); typeSpan.className = 'cell-type'; typeSpan.style.color = colors[type]||'var(--text2)'; typeSpan.textContent = labels[type]||type; header.appendChild(typeSpan);
  var numSpan = document.createElement('span'); numSpan.textContent = 'Cell #' + cellCounter; header.appendChild(numSpan);
  var timeSpan = document.createElement('span'); timeSpan.className = 'cell-time'; timeSpan.id = id+'-time'; header.appendChild(timeSpan);
  cell.appendChild(header);

  var inputDiv = document.createElement('div'); inputDiv.className = 'cell-input';
  var ta = document.createElement('textarea'); ta.id = id+'-input'; ta.placeholder = placeholders[type]||''; ta.value = placeholders[type]||'';
  inputDiv.appendChild(ta); cell.appendChild(inputDiv);

  if (type === 'screen' || type === 'benchmark') {
    var sl = document.createElement('div'); sl.className = 'threshold-slider';
    var slLbl = document.createElement('span'); slLbl.style.cssText='font-size:10px;color:var(--text2)'; slLbl.textContent='Threshold:'; sl.appendChild(slLbl);
    var range = document.createElement('input'); range.type='range'; range.min='0.3'; range.max='0.95'; range.step='0.05'; range.value='0.65'; range.id=id+'-threshold';
    var valSpan = document.createElement('span'); valSpan.className='val'; valSpan.textContent='0.65';
    range.oninput=function(){valSpan.textContent=this.value};
    sl.appendChild(range); sl.appendChild(valSpan); cell.appendChild(sl);
  }

  var outputDiv = document.createElement('div'); outputDiv.className='cell-output'; outputDiv.id=id+'-output'; cell.appendChild(outputDiv);

  var tb = document.createElement('div'); tb.className='toolbar';
  var runBtn = document.createElement('button'); runBtn.className='btn primary'; runBtn.textContent='Run'; runBtn.onclick=function(){runCell(id,type)}; tb.appendChild(runBtn);
  var rmBtn = document.createElement('button'); rmBtn.className='btn red'; rmBtn.textContent='Remove'; rmBtn.onclick=function(){cell.remove()}; tb.appendChild(rmBtn);
  cell.appendChild(tb);

  cells.appendChild(cell); ta.focus();
}

function clearCells(){document.getElementById('cells').textContent='';cellCounter=0}
function parseLines(text){return text.split('\n').map(function(l){return l.trim()}).filter(function(l){return l&&l.charAt(0)!=='#'})}

async function runCell(id, type) {
  var cell = document.getElementById(id);
  var input = document.getElementById(id+'-input').value;
  var output = document.getElementById(id+'-output');
  var timeEl = document.getElementById(id+'-time');
  cell.classList.add('running');
  output.className='cell-output has-data'; output.textContent='Running...'; output.style.color='';
  try {
    var t0=performance.now(), result;

    if (type==='exemplars') {
      var parts=input.split('---'); var catLine=(parts[0]||'').trim();
      var category=catLine.replace(/^category:\s*/i,'').trim().toLowerCase();
      var texts=parseLines(parts.slice(1).join('\n'));
      if(!category||!texts.length){output.textContent='Format: Category: name\\n---\\ntext1\\ntext2';return}
      result=await api('/exemplars',{category:category,texts:texts});
      output.textContent='Added '+result.added+' exemplars to "'+result.category+'" (total: '+result.total+')';
      output.style.color='var(--accent)'; refreshStatus();
    }
    else if (type==='screen') {
      var texts=parseLines(input);
      var threshold=parseFloat((document.getElementById(id+'-threshold')||{}).value||'0.65');
      result=await api('/screen',{texts:texts,threshold:threshold});
      renderScreen(output,result,threshold);
    }
    else if (type==='classify') {
      var texts=parseLines(input); result=await api('/classify',{texts:texts});
      renderClassify(output,result);
    }
    else if (type==='benchmark') {
      var texts=parseLines(input);
      var threshold=parseFloat((document.getElementById(id+'-threshold')||{}).value||'0.65');
      result=await api('/benchmark',{texts:texts,threshold:threshold});
      renderBenchmark(output,result);
    }
    else if (type==='similarity') {
      var texts=parseLines(input); result=await api('/cell',{action:'similarity',texts:texts});
      renderMatrix(output,result);
    }
    else if (type==='generate') {
      result=await api('/cell',{action:'generate',text:input});
      output.textContent=result.text||'(empty)';
    }
    else if (type==='pipeline') {
      var parts=input.split('---'); var pName=(parts[0]||'').trim().replace(/^pipeline\s*name:\s*/i,'').trim();
      var stageLines=parseLines(parts.slice(1).join('\n'));
      var stages=stageLines.map(function(line){
        var ps=line.split('|').map(function(s){return s.trim()});var config={};
        ps.slice(1).forEach(function(p){var kv=p.split('=');if(kv.length===2){var v=kv[1].trim();config[kv[0].trim()]=isNaN(parseFloat(v))?v:parseFloat(v)}});
        return{name:ps[0],mode:ps[0],config:config};
      });
      await api('/pipelines',{name:pName,stages:stages,description:'Pipeline Lab'});
      output.textContent='Pipeline "'+pName+'" saved ('+stages.length+' stages)';
      output.style.color='var(--accent)'; refreshStatus();
    }
    timeEl.textContent=Math.round(performance.now()-t0)+'ms'+(result&&result.time_ms?' (server: '+result.time_ms+'ms)':'');
  } catch(e){output.textContent='Error: '+e.message;output.style.color='var(--red)'} finally{cell.classList.remove('running')}
}

function renderScreen(el,results,threshold){
  el.textContent='';
  results.forEach(function(r){
    var row=document.createElement('div');row.className='result-row';
    var cat=document.createElement('span');cat.style.cssText='min-width:90px;font-weight:700;color:'+(r.above_threshold?'var(--accent)':'var(--text2)');cat.textContent=r.best_category||'none';row.appendChild(cat);
    var sim=document.createElement('span');sim.style.cssText='min-width:50px;font-weight:700;color:'+(r.similarity>=0.7?'var(--accent)':r.similarity>=threshold?'var(--gold)':'var(--text2)');sim.textContent=(r.similarity*100).toFixed(1)+'%';row.appendChild(sim);
    var bar=document.createElement('div');bar.className='score-bar';var fill=document.createElement('div');fill.className='score-fill';fill.style.width=(r.similarity*100)+'%';fill.style.background=r.similarity>=0.7?'var(--accent)':r.similarity>=threshold?'var(--gold)':'var(--red)';bar.appendChild(fill);row.appendChild(bar);
    var txt=document.createElement('span');txt.style.cssText='flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';txt.textContent=r.text;row.appendChild(txt);
    var badge=document.createElement('span');badge.style.cssText='font-size:9px;padding:2px 6px;border-radius:2px;border:1px solid;'+(r.above_threshold?'color:var(--accent);border-color:var(--accent)':'color:var(--text2);border-color:var(--border)');badge.textContent=r.above_threshold?'PASS':'FILTERED';row.appendChild(badge);
    el.appendChild(row);
  });
}

function renderClassify(el,results){
  el.textContent='';
  results.forEach(function(r){
    var row=document.createElement('div');row.className='result-row';
    var cat=document.createElement('span');cat.style.cssText='min-width:90px;font-weight:700;color:var(--blue)';cat.textContent=r.category;row.appendChild(cat);
    var conf=document.createElement('span');conf.style.cssText='min-width:50px;font-size:10px;color:'+(r.confidence==='high'?'var(--accent)':r.confidence==='medium'?'var(--gold)':'var(--text2)');conf.textContent=r.confidence;row.appendChild(conf);
    var txt=document.createElement('span');txt.style.flex='1';txt.textContent=r.text;row.appendChild(txt);
    el.appendChild(row);
  });
}

function renderBenchmark(el,result){
  el.textContent='';
  var summary=document.createElement('div');summary.style.cssText='display:flex;gap:12px;margin-bottom:12px;flex-wrap:wrap';
  [['Agreement',(result.agreement_rate*100).toFixed(1)+'%',result.agreement_rate>=0.8?'var(--accent)':'var(--gold)'],
   ['Speedup',result.speedup+'x',result.speedup>=2?'var(--accent)':'var(--text)'],
   ['Embed',result.embed_time_ms+'ms','var(--gold)'],
   ['LLM',result.llm_time_ms+'ms','var(--blue)'],
   ['Hybrid',result.hybrid_estimated_ms+'ms','var(--accent)'],
   ['Screened',result.texts_screened_out+'/'+result.total_texts,'var(--purple)']
  ].forEach(function(s){
    var box=document.createElement('div');box.style.cssText='background:rgba(0,0,0,0.2);padding:8px 12px;border-radius:3px;text-align:center';
    var lbl=document.createElement('div');lbl.style.cssText='font-size:9px;color:var(--text2);text-transform:uppercase;letter-spacing:0.5px';lbl.textContent=s[0];box.appendChild(lbl);
    var val=document.createElement('div');val.style.cssText='font-size:18px;font-weight:700;color:'+s[2];val.textContent=s[1];box.appendChild(val);
    summary.appendChild(box);
  });
  el.appendChild(summary);
  // Side by side
  var grid=document.createElement('div');grid.style.cssText='display:grid;grid-template-columns:1fr 1fr;gap:12px';
  [[result.embed_results||[],'EMBEDDING ('+result.embed_time_ms+'ms)','var(--gold)',true],
   [result.llm_results||[],'LLM ('+result.llm_time_ms+'ms)','var(--blue)',false]
  ].forEach(function(col){
    var div=document.createElement('div');div.style.cssText='background:rgba(0,0,0,0.2);border-radius:3px;padding:10px';
    var title=document.createElement('div');title.style.cssText='font-size:10px;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;font-weight:700;color:'+col[2];title.textContent=col[1];div.appendChild(title);
    col[0].forEach(function(r){
      var row=document.createElement('div');row.style.cssText='font-size:11px;padding:3px 0;display:flex;gap:6px;align-items:center';
      var c=document.createElement('span');c.style.cssText='min-width:70px;font-weight:700;color:'+(col[3]?(r.above_threshold?'var(--accent)':'var(--text2)'):'var(--blue)');c.textContent=(col[3]?r.best_category:r.category)||'none';row.appendChild(c);
      var s=document.createElement('span');s.style.cssText='min-width:40px;color:var(--text2)';s.textContent=col[3]?(r.similarity*100).toFixed(0)+'%':r.confidence;row.appendChild(s);
      var t=document.createElement('span');t.style.cssText='flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';t.textContent=r.text;row.appendChild(t);
      div.appendChild(row);
    });
    grid.appendChild(div);
  });
  el.appendChild(grid);
}

function renderMatrix(el,result){
  el.textContent='';
  var matrix=result.matrix||[],texts=result.texts||[];
  if(!matrix.length){el.textContent='No results';return}
  var tbl=document.createElement('table');tbl.style.cssText='border-collapse:collapse;font-size:11px;width:100%';
  var hdr=document.createElement('tr');var corner=document.createElement('th');hdr.appendChild(corner);
  texts.forEach(function(t){var th=document.createElement('th');th.style.cssText='padding:4px;color:var(--text2);font-size:9px;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';th.textContent=t.substring(0,20);th.title=t;hdr.appendChild(th)});
  tbl.appendChild(hdr);
  matrix.forEach(function(row,i){
    var tr=document.createElement('tr');
    var td0=document.createElement('td');td0.style.cssText='padding:4px;color:var(--text2);font-size:9px';td0.textContent=texts[i].substring(0,20);tr.appendChild(td0);
    row.forEach(function(v,j){
      var td=document.createElement('td');
      td.style.cssText='padding:4px;text-align:center;font-weight:'+(v>=0.7?'700':'400')+';color:'+(v>=0.8?'var(--accent)':v>=0.6?'var(--gold)':'var(--text2)')+';background:'+(i===j?'rgba(74,222,128,0.1)':v>=0.8?'rgba(74,222,128,0.15)':v>=0.6?'rgba(226,181,90,0.1)':'transparent');
      td.textContent=v.toFixed(2);tr.appendChild(td);
    });
    tbl.appendChild(tr);
  });
  el.appendChild(tbl);
}

refreshStatus();
</script>
</body></html>"""


if __name__ == "__main__":
    print(f"Pipeline Lab running on http://0.0.0.0:{PORT}")
    server = http.server.HTTPServer(("0.0.0.0", PORT), LabHandler)
    server.serve_forever()
