"""Build allowlisted, portable evidence without publishing raw settings or memory."""
import csv
import hashlib
import json
import os
import re
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
PRIVATE = ROOT / 'private'
SHARED = ROOT / 'shared'
KST = timezone(timedelta(hours=9))
os.umask(0o077)


def read(name):
    return json.loads((PRIVATE / name).read_text(encoding='utf-8'))


def write(name, data):
    path = SHARED / name
    with path.open('w', encoding='utf-8') as stream:
        json.dump(data, stream, ensure_ascii=False, indent=2)
        stream.write('\n')


def digest(path):
    h = hashlib.sha256()
    with path.open('rb') as stream:
        for block in iter(lambda: stream.read(1024 * 1024), b''):
            h.update(block)
    return h.hexdigest()


def first_matching(text, pattern):
    return next(line for line in text.splitlines() if re.search(pattern, line))


def mask(line):
    line = re.sub(r'\b(?:https?|wss?|rtsps?)://[^\s]+', '[ENDPOINT_REDACTED]', line)
    line = re.sub(r'(?<![\w.])\d{1,3}(?:\.\d{1,3}){3}(?![\w.])', '[HOST_REDACTED]', line)
    line = re.sub(r'(?i)(token|password|secret|authorization)=\S+', r'\1=[REDACTED]', line)
    return line


pre = read('prestate.json')['data']
baseline_containers = read('media_map.json')['data']['containers']
post = read('poststate.json')['data']
probe = read('network_probe.json')['data']
dump_record = read('preserve_dumps.json')['data']
container_keys = ('name', 'id', 'image_id', 'started', 'status', 'restart_count')
container_before = [{k: x[k] for k in container_keys} for x in baseline_containers]
container_after = [{k: x[k] for k in container_keys} for x in post['containers']]


def process_comparison(name, before_id):
    before = {x[before_id]: datetime.fromisoformat(x['started']) for x in pre[name]}
    after = {x['Id']: datetime.fromisoformat(x['started']) for x in post[name]}
    return before.keys() == after.keys() and all(
        abs((before[pid] - after[pid]).total_seconds()) <= 0.001 for pid in before
    )


checks = {
    'containers_identity_image_start_status_restart_unchanged': container_before == container_after,
    'chrome_pid_and_start_unchanged': process_comparison('chrome', 'Id'),
    'nginx_pid_and_start_unchanged': process_comparison('nginx', 'ProcessId'),
    'os_last_boot_unchanged': pre['os']['last_boot'] == post['last_boot'],
    'web_source_head_unchanged': pre['source_git']['head'] == post['source_git_head'],
    'web_source_status_unchanged': pre['source_git']['status'] == post['source_git_status'],
    'media_process_absent_before_after': probe['ivpm_process_count'] == post['ivpm_process_count'] == 0,
    'application_payload_not_sent_by_tcp_probe': probe['payload_sent'] is False,
    'dump_copies_match_original': all(x['copy_hash_matches'] and x['source_metadata_unchanged'] for x in dump_record['items']),
}
assert all(checks.values()), checks

