#!/usr/bin/env python3
"""Secure async network validation helper.

This script is a small, vendor-neutral preflight tool inspired by secure async/await
network programming practices. It does not perform real production traffic by default.
Instead, it helps you validate that a target URL, payload limit, timeout, and content
expectations are aligned before you wire the path into application code.

Capabilities:
- Parse and validate a target URL
- Send a GET request with a bounded timeout
- Enforce a maximum response size while reading incrementally
- Validate response status and optional content type
- Exit with clear codes for success and common failure modes

Notes:
- No credentials or secrets are embedded.
- Use only against systems you are authorized to test.
- For production application code, prefer HttpClient in C# with async/await and
  cancellation tokens; this script is a practical validation companion.
"""

from __future__ import annotations

import argparse
import sys
import time
from dataclasses import dataclass
from typing import Optional
from urllib.parse import urlparse
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError


DEFAULT_TIMEOUT_SECONDS = 10.0
DEFAULT_MAX_BYTES = 1_000_000
DEFAULT_USER_AGENT = "VectraOps-SecureAsyncValidator/1.0"


@dataclass
class Result:
    ok: bool
    message: str
    status_code: Optional[int] = None
    content_type: Optional[str] = None
    bytes_read: int = 0
    elapsed_seconds: float = 0.0


def validate_url(value: str) -> str:
    parsed = urlparse(value)
    if parsed.scheme not in {"http", "https"}:
        raise ValueError("URL must start with http:// or https://")
    if not parsed.netloc:
        raise ValueError("URL must include a host")
    return value


def normalize_content_type(header_value: Optional[str]) -> str:
    if not header_value:
        return ""
    return header_value.split(";", 1)[0].strip().lower()


def fetch_with_limits(url: str, timeout_seconds: float, max_bytes: int, expected_content_type: Optional[str]) -> Result:
    start = time.monotonic()
    request = Request(url, method="GET", headers={"User-Agent": DEFAULT_USER_AGENT})

    try:
        with urlopen(request, timeout=timeout_seconds) as response:
            status_code = getattr(response, "status", None) or response.getcode()
            content_type = normalize_content_type(response.headers.get("Content-Type"))

            if status_code < 200 or status_code >= 300:
                return Result(
                    ok=False,
                    message=f"Unexpected status code: {status_code}",
                    status_code=status_code,
                    content_type=content_type,
                    elapsed_seconds=time.monotonic() - start,
                )

            if expected_content_type and content_type != expected_content_type.lower():
                return Result(
                    ok=False,
                    message=f"Unexpected content type: {content_type or 'missing'}",
                    status_code=status_code,
                    content_type=content_type,
                    elapsed_seconds=time.monotonic() - start,
                )

            total = 0
            chunk_size = 8192
            while True:
                chunk = response.read(chunk_size)
                if not chunk:
                    break
                total += len(chunk)
                if total > max_bytes:
                    return Result(
                        ok=False,
                        message=f"Response too large: exceeded {max_bytes} bytes",
                        status_code=status_code,
                        content_type=content_type,
                        bytes_read=total,
                        elapsed_seconds=time.monotonic() - start,
                    )

            return Result(
                ok=True,
                message="Request completed within limits",
                status_code=status_code,
                content_type=content_type,
                bytes_read=total,
                elapsed_seconds=time.monotonic() - start,
            )

    except HTTPError as exc:
        return Result(
            ok=False,
            message=f"HTTP error: {exc.code} {exc.reason}",
            status_code=exc.code,
            elapsed_seconds=time.monotonic() - start,
        )
    except URLError as exc:
        return Result(
            ok=False,
            message=f"Transport error: {exc.reason}",
            elapsed_seconds=time.monotonic() - start,
        )
    except TimeoutError:
        return Result(
            ok=False,
            message="Timed out waiting for response",
            elapsed_seconds=time.monotonic() - start,
        )
    except KeyboardInterrupt:
        return Result(
            ok=False,
            message="Canceled by user",
            elapsed_seconds=time.monotonic() - start,
        )


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Validate timeout, content-type, and size-limit behavior for a network endpoint."
    )
    parser.add_argument("url", type=validate_url, help="Target HTTP or HTTPS URL")
    parser.add_argument(
        "--timeout",
        type=float,
        default=DEFAULT_TIMEOUT_SECONDS,
        help=f"Total timeout in seconds (default: {DEFAULT_TIMEOUT_SECONDS})",
    )
    parser.add_argument(
        "--max-bytes",
        type=int,
        default=DEFAULT_MAX_BYTES,
        help=f"Maximum allowed response size in bytes (default: {DEFAULT_MAX_BYTES})",
    )
    parser.add_argument(
        "--expect-content-type",
        default=None,
        help="Optional expected content type, such as application/json",
    )
    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()

    if args.timeout <= 0:
        parser.error("--timeout must be greater than 0")
    if args.max_bytes <= 0:
        parser.error("--max-bytes must be greater than 0")

    result = fetch_with_limits(
        url=args.url,
        timeout_seconds=args.timeout,
        max_bytes=args.max_bytes,
        expected_content_type=args.expect_content_type,
    )

    status_part = f"status={result.status_code}" if result.status_code is not None else "status=unknown"
    type_part = f"content_type={result.content_type or 'unknown'}"
    size_part = f"bytes={result.bytes_read}"
    time_part = f"elapsed={result.elapsed_seconds:.3f}s"

    print(f"{'OK' if result.ok else 'FAIL'}: {result.message} ({status_part}, {type_part}, {size_part}, {time_part})")
    return 0 if result.ok else 1


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