"""System Design Lesson 2: estimate notification fan-out and worker capacity.

This uses only the Python standard library.
"""

from __future__ import annotations

from dataclasses import dataclass
from math import ceil

SECONDS_PER_DAY = 86_400


@dataclass(frozen=True)
class NotificationLoad:
    requests_per_day: int
    channels_per_request: float
    peak_factor: float
    worker_deliveries_per_second: int
    target_utilization: float = 0.65

    def estimate(self) -> dict[str, float | int]:
        average_requests = self.requests_per_day / SECONDS_PER_DAY
        peak_requests = average_requests * self.peak_factor
        peak_deliveries = peak_requests * self.channels_per_request
        safe_worker_capacity = self.worker_deliveries_per_second * self.target_utilization
        workers = ceil(peak_deliveries / safe_worker_capacity)
        return {
            "average_request_qps": average_requests,
            "peak_request_qps": peak_requests,
            "peak_delivery_operations": peak_deliveries,
            "minimum_workers_at_target_utilization": workers,
        }


def main() -> None:
    load = NotificationLoad(
        requests_per_day=500_000_000,
        channels_per_request=3,
        peak_factor=5,
        worker_deliveries_per_second=250,
    )
    estimates = load.estimate()
    assert estimates["minimum_workers_at_target_utilization"] > 0
    for name, value in estimates.items():
        print(f"{name:40} {value:,.2f}" if isinstance(value, float) else f"{name:40} {value:,}")

    print("\nDesign questions")
    print("- Can a campaign starve transactional work?")
    print("- What happens when a provider is slower than incoming delivery work?")
    print("- Which retries are idempotent, bounded, and observable?")

    # Try it yourself:
    # 1. Split traffic into transactional and marketing priority pools.
    # 2. Model one provider at half capacity for 30 minutes.
    # 3. Calculate queue growth and the time required to drain it.


if __name__ == "__main__":
    main()