ns = {'e': 'http://schemas.microsoft.com/win/2004/08/events/event'}
events_xml = ET.parse(PRIVATE / 'ivpm-crash-events.xml')
crashes = []
for event in events_xml.findall('.//e:Event', ns):
    event_id = int(event.find('e:System/e:EventID', ns).text)
    if event_id != 1000:
        continue
    fields = {x.attrib['Name']: x.text for x in event.findall('e:EventData/e:Data', ns)}
    if fields.get('AppName') != 'IVPMPlus.exe':
        continue
    ticks = int(fields['ProcessCreationTime'], 16)
    started = datetime(1601, 1, 1, tzinfo=timezone.utc) + timedelta(microseconds=ticks // 10)
    at = event.find('e:System/e:TimeCreated', ns).attrib['SystemTime']
    crashes.append({
        'at_kst': datetime.fromisoformat(at.replace('Z', '+00:00')).astimezone(KST).isoformat(),
        'record_id': int(event.find('e:System/e:EventRecordID', ns).text),
        'event_id': event_id,
        'application': fields['AppName'], 'version': fields['AppVersion'],
        'module': fields['ModuleName'], 'module_version': fields['ModuleVersion'],
        'exception_code': '0x' + fields['ExceptionCode'],
        'offset': '0x' + fields['FaultingOffset'],
        'pid': int(fields['ProcessId'], 16),
        'process_started_kst': started.astimezone(KST).isoformat(),
        'report_id': fields['IntegratorReportId'],
    })
crashes.sort(key=lambda x: x['at_kst'])
assert len(crashes) == 2, len(crashes)
signature_keys = ('application', 'version', 'module', 'module_version', 'exception_code', 'offset')
assert all(crashes[0][key] == crashes[1][key] for key in signature_keys)
assert [x['pid'] for x in crashes] == [37748, 12452]

with (PRIVATE / 'chrome_system_summary.csv').open(encoding='utf-8-sig', newline='') as stream:
    rows = list(csv.DictReader(stream))
memory = {
    'rows': len(rows), 'first': rows[0]['Timestamp'], 'last': rows[-1]['Timestamp'],
    'free_ram_min_mib': min(float(x['FreeRAM_MB']) for x in rows),
    'chrome_private_min_mib': min(float(x['ChromePrivateCommit_MB']) for x in rows),
    'chrome_private_max_mib': max(float(x['ChromePrivateCommit_MB']) for x in rows),
    'covers_crash_time': False, 'leak_established': False,
}

db = read('db_camera_history.json')['data']['stdout']
db_row = first_matching(db, r'\tREC_STARTED\t').split('\t')
db_start = datetime.fromtimestamp(int(db_row[1]) / 1000, tz=timezone.utc).astimezone(KST)
recovery_log = (PRIVATE / 'web-app_recovery_1026_1330-1340.log').read_text(encoding='utf-8-sig')
crash_log = (PRIVATE / 'web-app_crash_1031_2330-2340.log').read_text(encoding='utf-8-sig')
current_log = read('current_app_log.json')['data']['stdout']
if not isinstance(current_log, str):
    current_log = '\n'.join(current_log)
excerpts = [
    {'source': 'web-app_recovery_1026_1330-1340.log', 'meaning': 'REC-01 control upgraded',
     'line': mask(first_matching(recovery_log, r'media_control_upgrade_succeeded.*REC-01'))},
    {'source': 'web-app_recovery_1026_1330-1340.log', 'meaning': 'REC-01 ready',
     'line': mask(first_matching(recovery_log, r'media_control_ready.*REC-01'))},
    {'source': 'web-app_crash_1031_2330-2340.log', 'meaning': 'camera24 input unavailable after crash',
     'line': mask(first_matching(crash_log, r'^2026-10-31T14:3[6-9]:.*ai_command_admission.*camera=24.*REC_STREAM_UNAVAILABLE'))},
    {'source': 'current_app_log.json', 'meaning': 'camera24 input unavailable at capture',
     'line': mask(first_matching(current_log, r'ai_command_admission.*camera=24.*REC_STREAM_UNAVAILABLE'))},
    {'source': 'current_app_log.json', 'meaning': 'REC-01 control connection failure at capture',
     'line': mask(first_matching(current_log, r'media_control_upgrade_failed.*REC-01'))},
]
write('evidence_summary.json', {
    'schema': 'icerti.media-incident-summary.v1',
    'generated_kst': datetime.now(KST).isoformat(),
    'runtime_changed_by_this_task': False,
    'crashes': crashes,
    'same_observed_signature': True,
    'same_internal_root_cause_proven': False,
    'rec_started': {'record_id': int(db_row[0]), 'at_kst': db_start.isoformat(), 'code': db_row[5]},
    'current': {'captured_kst': post['captured_at'], 'media_process_count': post['ivpm_process_count'],
                'tcp': probe['tcp'], 'rtc_udp_endpoint_count': len(probe['rtc_udp_8000']),
                'free_ram_gib': round(post['free_kib'] / 1024 ** 2, 2)},
    'checks': checks,
    'chrome_historical_memory': memory,
    'binary_fingerprints': post['binaries'],
    'preserved_dumps': [{k: x[k] for k in ('name', 'bytes', 'created', 'modified', 'sha256', 'copy_hash_matches')} for x in dump_record['items']],
    'dump_content_shared': False,
    'excerpts': excerpts,
    'time_limitations': 'All displayed times are KST; cross-host clock synchronization was not verified. Subsecond ordering across sources is not causal proof.',
    'scope_limitations': ['No browser tab, playback, stream Start, service restart or dump debugger analysis.',
                          'No exact operator identity or launch command established for October 26.',
                          'An early docker logs tail query was incomplete; corrected bounded no-tail capture is used.'],
})

preserved = read('preserve_crash.json')['data']
copied = [x for x in preserved['files'] if x.get('copied')]
assert all(x['hash_equal'] for x in copied)
write('preservation_index.json', {
    'schema': 'icerti.media-preservation-index.v1',
    'windows_location_role': 'Dedicated protected diagnostic evidence folder, readable by Administrators and SYSTEM only. Actual mapping is retained privately.',
    'retention': 'Manual review; no automatic removal.',
    'raw_files_not_in_share_bundle': [
        {'name': Path(x['destination'].replace('\\', '/')).name,
         'bytes': x['bytes'], 'sha256': x['copy_sha256'], 'hash_equal': x['hash_equal']} for x in copied
    ],
    'dumps_windows_only': [{k: x[k] for k in ('name', 'bytes', 'sha256', 'copy_hash_matches')} for x in dump_record['items']],
    'bounded_raw_web_logs': [
        {'name': path.name, 'bytes': path.stat().st_size, 'sha256': digest(path)}
        for path in sorted(PRIVATE.glob('web-app_*.log'))
    ],
    'crash_event_xml': {'name': 'ivpm-crash-events.xml', 'sha256': digest(PRIVATE / 'ivpm-crash-events.xml')},
    'privacy': 'Raw WER, memory dumps, process command lines and unmasked logs are excluded. The protected originals must not be forwarded casually.',
})
print(json.dumps({'checks': checks, 'crash_records': [x['record_id'] for x in crashes],
                  'recovery_at_kst': db_start.isoformat(), 'memory': memory}, ensure_ascii=False))
