"""Isolated two-camera test using the installed image; no live setting writes."""
import copy
import hashlib
import json
import os
from pathlib import Path
import secrets
import shutil
import sys
import time

ROOT=Path(__file__).resolve().parent
PRIVATE=ROOT/'private'
REPO=Path('/workspace/contextra-analytics-runtime-lifecycle-refactor')
TOOLS=Path('/usr/local/lib/contextra-maintenance/portable-20261001T182138')
PROJECT='ct-mroi-dual-20261013-r01'
sys.path.insert(0,str(TOOLS))
import field_profiles as fp
from maintenance_process import run
from deployment_kit.compose_snapshot import command_environment
import compose_backend as cb

def cmd(args,timeout=30,input=None):
    return run(args,timeout=timeout,input=input,capture_output=True,text=True,check=True,env=command_environment()).stdout

def write(path,value):fp.atomic(path,value)
def read(path):return json.loads(Path(path).read_text())
def digest(path):return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def compose(*args,timeout=60):
    return cmd(['docker','compose','--project-directory',str(ROOT),'-p',PROJECT,'-f',str(PRIVATE/'compose.json'),*args],timeout)

def info():
    return json.loads(cmd(['docker','inspect','contextra-analytics-1','contextra-redis-1','contextra-postgresql-1','contextra-ai-control-sidecar-1']))

