//! Bounded native HTTP capabilities client and per-category rate limiter.

#![allow(clippy::missing_errors_doc)] // Public result shapes are fixed by the compatibility map.
#![allow(clippy::needless_pass_by_value)] // Mapped APIs preserve owned CLR argument shapes.
#![allow(clippy::option_option)] // The fixed generated nullable-value mapping is Option<Option<T>>.
#![allow(clippy::too_many_arguments)] // HTTP request boundaries keep explicit policy inputs visible.

use crate::{CapsCategory, Error};
use flate2::read::{DeflateDecoder, GzDecoder, ZlibDecoder};
use futures_util::StreamExt;
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use libremetaverse_types::compat::{
    CancellationToken, CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse,
    IProgress, MediaTypeHeaderValue, RateLimitLease, TimeProvider, Uri,
};
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::io::Read;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, SystemTime};

const STREAM_CHUNK_SIZE: usize = 81_920;

fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
    value
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn read<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
    value
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn write<T>(value: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
    value
        .write()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

/// Explicit memory and redirect policy for capability HTTP traffic.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CapsHttpLimits {
    pub max_request_bytes: usize,
    pub max_response_bytes: usize,
    pub max_decompressed_bytes: usize,
    pub max_redirects: usize,
}

impl Default for CapsHttpLimits {
    fn default() -> Self {
        Self {
            max_request_bytes: 32 * 1024 * 1024,
            max_response_bytes: 64 * 1024 * 1024,
            max_decompressed_bytes: 128 * 1024 * 1024,
            max_redirects: 10,
        }
    }
}

impl CapsHttpLimits {
    fn validate(&self) -> Result<(), Error> {
        if self.max_request_bytes == 0
            || self.max_response_bytes == 0
            || self.max_decompressed_bytes == 0
        {
            return Err(Error::Argument);
        }
        Ok(())
    }
}

/// Typed upload/download progress matching the C# nested value type.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct HttpCapsClientProgressReport {
    total_bytes: Option<Option<i64>>,
    bytes_transferred: i64,
    percent: Option<Option<f64>>,
}

impl HttpCapsClientProgressReport {
    pub fn new(
        total_bytes: Option<Option<i64>>,
        bytes_transferred: i64,
        percent: Option<Option<f64>>,
    ) -> Result<Self, Error> {
        if bytes_transferred < 0 || total_bytes.flatten().is_some_and(|total| total < 0) {
            return Err(Error::Argument);
        }
        Ok(Self {
            total_bytes,
            bytes_transferred,
            percent,
        })
    }

    #[must_use]
    pub const fn bytes_transferred(&self) -> i64 {
        self.bytes_transferred
    }

    #[must_use]
    pub const fn percent(&self) -> Option<Option<f64>> {
        self.percent
    }

    #[must_use]
    pub const fn total_bytes(&self) -> Option<Option<i64>> {
        self.total_bytes
    }

    #[allow(clippy::cast_precision_loss)] // C# computes the same rounded percentage as double.
    fn known(total: usize, transferred: usize) -> Option<Self> {
        let total = i64::try_from(total).ok()?;
        let transferred = i64::try_from(transferred).ok()?;
        let percent = if total == 0 {
            None
        } else {
            Some(((transferred as f64 / total as f64) * 10_000.0).round() / 100.0)
        };
        Some(Self {
            total_bytes: Some(Some(total)),
            bytes_transferred: transferred,
            percent: Some(percent),
        })
    }

    fn unknown(transferred: usize) -> Option<Self> {
        Some(Self {
            total_bytes: Some(None),
            bytes_transferred: i64::try_from(transferred).ok()?,
            percent: Some(None),
        })
    }
}

/// Mapped token-bucket configuration.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CapsRateLimiterOptions {
    token_limit: i32,
    tokens_per_period: i32,
    replenishment_period: Duration,
    queue_limit: i32,
}

impl CapsRateLimiterOptions {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            token_limit: 0,
            tokens_per_period: 0,
            replenishment_period: Duration::ZERO,
            queue_limit: 0,
        })
    }

    #[must_use]
    pub const fn queue_limit(&self) -> i32 {
        self.queue_limit
    }

    pub fn set_queue_limit(&mut self, value: i32) {
        self.queue_limit = value;
    }

    #[must_use]
    pub const fn replenishment_period(&self) -> Duration {
        self.replenishment_period
    }

    pub fn set_replenishment_period(&mut self, value: Duration) {
        self.replenishment_period = value;
    }

    #[must_use]
    pub const fn token_limit(&self) -> i32 {
        self.token_limit
    }

    pub fn set_token_limit(&mut self, value: i32) {
        self.token_limit = value;
    }

    #[must_use]
    pub const fn tokens_per_period(&self) -> i32 {
        self.tokens_per_period
    }

    pub fn set_tokens_per_period(&mut self, value: i32) {
        self.tokens_per_period = value;
    }

    fn configured(
        token_limit: i32,
        tokens_per_period: i32,
        replenishment_period: Duration,
        queue_limit: i32,
    ) -> Self {
        Self {
            token_limit,
            tokens_per_period,
            replenishment_period,
            queue_limit,
        }
    }

    fn validate(&self) -> Result<(), Error> {
        if self.token_limit <= 0
            || self.tokens_per_period <= 0
            || self.replenishment_period.is_zero()
            || self.queue_limit < 0
        {
            return Err(Error::Argument);
        }
        Ok(())
    }
}

