#!/usr/bin/env python3
"""Validate legacy Windows lab-smoke evidence before pilot rollout."""

from __future__ import annotations

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


REQUIRED_CHECKS = (
    "os_identity",
    "task:SK Agent",
    "task:SK Watchdog",
    "task:SK Tray",
    "service:SKRecoverDaemon",
    "recover_listen_47778",
    "firewall:SKRecoverDaemon 47778",
    "file:sk-agent-legacy.ps1",
    "file:sk-watchdog-legacy.ps1",
    "file:sk-tray-legacy.ps1",
    "process:sk-agent-legacy",
    "process:sk-tray",
    "watchdog.log",
    "recover_health",
    "peer_recover",
    "status.json",
)

REQUIRED_COLLECT_FILES = (
    "legacy-windows-lab-smoke.json",
    "task-SK-Agent.txt",
    "task-SK-Watchdog.txt",
    "task-SK-Tray.txt",
    "service-SKRecoverDaemon.txt",
    "firewall-SiteKings-Recover-Daemon.txt",
    "netstat-ano.txt",
    "tasklist.txt",
    "operator-context.json",
    "watchdog.log",
)

OPTIONAL_INFORMATIONAL_CHECKS = {
    "win10_upgrade.status.json",
}

ACCEPTED_WIN10_BRIDGE_STAGES = {
    "setup_running",
    "completed",
}

WIN10_BRIDGE_START_PREFIX = "Starting Windows 10 bridge upgrade"
WIN10_MEDIA_URL_QUERY_RE = re.compile(r"\?[^\s]*(?:[A-Za-z0-9_.~-]+)=")

OS_IDENTITY_FIELDS = (
    "os_caption",
    "os_version",
    "os_service_pack",
    "service_pack",
)

KNOWN_OS_KINDS = {
    "windows_7_sp1",
    "server_2008_r2",
    "unsupported",
}


def _sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()


def _is_sha256_hex(value: Any) -> bool:
    if not isinstance(value, str):
        return False
    stripped = value.strip()
    if len(stripped) != 64:
        return False
    try:
        int(stripped, 16)
    except ValueError:
        return False
    return True


def _check_map(payload: dict[str, Any]) -> dict[str, bool]:
    checks: dict[str, bool] = {}
    for item in payload.get("checks") or []:
        if not isinstance(item, dict):
            continue
        raw_name = item.get("name")
        if not isinstance(raw_name, str):
            continue
        name = raw_name.strip()
        raw_ok = item.get("ok")
        if name and isinstance(raw_ok, bool):
            checks[name] = raw_ok
    return checks


def _check_detail_map(payload: dict[str, Any]) -> dict[str, str]:
    details: dict[str, str] = {}
    for item in payload.get("checks") or []:
        if not isinstance(item, dict):
            continue
        raw_name = item.get("name")
        raw_detail = item.get("detail")
        if isinstance(raw_name, str) and isinstance(raw_detail, str):
            name = raw_name.strip()
            if name:
                details[name] = raw_detail
    return details


def _validate_smoke_checks(payload: dict[str, Any], host: str) -> list[str]:
    errors: list[str] = []
    checks = payload.get("checks")
    if not isinstance(checks, list):
        return [f"{host}: smoke checks must be a list"]

    seen: set[str] = set()
    duplicates: set[str] = set()
    for item in checks:
        if not isinstance(item, dict):
            errors.append(f"{host}: smoke checks entries must be objects")
            continue
        raw_name = item.get("name")
        if not isinstance(raw_name, str):
            errors.append(f"{host}: smoke check names must be strings")
            continue
        name = raw_name.strip()
        if not name:
            errors.append(f"{host}: smoke check names must not be blank")
            continue
        if not isinstance(item.get("ok"), bool):
            errors.append(f"{host}: smoke check ok values must be booleans")
            continue
        if name in seen:
            duplicates.add(name)
        seen.add(name)
    for name in sorted(duplicates):
        errors.append(f"{host}: duplicate smoke check: {name}")
    return errors


def _load_json(path: Path) -> tuple[dict[str, Any] | None, str | None]:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception as exc:
        return None, f"{path}: cannot read JSON: {exc}"
    if not isinstance(data, dict):
        return None, f"{path}: root JSON value must be an object"
    return data, None


