import argparse, pathlib, re, hashlib, subprocess, os, json, datetime
ap=argparse.ArgumentParser()
ap.add_argument('--root',action='append',required=True)
ap.add_argument('--php',required=True)
ap.add_argument('--apply',action='store_true')
ap.add_argument('--dev-store')
ap.add_argument('--dev-api')
args=ap.parse_args()
stamp=datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
base=pathlib.Path('/root/incident-redis-alerts-20260926')/stamp
changes=[]
def guard(s,method,body,required=True):
    pat=r'(public\s+(?:static\s+)?function\s+'+method+r'\([^\n]*\)(?:\s*:\s*bool)?\s*\{)'
    matches=list(re.finditer(pat,s))
    if not matches and not required:return s
    if len(matches)!=1:raise RuntimeError('nonunique method '+method)
    m=matches[0]
    if 'redis-alert-isolation-20260926' in s[m.end():m.end()+350]:return s
    return s[:m.end()]+'\n        // redis-alert-isolation-20260926\n'+body+s[m.end():]
def add(p,new):
    old=p.read_bytes().decode()
    if old!=new:changes.append((p,old,new))
for root in args.root:
    root=pathlib.Path(root)
    p=root/'application/common/QwRobot.php';s=p.read_bytes().decode()
    if '$useQueue' not in s:raise RuntimeError('unsupported robot signature')
    s=guard(s,'pushMsg','        if ($useQueue) {\n            return true;\n        }\n')
    s=guard(s,'pushMsgFormat','        if ($useQueue) {\n            return true;\n        }\n')
    add(p,s)
    p=root/'application/common/Serve.php';s=p.read_bytes().decode()
    for method in ['push','pushJump','pushBatch']:
        s=guard(s,method,"        if ($action === 'sendQwRobot') {\n            return true;\n        }\n",method!='pushBatch')
    add(p,s)
    p=root/'application/common/queue/Serve.php';s=p.read_bytes().decode()
    s=guard(s,'fire',"        if (($data['action'] ?? '') === 'sendQwRobot') {\n            $job->delete();\n            return;\n        }\n")
    add(p,s)
    p=root/'application/common/service/Serve.php';s=p.read_bytes().decode()
    s=guard(s,'doJob',"        if (($data['action'] ?? '') === 'sendQwRobot') {\n            return true;\n        }\n")
    add(p,s)
if args.dev_store:
    p=pathlib.Path(args.dev_store)/'application/config/dev/zhct_dev/config.php';s=p.read_bytes().decode()
    pat=r"(\$config\['redis'\]\s*=\s*\[)(.*?)(\];)"
    ms=list(re.finditer(pat,s,re.S))
    if len(ms)!=1:raise RuntimeError('redis config anchor mismatch')
    m=ms[0];block=m[2]
    for key,value in [('host',"'127.0.0.1'"),('password',"''")]:
        block,n=re.subn(r"('"+key+r"'\s*=>\s*)[^,\n]+",lambda x:x[1]+value,block)
        if n!=1:raise RuntimeError('redis field mismatch '+key)
    add(p,s[:m.start(2)]+block+s[m.end(2):])
if args.dev_api:
    for envfile in ['.env.dev','.env.test']:
        p=pathlib.Path(args.dev_api)/envfile
        if not p.exists():continue
        s=p.read_bytes().decode();ms=list(re.finditer(r'(\[REDIS\])([^\[]+)',s))
        if len(ms)!=1:raise RuntimeError('env redis anchor mismatch')
        m=ms[0];block=m[2]
        for key,value in [('HOSTNAME','127.0.0.1'),('PASSWORD','')]:
            block,n=re.subn(r'(?:^|(?<=[\r\n]))('+key+r'[ \t]*=)[^\r\n]*',lambda x:x[1]+' '+value,block)
            if n!=1:raise RuntimeError('env field mismatch '+key)
        add(p,s[:m.start(2)]+block+s[m.end(2):])
base.mkdir(parents=True,mode=0o700)
manifest=[]
for i,(p,old,new) in enumerate(changes):
    stage=base/(str(i)+'.candidate'+p.suffix);stage.write_bytes(new.encode());stage.chmod(0o600)
    if p.suffix=='.php':
        check=subprocess.run([args.php,'-l',str(stage)],capture_output=True,text=True)
        if check.returncode:raise RuntimeError('lint failed '+str(p))
    manifest.append({'path':str(p),'before_sha256':hashlib.sha256(old.encode()).hexdigest(),'after_sha256':hashlib.sha256(new.encode()).hexdigest()})
if args.apply:
    done=[]
    try:
        for i,(p,old,new) in enumerate(changes):
            if p.read_bytes()!=old.encode():raise RuntimeError('file changed during preflight '+str(p))
            backup=base/(str(i)+'.original');backup.write_bytes(old.encode());backup.chmod(0o600)
            st=p.stat();tmp=p.with_name(p.name+'.redis-isolation-tmp')
            tmp.write_bytes(new.encode());os.chmod(str(tmp),st.st_mode&0o7777);os.chown(str(tmp),st.st_uid,st.st_gid);os.replace(str(tmp),str(p));done.append((p,old))
    except BaseException:
        for p,old in reversed(done):p.write_bytes(old.encode())
        raise
(base/'manifest.json').write_text(json.dumps(manifest,indent=2))
print(json.dumps({'applied':args.apply,'backup_dir':str(base),'changed_file_count':len(manifest)},ensure_ascii=False))
