import argparse, json, re, subprocess, time
from pathlib import Path

CAT=Path('C:/MCSJAgent/foster-workflow-catalog/catalog.json')
REG=Path('C:/MCSJAgent/foster-workflow-catalog/priority-task-registry.json')
FX=['python','C:/MCSJAgent/mcsj-fast-fx.py','state']

def toks(s):
    out=set()
    for x in re.findall(r'[a-z0-9]+',s.lower()):
        if len(x)<=2 or x in {'mcsj','the','and','for','with','from','into','setup','maintenance'}: continue
        out.add(x)
        # Lightweight operational stemming: reverse/reversing/reversed, entries/entry, payments/payment.
        y=x
        for suffix in ('ing','ed','es','s'):
            if y.endswith(suffix) and len(y)-len(suffix)>=4:
                y=y[:-len(suffix)]; break
        if y.endswith('i') and x.endswith('ies'): y=y[:-1]+'y'
        out.add(y)
    return out

def compact_state():
    t=time.perf_counter()
    p=subprocess.run(FX,capture_output=True,text=True,timeout=8)
    ms=round((time.perf_counter()-t)*1000,1)
    try: d=json.loads(p.stdout)
    except Exception: return {'ok':False,'ms':ms,'error':p.stderr.strip() or p.stdout[:200]}
    panels=[]
    for x in d.get('panels',[]):
        fields=[c.get('text','').strip() for c in x.get('controls',[]) if c.get('text','').strip() and ('TextField' in c.get('class','') or 'ComboBox' in c.get('class',''))]
        panels.append({'class':x.get('panelClass') or x.get('class'),'values':fields[:12]})
    return {'ok':p.returncode==0,'ms':ms,'database':d.get('databaseTitle') or d.get('title'),'panels':panels}

def search(q,limit):
    data=json.loads(CAT.read_text(encoding='utf-8')); qt=toks(q); out=[]
    # Curated task cards outrank broad corpus similarity. This prevents nearby conceptual docs
    # from beating the exact Foster workflow (for example PILOT printing vs utility register).
    if REG.exists():
        registry=json.loads(REG.read_text(encoding='utf-8'))
        for x in registry.get('tasks',[]):
            best=0
            for alias in x.get('aliases',[]):
                at=toks(alias); overlap=len(qt & at)
                coverage=overlap/max(1,len(at)); querycov=overlap/max(1,len(qt))
                exact=1 if q.strip().lower()==alias.strip().lower() else 0
                score=1000*exact+500*min(coverage,querycov)+10*overlap
                best=max(best,score)
            if best>=250:
                out.append({'score':round(best,1),'module':x['module'],'task':x['task'],'source':x['source'],'navigation':'','recipe_readiness':5,'registry_id':x['id'],'curated':True})
    for x in data['items']:
        if not x.get('operational'): continue
        text=' '.join([x.get('title',''),x.get('module',''),x.get('navigation',''),' '.join(x.get('sections',[])),' '.join(x.get('sources',[]))])
        tt=toks(text); overlap=len(qt & tt)
        phrase=3 if q.lower() in text.lower() else 0
        score=overlap*5+phrase+x.get('recipe_score',0)
        if score<=x.get('recipe_score',0) and qt: continue
        src=next((s for s in x['sources'] if s.lower().endswith(('.json','.md'))),x['sources'][0])
        out.append({'score':score,'module':x['module'],'task':x['title'],'source':src,'navigation':x.get('navigation',''),'recipe_readiness':x.get('recipe_score',0),'curated':False})
    return sorted(out,key=lambda x:(not x.get('curated',False),-x['score'],-x['recipe_readiness'],x['task']))[:limit]

ap=argparse.ArgumentParser(description='Prime live MCSJ and retrieve best Foster workflows fast.')
ap.add_argument('query'); ap.add_argument('--limit',type=int,default=5); ap.add_argument('--no-state',action='store_true')
a=ap.parse_args(); started=time.perf_counter()
result={'query':a.query,'matches':search(a.query,a.limit)}
if not a.no_state: result['live']=compact_state()
result['total_ms']=round((time.perf_counter()-started)*1000,1)
print(json.dumps(result,indent=2))