def _infer_legacy_os_kind(payload: dict[str, Any]) -> str:
    hay = " ".join(
        value.lower()
        for key in OS_IDENTITY_FIELDS
        if isinstance((value := payload.get(key)), str)
    )
    is_sp1 = "service pack 1" in hay or "sp1" in hay or "6.1.7601" in hay
    if "server 2008 r2" in hay and is_sp1:
        return "server_2008_r2"
    if ("windows 7" in hay or "windows nt 6.1" in hay) and is_sp1:
        return "windows_7_sp1"
    return "unsupported"


def _supported_legacy_os_identity(payload: dict[str, Any]) -> bool:
    return _infer_legacy_os_kind(payload) != "unsupported"


def _validate_os_identity_fields(payload: dict[str, Any], host: str) -> list[str]:
    errors: list[str] = []
    for key in OS_IDENTITY_FIELDS:
        value = payload.get(key)
        if value is not None and not isinstance(value, str):
            errors.append(f"{host}: OS identity field {key} must be a string")
    raw_os_kind = payload.get("os_kind")
    if raw_os_kind is not None and not isinstance(raw_os_kind, str):
        errors.append(f"{host}: OS identity field os_kind must be a string")
    elif isinstance(raw_os_kind, str):
        os_kind = raw_os_kind.strip()
        if os_kind not in KNOWN_OS_KINDS:
            errors.append(f"{host}: os_kind is not recognized: {os_kind}")
        inferred = _infer_legacy_os_kind(payload)
        if os_kind in KNOWN_OS_KINDS and os_kind != inferred:
            errors.append(f"{host}: os_kind {os_kind} disagrees with inferred OS identity {inferred}")
    return errors


def validate(path: Path, require_self_rescue: bool, require_win10_status: bool) -> list[str]:
    payload, error = _load_json(path)
    if error:
        return [error]
    assert payload is not None

    errors: list[str] = []
    raw_smoke_hostname = payload.get("hostname")
    smoke_hostname = raw_smoke_hostname.strip() if isinstance(raw_smoke_hostname, str) else ""
    host = smoke_hostname or path.name
    if payload.get("schema") != "legacy-windows-lab-smoke":
        errors.append(f"{host}: schema is not legacy-windows-lab-smoke")
    if raw_smoke_hostname is not None and not isinstance(raw_smoke_hostname, str):
        errors.append(f"{host}: smoke hostname must be a string")
    if not smoke_hostname:
        errors.append(f"{host}: smoke hostname is required")
    errors.extend(_validate_smoke_checks(payload, host))
    errors.extend(_validate_os_identity_fields(payload, host))
    if not _supported_legacy_os_identity(payload):
        errors.append(f"{host}: OS identity is not Windows 7 SP1 or Windows Server 2008 R2 SP1")
    failed = payload.get("failed") or []
    if not isinstance(failed, list):
        errors.append(f"{host}: smoke failed list must be a list")
        failed = []
    failed_names: list[str] = []
    for name in failed:
        if not isinstance(name, str):
            errors.append(f"{host}: smoke failed list names must be strings")
            continue
        name = name.strip()
        if not name:
            errors.append(f"{host}: smoke failed list names must not be blank")
            continue
        failed_names.append(name)
    hard_failed = [
        name
        for name in failed_names
        if name not in OPTIONAL_INFORMATIONAL_CHECKS
    ]
    if payload.get("ok") is not True:
        errors.append(f"{host}: smoke ok must be the boolean true")
        if hard_failed:
            errors.append(f"{host}: smoke ok is not true; failed={hard_failed}")
    elif hard_failed:
        errors.append(f"{host}: smoke failed list contains hard failures: {hard_failed}")

    checks = _check_map(payload)
    details = _check_detail_map(payload)
    for name in REQUIRED_CHECKS:
        if checks.get(name) is not True:
            errors.append(f"{host}: required check failed or missing: {name}")

    failed_names = [
        name for name, ok in checks.items()
        if ok is not True and name not in OPTIONAL_INFORMATIONAL_CHECKS
    ]
    if failed_names:
        errors.append(f"{host}: failed checks present: {', '.join(sorted(failed_names))}")

    if payload.get("exercise_peer_recover") is not True:
        errors.append(f"{host}: signed peer_recover was not exercised")
    if checks.get("peer_recover") is True and "fresh restart-agent.flag observed" not in details.get("peer_recover", ""):
        errors.append(f"{host}: peer_recover detail does not prove fresh restart-agent.flag")
    if require_self_rescue and checks.get("self_rescue") is not True:
        errors.append(f"{host}: --require-self-rescue requested but self_rescue did not pass")
    if require_self_rescue and payload.get("exercise_self_rescue") is not True:
        errors.append(f"{host}: --require-self-rescue requested but signed self_rescue was not exercised")
    if (
        require_self_rescue
        and checks.get("self_rescue") is True
        and "fresh restart-agent.flag and update-flag.txt observed" not in details.get("self_rescue", "")
    ):
        errors.append(f"{host}: self_rescue detail does not prove fresh restart-agent.flag/update-flag.txt")
    if require_win10_status and checks.get("win10_upgrade.status.json") is not True:
        errors.append(f"{host}: --require-win10-status requested but win10_upgrade.status.json is missing")

    return errors


