# Rotation Strategies

`stygian-proxy` ships four built-in rotation strategies. All implement the
`RotationStrategy` trait and operate on a slice of `ProxyCandidate` values
built from the live pool. Strategies that find zero healthy candidates return
`ProxyError::AllProxiesUnhealthy` rather than panicking.

---

## Comparison

| Strategy | Best for | Notes |
| --- | --- | --- |
| `RoundRobinStrategy` | Even distribution across identical proxies | Atomic counter, lock-free |
| `RandomStrategy` | Spreading load unpredictably | `rand::rng()` per call; no shared state |
| `WeightedStrategy` | Prioritising faster or higher-quota proxies | Weighted random sampling; O(n) |
| `LeastUsedStrategy` | Never overloading a single proxy | Picks the candidate with the lowest total request count |

---

## RoundRobinStrategy

Distributes requests evenly across healthy proxies in insertion order. Uses an atomic
counter so there is no `Mutex` on the hot path.

```rust,no_run
use std::sync::Arc;
use stygian_proxy::{MemoryProxyStore, ProxyConfig, ProxyManager};

let storage = Arc::new(MemoryProxyStore::default());
let manager = ProxyManager::with_round_robin(storage, ProxyConfig::default()).unwrap();
```

`ProxyManager::with_round_robin` is the recommended default. The counter wraps safely
at `u64::MAX`, which at 1 million requests per second takes ~585,000 years.

---

## RandomStrategy

Picks a healthy proxy at random on every call. Useful when you want to avoid any
predictable rotation pattern that fingerprinting could detect.

```rust,no_run
use std::sync::Arc;
use stygian_proxy::{MemoryProxyStore, ProxyConfig, ProxyManager};
use stygian_proxy::strategy::RandomStrategy;

let storage = Arc::new(MemoryProxyStore::default());
let manager = ProxyManager::builder()
    .storage(storage)
    .strategy(Arc::new(RandomStrategy))
    .config(ProxyConfig::default())
    .build()
    .unwrap();
```

---

## WeightedStrategy

Each `Proxy` has a `weight: u32` field (default `1`). `WeightedStrategy` performs weighted
random sampling so proxies with higher weights are selected proportionally more often.

```rust,no_run
use stygian_proxy::types::{Proxy, ProxyType};

// This proxy is 3× more likely to be selected than a weight-1 proxy.
let fast_proxy = Proxy {
    url: "http://fast.example.com:8080".into(),
    proxy_type: ProxyType::Http,
    weight: 3,
    ..Default::default()
};
```

Use this strategy when proxies have different capacities, quotas, or observed speeds.

---

## Capability filtering

All four strategies operate on a pre-filtered `ProxyCandidate` slice. Before
a strategy runs, `ProxyManager` filters the pool by `CapabilityRequirement`.
Use `acquire_with_capabilities()` to express requirements at call time:

```rust,no_run
use stygian_proxy::types::CapabilityRequirement;

let req = CapabilityRequirement {
    require_tls_profile: Some("chrome_131".into()),
    require_cdn_edge: false,
    require_tags: vec!["us-east".into()],
    ..CapabilityRequirement::default()
};
let handle = manager.acquire_with_capabilities(req).await?;
```

### ProxyCapabilities fields

| Field | Type | Description |
| --- | --- | --- |
| `tls_profile` | `Option<String>` | Named TLS fingerprint profile (`"chrome_131"`, `"firefox_133"`, …) |
| `is_cdn_edge` | `bool` | `true` for CDN-fronted egress nodes (`ProxyType::CdnEdge`) |
| `cdn_provider` | `Option<String>` | Optional CDN provider name (e.g. `"cloudflare"`) |
| `tags` | `Vec<String>` | Arbitrary operator-defined labels |

Annotate proxies at registration time:

```rust,no_run
use stygian_proxy::types::{Proxy, ProxyType, ProxyCapabilities};

manager.add_proxy(Proxy {
    url: "http://edge1.cdn.example.com:8080".into(),
    proxy_type: ProxyType::CdnEdge,
    capabilities: ProxyCapabilities {
        tls_profile: Some("chrome_131".into()),
        is_cdn_edge: true,
        cdn_provider: Some("cloudflare".into()),
        tags: vec!["eu-west".into()],
    },
    ..Default::default()
}).await?;
```

Proxies whose capabilities do not satisfy a `CapabilityRequirement` are
removed from the candidate slice before the rotation strategy runs.

---

## LeastUsedStrategy

Selects the healthy proxy with the **lowest total request count** at the time of the call.
This maximises even distribution over time even when proxies are added dynamically.

```rust,no_run
use std::sync::Arc;
use stygian_proxy::{MemoryProxyStore, ProxyConfig, ProxyManager};
use stygian_proxy::strategy::LeastUsedStrategy;

let storage = Arc::new(MemoryProxyStore::default());
let manager = ProxyManager::builder()
    .storage(storage)
    .strategy(Arc::new(LeastUsedStrategy))
    .config(ProxyConfig::default())
    .build()
    .unwrap();
```

---

## Custom strategies

Implement `RotationStrategy` to plug in your own selection logic:

```rust,no_run
use async_trait::async_trait;
use stygian_proxy::error::ProxyResult;
use stygian_proxy::strategy::{ProxyCandidate, RotationStrategy};

/// Always pick the proxy with the best success rate.
pub struct BestSuccessRateStrategy;

#[async_trait]
impl RotationStrategy for BestSuccessRateStrategy {
    async fn select<'a>(
        &self,
        candidates: &'a [ProxyCandidate],
    ) -> ProxyResult<&'a ProxyCandidate> {
        candidates
            .iter()
            .filter(|c| c.healthy)
            .max_by(|a, b| {
                let ra = a.metrics.success_rate();
                let rb = b.metrics.success_rate();
                ra.partial_cmp(&rb).unwrap_or(std::cmp::Ordering::Equal)
            })
            .ok_or(stygian_proxy::error::ProxyError::AllProxiesUnhealthy)
    }
}
```

Pass it to `ProxyManager::builder().storage(storage).strategy(Arc::new(BestSuccessRateStrategy)).config(config).build().unwrap()`.

---

## ProxyCandidate fields

| Field | Type | Description |
| --- | --- | --- |
| `id` | `Uuid` | Stable proxy identifier |
| `weight` | `u32` | Relative selection weight |
| `metrics` | `Arc<ProxyMetrics>` | Shared atomics: requests, failures, latency |
| `healthy` | `bool` | Result of the last health check |

`ProxyMetrics` exposes `success_rate() -> f64` and `avg_latency_ms() -> f64` computed from
the atomic counters without any locking.
