#!/usr/bin/env python3
"""Loopback/TCP JSONL GDR/XDVD host bridge.

This is a transport shim over gdr_host_jsonl_bridge.py.  It is still a
user-mode prototype, not an iSCSI target.  Each request is one JSON object per
line and each response is one JSON object per line.  It exists so the command
catalog/host bridge can be exercised through an actual socket boundary before
an iSCSI or virtual-device front-end is added.

Roundtrip smoke mode:
  python gdr_host_tcp_bridge.py disc.xdvd.json disc.iso --log disc.log \
      --script requests.jsonl --out responses.jsonl --verify-md verify.md

Server mode:
  python gdr_host_tcp_bridge.py disc.xdvd.json disc.iso --log disc.log \
      --serve --host 127.0.0.1 --port 32610 --catalog catalog.json
"""
from __future__ import annotations

import argparse, json, queue, socket, sys, threading, time
from pathlib import Path
from typing import Any

from gdr_scsi_command_parser import GDRScsiParser
from hlds_profiles import apply_session_to_namespace
from gdr_host_jsonl_bridge import Catalog, HostBridge, load_requests, write_outputs, write_verify


def make_bridge(args: argparse.Namespace) -> tuple[HostBridge, Catalog]:
    xdvd = Path(args.xdvd_json)
    log = Path(args.log) if args.log else xdvd.with_suffix('.log')
    parser = GDRScsiParser(
        xdvd,
        args.iso,
        log if log.exists() else None,
        read_map=getattr(args, 'xdvd_read_map', 'windows'),
        read_trace_path=getattr(args, 'read_trace_jsonl', None),
        read_trace_limit=getattr(args, 'read_trace_limit', 0),
        device_profile=getattr(args, 'device_profile', None),
    )
    if getattr(args, 'media_model_summary_json', None):
        parser.media_model.write_summary_json(args.media_model_summary_json)
    catalog = Catalog(args.catalog)
    return HostBridge(parser, catalog), catalog


def json_line(obj: dict[str, Any]) -> bytes:
    return (json.dumps(obj, separators=(',', ':')) + '\n').encode('utf-8')


def serve_client(conn: socket.socket, bridge: HostBridge, *, start_index: int = 0, rows_sink: list[dict[str, Any]] | None = None) -> int:
    count = 0
    with conn:
        rfile = conn.makefile('rb')
        wfile = conn.makefile('wb')
        index = start_index
        for raw in rfile:
            line = raw.decode('utf-8', errors='replace').strip()
            if not line or line.startswith('#'):
                continue
            try:
                req = json.loads(line)
                if not isinstance(req, dict):
                    raise ValueError('request must be a JSON object')
                row = bridge.response_for(req, index)
            except Exception as e:
                row = {
                    'ok': False,
                    'index': index,
                    'tag': f'req{index:03d}',
                    'scsi_status': 'CHECK CONDITION',
                    'sense_key': 'ILLEGAL REQUEST',
                    'sense_key_hex': '0x05',
                    'asc': '0x20',
                    'ascq': '0x00',
                    'direction': 'none',
                    'data_len': 0,
                    'data_sha256': '',
                    'data_hex': '',
                    'error': str(e),
                }
            wfile.write(json_line(row))
            wfile.flush()
            if rows_sink is not None:
                rows_sink.append(row)
            index += 1
            count += 1
    return count


def run_server(args: argparse.Namespace) -> int:
    bridge, _catalog = make_bridge(args)
    host = str(args.host)
    port = int(args.port)
    served_clients = 0
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        srv.bind((host, port))
        srv.listen(1)
        actual_host, actual_port = srv.getsockname()
        if args.ready_file:
            Path(args.ready_file).write_text(json.dumps({'host': actual_host, 'port': actual_port}, indent=2), encoding='utf-8')
        print(f'[TCP] listening on {actual_host}:{actual_port}', flush=True)
        while True:
            conn, addr = srv.accept()
            served_clients += 1
            print(f'[TCP] client {served_clients} from {addr[0]}:{addr[1]}', flush=True)
            n = serve_client(conn, bridge)
            print(f'[TCP] client {served_clients} requests={n}', flush=True)
            if args.once or (args.max_clients and served_clients >= int(args.max_clients)):
                break
    return 0


