#!/usr/bin/env python3
"""Stateful line-oriented GDR/XDVD SCSI JSONL host bridge.

This is still a user-mode bridge, not an iSCSI target.  It accepts JSON/JSONL
host requests, keeps lightweight drive/media/auth state, resolves raw CDBs
through the same XDVD sidecar parser used by the proven persistent firmware
session, and can compare responses against a generated command catalog.

Input request examples:
  {"tag":"tur", "cdb_hex":"00 00 00 00 00 00"}
  {"tag":"msel", "cdb_hex":"55 00 ...", "data_out_hex":"00 1A ..."}
  {"op":"state"}
  {"op":"eject"}
  {"op":"load"}
  {"op":"reset_auth", "stage":"locked"}
  {"op":"mount", "xdvd_json":"disc.xdvd.json", "iso":"disc.iso", "log":"disc.log"}
  {"op":"unmount"}

Output response examples:
  {"ok":true,"scsi_status":"GOOD","direction":"in","data_len":36,...}
  {"ok":false,"scsi_status":"CHECK CONDITION","sense_key":"NOT READY",...}
"""
from __future__ import annotations

import argparse, hashlib, json, sys
from pathlib import Path
from typing import Any

from gdr_scsi_command_parser import GDRScsiParser, parse_hex_bytes, hx
from hlds_profiles import apply_session_to_namespace


def norm_hex(text: Any) -> str:
    return hx(parse_hex_bytes(text))


def payload_bytes(cmd: dict[str, str]) -> bytes:
    direction = str(cmd.get('direction','')).lower()
    text = cmd.get('data_in_hex') if direction == 'in' else cmd.get('data_out_hex') if direction == 'out' else ''
    return parse_hex_bytes(text)


def load_requests(path: str | None) -> list[dict[str, Any]]:
    if not path or path == '-':
        rows=[]
        for line in sys.stdin:
            line=line.strip()
            if not line or line.startswith('#'):
                continue
            rows.append(json.loads(line))
        return rows
    p=Path(path)
    text=p.read_text(encoding='utf-8')
    stripped=text.lstrip()
    if stripped.startswith('[') or stripped.startswith('{'):
        try:
            raw=json.loads(text)
        except json.JSONDecodeError:
            raw=None
        if raw is not None:
            items=raw.get('commands', raw.get('requests', raw)) if isinstance(raw, dict) else raw
            if not isinstance(items, list):
                raise SystemExit('script JSON must contain a list, commands, or requests')
            return [dict(x) for x in items]
    rows=[]
    for line in text.splitlines():
        line=line.strip()
        if not line or line.startswith('#'):
            continue
        rows.append(json.loads(line))
    return rows


def fixed_sense(sense_key: int = 0x00, asc: int = 0x00, ascq: int = 0x00, alloc: int = 18) -> bytes:
    data=bytearray([0x70,0x00,sense_key,0x00,0x00,0x00,0x00,0x0A,0x00,0x00,0x00,0x00,asc,ascq,0x00,0x00,0x00,0x00])
    return bytes(data[:alloc or len(data)])


SENSE_NAMES={
    0x00:'NO SENSE',
    0x02:'NOT READY',
    0x05:'ILLEGAL REQUEST',
}


def media_identity(parser: GDRScsiParser, iso_path: Path | None) -> dict[str, Any]:
    image = parser.sidecar.get('image') or {}
    disc = parser.sidecar.get('disc') or {}
    mm = parser.media_model_summary() if hasattr(parser, 'media_model_summary') else {}
    return {
        'xdvd_json': str(parser.xdvd_json),
        'iso': str(iso_path) if iso_path else str(mm.get('iso_path') or ''),
        'image_file': str(image.get('file') or ''),
        'image_sha1': str(image.get('sha1') or ''),
        'disc_title': str(mm.get('title') or disc.get('title') or disc.get('name') or ''),
        'disc_id': str(mm.get('media_id') or disc.get('id') or disc.get('title_id') or disc.get('media_id') or ''),
        'media_model': mm,
        'iso_sector_count': int(mm.get('iso_sector_count') or 0),
        'iso_byte_count': int(mm.get('iso_byte_count') or 0),
        'xbox_physical_sectors': int(mm.get('xbox_physical_sectors') or 0),
        'layer_break_lba': int(mm.get('layer_break_lba') or 0),
        'media_model_warnings': list(mm.get('warnings') or []),
    }