def validate_collect_manifest(path: Path) -> list[str]:
    payload, error = _load_json(path)
    if error:
        return [error]
    assert payload is not None

    errors: list[str] = []
    raw_manifest_hostname = payload.get("hostname")
    manifest_hostname = raw_manifest_hostname.strip() if isinstance(raw_manifest_hostname, str) else ""
    host = manifest_hostname or path.name
    if payload.get("schema") != "legacy-windows-lab-collector":
        errors.append(f"{host}: schema is not legacy-windows-lab-collector")
    if raw_manifest_hostname is not None and not isinstance(raw_manifest_hostname, str):
        errors.append(f"{host}: collector manifest hostname must be a string")
    if not manifest_hostname:
        errors.append(f"{host}: collector manifest hostname is required")
    raw_os_kind = payload.get("os_kind")
    if raw_os_kind is not None and not isinstance(raw_os_kind, str):
        errors.append(f"{host}: collector manifest os_kind must be a string")
    elif isinstance(raw_os_kind, str):
        os_kind = raw_os_kind.strip()
        if os_kind and os_kind not in KNOWN_OS_KINDS:
            errors.append(f"{host}: collector manifest os_kind is not recognized: {os_kind}")

    files = payload.get("files")
    if not isinstance(files, list):
        errors.append(f"{host}: collector manifest files must be a list")
        files = []
    for item in files:
        if not isinstance(item, str):
            errors.append(f"{host}: collector manifest file names must be strings")
        elif not item.strip():
            errors.append(f"{host}: collector manifest file names must not be blank")
    file_names = [name.strip() for name in files if isinstance(name, str) and name.strip()]
    seen_files: set[str] = set()
    duplicate_files: set[str] = set()
    for name in file_names:
        if name in seen_files:
            duplicate_files.add(name)
        seen_files.add(name)
    for name in sorted(duplicate_files):
        errors.append(f"{host}: collector manifest duplicate evidence file: {name}")

    file_set = set(file_names)
    for name in sorted(file_set):
        candidate = Path(name)
        if candidate.name != name or candidate.is_absolute() or "\\" in name:
            errors.append(f"{host}: collector manifest file name must be a plain file name: {name}")
    for name in REQUIRED_COLLECT_FILES:
        if name not in file_set:
            errors.append(f"{host}: collector manifest missing required evidence file: {name}")

    for key in ("install_dir", "out_dir", "started", "finished"):
        value = payload.get(key)
        if not isinstance(value, str):
            errors.append(f"{host}: collector manifest {key} must be a string")
        elif not value.strip():
            errors.append(f"{host}: collector manifest missing {key}")
    raw_build_manifest_sha = payload.get("build_manifest_sha256")
    if not _is_sha256_hex(raw_build_manifest_sha):
        errors.append(f"{host}: collector manifest build_manifest_sha256 must be a 64-character hex string")
    raw_build_manifest_path = payload.get("build_manifest_path")
    if not isinstance(raw_build_manifest_path, str):
        errors.append(f"{host}: collector manifest build_manifest_path must be a string")
    elif not raw_build_manifest_path.strip():
        errors.append(f"{host}: collector manifest missing build_manifest_path")
    for key in ("collector_exit", "smoke_exit"):
        value = payload.get(key)
        if isinstance(value, bool) or not isinstance(value, int) or value != 0:
            errors.append(f"{host}: {key} is required and must be 0")

    return errors


