"""Build an exact R5 source payload on an inspected legacy field baseline."""
import ast
import hashlib
import json
from pathlib import Path
import shutil
import subprocess
import sys

R5_COMMIT = '458d5ead4e1e54e7e39c15f72a9ef5206fa768ed'
MODULES = ('__init__','config','core','decision','enrichment','episode','geometry','motion','rules','runtime','web')


def replace_node(source, node, text):
    lines = source.splitlines(keepends=True)
    return ''.join(lines[:node.lineno-1]) + text.rstrip() + '\n' + ''.join(lines[node.end_lineno:])


def legacy_control(source):
    tree = ast.parse(source)
    node = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == 'command_error')
    expected = ast.parse('''def command_error(error):
    import re
    value = str(error)
    if isinstance(error, ControlError) and re.fullmatch(r'[A-Z][A-Z0-9_]{0,63}', value):
        return value
    return 'RUNTIME_COMMAND_REJECTED'
''').body[0]
    if ast.dump(node) != ast.dump(expected):
        raise ValueError('R5_LEGACY_ERROR_FUNCTION_CHANGED')
    node.body.insert(1, ast.ImportFrom(module='app.fight_r5.config', names=[ast.alias(name='R5ConfigError')], level=0))
    call = next(n for n in ast.walk(node) if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id=='isinstance')
    call.args[1] = ast.Tuple(elts=[ast.Name(id='ControlError',ctx=ast.Load()),ast.Name(id='R5ConfigError',ctx=ast.Load())],ctx=ast.Load())
    return replace_node(source, node, ast.unparse(ast.fix_missing_locations(node)))


def legacy_settings(source):
    nodes = [n for n in ast.walk(ast.parse(source)) if isinstance(n,ast.Assign)
             and len(n.targets)==1 and isinstance(n.targets[0],ast.Name) and n.targets[0].id=='configured']
    expected = ast.parse('configured = parser.parse(dict(current, **value)).settings_payload()').body[0]
    matches = [n for n in nodes if ast.dump(n)==ast.dump(expected)]
    if len(matches)!=1:
        raise ValueError('R5_LEGACY_SETTINGS_FUNCTION_CHANGED')
    node=matches[0];pad=' '*node.col_offset
    text="\n".join(pad+line for line in (
        'merged=dict(current, **value)',
        "if field=='fight' and value.get('engine')=='legacy':merged.pop('r5',None)",
        'configured = parser.parse(merged).settings_payload()'))
    return replace_node(source,node,text)


def package(repo, root):
    repo,root=Path(repo),Path(root)
    payload=root/'package/payload';payload.mkdir(parents=True,exist_ok=False)
    before=json.loads((root/'baseline.json').read_text())['source_hashes']
    paths=[f'app/fight_r5/{name}.py' for name in MODULES]+['app/fight_runtime.py','app/scenario_parameters.py']
    after={}
    for name in paths:
        raw=subprocess.check_output(['git','show',f'{R5_COMMIT}:{name}'],cwd=repo)
        path=payload/name;path.parent.mkdir(parents=True,exist_ok=True);path.write_bytes(raw)
    for name,transform in (('app/secure_control.py',legacy_control),('app/stream_settings.py',legacy_settings)):
        original=(root/'baseline'/name).read_bytes()
        if hashlib.sha256(original).hexdigest()!=before[name]:raise ValueError('R5_BASELINE_COPY_CHANGED')
        (payload/name).write_text(transform(original.decode()))
    for path in payload.rglob('*.py'):
        ast.parse(path.read_text());path.chmod(0o644)
        after[str(path.relative_to(payload))]=hashlib.sha256(path.read_bytes()).hexdigest()
    preflight=json.loads((root/'preflight.json').read_text())
    manifest=dict(server='icerti-ai2',source_commit=R5_COMMIT,
        images={k:v['image'] for k,v in preflight['services'].items()},before=before,after=after,
        policy='Existing and new Web channels use R5; On/Off and non-FIGHT settings are preserved')
    (root/'package/manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
    for name in ('fight_r5_package.py','fight_r5_install.py','fight_r5_settings.py','trace_install.py','artifact_install.py'):
        shutil.copy2(repo/'tools/icerti_offline'/name,root/'package'/name)
    tests=root/'package/tests';tests.mkdir()
    for name in ('test_fight_r5.py','test_fight_r5_site.py','test_fight.py','test_loitering_settings.py','test_event_cooldown.py'):
        shutil.copy2(repo/'tests'/name,tests/name)
    print(json.dumps(dict(files=len(after),source_commit=R5_COMMIT)))


if __name__=='__main__':
    package(*sys.argv[1:])