#[derive(Debug)]
struct BucketState {
    tokens: u32,
    last_replenishment: SystemTime,
    queue: VecDeque<u64>,
}

struct CategoryBucket {
    options: CapsRateLimiterOptions,
    next_ticket: AtomicU64,
    state: Mutex<BucketState>,
}

impl CategoryBucket {
    fn new(options: CapsRateLimiterOptions, now: SystemTime) -> Result<Self, Error> {
        options.validate()?;
        Ok(Self {
            state: Mutex::new(BucketState {
                tokens: u32::try_from(options.token_limit).map_err(|_| Error::Argument)?,
                last_replenishment: now,
                queue: VecDeque::new(),
            }),
            options,
            next_ticket: AtomicU64::new(1),
        })
    }

    fn replenish(&self, state: &mut BucketState, now: SystemTime) {
        let Ok(elapsed) = now.duration_since(state.last_replenishment) else {
            state.last_replenishment = now;
            return;
        };
        let period_nanos = self.options.replenishment_period.as_nanos();
        if period_nanos == 0 {
            return;
        }
        let periods = elapsed.as_nanos() / period_nanos;
        if periods == 0 {
            return;
        }
        let added = periods.saturating_mul(u128::from(
            u32::try_from(self.options.tokens_per_period).unwrap_or(u32::MAX),
        ));
        let token_limit = u32::try_from(self.options.token_limit).unwrap_or(u32::MAX);
        state.tokens = token_limit.min(
            state
                .tokens
                .saturating_add(u32::try_from(added).unwrap_or(u32::MAX)),
        );
        let periods_u32 = u32::try_from(periods).unwrap_or(u32::MAX);
        state.last_replenishment += self.options.replenishment_period * periods_u32;
    }

    fn remove_ticket(&self, ticket: u64) {
        mutex(&self.state).queue.retain(|queued| *queued != ticket);
    }
}

struct CapsRateLimiterInner {
    buckets: HashMap<CapsCategory, Arc<CategoryBucket>>,
    uri_categories: RwLock<HashMap<String, CapsCategory>>,
    clock: TimeProvider,
    shutdown: CancellationTokenSource,
    disposed: AtomicBool,
}

/// Per-capability-category token buckets with a bounded oldest-first queue.
#[derive(Clone)]
pub struct CapsRateLimiter(Arc<CapsRateLimiterInner>);

impl CapsRateLimiter {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Self::new_with_clock_and_overrides(TimeProvider::system(), None)
    }

    pub fn new_with_i_read_only_dictionary(
        overrides: Option<HashMap<CapsCategory, CapsRateLimiterOptions>>,
    ) -> Result<Self, Error> {
        Self::new_with_clock_and_overrides(TimeProvider::system(), overrides)
    }

    pub fn new_with_clock_and_overrides(
        clock: TimeProvider,
        overrides: Option<HashMap<CapsCategory, CapsRateLimiterOptions>>,
    ) -> Result<Self, Error> {
        let mut options = default_rate_options();
        if let Some(overrides) = overrides {
            options.extend(overrides);
        }
        let fallback = options
            .get(&CapsCategory::Default)
            .cloned()
            .ok_or(Error::InvalidOperation)?;
        let now = clock.get_utc_now();
        let mut buckets = HashMap::new();
        for category in all_categories() {
            let configured = options
                .get(&category)
                .cloned()
                .unwrap_or_else(|| fallback.clone());
            buckets.insert(category, Arc::new(CategoryBucket::new(configured, now)?));
        }
        Ok(Self(Arc::new(CapsRateLimiterInner {
            buckets,
            uri_categories: RwLock::new(HashMap::new()),
            clock,
            shutdown: CancellationTokenSource::new(),
            disposed: AtomicBool::new(false),
        })))
    }

    pub async fn acquire(
        &self,
        uri: Uri,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<RateLimitLease, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        if self.0.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        let key = parse_http_uri(&uri)?.to_string();
        let category = read(&self.0.uri_categories)
            .get(&key)
            .copied()
            .unwrap_or(CapsCategory::Default);
        let bucket = self
            .0
            .buckets
            .get(&category)
            .cloned()
            .ok_or(Error::InvalidOperation)?;
        let mut ticket = None;

        loop {
            if cancellation_token.is_cancellation_requested() {
                if let Some(ticket) = ticket {
                    bucket.remove_ticket(ticket);
                }
                return Err(Error::Cancelled);
            }
            if self.0.disposed.load(Ordering::Acquire) {
                if let Some(ticket) = ticket {
                    bucket.remove_ticket(ticket);
                }
                return Err(Error::InvalidOperation);
            }

            let now = self.0.clock.get_utc_now();
            let wait = {
                let mut state = mutex(&bucket.state);
                bucket.replenish(&mut state, now);
                let is_front = ticket.map_or_else(
                    || state.queue.is_empty(),
                    |ticket| state.queue.front() == Some(&ticket),
                );
                if is_front && state.tokens > 0 {
                    state.tokens -= 1;
                    if ticket.is_some() {
                        state.queue.pop_front();
                    }
                    return Ok(RateLimitLease::acquired());
                }
                if ticket.is_none() {
                    let queue_limit =
                        usize::try_from(bucket.options.queue_limit).map_err(|_| Error::Argument)?;
                    if state.queue.len() >= queue_limit {
                        return Ok(RateLimitLease::rejected());
                    }
                    let next = bucket.next_ticket.fetch_add(1, Ordering::Relaxed);
                    state.queue.push_back(next);
                    ticket = Some(next);
                }
                let elapsed = now
                    .duration_since(state.last_replenishment)
                    .unwrap_or_default();
                bucket
                    .options
                    .replenishment_period
                    .saturating_sub(elapsed)
                    .max(Duration::from_millis(1))
            };
            if let Err(error) =
                wait_executor_neutral(wait, cancellation_token.clone(), self.0.shutdown.token())
                    .await
            {
                if let Some(ticket) = ticket {
                    bucket.remove_ticket(ticket);
                }
                return Err(error);
            }
        }
    }

    pub fn register_cap_uri(&self, cap_name: String, uri: Uri) -> Result<(), Error> {
        if self.0.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        let category = category_for_cap_name(&cap_name);
        write(&self.0.uri_categories).insert(parse_http_uri(&uri)?.to_string(), category);
        Ok(())
    }

    pub fn dispose(&self) -> Result<(), Error> {
        if self.0.disposed.swap(true, Ordering::AcqRel) {
            return Ok(());
        }
        self.0.shutdown.cancel();
        write(&self.0.uri_categories).clear();
        for bucket in self.0.buckets.values() {
            mutex(&bucket.state).queue.clear();
        }
        Ok(())
    }
}