def validate_collect_build_manifest(manifest_path: Path, build_manifest_path: Path) -> list[str]:
    payload, error = _load_json(manifest_path)
    if error:
        return [error]
    assert payload is not None
    raw_manifest_hostname = payload.get("hostname")
    host = raw_manifest_hostname.strip() if isinstance(raw_manifest_hostname, str) and raw_manifest_hostname.strip() else manifest_path.name
    expected = payload.get("build_manifest_sha256")
    errors: list[str] = []
    if not _is_sha256_hex(expected):
        errors.append(f"{host}: collector manifest build_manifest_sha256 must be a 64-character hex string")
        return errors
    try:
        actual = _sha256(build_manifest_path)
    except Exception as exc:
        return [f"{build_manifest_path}: cannot hash supplied build manifest: {exc}"]
    if str(expected).strip().lower() != actual.lower():
        errors.append(f"{host}: collector manifest build_manifest_sha256 must match supplied build manifest")
    return errors


def validate_smoke_collect_hostname(smoke_path: Path, collect_manifest_path: Path) -> list[str]:
    smoke, smoke_error = _load_json(smoke_path)
    collect, collect_error = _load_json(collect_manifest_path)
    errors = [error for error in (smoke_error, collect_error) if error]
    if errors:
        return errors
    assert smoke is not None
    assert collect is not None

    raw_smoke_host = smoke.get("hostname")
    raw_collect_host = collect.get("hostname")
    smoke_host = raw_smoke_host.strip() if isinstance(raw_smoke_host, str) else ""
    collect_host = raw_collect_host.strip() if isinstance(raw_collect_host, str) else ""
    if smoke_host and collect_host and smoke_host.lower() != collect_host.lower():
        return [f"{collect_manifest_path.name}: collector manifest hostname must match smoke hostname: {smoke_host}"]
    raw_smoke_os_kind = smoke.get("os_kind")
    raw_collect_os_kind = collect.get("os_kind")
    smoke_os_kind = raw_smoke_os_kind.strip() if isinstance(raw_smoke_os_kind, str) else ""
    collect_os_kind = raw_collect_os_kind.strip() if isinstance(raw_collect_os_kind, str) else ""
    if smoke_os_kind and collect_os_kind and smoke_os_kind != collect_os_kind:
        return [f"{collect_manifest_path.name}: collector manifest os_kind must match smoke os_kind: {smoke_os_kind}"]
    return []


def validate_collect_root_smoke(smoke_path: Path, collect_root: Path) -> list[str]:
    collect_smoke_path = collect_root / "legacy-windows-lab-smoke.json"
    smoke, smoke_error = _load_json(smoke_path)
    collect_smoke, collect_smoke_error = _load_json(collect_smoke_path)
    errors = [error for error in (smoke_error, collect_smoke_error) if error]
    if errors:
        return errors
    assert smoke is not None
    assert collect_smoke is not None

    raw_smoke_hostname = smoke.get("hostname")
    raw_collect_hostname = collect_smoke.get("hostname")
    smoke_hostname = raw_smoke_hostname.strip() if isinstance(raw_smoke_hostname, str) else ""
    collect_hostname = raw_collect_hostname.strip() if isinstance(raw_collect_hostname, str) else ""
    if smoke_hostname and collect_hostname and smoke_hostname.lower() != collect_hostname.lower():
        errors.append(
            f"{collect_smoke_path.name}: collect-root smoke hostname must match lab smoke hostname: {smoke_hostname}"
        )
    if smoke != collect_smoke:
        errors.append(f"{collect_smoke_path.name}: collect-root smoke artifact must match lab smoke artifact")
    return errors


def _collector_manifest_files(path: Path) -> tuple[dict[str, Any] | None, set[str], list[str]]:
    payload, error = _load_json(path)
    if error:
        return None, set(), [error]
    assert payload is not None
    errors = validate_collect_manifest(path)
    files = payload.get("files")
    if not isinstance(files, list):
        files = []
    file_set = {name.strip() for name in files if isinstance(name, str) and name.strip()}
    return payload, file_set, errors


