#!/usr/bin/env python3
"""Build HLDS emulator session configs and helper run commands.

Analyzer output owns device profiles. Dumper/media tooling owns media profiles.
This builder owns the run/session binding: device + optional media + firmware
image + bridge/listen/logging/safety settings.
"""
from __future__ import annotations

import argparse, json, re
from pathlib import Path
from typing import Any

from hlds_profiles import load_device_profile, load_media_profile, effective_session, write_json, infer_target_name_from_device


def safe_stem(text: str) -> str:
    text = str(text or 'hlds_session')
    text = re.sub(r'[^A-Za-z0-9_.-]+', '_', text).strip('_')
    return text or 'hlds_session'


def default_stem(device: dict[str, Any], media_path: str | None, bridge: str) -> str:
    model = str(device.get('model') or device.get('identity', {}).get('inquiry_product') or 'hlds').lower().replace('-', '')
    rev = str(device.get('revision') or device.get('identity', {}).get('inquiry_revision') or '').lower()
    media = Path(media_path).stem if media_path else 'no_media'
    return safe_stem(f'{model}_{rev}_{media}_{bridge}')


def build_session(args: argparse.Namespace) -> dict[str, Any]:
    device = load_device_profile(args.device_profile) if args.device_profile else {}
    media = load_media_profile(args.media_profile) if args.media_profile else {}
    stem = args.stem or default_stem(device, args.media_profile, args.bridge)
    target_name = args.target_name or infer_target_name_from_device(device)
    bridge = {
        'mode': args.bridge,
        'target_name': target_name,
        'listen_host': args.listen_host,
        'listen_port': int(args.listen_port),
        'advertise_host': args.advertise_host,
        'advertise_port': int(args.advertise_port or args.listen_port),
        'read_map': args.read_map,
        'live_read_map': args.live_read_map or args.read_map,
    }
    logging = {
        'live_jsonl': f'{stem}_live_windows_iscsi.jsonl',
        'read_trace_jsonl': f'{stem}_read_trace.jsonl',
        'read_trace_limit': int(args.read_trace_limit),
        'response_dir': f'{stem}_live_windows_iscsi_responses',
        'media_model_summary_json': f'{stem}_live_media_model.json',
        'capture_data_out': True,
    }
    safety = {
        'firmware_update_capture_only': bool(args.firmware_update_capture_only),
        'allow_flash_mutation': bool(args.allow_flash_mutation),
        'media_write_capture_only': bool(args.media_write_capture_only),
        'allow_media_image_mutation': bool(args.allow_media_image_mutation),
        'allow_media_blank_or_erase': bool(args.allow_media_blank_or_erase),
        'unknown_vendor_command_policy': args.unknown_vendor_command_policy,
    }
    instr = {
        'firmware_lba_state': bool(args.firmware_lba_state),
        'read_translation_trace': bool(args.read_translation_trace),
        'auth_transition_trace': bool(args.auth_transition_trace),
        'mmio_trace': bool(args.mmio_trace),
    }
    sess: dict[str, Any] = {
        'format': 'hlds-emulator-session',
        'version': 1,
        'stem': stem,
        'device_profile': str(Path(args.device_profile).resolve()) if args.device_profile else None,
        'media_profile': str(Path(args.media_profile).resolve()) if args.media_profile else None,
        'firmware_image': str(Path(args.firmware_image).resolve()) if args.firmware_image else None,
        'bridge': bridge,
        'logging': logging,
        'safety': safety,
        'instrumentation': instr,
        'notes': [],
    }
    if media and not args.iso:
        image = media.get('image') or {}
        if image.get('file'):
            image_path = Path(str(image.get('file')))
            if args.media_profile and not image_path.is_absolute():
                image_path = Path(args.media_profile).resolve().parent / image_path
            sess.setdefault('media', {})['image'] = {'file': str(image_path)}
    if args.iso:
        sess.setdefault('media', {})['image'] = {'file': str(Path(args.iso))}
    log_path = Path(args.log) if args.log else None
    if log_path is None and args.media_profile:
        mp = Path(args.media_profile)
        if mp.name.lower().endswith('.xdvd.json'):
            cand = mp.with_name(mp.name[:-10] + '.log')
        else:
            cand = mp.with_suffix('.log')
        log_path = cand
    if log_path is not None:
        sess.setdefault('media', {})['unlock_log'] = str(log_path)
    return sess