class Catalog:
    def __init__(self, path: str | None):
        self.path = Path(path) if path else None
        self.entries: list[dict[str, Any]] = []
        if self.path and self.path.exists():
            doc=json.loads(self.path.read_text(encoding='utf-8'))
            self.entries=[dict(x) for x in doc.get('entries', [])]
            for e in self.entries:
                e['_cdb_norm']=norm_hex(e.get('cdb_hex',''))

    def match(self, cdb_hex: str, role: str, payload_sha256: str) -> dict[str, Any] | None:
        cdb_norm=norm_hex(cdb_hex)
        for e in self.entries:
            if e.get('_cdb_norm') != cdb_norm:
                continue
            if e.get('data_role') != role:
                continue
            if str(e.get('payload_sha256','')) == str(payload_sha256 or ''):
                return e
        return None

    def candidate_count(self, cdb_hex: str) -> int:
        cdb_norm=norm_hex(cdb_hex)
        return sum(1 for e in self.entries if e.get('_cdb_norm') == cdb_norm)


class HostBridge:
    def __init__(self, parser: GDRScsiParser, catalog: Catalog | None = None):
        self.parser=parser
        self.catalog=catalog or Catalog(None)
        self.media_present=True
        self.tray_state='closed'
        self.prevent_removal=False
        self.last_sense=(0x00,0x00,0x00)
        self.media = media_identity(self.parser, self.parser.iso_path)

    def sense_response(self, cdb: bytes, tag: str, index: int) -> dict[str, Any]:
        alloc=cdb[4] if len(cdb) >= 5 else 18
        sk,asc,ascq=self.last_sense
        data=fixed_sense(sk, asc, ascq, alloc)
        # Leave REQUEST SENSE non-destructive enough for repeated probes, but clear
        # a prior CHECK CONDITION once it has been observed.
        self.last_sense=(0x00,0x00,0x00)
        return self.good_row(index, tag, cdb, 'in', data, note='request_sense_from_bridge_state')

    def check_row(self, index: int, tag: str, cdb: bytes | None, sense_key: int, asc: int, ascq: int, error: str) -> dict[str, Any]:
        self.last_sense=(sense_key,asc,ascq)
        sense_data = fixed_sense(sense_key, asc, ascq, 18)
        return {
            'ok': False,
            'index': index,
            'tag': tag,
            'auth_stage_after': self.parser.auth_stage,
            'media_present': self.media_present,
            'tray_state': self.tray_state,
            'xdvd_json': self.media.get('xdvd_json',''),
            'iso': self.media.get('iso',''),
            'image_sha1': self.media.get('image_sha1',''),
            'disc_title': self.media.get('disc_title',''),
            'disc_id': self.media.get('disc_id',''),
            'iso_sector_count': self.media.get('iso_sector_count',0),
            'iso_byte_count': self.media.get('iso_byte_count',0),
            'xbox_physical_sectors': self.media.get('xbox_physical_sectors',0),
            'layer_break_lba': self.media.get('layer_break_lba',0),
            'scsi_status': 'CHECK CONDITION',
            'sense_key': SENSE_NAMES.get(sense_key, f'0x{sense_key:02X}'),
            'sense_key_hex': f'0x{sense_key:02X}',
            'asc': f'0x{asc:02X}',
            'ascq': f'0x{ascq:02X}',
            'direction': 'none',
            'cdb_hex': hx(cdb) if cdb else '',
            'data_len': 0,
            'data_sha256': '',
            'data_hex': '',
            'data_preview_hex': '',
            'sense_data_hex': hx(sense_data),
            'sense_data_sha256': hashlib.sha256(sense_data).hexdigest(),
            'error': error,
        }

    def good_row(self, index: int, tag: str, cdb: bytes, direction: str, payload: bytes, *, note: str = '') -> dict[str, Any]:
        role='device_to_host' if direction == 'in' else 'host_to_device' if direction == 'out' else 'no_data'
        sha=hashlib.sha256(payload).hexdigest() if payload else ''
        cdb_text=hx(cdb)
        cat_match=self.catalog.match(cdb_text, role, sha)
        row={
            'ok': True,
            'index': index,
            'tag': tag,
            'auth_stage_after': self.parser.auth_stage,
            'media_present': self.media_present,
            'tray_state': self.tray_state,
            'prevent_removal': self.prevent_removal,
            'xdvd_json': self.media.get('xdvd_json',''),
            'iso': self.media.get('iso',''),
            'image_sha1': self.media.get('image_sha1',''),
            'disc_title': self.media.get('disc_title',''),
            'disc_id': self.media.get('disc_id',''),
            'iso_sector_count': self.media.get('iso_sector_count',0),
            'iso_byte_count': self.media.get('iso_byte_count',0),
            'xbox_physical_sectors': self.media.get('xbox_physical_sectors',0),
            'layer_break_lba': self.media.get('layer_break_lba',0),
            'scsi_status': 'GOOD',
            'sense_key': 'NO SENSE',
            'sense_key_hex': '0x00',
            'asc': '0x00',
            'ascq': '0x00',
            'direction': direction,
            'cdb_hex': cdb_text,
            'data_len': len(payload),
            'data_sha256': sha,
            'data_hex': hx(payload),
            'data_preview_hex': hx(payload[:64]),
            'catalog_candidates': self.catalog.candidate_count(cdb_text),
            'catalog_match': bool(cat_match),
            'catalog_tag': cat_match.get('tag','') if cat_match else '',
        }
        if note:
            row['note']=note
        return row

    def control_response(self, req: dict[str, Any], index: int) -> dict[str, Any]:
        op=str(req.get('op','')).lower().strip()
        tag=str(req.get('tag') or op or f'req{index:03d}')

        def state_row(**extra: Any) -> dict[str, Any]:
            row={
                'ok': True, 'index': index, 'tag': tag, 'op': op, 'scsi_status': 'GOOD',
                'auth_stage_after': self.parser.auth_stage,
                'media_present': self.media_present, 'tray_state': self.tray_state,
                'prevent_removal': self.prevent_removal,
                'xdvd_json': self.media.get('xdvd_json',''),
                'iso': self.media.get('iso',''),
                'image_sha1': self.media.get('image_sha1',''),
                'disc_title': self.media.get('disc_title',''),
                'disc_id': self.media.get('disc_id',''),
                'iso_sector_count': self.media.get('iso_sector_count',0),
                'iso_byte_count': self.media.get('iso_byte_count',0),
                'xbox_physical_sectors': self.media.get('xbox_physical_sectors',0),
                'layer_break_lba': self.media.get('layer_break_lba',0),
                'media_model_warnings': self.media.get('media_model_warnings',[]),
            }
            row.update(extra)
            return row

        if op in ('state','status','media_info'):
            return state_row(firmware_lba_state=self.parser.firmware_lba_state() if hasattr(self.parser, 'firmware_lba_state') else {})
        if op in ('media_model','media_summary'):
            return state_row(media_model=self.media.get('media_model', {}))
        if op in ('firmware_lba_state','firmware_state','lba_state'):
            return state_row(firmware_state=self.parser.firmware_state_summary() if hasattr(self.parser, 'firmware_state_summary') else {})
        if op == 'reset_auth':
            stage=str(req.get('stage','locked'))
            if hasattr(self.parser, '_set_auth_stage'):
                self.parser._set_auth_stage(stage, event='bridge_reset_auth', detail='jsonl_control')
            else:
                self.parser.auth_stage=stage
            self.last_sense=(0x00,0x00,0x00)
            return state_row()
        if op == 'eject':
            if self.prevent_removal and not req.get('force'):
                return self.check_row(index, tag, None, 0x05, 0x53, 0x02, 'medium removal prevented')
            self.media_present=False
            self.tray_state='open'
            if hasattr(self.parser, '_set_auth_stage'):
                self.parser._set_auth_stage('locked', event='bridge_media_transition', detail=op)
            else:
                self.parser.auth_stage='locked'
            return state_row()
        if op in ('load','close'):
            self.media_present=True
            self.tray_state='closed'
            if req.get('reset_auth', True):
                if hasattr(self.parser, '_set_auth_stage'):
                    self.parser._set_auth_stage('locked', event='bridge_media_transition', detail=op)
                else:
                    self.parser.auth_stage='locked'
            self.last_sense=(0x00,0x00,0x00)
            return state_row()
        if op in ('unmount','remove_media'):
            if self.prevent_removal and not req.get('force'):
                return self.check_row(index, tag, None, 0x05, 0x53, 0x02, 'medium removal prevented')
            self.media_present=False
            self.tray_state='open'
            if hasattr(self.parser, '_set_auth_stage'):
                self.parser._set_auth_stage('locked', event='bridge_media_transition', detail=op)
            else:
                self.parser.auth_stage='locked'
            self.last_sense=(0x00,0x00,0x00)
            return state_row(note='media_unmounted')
        if op in ('mount','select_media'):
            xdvd = Path(str(req.get('xdvd_json') or req.get('xdvd') or self.parser.xdvd_json))
            iso_value = req.get('iso') if req.get('iso') is not None else req.get('iso_path')
            iso = Path(str(iso_value)) if iso_value else self.parser.iso_path
            log_value = req.get('log') if req.get('log') is not None else req.get('log_path')
            log = Path(str(log_value)) if log_value else (xdvd.with_suffix('.log'))
            if not xdvd.exists():
                return self.check_row(index, tag, None, 0x05, 0x3A, 0x00, f'xdvd_json not found: {xdvd}')
            if iso and not iso.exists():
                return self.check_row(index, tag, None, 0x05, 0x3A, 0x00, f'iso not found: {iso}')
            self.parser = GDRScsiParser(xdvd, iso, log if log and log.exists() else None, read_map=getattr(self.parser, 'read_map', 'windows'), read_trace_path=getattr(self.parser, 'read_trace_path', None), read_trace_limit=getattr(self.parser, 'read_trace_limit', 0), device_profile=getattr(self.parser, 'device_profile', None))
            self.media = media_identity(self.parser, self.parser.iso_path)
            self.media_present=True
            self.tray_state='closed'
            self.prevent_removal=False
            if req.get('stage'):
                stage = str(req.get('stage'))
                if hasattr(self.parser, '_set_auth_stage'):
                    self.parser._set_auth_stage(stage, event='bridge_mount_stage', detail='explicit_stage')
                else:
                    self.parser.auth_stage=stage
            elif req.get('unlocked'):
                if hasattr(self.parser, '_set_auth_stage'):
                    self.parser._set_auth_stage('unlocked', event='bridge_mount_stage', detail='unlocked_flag')
                else:
                    self.parser.auth_stage='unlocked'
            else:
                if hasattr(self.parser, '_set_auth_stage'):
                    self.parser._set_auth_stage('locked', event='bridge_media_transition', detail=op)
                else:
                    self.parser.auth_stage='locked'
            self.last_sense=(0x00,0x00,0x00)
            return state_row(note='media_mounted')
        return self.check_row(index, tag, None, 0x05, 0x20, 0x00, f'unsupported control op: {op}')

    def response_for(self, req: dict[str, Any], index: int) -> dict[str, Any]:
        if req.get('op'):
            return self.control_response(req, index)
        tag=str(req.get('tag') or req.get('title') or f'req{index:03d}')
        try:
            if req.get('reset_auth_stage'):
                self.parser.auth_stage = str(req.get('reset_auth_stage'))
            cdb=parse_hex_bytes(req.get('cdb_hex', req.get('cdb')))
            data_out=parse_hex_bytes(req.get('data_out_hex', req.get('data_out')))
            if not cdb:
                return self.check_row(index, tag, None, 0x05, 0x20, 0x00, 'empty CDB')
            op=cdb[0]

            # Bridge-level media/tray state, enough for command-server prototyping.
            if op == 0x03:
                return self.sense_response(cdb, tag, index)
            if op == 0x00 and not self.media_present:
                return self.check_row(index, tag, cdb, 0x02, 0x3A, 0x00, 'medium not present')
            if op == 0x1B:
                loej = bool(cdb[4] & 0x02) if len(cdb) >= 5 else False
                start = bool(cdb[4] & 0x01) if len(cdb) >= 5 else False
                if loej and not start:
                    if self.prevent_removal:
                        return self.check_row(index, tag, cdb, 0x05, 0x53, 0x02, 'medium removal prevented')
                    self.media_present=False
                    self.tray_state='open'
                    if hasattr(self.parser, '_set_auth_stage'):
                        self.parser._set_auth_stage('locked', event='bridge_media_transition', detail='start_stop_eject')
                    else:
                        self.parser.auth_stage='locked'
                elif loej and start:
                    self.media_present=True
                    self.tray_state='closed'
                    if hasattr(self.parser, '_set_auth_stage'):
                        self.parser._set_auth_stage('locked', event='bridge_media_transition', detail='start_stop_load')
                    else:
                        self.parser.auth_stage='locked'
                return self.good_row(index, tag, cdb, 'none', b'', note='start_stop_unit_stateful')
            if op == 0x1E:
                self.prevent_removal=bool(cdb[4] & 0x01) if len(cdb) >= 5 else False
                return self.good_row(index, tag, cdb, 'none', b'', note='prevent_allow_stateful')
            media_required = op not in (0x00,0x03,0x12,0x1B,0x1E)
            if media_required and not self.media_present:
                return self.check_row(index, tag, cdb, 0x02, 0x3A, 0x00, 'medium not present')

            cmd=self.parser.resolve(cdb, data_out=data_out, tag=tag, title=str(req.get('title') or tag))
            direction=str(cmd.get('direction','none')).lower()
            payload=payload_bytes(cmd)
            row = self.good_row(index, tag, cdb, direction, payload)
            for meta_key in (
                'read_map_mode','read_map_note','auth_stage','host_lba','iso_lba','blocks','bytes',
                'signature','read_preview_hex','visible_sector_count','game_output_start_lba',
                'game_unlocked_source_start_lba','game_sector_count','host_end_lba','host_layer',
                'host_crosses_layer_break','iso_end_lba','iso_layer','iso_crosses_layer_break',
                'layer_break_lba','xbox_physical_sectors','iso_sector_count','media_id',
                'firmware_window_name','firmware_current_lba_base','firmware_host_start_lba',
                'firmware_host_end_lba','firmware_iso_start_lba','firmware_iso_end_lba',
                'firmware_window_sector_count','firmware_capacity_sector_count',
                'firmware_state_source','firmware_event_count'
            ):
                if meta_key in cmd:
                    row[meta_key] = cmd.get(meta_key)
            return row
        except Exception as e:
            cdb = parse_hex_bytes(req.get('cdb_hex', req.get('cdb'))) if (req.get('cdb_hex') or req.get('cdb')) else b''
            row = self.check_row(index, tag, cdb, 0x05, 0x20, 0x00, str(e))
            data_out = parse_hex_bytes(req.get('data_out_hex', req.get('data_out'))) if (req.get('data_out_hex') or req.get('data_out')) else b''
            if data_out:
                row['direction'] = 'out'
                row['data_out_len'] = len(data_out)
                row['data_out_sha256'] = __import__('hashlib').sha256(data_out).hexdigest()
                row['data_out_hex'] = hx(data_out)
                row['captured_data_out'] = True
            return row


