"""Reproduce the SYN-ML-M01 dataset, offline training and browser artifacts.
Activate a venv with requirements-ml.txt before running from the Site root.
All data and operating mechanisms are fictional. No real deployment efficacy.
"""
import csv
import hashlib
import json
import platform
from pathlib import Path
import numpy as np
import sklearn
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import brier_score_loss, log_loss

ROOT = Path(__file__).resolve().parents[1]
PUBLIC = ROOT / "public/documents/engineering-lab/ml"
DATA = ROOT / "data/factory/ml"
PUBLIC.mkdir(parents=True, exist_ok=True)
DATA.mkdir(parents=True, exist_ok=True)
SEED = 581037
FEATURES = ["toolCuts", "feedMmRev", "speedMMin", "coolantLMin", "coolantTempC", "setupOffsetUm", "hardnessHB"]
rng = np.random.default_rng(SEED)
batches, pieces = [], []
tool_cuts, reset_at = 0, 8
for i in range(1, 721):
    if i == reset_at:
        tool_cuts = 0
        reset_at += int(rng.integers(6, 11))
    feed, speed, flow = float(rng.choice([.12, .16, .20])), float(rng.choice([120, 150, 180])), float(rng.choice([6, 8, 10]))
    temp, offset, hardness = round(float(rng.normal(24, 2.2)), 2), round(float(rng.uniform(-14, 14)), 2), round(float(rng.normal(211, 10)), 2)
    flow = round(flow + float(rng.normal(0, .18)), 2)
    changed = i >= 625
    actual_flow = flow - (1.2 if changed else 0)
    a, b, c, wear = (feed-.16)/.04, (speed-150)/30, (actual_flow-8)/2, tool_cuts/240
    mean_um = 15 + 50*wear + 12*a + 9*b - 12*c + .9*(temp-24) + offset + .12*(hardness-210) + 24*(a>.5 and b>.5 and actual_flow<8) + 6*rng.normal() + 18*changed
    sigma_um = 10 + 4*wear
    diameters = np.round(25 + (mean_um + sigma_um*rng.normal(size=24))/1000, 5)
    defect_count = int(np.sum((diameters < 24.90) | (diameters > 25.10)))
    inputs = [float(tool_cuts), feed, speed, flow, temp, offset, hardness]
    for j, probability in [(3, .06), (4, .04), (6, .03)]:
        if rng.random() < probability + (.03 if changed else 0): inputs[j] = None
    batch_id = f"SYN-ML-{i:04d}"
    row = {"id": batch_id, "sequence": i, "split": "train" if i<=432 else "validation" if i<=576 else "holdout", "condition": "changed" if changed else "nominal", "features": inputs, "label": int(defect_count>0), "defectCount": defect_count, "pieces": 24}
    batches.append(row)
    for n, diameter in enumerate(diameters, 1):
        pieces.append({"data_origin": "synthetic demonstration", "batch_id": batch_id, "piece": n, "diameter_mm": float(diameter), "nonconforming": int(diameter<24.90 or diameter>25.10)})
    tool_cuts += 24

X = np.array([r["features"] for r in batches], dtype=float)
y = np.array([r["label"] for r in batches])
train, val, test = slice(0,432), slice(432,576), slice(576,720)
assert all(len(np.unique(y[s]))==2 for s in [train,val,test])
imputer = SimpleImputer(strategy="median", add_indicator=True).fit(X[train])
Xt = imputer.transform(X)
scaler = StandardScaler().fit(Xt[train])
Xs = scaler.transform(Xt)
prevalence = float(y[train].mean())
predictions = {"baseline": np.repeat(prevalence, 720)}
models = {"baseline": {"probability": prevalence}}
candidates = []
logistics, trees = [] , []
for c in [.1, 1., 10.]:
    model = LogisticRegression(C=c, max_iter=3000, solver="lbfgs", random_state=SEED).fit(Xs[train], y[train])
    p = model.predict_proba(Xs[val])[:,1]
    score = float(brier_score_loss(y[val],p))
    candidates.append({"model":"logistic", "C":c, "validationBrier":score})
    logistics.append((score,c,model))
_, c, logistic = min(logistics, key=lambda z:z[0])
predictions["logistic"] = logistic.predict_proba(Xs)[:,1]
models["logistic"] = {"C":c, "coefficients":logistic.coef_[0].tolist(), "intercept":float(logistic.intercept_[0]), "iterations":int(logistic.n_iter_[0])}
for depth in [3,4]:
    for leaf in [16,32]:
        model = DecisionTreeClassifier(max_depth=depth,min_samples_leaf=leaf,random_state=SEED).fit(Xt[train], y[train])
        p = model.predict_proba(Xt[val])[:,1]
        score = float(brier_score_loss(y[val],p))
        candidates.append({"model":"tree", "maxDepth":depth, "minLeaf":leaf,"validationBrier":score})
        trees.append((score,depth,leaf,model))
_, depth, leaf, tree = min(trees,key=lambda z:z[0])
t = tree.tree_
predictions["tree"] = tree.predict_proba(Xt)[:,1]
models["tree"] = {"maxDepth":depth,"minLeaf":leaf,"left":t.children_left.tolist(),"right":t.children_right.tolist(),"feature":t.feature.tolist(),"threshold":t.threshold.tolist(),"probability":(t.value[:,0,1]/t.value[:,0,:].sum(axis=1)).tolist(),"samples":t.n_node_samples.tolist()}

