"""LearnAI C09.01: dependency-free, bounded JSON model-component runtime.
python3 model-component.py --bundle bundle.json --request request.json
Only inert JSON is read. No pickle, eval, dynamic code, network, training or services.
SHA-256 checks consistency, not publisher identity. Obtain artifacts from a trusted source.
"""
import argparse
import hashlib
import json
import math
from pathlib import Path

FEATURES=['sepal_length','sepal_width','petal_length','petal_width']

def obj(value,keys):
    if not isinstance(value,dict) or set(value)!=set(keys):raise ValueError('Unexpected object fields')
    return value

def number(value,low,high,exclusive=False):
    return type(value) in (float,int) and math.isfinite(value) and (value>low if exclusive else value>=low) and value<=high

def vector(value,low,high,exclusive=False):
    if not isinstance(value,list) or len(value)!=4 or not all(number(x,low,high,exclusive) for x in value):raise ValueError('Expected four bounded numbers')
    return value.copy()

def read_json(path):
    with Path(path).open('rb') as handle:raw=handle.read(16001)
    if len(raw)>16000:raise ValueError('File exceeds 16,000 bytes')
    return json.loads(raw)

def artifact(value):
    f=obj(value,['text','sha256'])
    if not isinstance(f['text'],str) or len(f['text'])>6000 or not isinstance(f['sha256'],str) or hashlib.sha256(f['text'].encode()).hexdigest()!=f['sha256']:raise ValueError('Artifact bytes or digest invalid')
    return json.loads(f['text'])

def load_component(bundle):
    b=obj(bundle,['format','id','contract','preprocess','model'])
    if b['format']!='learnai-component-v1' or b['id']!='iris-virginica-v1':raise ValueError('Unsupported component version')
    c=obj(artifact(b['contract']),['version','features','unit','range','missing','labels','positiveIndex','threshold'])
    if c!={'version':'iris-cm-v1','features':FEATURES,'unit':'cm','range':[0,10],'missing':'training-mean','labels':['other','Iris-virginica'],'positiveIndex':1,'threshold':.5} or type(c['positiveIndex']) is not int or any(type(x) is not int for x in c['range']) or type(c['threshold']) not in (int,float):raise ValueError('Unsupported input/output contract')
    p=obj(artifact(b['preprocess']),['version','contractSha256','fill','mean','scale'])
    if p['version']!='iris-scaler-v1' or p['contractSha256']!=b['contract']['sha256']:raise ValueError('Scaler contract mismatch')
    p={**p,'fill':vector(p['fill'],0,10,True),'mean':vector(p['mean'],0,10,True),'scale':vector(p['scale'],.000001,10,True)}
    m=obj(artifact(b['model']),['version','contractSha256','preprocessSha256','weights','bias'])
    if m['version']!='binary-logistic-v1' or m['contractSha256']!=b['contract']['sha256'] or m['preprocessSha256']!=b['preprocess']['sha256']:raise ValueError('Model scaler/contract mismatch')
    m={**m,'weights':vector(m['weights'],-100,100)}
    if not number(m['bias'],-100,100):raise ValueError('Invalid model bias')
    return c,p,m

def predict(component,request):
    c,p,m=component;r=obj(request,['schema','unit','values'])
    if r['schema']!='iris-cm-v1' or r['unit']!='cm':raise ValueError('Request schema or unit mismatch')
    values=obj(r['values'],FEATURES)
    if any(x is not None and not number(x,0,10,True) for x in values.values()):raise ValueError('Measurements must be null or numbers in (0,10] cm')
    filled=[p['fill'][i] if values[key] is None else values[key] for i,key in enumerate(FEATURES)]
    scaled=[(x-p['mean'][i])/p['scale'][i] for i,x in enumerate(filled)]
    score=sum(x*w for x,w in zip(scaled,m['weights']))+m['bias']
    probability=1/(1+math.exp(-score)) if score>=0 else math.exp(score)/(1+math.exp(score))
    return {'component':'iris-virginica-v1','probability':probability,'label':c['labels'][int(probability>=c['threshold'])],'scaled':scaled,'missing':sum(x is None for x in values.values())}

def main():
    parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--bundle',required=True);parser.add_argument('--request',required=True);args=parser.parse_args()
    try:result=predict(load_component(read_json(args.bundle)),read_json(args.request))
    except (ValueError,TypeError,OSError,OverflowError) as error:parser.error(str(error))
    print(json.dumps(result,allow_nan=False))

if __name__=='__main__':main()