impl fmt::Debug for CapsRateLimiter {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("CapsRateLimiter")
            .field("categories", &self.0.buckets.len())
            .field("registered_uri_count", &read(&self.0.uri_categories).len())
            .field("disposed", &self.0.disposed.load(Ordering::Acquire))
            .finish()
    }
}

async fn wait_executor_neutral(
    duration: Duration,
    cancellation_token: CancellationToken,
    shutdown: CancellationToken,
) -> Result<(), Error> {
    tokio::select! {
        () = tokio::time::sleep(duration) => Ok(()),
        () = cancellation_token.cancelled() => Err(Error::Cancelled),
        () = shutdown.cancelled() => Err(Error::InvalidOperation),
    }
}

fn all_categories() -> [CapsCategory; 8] {
    [
        CapsCategory::Default,
        CapsCategory::RenderMaterials,
        CapsCategory::AssetFetch,
        CapsCategory::AssetUpload,
        CapsCategory::Inventory,
        CapsCategory::EventQueue,
        CapsCategory::DisplayName,
        CapsCategory::Voice,
    ]
}

fn default_rate_options() -> HashMap<CapsCategory, CapsRateLimiterOptions> {
    let second = Duration::from_secs(1);
    HashMap::from([
        (
            CapsCategory::Default,
            CapsRateLimiterOptions::configured(20, 10, second, 30),
        ),
        (
            CapsCategory::RenderMaterials,
            CapsRateLimiterOptions::configured(4, 2, second, 20),
        ),
        (
            CapsCategory::AssetFetch,
            CapsRateLimiterOptions::configured(24, 12, second, 60),
        ),
        (
            CapsCategory::AssetUpload,
            CapsRateLimiterOptions::configured(4, 2, second, 10),
        ),
        (
            CapsCategory::Inventory,
            CapsRateLimiterOptions::configured(6, 3, second, 20),
        ),
        (
            CapsCategory::EventQueue,
            CapsRateLimiterOptions::configured(3, 2, second, 3),
        ),
        (
            CapsCategory::DisplayName,
            CapsRateLimiterOptions::configured(5, 2, second, 15),
        ),
        (
            CapsCategory::Voice,
            CapsRateLimiterOptions::configured(10, 5, second, 20),
        ),
    ])
}

fn category_for_cap_name(name: &str) -> CapsCategory {
    match name.to_ascii_lowercase().as_str() {
        "rendermaterials" | "modifymaterialparams" | "modifyregion" => {
            CapsCategory::RenderMaterials
        }
        "gettexture"
        | "viewerasset"
        | "getmesh"
        | "getmesh2"
        | "getmetadata"
        | "requesttexturedownload" => CapsCategory::AssetFetch,
        "newfileagentinventory"
        | "newfileagentinventoryvariableprice"
        | "uploadbakedtexture"
        | "updateavatarappearance"
        | "inventorythumbnailupload"
        | "updatematerialagentinventory"
        | "updatematerialtaskinventory" => CapsCategory::AssetUpload,
        "fetchinventory2"
        | "fetchinventorydescendents2"
        | "fetchlib2"
        | "fetchlibdescendents2"
        | "inventoryapiv3"
        | "libraryapiv3"
        | "requesttaskinventory" => CapsCategory::Inventory,
        "eventqueueget" => CapsCategory::EventQueue,
        "getdisplaynames" | "setdisplayname" | "avatarpickersearch" | "agentprofile" => {
            CapsCategory::DisplayName
        }
        "provisionvoiceaccountrequest" | "voicesignalingrequest" | "parcelvoiceinforequest" => {
            CapsCategory::Voice
        }
        _ => CapsCategory::Default,
    }
}