def metrics(labels, probabilities, threshold, inspection=18, missed=180):
    flags = probabilities>=threshold
    tp,fp,fn,tn = [int(v) for v in [np.sum(flags & (labels==1)), np.sum(flags & (labels==0)),np.sum(~flags & (labels==1)),np.sum(~flags & (labels==0))]]
    return {"n":len(labels),"tp":tp,"fp":fp,"fn":fn,"tn":tn,"precision":tp/(tp+fp) if tp+fp else None,"recall":tp/(tp+fn) if tp+fn else None,"flagged":tp+fp,"cost":(tp+fp)*inspection+fn*missed,"brier":float(brier_score_loss(labels,probabilities)),"logLoss":float(log_loss(labels,probabilities,labels=[0,1]))}
selected = min(predictions, key=lambda k: brier_score_loss(y[val],predictions[k][val]))
thresholds = [round(i/100,2) for i in range(2,99,2)]
threshold = min(thresholds,key=lambda v:(metrics(y[val],predictions[selected][val],v)["cost"],v))
for r,i in zip(batches,range(720)):
    r["scores"]={k:float(p[i]) for k,p in predictions.items()}

artifact = {"version":"ml-1.0", "seed":SEED,"features":FEATURES,"preprocessing":{"medians":imputer.statistics_.tolist(),"indicatorIndices":imputer.indicator_.features_.tolist(),"means":scaler.mean_.tolist(),"scales":scaler.scale_.tolist(),"trainRanges":[[float(np.nanmin(X[train,j])),float(np.nanmax(X[train,j]))] for j in range(7)]},"models":models,"referencePolicy":{"model":selected,"threshold":threshold,"inspectionCost":18,"missedCost":180,"selection":"lowest validation Brier; validation cost minimum on 0.02–0.98 grid, lower threshold wins a tie"}}
upcoming=[]
for n,f in enumerate([[168,.20,180,6.2,26.4,8,218],[24,.12,120,10,23,-4,205],[120,.16,150,None,25,2,None],[192,.20,150,8,28,10,222],[72,.16,120,8,24,0,210],[48,.12,150,10,22,-8,198]],1):
    upcoming.append({"id":f"SYN-ML-NEXT-{n:02d}","features":f})
evaluation = {"version":"ml-1.0","candidates":candidates,"validation":batches[432:576],"holdout":batches[576:],"upcoming":upcoming,"reference":{k:{"validation":metrics(y[val],p[val],threshold),"holdout":metrics(y[test],p[test],threshold),"nominal":metrics(y[576:624],p[576:624],threshold),"changed":metrics(y[624:],p[624:],threshold)} for k,p in predictions.items()},"splitCounts":[{"split":name,"start":a+1,"end":b,"batches":b-a,"positives":int(y[a:b].sum()),"missingInputs":int(np.isnan(X[a:b]).sum())} for name,a,b in [("train",0,432),("validation",432,576),("holdout",576,720)]]}

def write_json(path,data):
    path.write_text(json.dumps(data,indent=2,allow_nan=False)+"\n")
write_json(DATA/"models.json",artifact)
write_json(DATA/"evaluation.json",evaluation)
write_json(PUBLIC/"trained-models.json",artifact)
write_json(PUBLIC/"evaluation.json",evaluation)
with (PUBLIC/"batch-history.csv").open("w",newline="") as out:
    w=csv.writer(out);w.writerow(["data_origin","batch_id","sequence","split","condition",*FEATURES,"pieces","nonconforming_pieces","batch_nonconformance"])
    for r in batches:w.writerow(["synthetic demonstration",r["id"],r["sequence"],r["split"],r["condition"],*r["features"],24,r["defectCount"],r["label"]])
with (PUBLIC/"piece-inspection.csv").open("w",newline="") as out:
    w=csv.DictWriter(out,fieldnames=pieces[0].keys());w.writeheader();w.writerows(pieces)
write_json(PUBLIC/"reference-predictions.json",[{"id":r["id"],"features":r["features"],"label":r["label"],"scores":r["scores"]} for r in batches])
manifest = {"disclosure":"SYNTHETIC DEMONSTRATION. Fictional operating history, material variation, measurements and changed condition; no employer/customer data.","version":"ml-1.0","seed":SEED,"factory":"SYN-OPS-01","part":"SYN-VS25","machine":"SYN-MC-01","station":"SYN-ST-M01","batchCount":720,"piecesPerBatch":24,"pieceCount":17280,"target":"One or more of 24 final diameters strictly outside 24.90–25.10 mm","cutoff":"Before batch machining begins","features":FEATURES,"excluded":["final diameters","defect count","label","downstream dispositions","batch sequence","split","hidden condition"],"splits":evaluation["splitCounts"],"changedCondition":"Batch 625 onwards: fictional unobserved +18 µm thermal offset and coolant reading overstates actual flow by 1.2 L/min. Missingness also increases. Condition is evaluation metadata, never a model input.","scope":"Separate long synthetic learning history; not an inventory ledger, weekly throughput claim, DOE reuse or confirmation evidence.","versions":{"python":platform.python_version(),"numpy":np.__version__,"sklearn":sklearn.__version__},"sha256":{name:hashlib.sha256((PUBLIC/name).read_bytes()).hexdigest() for name in ["batch-history.csv","piece-inspection.csv","trained-models.json","reference-predictions.json","evaluation.json"]}}
write_json(PUBLIC/"dataset-manifest.json",manifest)
(PUBLIC/"train-manufacturing-ml.py").write_text(Path(__file__).read_text())
(PUBLIC/"requirements-ml.txt").write_text((ROOT/"requirements-ml.txt").read_text())
print(json.dumps({"policy":artifact["referencePolicy"],"splits":evaluation["splitCounts"],"reference":evaluation["reference"]},indent=2))