def validate_operator_context(path: Path, expected_hostname: str | None = None) -> list[str]:
    payload, error = _load_json(path)
    if error:
        return [error]
    assert payload is not None

    errors: list[str] = []
    if payload.get("schema") != "legacy-windows-lab-operator-context":
        errors.append(f"{path.name}: schema is not legacy-windows-lab-operator-context")
    if payload.get("is_elevated") is not True:
        errors.append(f"{path.name}: is_elevated must be true")
    for key in ("username", "computer_name", "script_root"):
        value = payload.get(key)
        if not isinstance(value, str):
            errors.append(f"{path.name}: {key} must be a string")
        elif not value.strip():
            errors.append(f"{path.name}: {key} is required")
    expected = str(expected_hostname or "").strip().lower()
    actual = str(payload.get("computer_name") or "").strip().lower()
    if expected and actual and actual != expected:
        errors.append(f"{path.name}: computer_name must match collector manifest hostname: {expected_hostname}")
    return errors


def validate_collect_root(manifest_path: Path, collect_root: Path) -> list[str]:
    payload, file_set, errors = _collector_manifest_files(manifest_path)
    raw_host = (payload or {}).get("hostname")
    expected_host = raw_host.strip() if isinstance(raw_host, str) and raw_host.strip() else None
    host = expected_host or manifest_path.name
    if not collect_root.exists() or not collect_root.is_dir():
        return errors + [f"{host}: collector root is not a directory: {collect_root}"]

    for name in sorted(file_set):
        path = collect_root / name
        if not path.exists() or not path.is_file():
            errors.append(f"{host}: collector root missing required evidence file: {name}")
        elif path.stat().st_size <= 0:
            errors.append(f"{host}: collector root evidence file is empty: {name}")
    operator_context = collect_root / "operator-context.json"
    if operator_context.exists() and operator_context.is_file() and operator_context.stat().st_size > 0:
        errors.extend(validate_operator_context(operator_context, expected_host))
    return errors


def _validate_redacted_win10_media_url(log_text: str, log_name: str) -> list[str]:
    errors: list[str] = []
    start_lines = [
        line for line in log_text.splitlines()
        if WIN10_BRIDGE_START_PREFIX in line
    ]
    for line in start_lines:
        if "URL=" not in line:
            continue
        media_url = line.split("URL=", 1)[1].strip()
        redacted_url = "<redacted>" in media_url or "%3credacted%3e" in media_url.lower()
        if WIN10_MEDIA_URL_QUERY_RE.search(media_url) and not redacted_url:
            errors.append(f"{log_name}: bridge start log exposes unredacted media URL query string")
    return errors


def validate_win10_bridge_artifacts(status_path: Path, log_path: Path | None = None) -> list[str]:
    payload, error = _load_json(status_path)
    if error:
        return [error]
    assert payload is not None

    errors: list[str] = []
    raw_stage = payload.get("stage")
    if not isinstance(raw_stage, str):
        errors.append(f"{status_path.name}: stage must be a string")
        stage = ""
    else:
        stage = raw_stage.strip()
    if stage not in ACCEPTED_WIN10_BRIDGE_STAGES:
        errors.append(
            f"{status_path.name}: stage {stage or '<missing>'} is not an accepted bridge launch stage"
        )

    percent = payload.get("percent")
    if isinstance(percent, bool) or not isinstance(percent, int) or percent < 0 or percent > 100:
        errors.append(f"{status_path.name}: percent must be an integer from 0 through 100")
    elif stage == "setup_running" and percent < 60:
        errors.append(f"{status_path.name}: setup_running percent must be at least 60")
    elif stage == "completed" and percent != 100:
        errors.append(f"{status_path.name}: completed percent must be 100")

    for key in ("message", "updated_at", "path"):
        value = payload.get(key)
        if not isinstance(value, str):
            errors.append(f"{status_path.name}: {key} must be a string")
        elif not value.strip():
            errors.append(f"{status_path.name}: {key} is required")
    raw_path = payload.get("path")
    if isinstance(raw_path, str) and raw_path.strip() != "media":
        errors.append(f"{status_path.name}: path must be media")
    probe_verdict = payload.get("probe_verdict")
    if probe_verdict != "candidate":
        errors.append(f"{status_path.name}: probe_verdict must be candidate")

    if log_path is not None:
        try:
            log_text = log_path.read_text(encoding="utf-8", errors="replace")
        except Exception as exc:
            errors.append(f"{log_path}: cannot read log: {exc}")
        else:
            if "[win10-media]" not in log_text:
                errors.append(f"{log_path.name}: missing [win10-media] log marker")
            if "Starting Windows 10 bridge upgrade" not in log_text:
                errors.append(f"{log_path.name}: missing bridge start log entry")
            errors.extend(_validate_redacted_win10_media_url(log_text, log_path.name))
            if "Probe verdict candidate confirmed before media launch" not in log_text:
                errors.append(f"{log_path.name}: missing bridge probe verdict log entry")
            if "Operator acknowledgements recorded" not in log_text:
                errors.append(f"{log_path.name}: missing bridge acknowledgement log entry")
            if "Operator evidence notes recorded: customer-scoped enablement" not in log_text:
                errors.append(f"{log_path.name}: missing bridge evidence notes log entry")
            if stage == "setup_running" and "Launching setup.exe" not in log_text:
                errors.append(f"{log_path.name}: missing setup.exe launch log entry")

    return errors