enum HttpBackend {
    Handler(HttpMessageHandler),
    Reqwest(reqwest::Client),
}

struct HttpCapsClientInner {
    backend: HttpBackend,
    limits: CapsHttpLimits,
    rate_limiter: RwLock<Option<CapsRateLimiter>>,
    request_slots: Arc<tokio::sync::Semaphore>,
    shutdown: CancellationTokenSource,
    disposed: AtomicBool,
}

/// Native capability HTTP client with injectable fake and production backends.
#[derive(Clone)]
pub struct HttpCapsClient(Arc<HttpCapsClientInner>);

impl HttpCapsClient {
    pub const LLSD_XML: &'static str = "application/llsd+xml";
    pub const LLSD_BINARY: &'static str = "application/llsd+binary";
    pub const LLSD_JSON: &'static str = "application/llsd+json";

    #[must_use]
    pub fn hdr_llsd_xml() -> MediaTypeHeaderValue {
        MediaTypeHeaderValue(Self::LLSD_XML.to_owned())
    }

    #[must_use]
    pub fn hdr_llsd_binary() -> MediaTypeHeaderValue {
        MediaTypeHeaderValue(Self::LLSD_BINARY.to_owned())
    }

    #[must_use]
    pub fn hdr_llsd_json() -> MediaTypeHeaderValue {
        MediaTypeHeaderValue(Self::LLSD_JSON.to_owned())
    }

    pub fn new(handler: HttpMessageHandler) -> Result<Self, Error> {
        Self::with_handler_and_limits(handler, CapsHttpLimits::default())
    }

    pub fn with_handler_and_limits(
        handler: HttpMessageHandler,
        limits: CapsHttpLimits,
    ) -> Result<Self, Error> {
        Self::with_handler_policy(handler, None, limits, 32)
    }

    /// Creates a fully injected fake/backend client with explicit limiter and
    /// concurrency policy. This is the deterministic test and embedding seam.
    pub fn with_handler_policy(
        handler: HttpMessageHandler,
        rate_limiter: Option<CapsRateLimiter>,
        limits: CapsHttpLimits,
        max_concurrent_requests: usize,
    ) -> Result<Self, Error> {
        limits.validate()?;
        if max_concurrent_requests == 0 {
            return Err(Error::Argument);
        }
        Ok(Self(Arc::new(HttpCapsClientInner {
            backend: HttpBackend::Handler(handler),
            limits,
            rate_limiter: RwLock::new(rate_limiter),
            request_slots: Arc::new(tokio::sync::Semaphore::new(max_concurrent_requests)),
            shutdown: CancellationTokenSource::new(),
            disposed: AtomicBool::new(false),
        })))
    }

    /// Creates the production HTTP backend without exposing the selected HTTP
    /// implementation in the public API.
    pub fn with_native_transport(
        user_agent: &str,
        timeout: Duration,
        max_connections: usize,
        rate_limiter: Option<CapsRateLimiter>,
        limits: CapsHttpLimits,
    ) -> Result<Self, Error> {
        if timeout.is_zero() || max_connections == 0 {
            return Err(Error::Argument);
        }
        limits.validate()?;
        let max_redirects = limits.max_redirects;
        let redirect = reqwest::redirect::Policy::custom(move |attempt| {
            if attempt.previous().len() >= max_redirects {
                return attempt.stop();
            }
            match attempt.url().scheme() {
                "http" | "https" => attempt.follow(),
                _ => attempt.stop(),
            }
        });
        let client = reqwest::Client::builder()
            .redirect(redirect)
            .timeout(timeout)
            .connect_timeout(timeout)
            .pool_max_idle_per_host(max_connections)
            .user_agent(user_agent)
            .build()
            .map_err(|_| Error::HttpRequest)?;
        Self::with_reqwest_client(client, rate_limiter, limits, max_connections)
    }

    fn with_reqwest_client(
        client: reqwest::Client,
        rate_limiter: Option<CapsRateLimiter>,
        limits: CapsHttpLimits,
        max_connections: usize,
    ) -> Result<Self, Error> {
        limits.validate()?;
        Ok(Self(Arc::new(HttpCapsClientInner {
            backend: HttpBackend::Reqwest(client),
            limits,
            rate_limiter: RwLock::new(rate_limiter),
            request_slots: Arc::new(tokio::sync::Semaphore::new(max_connections)),
            shutdown: CancellationTokenSource::new(),
            disposed: AtomicBool::new(false),
        })))
    }

