# -*- coding: utf-8 -*-
"""原创离线合成样例；不连接模型、不读取账户。输出位于脚本旁。
已有输入保留，修改输入后可以重跑。失败边界由 --negative 单独演示。
"""
from pathlib import Path
import sys, json, csv, math, io, hashlib, html, base64, re
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
BASE = Path(__file__).resolve().parent
NEG = '--negative' in sys.argv
def write(name, value):
    p=BASE/name; p.parent.mkdir(parents=True,exist_ok=True)
    if isinstance(value,bytes): p.write_bytes(value)
    else: p.write_text(value,encoding='utf-8',newline='')
def seed(name,value):
    if not (BASE/name).exists(): write(name,value)
def jwrite(name,value): write(name,json.dumps(value,ensure_ascii=False,indent=2))
def jseed(name,value):
    if not (BASE/name).exists(): jwrite(name,value)
def load(name): return json.loads((BASE/name).read_text(encoding='utf-8'))
def table(name,heads,rows):
    f=io.StringIO(newline='');w=csv.writer(f);w.writerow(heads);w.writerows(rows);write(name,f.getvalue())
def page(name,title,body):
    write(name,'<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>'+html.escape(title)+'</title><style>body{max-width:1000px;margin:2rem auto;padding:0 1rem;font:17px/1.7 system-ui}table{border-collapse:collapse}td,th{border:1px solid #aaa;padding:.5rem}pre{white-space:pre-wrap;overflow-wrap:anywhere}img,svg{max-width:100%;height:auto}a{color:#125caa}</style><h1>'+html.escape(title)+'</h1>'+body+'</html>')
def emit(data): print(json.dumps(data,ensure_ascii=False))

seed('tasks.csv','task,duration,predecessors\nA,2,\nB,3,A\nC,4,A\nD,2,B;C\n')
tasks={r['task']:{'d':int(r['duration']),'pre':[x for x in r['predecessors'].split(';') if x]} for r in csv.DictReader((BASE/'tasks.csv').open())}
if NEG:tasks['A']['pre']=['D']
order=[];es={};ef={}
while len(order)<len(tasks):
    ready=[k for k,v in tasks.items() if k not in order and all(p in order for p in v['pre'])]
    if not ready:raise ValueError('依赖有环或引用不存在的任务')
    for k in sorted(ready):es[k]=max([ef[p] for p in tasks[k]['pre']],default=0);ef[k]=es[k]+tasks[k]['d'];order.append(k)
finish=max(ef.values());ls={};lf={}
for k in reversed(order):
    succ=[s for s in tasks if k in tasks[s]['pre']];lf[k]=min([ls[s] for s in succ],default=finish);ls[k]=lf[k]-tasks[k]['d']
rows=[[k,tasks[k]['d'],es[k],ef[k],ls[k],lf[k],ls[k]-es[k]] for k in order];table('schedule.csv',['task','duration','ES','EF','LS','LF','float'],rows)
write('gantt.mmd','gantt\n  dateFormat X\n  axisFormat %s\n'+''.join(f'  {k} : {es[k]}, {tasks[k]["d"]}s\n' for k in order));emit({'finish':finish,'critical':[k for k in order if ls[k]==es[k]],'schedule':rows})