def write_verify(path: Path, rows: list[dict[str, Any]], catalog: Catalog) -> None:
    ok=sum(1 for r in rows if r.get('ok'))
    check=sum(1 for r in rows if r.get('scsi_status') == 'CHECK CONDITION')
    payload=sum(1 for r in rows if int(r.get('data_len') or 0) > 0)
    cat=sum(1 for r in rows if r.get('catalog_match'))
    lines=[]
    lines.append('# GDR JSONL host bridge verification')
    lines.append('')
    lines.append(f'requests={len(rows)}')
    lines.append(f'ok={ok}')
    lines.append(f'check_condition={check}')
    lines.append(f'payload_responses={payload}')
    lines.append(f'catalog_entries={len(catalog.entries)}')
    lines.append(f'catalog_matches={cat}')
    lines.append('')
    lines.append('| index | tag | status | direction | len | auth_stage | catalog |')
    lines.append('|---:|---|---|---|---:|---|---|')
    for r in rows:
        lines.append(f"| {r.get('index')} | `{r.get('tag','')}` | {r.get('scsi_status','')} | {r.get('direction','')} | {r.get('data_len',0)} | {r.get('auth_stage_after','')} | {str(bool(r.get('catalog_match'))).lower()} |")
    path.write_text('\n'.join(lines)+'\n', encoding='utf-8')