    pub(crate) fn production(
        user_agent: &str,
        timeout: Duration,
        max_connections: usize,
        rate_limiter: CapsRateLimiter,
    ) -> Result<Self, Error> {
        Self::with_native_transport(
            user_agent,
            timeout,
            max_connections,
            Some(rate_limiter),
            CapsHttpLimits::default(),
        )
    }

    pub(crate) fn set_rate_limiter(&self, rate_limiter: Option<CapsRateLimiter>) {
        *write(&self.0.rate_limiter) = rate_limiter;
    }

    pub(crate) fn shutdown(&self) {
        if !self.0.disposed.swap(true, Ordering::AcqRel) {
            self.0.shutdown.cancel();
            self.0.request_slots.close();
        }
    }

    pub async fn get(
        &self,
        uri: Uri,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        self.send("GET", uri, None, Vec::new(), cancellation_token, progress)
            .await
    }

    /// Sends a capability request whose verb or headers are not covered by the
    /// fixed public compatibility overloads (`AISv3` uses `COPY` and
    /// `Destination`). The request still passes through all normal limits,
    /// cancellation, rate limiting, and injected-handler recording.
    pub(crate) async fn send_custom(
        &self,
        method: &str,
        uri: Uri,
        headers: std::collections::BTreeMap<String, String>,
        content_type: Option<String>,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        self.send_with_headers(
            method,
            uri,
            headers,
            content_type,
            payload,
            cancellation_token,
            None,
        )
        .await
    }

