```python
#!/usr/bin/env python3
"""worker_threads_readiness.py

A small, vendor-neutral readiness checker for deciding whether a CPU-heavy
Node.js workload is a good candidate for worker threads.

What it does:
- Collects simple workload characteristics from the operator
- Estimates whether the task is likely CPU-bound enough to benefit from offloading
- Highlights operational concerns such as payload size, worker churn, memory overhead,
  and failure-handling readiness
- Produces a concise recommendation

This script is intentionally lightweight and safe:
- No network calls
- No secrets
- No destructive actions
- No Node.js runtime dependency

Usage examples:
  python worker_threads_readiness.py
  python worker_threads_readiness.py --task-ms 120 --payload-kb 32 --requests-per-sec 20
  python worker_threads_readiness.py --json
"""

from __future__ import annotations

import argparse
import json
import math
import sys
from dataclasses import dataclass, asdict
from typing import Any, Dict, List


@dataclass
class ReadinessResult:
    score: int
    recommendation: str
    verdict: str
    notes: List[str]
    warnings: List[str]
    inputs: Dict[str, Any]


def positive_int(value: str) -> int:
    try:
        ivalue = int(value)
    except ValueError as exc:
        raise argparse.ArgumentTypeError(f"Expected an integer, got {value!r}") from exc
    if ivalue < 0:
        raise argparse.ArgumentTypeError("Value must be non-negative")
    return ivalue


def bounded_float(value: str) -> float:
    try:
        fvalue = float(value)
    except ValueError as exc:
        raise argparse.ArgumentTypeError(f"Expected a number, got {value!r}") from exc
    if fvalue < 0:
        raise argparse.ArgumentTypeError("Value must be non-negative")
    return fvalue


def rate_label(requests_per_sec: float) -> str:
    if requests_per_sec <= 1:
        return "low"
    if requests_per_sec <= 10:
        return "moderate"
    return "high"


def estimate_score(task_ms: float, payload_kb: float, requests_per_sec: float, worker_pool_size: int,
                   event_loop_delay_ms: float, memory_limit_mb: float, workers_planned: int,
                   is_cpu_bound: bool, is_self_contained: bool, short_lived: bool) -> ReadinessResult:
    notes: List[str] = []
    warnings: List[str] = []
    score = 0

    # CPU suitability
    if is_cpu_bound:
        score += 30
        notes.append("Workload is CPU-bound, which is the main reason to consider worker threads.")
    else:
        score -= 20
        warnings.append("Workload does not appear CPU-bound; worker threads may add complexity without much benefit.")

    # Task duration vs overhead
    if task_ms >= 25:
        score += min(20, int(task_ms / 10))
        notes.append("Task duration is long enough that thread messaging overhead is less likely to dominate.")
    else:
        score -= 15
        warnings.append("Task duration is short; worker startup and messaging overhead may outweigh any gain.")

    # Event-loop impact
    if event_loop_delay_ms >= 20:
        score += 20
        notes.append("Event-loop delay is elevated during the hotspot, suggesting offloading could protect latency.")
    elif event_loop_delay_ms >= 5:
        score += 10
        notes.append("Moderate event-loop delay is present; profiling and targeted offloading may help.")
    else:
        score -= 5
        warnings.append("Event-loop delay is low; confirm the bottleneck before introducing workers.")

    # Payload size
    if payload_kb <= 64:
        score += 10
        notes.append("Payload size is manageable, which reduces serialization cost.")
    elif payload_kb <= 512:
        score += 0
        warnings.append("Payload size is moderate; validate serialization cost and copy semantics.")
    else:
        score -= 15
        warnings.append("Payload is large; copying data to workers may offset latency gains.")

    # Worker pool / churn
    if workers_planned <= 0:
        workers_planned = max(1, worker_pool_size)
    if workers_planned <= worker_pool_size:
        score += 10
        notes.append("Planned worker count appears bounded, which is safer than spawning unbounded workers.")
    else:
        score -= 10
        warnings.append("Planned worker count exceeds the stated pool size; avoid unbounded worker creation.")

    # Memory overhead heuristic
    estimated_worker_overhead_mb = 25 * workers_planned
    if estimated_worker_overhead_mb < memory_limit_mb * 0.35:
        score += 10
        notes.append("Estimated worker memory overhead appears within a reasonable share of the memory limit.")
    else:
        score -= 15
        warnings.append("Estimated worker memory overhead may be high relative to the memory limit.")

    # Structural suitability
    if is_self_contained:
        score += 15
        notes.append("The work is self-contained, which fits the input/output style worker threads handle well.")
    else:
        score -= 20
        warnings.append("The work is not self-contained; shared mutable state or side effects can make worker design brittle.")

    if short_lived:
        score += 5
        notes.append("The task is bounded and short-lived, which is a better fit than long-running request logic.")
    else:
        score -= 5
        warnings.append("The task appears long-lived; consider whether a queue or separate service is a better fit.")

    # Rate pressure
    if requests_per_sec >= 25:
        score += 10
        notes.append("Traffic level is high enough that protecting the main thread could improve overall stability.")
    elif requests_per_sec <= 2:
        warnings.append("Traffic is low; a simpler optimization may be sufficient.")

    # Normalize score
    score = max(0, min(100, score))

    if score >= 75:
        verdict = "strong candidate"
        recommendation = (
            "Proceed with a bounded worker-pool prototype and validate latency, throughput, and memory in a staging environment."
        )
    elif score >= 50:
        verdict = "possible candidate"
        recommendation = (
            "Worker threads may help, but validate profiling evidence first and keep the worker boundary narrow."
        )
    else:
        verdict = "weak candidate"
        recommendation = (
            "Do not prioritize worker threads yet; profile for alternative bottlenecks such as I/O, memory retention, or architecture issues."
        )

    # Operational readiness checks
    if payload_kb > 512:
        warnings.append("Add a plan to avoid sending large objects across the thread boundary.")
    if requests_per_sec > 0 and task_ms > 0:
        per_second_cpu_ms = requests_per_sec * task_ms
        if per_second_cpu_ms > 1000:
            warnings.append("CPU demand appears high; verify that parallelism gains exceed scheduling overhead.")
    if worker_pool_size < 1:
        warnings.append("Worker pool size should be at least 1.")

    return ReadinessResult(
        score=score,
        recommendation=recommendation,
        verdict=verdict,
        notes=notes,
        warnings=warnings,
        inputs={
            "task_ms": task_ms,
            "payload_kb": payload_kb,
            "requests_per_sec": requests_per_sec,
            "worker_pool_size": worker_pool_size,
            "event_loop_delay_ms": event_loop_delay_ms,
            "memory_limit_mb": memory_limit_mb,
            "workers_planned": workers_planned,
            "is_cpu_bound": is_cpu_bound,
            "is_self_contained": is_self_contained,
            "short_lived": short_lived,
            "traffic_level": rate_label(requests_per_sec),
        },
    )


def parse_args(argv: List[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Estimate whether a Node.js workload is a practical candidate for worker threads."
    )
    parser.add_argument("--task-ms", type=bounded_float, default=50.0,
                        help="Estimated synchronous CPU time of the hotspot in milliseconds (default: 50)")
    parser.add_argument("--payload-kb", type=bounded_float, default=32.0,
                        help="Approximate message payload size sent to the worker in kilobytes (default: 32)")
    parser.add_argument("--requests-per-sec", type=bounded_float, default=10.0,
                        help="Approximate request rate for the affected endpoint (default: 10)")
    parser.add_argument("--worker-pool-size", type=positive_int, default=4,
                        help="Target maximum number of workers in the pool (default: 4)")
    parser.add_argument("--event-loop-delay-ms", type=bounded_float, default=10.0,
                        help="Observed or estimated event-loop delay during the hotspot in milliseconds (default: 10)")
    parser.add_argument("--memory-limit-mb", type=bounded_float, default=512.0,
                        help="Approximate container or process memory limit in megabytes (default: 512)")
    parser.add_argument("--workers-planned", type=positive_int, default=2,
                        help="How many workers you expect to run concurrently (default: 2)")
    parser.add_argument("--cpu-bound", action="store_true",
                        help="Mark the workload as CPU-bound")
    parser.add_argument("--not-cpu-bound", dest="cpu_bound", action="store_false",
                        help="Mark the workload as not CPU-bound")
    parser.set_defaults(cpu_bound=True)
    parser.add_argument("--self-contained", action="store_true",
                        help="Mark the work as self-contained input/output processing")
    parser.add_argument("--not-self-contained", dest="self_contained", action="store_false",
                        help="Mark the work as not self-contained")
    parser.set_defaults(self_contained=True)
    parser.add_argument("--short-lived", action="store_true",
                        help="Mark the task as short-lived")
    parser.add_argument("--not-short-lived", dest="short_lived", action="store_false",
                        help="Mark the task as long-lived")
    parser.set_defaults(short_lived=True)
    parser.add_argument("--json", action="store_true", help="Output JSON instead of human-readable text")
    return parser.parse_args(argv)


def main(argv: List[str]) -> int:
    args = parse_args(argv)
    result = estimate_score(
        task_ms=args.task_ms,
        payload_kb=args.payload_kb,
        requests_per_sec=args.requests_per_sec,
        worker_pool_size=args.worker_pool_size,
        event_loop_delay_ms=args.event_loop_delay_ms,
        memory_limit_mb=args.memory_limit_mb,
        workers_planned=args.workers_planned,
        is_cpu_bound=args.cpu_bound,
        is_self_contained=args.self_contained,
        short_lived=args.short_lived,
    )

    if args.json:
        print(json.dumps(asdict(result), indent=2))
        return 0

    print("Node.js Worker Threads Readiness Report")
    print("=" * 40)
    print(f"Score: {result.score}/100")
    print(f"Verdict: {result.verdict}")
    print(f"Recommendation: {result.recommendation}")
    print()
    print("Inputs:")
    for key, value in result.inputs.items():
        print(f"  - {key}: {value}")
    if result.notes:
        print()
        print("Positive signals:")
        for item in result.notes:
            print(f"  - {item}")
    if result.warnings:
        print()
        print("Warnings to verify before production:")
        for item in result.warnings:
            print(f"  - {item}")
    print()
    print("Suggested validation steps:")
    print("  1. Profile the hotspot and confirm the bottleneck is CPU, not I/O or memory retention.")
    print("  2. Prototype a bounded worker pool with a small, structured message contract.")
    print("  3. Add timeout, error, and termination handling for worker failure.")
    print("  4. Measure p95 latency, throughput, and memory before and after the change.")
    print("  5. Verify that large payloads are not eroding the performance gain.")
    return 0


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