def write_outputs(rows: list[dict[str, Any]], out_path: str | None, response_dir: str | None) -> None:
    if response_dir:
        rd=Path(response_dir)
        rd.mkdir(parents=True, exist_ok=True)
        for row in rows:
            safe=str(row.get('tag') or f"req{row.get('index',0):03d}").replace('/','_').replace('\\','_')
            if row.get('ok') and row.get('data_len',0):
                data=parse_hex_bytes(row.get('data_hex',''))
                fn=f"{int(row.get('index',0)):03d}_{safe}.bin"
                (rd/fn).write_bytes(data)
                row['data_file']=fn
            if row.get('data_out_hex'):
                data_out=parse_hex_bytes(row.get('data_out_hex',''))
                fn=f"{int(row.get('index',0)):03d}_{safe}_data_out.bin"
                (rd/fn).write_bytes(data_out)
                row['data_out_file']=fn
    lines='\n'.join(json.dumps(r, separators=(',',':')) for r in rows) + ('\n' if rows else '')
    if out_path and out_path != '-':
        Path(out_path).write_text(lines, encoding='utf-8')
    else:
        sys.stdout.write(lines)


def main() -> int:
    ap=argparse.ArgumentParser(description='Stateful GDR/XDVD SCSI JSONL 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', help='unlock log path; required to resolve captured authentication challenge responses')
    ap.add_argument('--script', '-i', help='JSON/JSONL commands to process; stdin when omitted or -')
    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 the JSONL bridge output')
    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; physical_raw/raw_full is direct full-disc ISO, xiso_alias/dual_alias remaps unlocked game LBAs for XISO extraction')
    ap.add_argument('--read-trace-jsonl', default=None, help='write compact READ(10/12) host_lba->iso_lba 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'

    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=args.xdvd_read_map, read_trace_path=args.read_trace_jsonl, read_trace_limit=args.read_trace_limit, device_profile=getattr(args, 'device_profile', None))
    if args.media_model_summary_json:
        parser.media_model.write_summary_json(args.media_model_summary_json)
    catalog=Catalog(args.catalog)
    bridge=HostBridge(parser, catalog)
    reqs=load_requests(args.script)
    rows=[bridge.response_for(req, i) for i,req in enumerate(reqs)]
    write_outputs(rows, args.out, args.write_response_dir)
    if args.verify_md:
        write_verify(Path(args.verify_md), rows, catalog)
    return 0 if all(r.get('ok') for r in rows) else 2


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