    pub async fn get_request(
        &self,
        uri: Uri,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(), Error> {
        self.get(uri, cancellation_token, progress)
            .await
            .map(|_| ())
    }

    pub async fn post_with_uri_string_bytes_cancellation_token_i_progress(
        &self,
        uri: Uri,
        content_type: String,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        self.send(
            "POST",
            uri,
            Some(content_type),
            payload,
            cancellation_token,
            progress,
        )
        .await
    }

    pub async fn put_with_uri_string_bytes_cancellation_token_i_progress(
        &self,
        uri: Uri,
        content_type: String,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        self.send(
            "PUT",
            uri,
            Some(content_type),
            payload,
            cancellation_token,
            progress,
        )
        .await
    }

    pub async fn patch_with_uri_string_bytes_cancellation_token_i_progress(
        &self,
        uri: Uri,
        content_type: String,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        self.send(
            "PATCH",
            uri,
            Some(content_type),
            payload,
            cancellation_token,
            progress,
        )
        .await
    }

    pub async fn delete_with_uri_string_bytes_cancellation_token_i_progress(
        &self,
        uri: Uri,
        content_type: String,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        self.send(
            "DELETE",
            uri,
            Some(content_type),
            payload,
            cancellation_token,
            progress,
        )
        .await
    }

    pub async fn post_with_uri_osd_format_osd_cancellation_token_i_progress(
        &self,
        uri: Uri,
        format: OSDFormat,
        payload: OSD,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        let (content_type, payload) = serialize_osd(format, payload)?;
        self.send(
            "POST",
            uri,
            Some(content_type),
            payload,
            cancellation_token,
            progress,
        )
        .await
    }

    pub async fn put_with_uri_osd_format_osd_cancellation_token_i_progress(
        &self,
        uri: Uri,
        format: OSDFormat,
        payload: OSD,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        let (content_type, payload) = serialize_osd(format, payload)?;
        self.send(
            "PUT",
            uri,
            Some(content_type),
            payload,
            cancellation_token,
            progress,
        )
        .await
    }

    pub async fn patch_with_uri_osd_format_osd_cancellation_token_i_progress(
        &self,
        uri: Uri,
        format: OSDFormat,
        payload: OSD,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        let (content_type, payload) = serialize_osd(format, payload)?;
        self.send(
            "PATCH",
            uri,
            Some(content_type),
            payload,
            cancellation_token,
            progress,
        )
        .await
    }

    pub async fn delete_with_uri_osd_format_osd_cancellation_token_i_progress(
        &self,
        uri: Uri,
        format: OSDFormat,
        payload: OSD,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        let (content_type, payload) = serialize_osd(format, payload)?;
        self.send(
            "DELETE",
            uri,
            Some(content_type),
            payload,
            cancellation_token,
            progress,
        )
        .await
    }

    pub async fn post_request_with_uri_string_bytes_cancellation_token_i_progress(
        &self,
        uri: Uri,
        content_type: String,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(), Error> {
        self.post_with_uri_string_bytes_cancellation_token_i_progress(
            uri,
            content_type,
            payload,
            cancellation_token,
            progress,
        )
        .await
        .map(|_| ())
    }

    pub async fn put_request_with_uri_string_bytes_cancellation_token_i_progress(
        &self,
        uri: Uri,
        content_type: String,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(), Error> {
        self.put_with_uri_string_bytes_cancellation_token_i_progress(
            uri,
            content_type,
            payload,
            cancellation_token,
            progress,
        )
        .await
        .map(|_| ())
    }

    pub async fn patch_request_with_uri_string_bytes_cancellation_token_i_progress(
        &self,
        uri: Uri,
        content_type: String,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(), Error> {
        self.patch_with_uri_string_bytes_cancellation_token_i_progress(
            uri,
            content_type,
            payload,
            cancellation_token,
            progress,
        )
        .await
        .map(|_| ())
    }

    pub async fn delete_request_with_uri_string_bytes_cancellation_token_i_progress(
        &self,
        uri: Uri,
        content_type: String,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(), Error> {
        self.delete_with_uri_string_bytes_cancellation_token_i_progress(
            uri,
            content_type,
            payload,
            cancellation_token,
            progress,
        )
        .await
        .map(|_| ())
    }

    pub async fn post_request_with_uri_osd_format_osd_cancellation_token_i_progress(
        &self,
        uri: Uri,
        format: OSDFormat,
        payload: OSD,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(), Error> {
        self.post_with_uri_osd_format_osd_cancellation_token_i_progress(
            uri,
            format,
            payload,
            cancellation_token,
            progress,
        )
        .await
        .map(|_| ())
    }

    pub async fn put_request_with_uri_osd_format_osd_cancellation_token_i_progress(
        &self,
        uri: Uri,
        format: OSDFormat,
        payload: OSD,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(), Error> {
        self.put_with_uri_osd_format_osd_cancellation_token_i_progress(
            uri,
            format,
            payload,
            cancellation_token,
            progress,
        )
        .await
        .map(|_| ())
    }

    pub async fn patch_request_with_uri_osd_format_osd_cancellation_token_i_progress(
        &self,
        uri: Uri,
        format: OSDFormat,
        payload: OSD,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(), Error> {
        self.patch_with_uri_osd_format_osd_cancellation_token_i_progress(
            uri,
            format,
            payload,
            cancellation_token,
            progress,
        )
        .await
        .map(|_| ())
    }

    pub async fn delete_request_with_uri_osd_format_osd_cancellation_token_i_progress(
        &self,
        uri: Uri,
        format: OSDFormat,
        payload: OSD,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(), Error> {
        self.delete_with_uri_osd_format_osd_cancellation_token_i_progress(
            uri,
            format,
            payload,
            cancellation_token,
            progress,
        )
        .await
        .map(|_| ())
    }

    async fn send(
        &self,
        method: &str,
        uri: Uri,
        content_type: Option<String>,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        self.send_with_headers(
            method,
            uri,
            std::collections::BTreeMap::new(),
            content_type,
            payload,
            cancellation_token,
            progress,
        )
        .await
    }

    async fn send_with_headers(
        &self,
        method: &str,
        uri: Uri,
        headers: std::collections::BTreeMap<String, String>,
        content_type: Option<String>,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        if self.0.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        cancellation_token.throw_if_cancellation_requested()?;
        let operation_cancellation =
            CancellationTokenSource::new_linked(&[cancellation_token, self.0.shutdown.token()]);
        let cancellation_token = operation_cancellation.token();
        if payload.len() > self.0.limits.max_request_bytes {
            return Err(Error::HttpRequest);
        }
        let parsed = parse_http_uri(&uri)?;
        let limiter = { read(&self.0.rate_limiter).clone() };
        if let Some(limiter) = limiter {
            let _ = limiter
                .acquire(uri.clone(), Some(cancellation_token.clone()))
                .await?;
        }
        let request_slot = Arc::clone(&self.0.request_slots);
        let _permit = tokio::select! {
            permit = request_slot.acquire_owned() => permit.map_err(|_| Error::InvalidOperation)?,
            () = cancellation_token.cancelled() => return Err(Error::Cancelled),
        };
        let progress: Option<Arc<dyn IProgress<HttpCapsClientProgressReport>>> =
            progress.map(Arc::from);
        let result = match &self.0.backend {
            HttpBackend::Handler(handler) => {
                self.send_handler(
                    handler,
                    method,
                    uri,
                    headers,
                    content_type,
                    payload,
                    cancellation_token,
                    progress,
                )
                .await
            }
            HttpBackend::Reqwest(client) => {
                self.send_reqwest(
                    client,
                    method,
                    parsed,
                    headers,
                    content_type,
                    payload,
                    cancellation_token,
                    progress,
                )
                .await
            }
        };
        drop(operation_cancellation);
        result
    }

    async fn send_handler(
        &self,
        handler: &HttpMessageHandler,
        method: &str,
        uri: Uri,
        headers: std::collections::BTreeMap<String, String>,
        content_type: Option<String>,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Arc<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        report_buffer_progress(progress.as_ref(), payload.len());
        let request = HttpRequest {
            method: method.to_owned(),
            uri,
            headers,
            content_type,
            body: payload,
        };
        let response = tokio::select! {
            biased;
            response = handler.send(request, cancellation_token.clone()) => response,
            () = cancellation_token.cancelled() => return Err(Error::Cancelled),
        };
        cancellation_token.throw_if_cancellation_requested()?;
        self.finish_response(response, progress.as_ref(), true)
    }

    async fn send_reqwest(
        &self,
        client: &reqwest::Client,
        method: &str,
        uri: reqwest::Url,
        headers: std::collections::BTreeMap<String, String>,
        content_type: Option<String>,
        payload: Vec<u8>,
        cancellation_token: CancellationToken,
        progress: Option<Arc<dyn IProgress<HttpCapsClientProgressReport>>>,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        let method = reqwest::Method::from_bytes(method.as_bytes()).map_err(|_| Error::Argument)?;
        let mut request = client.request(method, uri);
        for (name, value) in headers {
            request = request.header(name, value);
        }
        if let Some(content_type) = content_type {
            request = request.header(reqwest::header::CONTENT_TYPE, content_type);
        }
        request = request.header(reqwest::header::ACCEPT_ENCODING, "gzip, deflate");
        if !payload.is_empty() {
            request = request.header(reqwest::header::CONTENT_LENGTH, payload.len());
            let stream = upload_stream(payload, progress.clone());
            request = request.body(reqwest::Body::wrap_stream(stream));
        }
        let response = tokio::select! {
            response = request.send() => response.map_err(|_| Error::HttpRequest)?,
            () = cancellation_token.cancelled() => return Err(Error::Cancelled),
        };
        cancellation_token.throw_if_cancellation_requested()?;
        let status_code = response.status().as_u16();
        let headers = response
            .headers()
            .iter()
            .map(|(name, value)| {
                (
                    name.as_str().to_owned(),
                    String::from_utf8_lossy(value.as_bytes()).into_owned(),
                )
            })
            .collect();
        let content_type = response
            .headers()
            .get(reqwest::header::CONTENT_TYPE)
            .map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned());
        let declared_length = response
            .headers()
            .get(reqwest::header::CONTENT_LENGTH)
            .and_then(|value| value.to_str().ok())
            .and_then(|value| value.parse::<usize>().ok());
        if declared_length.is_some_and(|length| length > self.0.limits.max_response_bytes) {
            return Err(Error::HttpRequest);
        }
        let mut stream = response.bytes_stream();
        let mut body = Vec::new();
        while let Some(chunk) = tokio::select! {
            chunk = stream.next() => chunk,
            () = cancellation_token.cancelled() => return Err(Error::Cancelled),
        } {
            let chunk = chunk.map_err(|_| Error::HttpRequest)?;
            let new_length = body
                .len()
                .checked_add(chunk.len())
                .ok_or(Error::HttpRequest)?;
            if new_length > self.0.limits.max_response_bytes {
                return Err(Error::HttpRequest);
            }
            body.extend_from_slice(&chunk);
            report_stream_progress(progress.as_ref(), declared_length, body.len());
        }
        if body.is_empty() {
            report_stream_progress(progress.as_ref(), declared_length, 0);
        }
        cancellation_token.throw_if_cancellation_requested()?;
        self.finish_response(
            HttpResponse {
                status_code,
                headers,
                content_type,
                body,
            },
            progress.as_ref(),
            false,
        )
    }

    fn finish_response(
        &self,
        mut response: HttpResponse,
        progress: Option<&Arc<dyn IProgress<HttpCapsClientProgressReport>>>,
        report_progress: bool,
    ) -> Result<(HttpResponse, Vec<u8>), Error> {
        if response.body.len() > self.0.limits.max_response_bytes {
            return Err(Error::HttpRequest);
        }
        if let Some(length) = header_value(&response.headers, "content-length")
            .and_then(|value| value.parse::<usize>().ok())
            && length > self.0.limits.max_response_bytes
        {
            return Err(Error::HttpRequest);
        }
        let encoding = header_value(&response.headers, "content-encoding");
        let encoded = std::mem::take(&mut response.body);
        let body = decode_content(encoded, encoding, self.0.limits.max_decompressed_bytes)?;
        if report_progress {
            report_download_progress(
                progress,
                &body,
                header_value(&response.headers, "content-length"),
            );
        }
        response.body.clone_from(&body);
        Ok((response, body))
    }
}

impl fmt::Debug for HttpCapsClient {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let backend = match &self.0.backend {
            HttpBackend::Handler(_) => "injected-handler",
            HttpBackend::Reqwest(_) => "reqwest",
        };
        formatter
            .debug_struct("HttpCapsClient")
            .field("backend", &backend)
            .field("limits", &self.0.limits)
            .field("disposed", &self.0.disposed.load(Ordering::Acquire))
            .finish()
    }
}

fn parse_http_uri(uri: &Uri) -> Result<reqwest::Url, Error> {
    let parsed = reqwest::Url::parse(&uri.0).map_err(|_| Error::Argument)?;
    if !matches!(parsed.scheme(), "http" | "https") || parsed.host().is_none() {
        return Err(Error::Argument);
    }
    Ok(parsed)
}

fn serialize_osd(format: OSDFormat, payload: OSD) -> Result<(String, Vec<u8>), Error> {
    match format {
        OSDFormat::Xml => Ok((
            HttpCapsClient::LLSD_XML.to_owned(),
            OSDParser::serialize_llsd_xml_bytes(payload)?,
        )),
        OSDFormat::Binary => Ok((
            HttpCapsClient::LLSD_BINARY.to_owned(),
            OSDParser::serialize_llsd_binary_with_osd(payload)?,
        )),
        OSDFormat::Json => Ok((
            HttpCapsClient::LLSD_JSON.to_owned(),
            OSDParser::serialize_json_string(payload, None)?.into_bytes(),
        )),
    }
}

fn upload_stream(
    payload: Vec<u8>,
    progress: Option<Arc<dyn IProgress<HttpCapsClientProgressReport>>>,
) -> impl futures_util::Stream<Item = Result<Vec<u8>, std::io::Error>> + Send + 'static {
    let payload = Arc::new(payload);
    futures_util::stream::unfold(
        (payload, 0_usize, progress),
        |(payload, offset, progress)| async move {
            if offset >= payload.len() {
                return None;
            }
            let end = offset.saturating_add(STREAM_CHUNK_SIZE).min(payload.len());
            if let (Some(progress), Some(report)) = (
                progress.as_ref(),
                HttpCapsClientProgressReport::known(payload.len(), end),
            ) {
                progress.report(report);
            }
            Some((Ok(payload[offset..end].to_vec()), (payload, end, progress)))
        },
    )
}

fn report_buffer_progress(
    progress: Option<&Arc<dyn IProgress<HttpCapsClientProgressReport>>>,
    length: usize,
) {
    let Some(progress) = progress else {
        return;
    };
    if length == 0 {
        if let Some(report) = HttpCapsClientProgressReport::known(0, 0) {
            progress.report(report);
        }
        return;
    }
    for transferred in (STREAM_CHUNK_SIZE..length)
        .step_by(STREAM_CHUNK_SIZE)
        .chain(std::iter::once(length))
    {
        if let Some(report) = HttpCapsClientProgressReport::known(length, transferred) {
            progress.report(report);
        }
    }
}

fn report_download_progress(
    progress: Option<&Arc<dyn IProgress<HttpCapsClientProgressReport>>>,
    body: &[u8],
    declared_length: Option<&str>,
) {
    let Some(progress) = progress else {
        return;
    };
    let total = declared_length
        .and_then(|value| value.parse::<usize>().ok())
        .filter(|total| *total == body.len());
    if body.is_empty() {
        let report = total.and_then(|total| HttpCapsClientProgressReport::known(total, 0));
        if let Some(report) = report.or_else(|| HttpCapsClientProgressReport::unknown(0)) {
            progress.report(report);
        }
        return;
    }
    for transferred in (STREAM_CHUNK_SIZE..body.len())
        .step_by(STREAM_CHUNK_SIZE)
        .chain(std::iter::once(body.len()))
    {
        let report = total
            .and_then(|total| HttpCapsClientProgressReport::known(total, transferred))
            .or_else(|| HttpCapsClientProgressReport::unknown(transferred));
        if let Some(report) = report {
            progress.report(report);
        }
    }
}

fn report_stream_progress(
    progress: Option<&Arc<dyn IProgress<HttpCapsClientProgressReport>>>,
    total: Option<usize>,
    transferred: usize,
) {
    let Some(progress) = progress else {
        return;
    };
    let report = total
        .and_then(|total| HttpCapsClientProgressReport::known(total, transferred))
        .or_else(|| HttpCapsClientProgressReport::unknown(transferred));
    if let Some(report) = report {
        progress.report(report);
    }
}

fn header_value<'a>(
    headers: &'a std::collections::BTreeMap<String, String>,
    name: &str,
) -> Option<&'a str> {
    headers
        .iter()
        .find_map(|(key, value)| key.eq_ignore_ascii_case(name).then_some(value.as_str()))
}

fn decode_content(
    mut body: Vec<u8>,
    encoding: Option<&str>,
    limit: usize,
) -> Result<Vec<u8>, Error> {
    let Some(encoding) = encoding else {
        return Ok(body);
    };
    let encodings: Vec<_> = encoding
        .split(',')
        .map(str::trim)
        .filter(|encoding| !encoding.is_empty() && !encoding.eq_ignore_ascii_case("identity"))
        .collect();
    for encoding in encodings.into_iter().rev() {
        body = if encoding.eq_ignore_ascii_case("gzip") || encoding.eq_ignore_ascii_case("x-gzip") {
            read_bounded(GzDecoder::new(body.as_slice()), limit)?
        } else if encoding.eq_ignore_ascii_case("deflate") {
            match read_bounded(ZlibDecoder::new(body.as_slice()), limit) {
                Ok(decoded) => decoded,
                Err(_) => read_bounded(DeflateDecoder::new(body.as_slice()), limit)?,
            }
        } else {
            return Err(Error::HttpRequest);
        };
    }
    Ok(body)
}

fn read_bounded(mut reader: impl Read, limit: usize) -> Result<Vec<u8>, Error> {
    let take_limit = u64::try_from(limit).unwrap_or(u64::MAX).saturating_add(1);
    let mut output = Vec::new();
    reader
        .by_ref()
        .take(take_limit)
        .read_to_end(&mut output)
        .map_err(|_| Error::HttpRequest)?;
    if output.len() > limit {
        return Err(Error::HttpRequest);
    }
    Ok(output)
}