def prepare():
    assert not (PRIVATE/'compose.json').exists()
    assert shutil.disk_usage('/').free>15*1024**3
    rows=info(); app,redis,pg,side=rows
    assert app['Image'].startswith('sha256:4f660ae3b484'), 'INSTALLED_IMAGE_CHANGED'
    env={k:v for x in app['Config']['Env'] for k,v in [x.split('=',1)]}
    # Selected source values stay in mode600 private files and never reach command arguments.
    extraction="""import os,pathlib,yaml,json
base=yaml.safe_load(pathlib.Path(os.environ['STREAMS_CONFIG_PATH']).read_text())['streams']
overlay=yaml.safe_load(pathlib.Path(os.environ['STREAMS_RUNTIME_PATH']).read_text())
merged={s['stream_id']:s for s in base}
for name in overlay.get('removed',[]):merged.pop(name,None)
for s in overlay.get('added',[]):merged[s['stream_id']]=s
selected={str(s.get('camera_id')):s for s in merged.values() if str(s.get('camera_id')) in ('3','5')}
assert set(selected)=={'3','5'}
print(json.dumps({'selected':selected,'template':json.loads(pathlib.Path(os.environ['CONTEXTRA_CONTROL_TEMPLATE_FILE']).read_text())}))
"""
    source=json.loads(cmd(['docker','exec','contextra-analytics-1','python3','-B','-c',extraction],10))
    write(PRIVATE/'source.json',source)
    for name in ('config','runtime','artifacts','visual','policy','tokens','db','redis','sink','diagnostics','metadata-samples'):
        (PRIVATE/name).mkdir(mode=0o700)
    os.chown(PRIVATE/'sink',10001,10001)
    cfg=next(m['Source'] for m in app['Mounts'] if m['Destination']=='/etc/contextra')
    import yaml
    system=yaml.safe_load((Path(cfg)/'system_config.yaml').read_text());system['active_learning']['enabled']=False
    write(PRIVATE/'config/system_config.yaml',system)
    write(PRIVATE/'config/streams.yaml',{'streams':[]});write(PRIVATE/'config/camera_ptz_config.yaml',{'cameras':{}})
    write(PRIVATE/'runtime/streams_runtime.yaml',{'added':[],'removed':[]})
    template=source['template'];template.update(store=False,active_learning=False,autostart=False)
    write(PRIVATE/'config/template.json',template)
    policy=dict(mode='multi_roi',max_rois=8,max_rules=48,full_frame_roi_id=0)
    write(PRIVATE/'policy/roi-policy.json',policy);write(PRIVATE/'policy/roi-policy.applied.json',policy)
    write(PRIVATE/'tokens/control-token',secrets.token_hex(32).encode())
    password=secrets.token_hex(24);write(PRIVATE/'tokens/db-password',password.encode())
    redis_password=secrets.token_hex(24)
    write(PRIVATE/'redis-server.conf',('requirepass '+redis_password+'\nsave ""\nappendonly no\nmaxmemory 96mb\nmaxmemory-policy allkeys-lru\n').encode())
    os.chown(PRIVATE/'redis-server.conf',999,1000)
    write(PRIVATE/'visual/capture.enabled',b'1')
    for k in list(env):
        if k in ('CONTEXTRA_RELAY_TOKEN_FILE','CONTEXTRA_TEST_FRAME_ANCHOR_FILE','CONTEXTRA_TRAINING_ROOT'):
            env.pop(k)
    env.update(DATABASE_URL='postgresql://trial:'+password+'@db:5432/trial',
        REDIS_URL='redis://:'+redis_password+'@redis:6379/0',
        CONTEXTRA_METADATA_REDIS_URL='redis://:'+redis_password+'@redis:6379/0',
        CONTEXTRA_EVENT_TRANSPORT='direct',CONTEXTRA_EVENT_ENDPOINT='http://sink:8080/events',
        CONTEXTRA_ARTIFACT_PUBLIC_BASE='https://evidence.example.invalid/artifacts',
        CONTEXTRA_ARTIFACT_ROOT='/trial/artifacts',CONTEXTRA_EVENT_ARTIFACT_ENABLED='1',CONTEXTRA_ARTIFACT_HTTP_ENABLED='1',
        CONTEXTRA_ARTIFACT_CAP_BYTES=str(4*1024**3),CONTEXTRA_ARTIFACT_MIN_FREE_BYTES=str(100*1024**3),
        CONTEXTRA_ARTIFACT_RETENTION_ENABLED='false',CONTEXTRA_ARTIFACT_DELIVERY_CAMERAS='3,5',
        CONTEXTRA_METADATA_NAMESPACE='mroi-trial',CONTEXTRA_METADATA_VISUAL_DIR='/trial/visual',
        CONTEXTRA_METADATA_SAMPLES_DIR='/trial/metadata-samples',CONTEXTRA_START_DIAGNOSTICS_DIR='/trial/diagnostics',
        CONTEXTRA_ROI_POLICY_FILE='/trial/policy/roi-policy.json',CONTEXTRA_CONTROL_TOKEN_FILE='/trial/tokens/control-token',
        CONTEXTRA_CONTROL_TEMPLATE_FILE='/etc/contextra/template.json',AI_CONTROL_SERVER_ID='mroi-isolated-test',
        STREAMS_CONFIG_PATH='/etc/contextra/streams.yaml',STREAMS_RUNTIME_PATH='/trial/runtime/streams_runtime.yaml',
        CONTEXTRA_CHANNEL_PROCESS_LIMIT='2',PYTHONDONTWRITEBYTECODE='1',FRAME_RETENTION_DAYS='0')
    models=[{'type':'bind','source':m['Source'],'target':m['Destination'],'read_only':True}
            for m in app['Mounts'] if m['Destination'].startswith('/opt/contextra/models/')]
    volumes=models+[{'type':'bind','source':str(PRIVATE/n),'target':'/trial/'+n} for n in ('runtime','artifacts','visual','policy','tokens','diagnostics','metadata-samples')]
    volumes.append({'type':'bind','source':str(PRIVATE/'config'),'target':'/etc/contextra','read_only':True})
    doc={'services':{
        'analytics':{'image':app['Image'],'environment':env,'volumes':volumes,'restart':'no','mem_limit':'10g','cpus':8,'pids_limit':1024,
             'shm_size':'1gb','deploy':{'resources':{'reservations':{'devices':[{'driver':'nvidia','device_ids':['0'],'capabilities':['gpu']}]}}}},
        'db':{'image':pg['Image'],'environment':{'POSTGRES_DB':'trial','POSTGRES_USER':'trial','POSTGRES_PASSWORD_FILE':'/run/secrets/db-password'},
             'volumes':[{'type':'bind','source':str(PRIVATE/'tokens/db-password'),'target':'/run/secrets/db-password','read_only':True},
                        {'type':'bind','source':str(PRIVATE/'db'),'target':'/var/lib/postgresql/data'}],
             'mem_limit':'512m','cpus':1,'restart':'no'},
        'redis':{'image':redis['Image'],'command':['redis-server','/etc/redis-trial.conf'],
                 'volumes':[{'type':'bind','source':str(PRIVATE/'redis-server.conf'),'target':'/etc/redis-trial.conf','read_only':True}],
                 'mem_limit':'160m','cpus':1,'restart':'no'},
        'sink':{'image':side['Image'],'entrypoint':['python3','-B','/trial/sink.py'],'healthcheck':{'disable':True},
                'volumes':[{'type':'bind','source':str(ROOT/'sink.py'),'target':'/trial/sink.py','read_only':True},
                           {'type':'bind','source':str(PRIVATE/'sink'),'target':'/results'}],
                'mem_limit':'128m','cpus':0.5,'restart':'no'}}}
    write(PRIVATE/'compose.json',doc)
    active=cb.active(); paths=[Path(active['compose']),cb.state_path()/'active.json',fp.SETUP/'roi/roi-policy.json',fp.SETUP/'roi/roi-policy.applied.json',Path('/data/contextra/runtime-state/streams_runtime.yaml')]
    write(PRIVATE/'operational-before.json',{'containers':{r['Name']:{'id':r['Id'],'image':r['Image'],'started':r['State']['StartedAt']} for r in rows},
        'files':{str(p):digest(p) for p in paths},'policy':read(fp.SETUP/'roi/roi-policy.json')})
    left=[[0,0],[.5,0],[.5,1],[0,1]]
    reqs={}
    for camera in (3,5):
        reqs[str(camera)]={'contract':'multi_roi.internal.v1','cameraId':camera,
            'rtspUrl':source['selected'][str(camera)]['uri'],'startWallTimeMs':1000,'rtpClockRate':90000,
            'rois':[{'roiId':camera*100+1,'points':left,'label':'LEFT'},
                    {'roiId':camera*100+2,'points':left,'outside':True,'label':'OUTSIDE LEFT'}],'rules':[]}
    def rule(camera,uid,rid,code,**params):
        reqs[str(camera)]['rules'].append({'ruleId':uid,'roiId':rid,'scenarioCode':code,'parameters':dict(params,cooldown_seconds=30)})
    rule(3,1,301,'INTRUSION',dwell_seconds=2);rule(3,2,302,'INTRUSION',dwell_seconds=4)
    rule(3,3,301,'LOITERING',dwell_seconds=8);rule(3,4,302,'LOITERING',dwell_seconds=12)
    rule(3,5,301,'FALL');rule(3,6,302,'FALL');rule(3,7,0,'FIGHT')
    crowd=dict(reference_count=5,safe_ratio=.2,caution_ratio=.4,warning_ratio=.6,danger_ratio=.8)
    rule(3,8,0,'CROWD',**crowd)
    rule(5,1,501,'FIRE');rule(5,2,502,'FIRE');rule(5,3,0,'FIRE')
    write(PRIVATE/'requests.json',reqs)
    safe=copy.deepcopy(reqs)
    for req in safe.values():req['rtspUrl']='<private existing camera URI>';req['timingScope']='SYNTHETIC_TEST_ANCHOR_NOT_ABSOLUTE_TIME_VALIDATION'
    write(ROOT/'test-cases.redacted.json',safe)
    assert all('ports' not in s for s in doc['services'].values())
    compose('config','--quiet',timeout=20)
    print(json.dumps({'phase':'PREPARED','project':PROJECT,'cameras':[3,5],'same_uri':source['selected']['3']['uri']==source['selected']['5']['uri'],
                      'image':app['Image'],'production_changed':False}))