def main(argv: list[str]) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("evidence", nargs="+", type=Path, help="legacy-windows-lab-smoke.json file(s)")
    parser.add_argument("--require-self-rescue", action="store_true", help="Require signed /self-rescue evidence")
    parser.add_argument("--require-win10-status", action="store_true", help="Require win10_upgrade.status.json evidence")
    parser.add_argument("--win10-upgrade-status", type=Path, help="Validate retained win10_upgrade.status.json after a bridge attempt")
    parser.add_argument("--win10-upgrade-log", type=Path, help="Validate retained win10_upgrade.log alongside --win10-upgrade-status")
    parser.add_argument("--collect-manifest", type=Path, help="Validate retained legacy-windows-lab-collector-manifest.json")
    parser.add_argument("--collect-root", type=Path, help="Validate files under a retained legacy-windows-lab-evidence-* folder")
    args = parser.parse_args(argv)

    all_errors: list[str] = []
    for path in args.evidence:
        errors = validate(path, args.require_self_rescue, args.require_win10_status)
        all_errors.extend(errors)
        if not errors:
            payload, _ = _load_json(path)
            host = (payload or {}).get("hostname") or path.name
            print(f"{host}: legacy Windows lab evidence passed")

    if args.win10_upgrade_log and not args.win10_upgrade_status:
        all_errors.append("--win10-upgrade-log requires --win10-upgrade-status")
    if args.win10_upgrade_status:
        win10_errors = validate_win10_bridge_artifacts(args.win10_upgrade_status, args.win10_upgrade_log)
        all_errors.extend(win10_errors)
        if not win10_errors:
            print(f"{args.win10_upgrade_status.name}: Windows 10 bridge artifacts passed")
    if args.collect_manifest:
        collect_errors = validate_collect_manifest(args.collect_manifest)
        for path in args.evidence:
            collect_errors.extend(validate_smoke_collect_hostname(path, args.collect_manifest))
        all_errors.extend(collect_errors)
        if not collect_errors:
            print(f"{args.collect_manifest.name}: legacy Windows lab collect manifest passed")
    if args.collect_root and not args.collect_manifest:
        all_errors.append("--collect-root requires --collect-manifest")
    if args.collect_root and args.collect_manifest:
        collect_root_errors = validate_collect_root(args.collect_manifest, args.collect_root)
        for path in args.evidence:
            collect_root_errors.extend(validate_collect_root_smoke(path, args.collect_root))
        all_errors.extend(collect_root_errors)
        if not collect_root_errors:
            print(f"{args.collect_root.name}: legacy Windows lab collect files passed")

    if all_errors:
        print("legacy Windows lab evidence failed", file=sys.stderr)
        for error in all_errors:
            print(f"- {error}", file=sys.stderr)
        return 1

    print("legacy Windows lab evidence passed")
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