def run_roundtrip(args: argparse.Namespace) -> int:
    if not args.script:
        raise SystemExit('--script is required for roundtrip mode')
    reqs = load_requests(args.script)
    bridge, catalog = make_bridge(args)
    rows_from_server: list[dict[str, Any]] = []
    ready: queue.Queue[tuple[str, int]] = queue.Queue(maxsize=1)
    errors: queue.Queue[str] = queue.Queue()

    def server_thread() -> None:
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
                srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                srv.bind((args.host, 0 if int(args.port) == 0 else int(args.port)))
                srv.listen(1)
                host, port = srv.getsockname()
                ready.put((host, port))
                conn, _addr = srv.accept()
                serve_client(conn, bridge, rows_sink=rows_from_server)
        except Exception as e:
            errors.put(str(e))

    th = threading.Thread(target=server_thread, daemon=True)
    th.start()
    host, port = ready.get(timeout=float(args.timeout))
    rows: list[dict[str, Any]] = []
    with socket.create_connection((host, port), timeout=float(args.timeout)) as sock:
        sock.settimeout(float(args.timeout))
        for req in reqs:
            sock.sendall(json_line(req))
        sock.shutdown(socket.SHUT_WR)
        buf = b''
        while True:
            chunk = sock.recv(65536)
            if not chunk:
                break
            buf += chunk
    for line in buf.decode('utf-8', errors='replace').splitlines():
        if line.strip():
            rows.append(json.loads(line))
    th.join(timeout=float(args.timeout))
    if not errors.empty():
        raise SystemExit(f'TCP bridge server error: {errors.get()}')
    if len(rows) != len(reqs):
        raise SystemExit(f'TCP bridge response count mismatch: got={len(rows)} expected={len(reqs)}')
    write_outputs(rows, args.out, args.write_response_dir)
    if args.verify_md:
        write_verify(Path(args.verify_md), rows, catalog)
    print(f'[TCP] roundtrip requests={len(reqs)} responses={len(rows)} ok={sum(1 for r in rows if r.get("ok"))}', flush=True)
    return 0 if all(r.get('ok') for r in rows) else 2


def main() -> int:
    ap = argparse.ArgumentParser(description='Loopback/TCP JSONL GDR/XDVD host bridge')
    ap.add_argument('xdvd_json', nargs='?', help='media profile / XDVD sidecar path; optional when --session-profile supplies media_profile')
    ap.add_argument('iso', nargs='?')
    ap.add_argument('--session-profile', help='HLDS emulator session JSON/JSONC generated by hlds_session_builder.py')
    ap.add_argument('--device-profile', help='HLDS device profile JSON/JSONC generated by analyzer chain')
    ap.add_argument('--media-profile', help='HLDS media profile / XDVD sidecar generated by dumper/media tooling')
    ap.add_argument('--log')
    ap.add_argument('--script', '-i', help='JSON/JSONL requests for roundtrip mode')
    ap.add_argument('--out', '-o', help='write JSONL responses here; stdout when omitted or -')
    ap.add_argument('--write-response-dir', help='optional directory for binary DATA IN/OUT payloads')
    ap.add_argument('--catalog', help='optional generated command catalog used to annotate exact catalog matches')
    ap.add_argument('--verify-md', help='optional compact verification markdown for TCP bridge output')
    ap.add_argument('--host', default='127.0.0.1')
    ap.add_argument('--port', default='0', help='TCP port; 0 picks an ephemeral port in roundtrip mode')
    ap.add_argument('--timeout', default='10')
    ap.add_argument('--serve', action='store_true', help='run as a TCP server instead of self-contained roundtrip')
    ap.add_argument('--once', action='store_true', help='server mode: exit after one client')
    ap.add_argument('--max-clients', type=int, default=0, help='server mode: exit after N clients; 0 means unlimited')
    ap.add_argument('--ready-file', help='server mode: write {host,port} JSON after bind')
    ap.add_argument('--xdvd-read-map', default=None, choices=['windows','direct','v157','firmware_window','firmware_lba_state','firmware','level2','physical_raw','raw_full','raw_iso','option1_raw','full_raw','xiso_alias','xbox_xiso','game_partition','xbox_raw','dual_alias','xbox_alias','game_offset0','game_zero','physical','output'], help='READ(10/12) LBA mapping mode')
    ap.add_argument('--read-trace-jsonl', default=None, help='write compact READ trace rows')
    ap.add_argument('--read-trace-limit', type=int, default=0, help='maximum compact read trace rows; 0 means unlimited')
    ap.add_argument('--media-model-summary-json', default=None, help='write v165 media model summary JSON at startup')
    args = ap.parse_args()
    args = apply_session_to_namespace(args)
    if not args.xdvd_json:
        raise SystemExit('xdvd_json/media_profile is required unless supplied by --session-profile')
    if not getattr(args, 'xdvd_read_map', None):
        args.xdvd_read_map = 'windows'
    if args.serve:
        return run_server(args)
    return run_roundtrip(args)


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