def write_helper_commands(out_dir: Path, session_path: Path, stem: str) -> None:
    # Generic helpers that defer all real runtime choices to --session-profile.
    start = out_dir / f'{stem}_start_from_session.cmd'
    start.write_text('@echo off\r\npython "%~dp0gdr_host_iscsi_pdu_bridge.py" --session-profile "%~dp0' + session_path.name + '" --serve %*\r\n', encoding='utf-8')
    run = out_dir / f'{stem}_run_validation_from_session.cmd'
    run.write_text('@echo off\r\npython "%~dp0run_gdr8050l_auth_packet_v166_windows_iscsi_bridge.py" --session-profile "%~dp0' + session_path.name + '" %*\r\n', encoding='utf-8')


def main() -> int:
    ap = argparse.ArgumentParser(description='Build an HLDS emulator session profile from device/media/runtime selections')
    ap.add_argument('--device-profile', required=True, help='device profile generated by analyzer chain')
    ap.add_argument('--media-profile', default=None, help='media profile/xdvd sidecar generated by dumper/media tooling; optional for no-media/updater probes')
    ap.add_argument('--firmware-image', default=None, help='firmware binary path used by the emulator session')
    ap.add_argument('--iso', default=None, help='optional raw ISO path override for the media image file')
    ap.add_argument('--log', default=None, help='optional unlock/auth log path; defaults to .log next to .xdvd.json media profile when present')
    ap.add_argument('--bridge', default='windows_iscsi', choices=['windows_iscsi','tcp_bridge','offline_jsonl'])
    ap.add_argument('--target-name', default=None)
    ap.add_argument('--listen-host', default='0.0.0.0')
    ap.add_argument('--listen-port', default='3260')
    ap.add_argument('--advertise-host', default='127.0.0.1')
    ap.add_argument('--advertise-port', default=None)
    ap.add_argument('--read-map', default='firmware_window')
    ap.add_argument('--live-read-map', default=None, help='optional live target read map override')
    ap.add_argument('--stem', default=None)
    ap.add_argument('--read-trace-limit', default='0')
    ap.add_argument('--firmware-update-capture-only', action=argparse.BooleanOptionalAction, default=True)
    ap.add_argument('--allow-flash-mutation', action='store_true')
    ap.add_argument('--media-write-capture-only', action=argparse.BooleanOptionalAction, default=True)
    ap.add_argument('--allow-media-image-mutation', action='store_true')
    ap.add_argument('--allow-media-blank-or-erase', action='store_true')
    ap.add_argument('--unknown-vendor-command-policy', default='capture_and_check_condition')
    ap.add_argument('--firmware-lba-state', action=argparse.BooleanOptionalAction, default=True)
    ap.add_argument('--read-translation-trace', action=argparse.BooleanOptionalAction, default=True)
    ap.add_argument('--auth-transition-trace', action=argparse.BooleanOptionalAction, default=True)
    ap.add_argument('--mmio-trace', action='store_true')
    ap.add_argument('--output', '-o', required=True, help='session JSON output path')
    ap.add_argument('--write-helper-commands', action='store_true', help='write tiny .cmd launchers next to the session file')
    args = ap.parse_args()
    sess = build_session(args)
    out = Path(args.output)
    write_json(out, sess)
    print(f'[OK] wrote session profile: {out}')
    if args.write_helper_commands:
        write_helper_commands(out.parent, out, str(sess.get('stem') or out.stem))
        print(f'[OK] wrote helper commands in: {out.parent}')
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