def start():
    assert not (PRIVATE/'started.json').exists()
    compose('up','-d','--pull','never','--no-build','db','redis','sink',timeout=60)
    end=time.monotonic()+30
    while time.monotonic()<end:
        r=run(['docker','compose','-p',PROJECT,'-f',str(PRIVATE/'compose.json'),'exec','-T','db','pg_isready','-U','trial'],capture_output=True,timeout=5)
        if r.returncode==0:break
        time.sleep(1)
    else:raise RuntimeError('ISOLATED_DB_NOT_READY')
    compose('up','-d','--pull','never','--no-build','analytics',timeout=60)
    write(PRIVATE/'started.json',{'started':time.time(),'channels_started':False})
    print('ISOLATED_SERVICES_STARTED_NO_CHANNELS')

def execute(script,timeout=30):
    return compose('exec','-T','analytics','python3','-B','-c',script,timeout=timeout)

def setup_repair():
    # The test Redis image drops to uid999; preserve 0600 rather than opening a secret file.
    os.chown(PRIVATE/'redis-server.conf',999,1000)
    doc=read(PRIVATE/'compose.json');doc['services']['sink']['healthcheck']={'disable':True}
    write(PRIVATE/'compose.json',doc)
    compose('stop','--timeout','90','analytics',timeout=110)
    compose('up','-d','--no-deps','--pull','never','--no-build','redis','sink',timeout=40)
    check="import redis,os; assert redis.from_url(os.environ['REDIS_URL'],socket_connect_timeout=3).ping(); print('TEST_REDIS_READY')"
    # No channel has been started. Reload the app once to initialize its existing legacy publisher.
    compose('up','-d','--no-deps','--pull','never','--no-build','analytics',timeout=60)
    print('ISOLATED_SETUP_REPAIRED_NO_CHANNEL_START')

def main():
    {'prepare':prepare,'start':start,'setup-repair':setup_repair}[sys.argv[1]]()

if __name__=='__main__':main()
