use std::error::Error as StdError;
use std::time::Duration;

use reqwest::blocking::Response;
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderValue, USER_AGENT};

use crate::auth::{Auth, TokenStore};
use crate::error::{Error, Result};
use crate::types::{
    AccountInfo, ApiVersion, DevicePollResponse, DeviceStartResponse, MessageResponse, Package,
    PackageDependency, PackageListResponse, PublishDraftArtifact, PublishFinalizeResponse,
    PublishSession, PublishSessionCreateRequest, PublishUploadUrl, PublishUploadUrlRequest, Release,
    ReleaseListResponse,
};

/// Default Nodos Store API base URL.
pub const DEFAULT_BASE_URL: &str = "https://store.nodos.dev";

/// User-Agent sent with every request.
const USER_AGENT_VALUE: &str = concat!(
    env!("CARGO_PKG_NAME"),
    "/",
    env!("CARGO_PKG_VERSION"),
);

// -----------------------------------------------------------------------
// Timeouts
// -----------------------------------------------------------------------

/// How long to wait for a TCP connection to be established.
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);

/// Maximum time for a complete API request (connect + send + receive).
/// Does NOT apply to artifact uploads, which use `upload_http`.
const API_TIMEOUT: Duration = Duration::from_secs(30);

/// TCP keepalive probe interval. Prevents silent connection drops on idle
/// sockets, which is especially important during long-running uploads.
/// 60 s matches the AWS/GCS idle timeout floor.
const TCP_KEEPALIVE: Duration = Duration::from_secs(60);

// -----------------------------------------------------------------------
// StoreClient
// -----------------------------------------------------------------------

pub struct StoreClient {
    base_url: String,
    /// Short-timeout client for all API calls (list, publish sessions, etc.)
    http: reqwest::blocking::Client,
    /// No read/write timeout — used only for artifact uploads which can take
    /// arbitrarily long depending on file size and network speed.
    upload_http: reqwest::blocking::Client,
    token_store: Option<TokenStore>,
    auth: Auth,
}

impl std::fmt::Debug for StoreClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("StoreClient")
            .field("base_url", &self.base_url)
            .finish_non_exhaustive()
    }
}

impl StoreClient {
    /// Creates a new builder with the default base URL (`https://store.nodos.dev`).
    pub fn builder() -> ClientBuilder {
        ClientBuilder {
            base_url: DEFAULT_BASE_URL.to_string(),
            token: None,
            token_store: None,
        }
    }

    /// Creates a client with the default base URL and auth from environment variables.
    pub fn default_with_auth() -> Self {
        Self::builder()
            .with_auth(Auth::from_env().unwrap_or(Auth::None))
            .build()
            .expect("Failed to build client")
    }

    /// Returns the base URL for the store API.
    pub fn base_url(&self) -> &str {
        &self.base_url
    }

    fn api_url(&self, path: &str) -> String {
        format!("{}/api/v1{}", self.base_url.trim_end_matches('/'), path)
    }

    fn auth_header(&self) -> Option<HeaderValue> {
        self.auth.token().map(|token| {
            HeaderValue::from_str(&format!("Bearer {}", token))
                .expect("token contains non-ASCII characters")
        })
    }

    // -----------------------------------------------------------------------
    // Private HTTP helpers — API client (short timeout)
    // -----------------------------------------------------------------------

    fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
        let url = self.api_url(path);
        let mut req = self.http.get(&url);
        if let Some(auth) = self.auth_header() {
            req = req.header(AUTHORIZATION, auth);
        }
        let response = req
            .send()
            .map_err(|e| Error::api(format!("GET {url}: {e}")))?;
        self.handle_response(response)
    }

    fn post<T: serde::de::DeserializeOwned, B: serde::Serialize>(
        &self,
        path: &str,
        body: &B,
    ) -> Result<T> {
        let url = self.api_url(path);
        let mut req = self.http.post(&url).json(body);
        if let Some(auth) = self.auth_header() {
            req = req.header(AUTHORIZATION, auth);
        }
        let response = req
            .send()
            .map_err(|e| Error::api(format!("POST {url}: {e}")))?;
        self.handle_response(response)
    }

    fn post_empty(&self, path: &str) -> Result<Response> {
        let url = self.api_url(path);
        let mut req = self.http.post(&url);
        if let Some(auth) = self.auth_header() {
            req = req.header(AUTHORIZATION, auth);
        }
        let response = req
            .send()
            .map_err(|e| Error::api(format!("POST {url}: {e}")))?;
        if !response.status().is_success() {
            return Err(self.read_error(response));
        }
        Ok(response)
    }

    fn delete(&self, path: &str) -> Result<()> {
        let url = self.api_url(path);
        let mut req = self.http.delete(&url);
        if let Some(auth) = self.auth_header() {
            req = req.header(AUTHORIZATION, auth);
        }
        let response = req
            .send()
            .map_err(|e| Error::api(format!("DELETE {url}: {e}")))?;
        if !response.status().is_success() {
            return Err(self.read_error(response));
        }
        Ok(())
    }

    fn delete_json<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
        let url = self.api_url(path);
        let mut req = self.http.delete(&url);
        if let Some(auth) = self.auth_header() {
            req = req.header(AUTHORIZATION, auth);
        }
        let response = req
            .send()
            .map_err(|e| Error::api(format!("DELETE {url}: {e}")))?;
        self.handle_response(response)
    }

    // -----------------------------------------------------------------------
    // Private HTTP helpers — upload client (no read/write timeout)
    // -----------------------------------------------------------------------

    /// PUTs a streaming body to an arbitrary URL.
    ///
    /// Uses the dedicated upload client which has no read/write timeout, so
    /// large artifacts are not killed mid-transfer. The connect timeout still
    /// applies to ensure we fail fast if the server is unreachable.
    fn put_upload(
        &self,
        url: &str,
        body: impl Into<reqwest::blocking::Body>,
        content_type: &str,
    ) -> Result<()> {
        let mut req = self
            .upload_http
            .put(url)
            .body(body)
            .header(CONTENT_TYPE, content_type);
        if let Some(auth) = self.auth_header() {
            req = req.header(AUTHORIZATION, auth);
        }
        let response = req.send().map_err(|e| {
            let mut msg = format!("PUT {url}: {e}");
            let mut source: Option<&dyn StdError> = StdError::source(&e);
            while let Some(cause) = source {
                msg.push_str(&format!("\n  caused by: {cause}"));
                source = cause.source();
            }
            Error::api(msg)
        })?;
        if !response.status().is_success() {
            return Err(self.read_error(response));
        }
        Ok(())
    }

    // -----------------------------------------------------------------------
    // Response handling
    // -----------------------------------------------------------------------

    fn handle_response<T: serde::de::DeserializeOwned>(&self, response: Response) -> Result<T> {
        if !response.status().is_success() {
            return Err(self.read_error(response));
        }
        let body = response.json::<serde_json::Value>()?;
        Ok(serde_json::from_value(body)?)
    }

    fn read_error(&self, response: Response) -> Error {
        let status = response.status().as_u16();
        let body = response.text().unwrap_or_default();
        let message =
            if let Ok(error) = serde_json::from_str::<crate::types::ApiErrorResponse>(&body) {
                error.message
            } else if body.trim().is_empty() {
                format!("Request failed with status {status}")
            } else {
                format!("Request failed with status {status}: {}", body.trim())
            };
        Error::from_status(status, message)
    }

    // -----------------------------------------------------------------------
    // Account
    // -----------------------------------------------------------------------

    pub fn validate_token(&self) -> Result<AccountInfo> {
        self.get("/account/me")
    }

    // -----------------------------------------------------------------------
    // Packages
    // -----------------------------------------------------------------------

    pub fn list_packages(&self) -> Result<Vec<Package>> {
        let mut page = 1;
        let mut packages = Vec::new();
        loop {
            let response: PackageListResponse =
                self.get(&format!("/packages?page={page}&per_page=48"))?;
            packages.extend(response.packages);
            if !response.has_next_page {
                break;
            }
            page += 1;
        }
        Ok(packages)
    }

    pub fn get_package(&self, name: &str) -> Result<Package> {
        let response: crate::types::PackageDetailResponse =
            self.get(&format!("/packages/{name}"))?;
        Ok(response.package)
    }

    // -----------------------------------------------------------------------
    // Releases
    // -----------------------------------------------------------------------

    pub fn get_releases(&self, name: &str) -> Result<Vec<Release>> {
        let mut page = 1;
        let mut releases = Vec::new();
        loop {
            let response: ReleaseListResponse = self.get(&format!(
                "/packages/{name}/releases?page={page}&per_page=48"
            ))?;
            releases.extend(response.releases);
            if !response.has_next_page {
                break;
            }
            page += 1;
        }
        Ok(releases)
    }

    pub fn get_my_releases(&self, name: &str) -> Result<Vec<Release>> {
        let mut page = 1;
        let mut releases = Vec::new();
        loop {
            let response: ReleaseListResponse = self.get(&format!(
                "/account/packages/{name}/releases?page={page}&per_page=48"
            ))?;
            releases.extend(response.releases);
            if !response.has_next_page {
                break;
            }
            page += 1;
        }
        Ok(releases)
    }

    /// Searches the authenticated user's releases for a specific version.
    /// Returns `None` if the package or version is not found.
    pub fn get_account_release_by_version(
        &self,
        package_name: &str,
        version: &str,
    ) -> Result<Option<Release>> {
        let mut page = 1;
        loop {
            let response: ReleaseListResponse = match self.get(&format!(
                "/account/packages/{package_name}/releases?page={page}&per_page=50"
            )) {
                Ok(r) => r,
                Err(Error::NotFound(_)) => return Ok(None),
                Err(e) => return Err(e),
            };

            if let Some(release) = response.releases.into_iter().find(|r| r.version == version) {
                return Ok(Some(release));
            }

            if !response.has_next_page {
                return Ok(None);
            }
            page += 1;
        }
    }

    // -----------------------------------------------------------------------
    // Downloads
    // -----------------------------------------------------------------------

    /// Returns a temporary signed download URL for the given artifact ID.
    pub fn get_artifact_download_url(&self, artifact_id: i64) -> Result<String> {
        let token_url = self.api_url(&format!("/release-artifacts/{artifact_id}/download-token"));
        let response: crate::types::DownloadTokenResponse = self
            .http
            .post(&token_url)
            .send()
            .map_err(|e| Error::api(format!("POST {token_url}: {e}")))?
            .error_for_status()
            .map_err(|e| Error::api(format!("Failed to request download URL: {e}")))?
            .json()?;
        Ok(response.download_url)
    }

    /// Downloads an artifact by ID into the provided writer.
    ///
    /// Obtains a signed download URL via [`get_artifact_download_url`] and
    /// streams the response body into `writer`. Uses the upload HTTP client
    /// (no read/write timeout) so large artifacts are not killed mid-transfer.
    pub fn download_artifact(
        &self,
        artifact_id: i64,
        writer: &mut impl std::io::Write,
    ) -> Result<u64> {
        let url = self.get_artifact_download_url(artifact_id)?;
        let mut response = self
            .upload_http
            .get(&url)
            .send()
            .map_err(|e| Error::api(format!("GET {url}: {e}")))?
            .error_for_status()
            .map_err(|e| Error::api(format!("Failed to download artifact: {e}")))?;
        let bytes = std::io::copy(&mut response, writer)?;
        Ok(bytes)
    }

    /// Downloads a store artifact and extracts it into `target_dir`.
    ///
    /// The archive format is detected by magic bytes:
    /// - `PK` (0x50 0x4B) → ZIP
    /// - 0x1F 0x8B → gzip (assumed tar.gz)
    pub fn install_artifact(
        &self,
        artifact_id: i64,
        target_dir: &std::path::Path,
    ) -> Result<()> {
        use std::io::{Read, Seek, SeekFrom};

        let mut tmpfile = tempfile::tempfile()?;
        self.download_artifact(artifact_id, &mut tmpfile)?;
        tmpfile.seek(SeekFrom::Start(0))?;

        let mut magic = [0u8; 2];
        tmpfile.read_exact(&mut magic)?;
        tmpfile.seek(SeekFrom::Start(0))?;

        std::fs::create_dir_all(target_dir)?;

        if magic == [0x50, 0x4B] {
            Self::extract_zip(tmpfile, target_dir)
        } else if magic == [0x1F, 0x8B] {
            Self::extract_tar_gz(tmpfile, target_dir)
        } else {
            Err(Error::Extract(format!(
                "Unrecognized archive format (magic bytes: [{:#04X}, {:#04X}])",
                magic[0], magic[1]
            )))
        }
    }

    fn extract_zip(file: std::fs::File, target_dir: &std::path::Path) -> Result<()> {
        let mut archive = zip::ZipArchive::new(file)
            .map_err(|e| Error::Extract(format!("Failed to open ZIP archive: {e}")))?;

        for i in 0..archive.len() {
            let mut entry = archive.by_index(i)
                .map_err(|e| Error::Extract(format!("Failed to read ZIP entry {i}: {e}")))?;

            let name = entry.name().replace('\\', "/");
            let outpath = target_dir.join(&name);

            if entry.is_dir() {
                std::fs::create_dir_all(&outpath)?;
            } else {
                if let Some(parent) = outpath.parent() {
                    std::fs::create_dir_all(parent)?;
                }
                let mut outfile = std::fs::File::create(&outpath)?;
                std::io::copy(&mut entry, &mut outfile)?;
            }
        }
        Ok(())
    }

    fn extract_tar_gz(file: std::fs::File, target_dir: &std::path::Path) -> Result<()> {
        let decoder = flate2::read::GzDecoder::new(file);
        let mut archive = tar::Archive::new(decoder);
        archive.unpack(target_dir)
            .map_err(|e| Error::Extract(format!("Failed to extract tar.gz: {e}")))?;
        Ok(())
    }

    // -----------------------------------------------------------------------
    // Device authentication
    // -----------------------------------------------------------------------

    pub fn start_device_login(&self) -> Result<DeviceStartResponse> {
        #[derive(serde::Serialize)]
        struct Request<'a> {
            client_name: &'a str,
        }

        let hostname = hostname::get()
            .map(|h| h.to_string_lossy().into_owned())
            .unwrap_or_else(|_| "unknown".to_string());

        self.post(
            "/auth/device/start",
            &Request {
                client_name: &format!("nodos-store on {hostname}"),
            },
        )
    }

    pub fn poll_device_login(&self, device_code: &str) -> Result<DevicePollResponse> {
        #[derive(serde::Serialize)]
        struct Request<'a> {
            device_code: &'a str,
        }

        let poll_url = self.api_url("/auth/device/poll");
        let response = self
            .http
            .post(&poll_url)
            .json(&Request { device_code })
            .send()
            .map_err(|e| Error::api(format!("POST {poll_url}: {e}")))?;

        if response.status() == reqwest::StatusCode::ACCEPTED {
            return Err(Error::DeviceFlowPending);
        }
        if !response.status().is_success() {
            return Err(self.read_error(response));
        }

        Ok(response.json()?)
    }

    pub fn ensure_access_token(&mut self) -> Result<String> {
        // Environment variables take priority — useful for CI/CD and overriding stored tokens.
        if let Some(Auth::Token(token_str)) = Auth::from_env().as_ref() {
            if let Ok(me) = self.validate_token_with_token(token_str) {
                if me.needs_onboarding {
                    return Err(Error::NeedsOnboarding);
                }
                self.auth = Auth::Token(token_str.clone());
                return Ok(token_str.clone());
            }
            return Err(Error::Auth(
                "Environment variable token is no longer valid.".to_string(),
            ));
        }

        // Fall back to the stored / explicitly-provided token.
        if let Some(token) = self.auth.token() {
            if let Ok(me) = self.validate_token() {
                if me.needs_onboarding {
                    return Err(Error::NeedsOnboarding);
                }
                return Ok(token.to_string());
            }
            return Err(Error::Auth(
                "Stored Nodos Store token is no longer valid. Run `nosman auth login` to re-authenticate.".to_string(),
            ));
        }

        Err(Error::Auth(
            "Not authenticated. Run `nosman auth login` first.".to_string(),
        ))
    }

    /// Completes a device-flow login started with [`start_device_login`].
    /// Polls until the user approves, the flow expires, or an error occurs.
    /// On success, stores the token in memory and in the configured token store.
    pub fn complete_device_login(&mut self, start: &DeviceStartResponse) -> Result<()> {
        let token = self.poll_until_approved(start)?;
        self.auth = Auth::Token(token.clone());
        if let Some(ref store) = self.token_store {
            store.save(&token)?;
        }
        Ok(())
    }

    /// Removes the stored token. The next authenticated operation will require
    /// re-login (or a valid environment variable token).
    pub fn logout(&mut self) -> Result<()> {
        if let Some(ref store) = self.token_store {
            store.delete()?;
        }
        self.auth = Auth::None;
        Ok(())
    }

    fn validate_token_with_token(&self, token: &str) -> Result<AccountInfo> {
        let url = self.api_url("/account/me");
        let response = self
            .http
            .get(&url)
            .header(
                AUTHORIZATION,
                HeaderValue::from_str(&format!("Bearer {token}"))
                    .expect("token contains non-ASCII characters"),
            )
            .send()
            .map_err(|e| Error::api(format!("GET {url}: {e}")))?;
        self.handle_response(response)
    }

    fn poll_until_approved(&self, start: &DeviceStartResponse) -> Result<String> {
        let expires_at = chrono::DateTime::parse_from_rfc3339(&start.expires_at)
            .map_err(|e| Error::api(format!("Failed to parse device sign-in expiration: {e}")))?
            .with_timezone(&chrono::Utc);

        let interval = start.interval_seconds.max(1);

        loop {
            if chrono::Utc::now() >= expires_at {
                return Err(Error::DeviceFlowExpired);
            }

            std::thread::sleep(Duration::from_secs(interval));

            match self.poll_device_login(&start.device_code) {
                Ok(poll) => {
                    if let Some(token) = poll.access_token {
                        return Ok(token);
                    }
                }
                Err(Error::DeviceFlowPending) => continue,
                Err(e) => return Err(e),
            }
        }
    }

    // -----------------------------------------------------------------------
    // Publishing — low-level session API
    // -----------------------------------------------------------------------

    /// Creates a new publish session.
    pub fn create_publish_session(
        &self,
        request: &PublishSessionCreateRequest,
    ) -> Result<PublishSession> {
        self.post("/publish/sessions", request)
    }

    /// Requests a signed upload URL for an artifact in a publish session.
    pub fn request_artifact_upload_url(
        &self,
        session_id: i64,
        target_platform: &str,
    ) -> Result<PublishUploadUrl> {
        self.post(
            &format!("/publish/sessions/{session_id}/artifact-upload-url"),
            &PublishUploadUrlRequest {
                target_platform: target_platform.to_string(),
            },
        )
    }

    /// Uploads artifact data to a signed upload URL.
    ///
    /// Uses the dedicated upload HTTP client (no read/write timeout) so large
    /// artifacts are not killed mid-transfer. Accepts any type that converts
    /// into a [`reqwest::blocking::Body`], including `Vec<u8>`, `File`, and
    /// `impl Read`.
    pub fn upload_artifact(
        &self,
        upload_url: &str,
        body: impl Into<reqwest::blocking::Body>,
    ) -> Result<()> {
        self.put_upload(upload_url, body, "application/octet-stream")
    }

    /// Finalizes a publish session, creating the release.
    pub fn finalize_publish_session(&self, session_id: i64) -> Result<PublishFinalizeResponse> {
        let response =
            self.post_empty(&format!("/publish/sessions/{session_id}/finalize"))?;
        Ok(response.json()?)
    }

    /// Discards a publish session. Best-effort — errors are silently ignored.
    pub fn discard_publish_session(&self, session_id: i64) {
        let _ =
            self.delete_json::<MessageResponse>(&format!("/publish/sessions/{session_id}"));
    }

    // -----------------------------------------------------------------------
    // Publishing — high-level convenience
    // -----------------------------------------------------------------------

    /// Publishes a single-artifact release in one call.
    ///
    /// This is a convenience method that creates a session, uploads the
    /// artifact, and finalizes. For multi-artifact releases, use the
    /// low-level session methods directly.
    ///
    /// `changelog` is optional release notes; when `None`, the Nodos Store
    /// derives a changelog from the artifact diff at finalize time.
    #[allow(clippy::too_many_arguments)]
    pub fn publish_release(
        &mut self,
        name: &str,
        display_name: &str,
        description: &str,
        package_type: &str,
        category: &str,
        version: &str,
        api_version: Option<&ApiVersion>,
        dependencies: Vec<PackageDependency>,
        tags: Vec<String>,
        target_platform: &str,
        artifact_data: Vec<u8>,
        visibility: crate::types::PackageVisibility,
        changelog: Option<String>,
    ) -> Result<Release> {
        let _token = self.ensure_access_token()?;

        let session = self.create_publish_session(&PublishSessionCreateRequest {
            name: name.to_string(),
            display_name: display_name.to_string(),
            description: description.to_string(),
            package_type: crate::types::PackageType::from_str(package_type),
            category: category.to_string(),
            version: version.to_string(),
            api_version: api_version.cloned(),
            dependencies,
            tags,
            artifacts: vec![PublishDraftArtifact {
                target_platform: target_platform.to_string(),
            }],
            visibility,
            changelog,
        })?;

        let session_id = session.session.id;
        let upload = self.request_artifact_upload_url(session_id, target_platform)?;


        self.upload_artifact(&upload.upload_url, artifact_data)?;

        let finalize = self.finalize_publish_session(session_id)?;
        Ok(finalize.release)
    }

    // -----------------------------------------------------------------------
    // Release deletion
    // -----------------------------------------------------------------------

    /// Deletes a specific release by ID.
    pub fn delete_release_by_id(&self, package_name: &str, release_id: i64) -> Result<()> {
        self.delete(&format!(
            "/account/packages/{package_name}/releases/{release_id}"
        ))
    }

    /// Deletes releases for a package by version.
    /// If `version` is `None`, all releases for the package are deleted.
    pub fn delete_release(&mut self, package_name: &str, version: Option<&str>) -> Result<()> {
        let _token = self.ensure_access_token()?;

        let releases = self.get_my_releases(package_name)?;

        let release_ids: Vec<i64> = if let Some(version) = version {
            releases
                .iter()
                .filter(|r| r.version == version)
                .map(|r| r.id)
                .collect()
        } else {
            releases.iter().map(|r| r.id).collect()
        };

        if release_ids.is_empty() {
            return Err(Error::NotFound(match version {
                Some(v) => format!("No release found for {package_name} version {v}"),
                None => format!("No releases found for {package_name}"),
            }));
        }

        for release_id in release_ids {
            self.delete_release_by_id(package_name, release_id)?;
        }
        Ok(())
    }
}

// -----------------------------------------------------------------------
// ClientBuilder
// -----------------------------------------------------------------------

pub struct ClientBuilder {
    base_url: String,
    token: Option<String>,
    token_store: Option<TokenStore>,
}

impl ClientBuilder {
    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
        self.base_url = base_url.into();
        self
    }

    pub fn with_token(mut self, token: impl Into<String>) -> Self {
        self.token = Some(token.into());
        self
    }

    pub fn with_auth(mut self, auth: Auth) -> Self {
        if let Auth::Token(t) = auth {
            self.token = Some(t);
        }
        self
    }

    pub fn with_token_store(mut self, store: TokenStore) -> Self {
        self.token_store = Some(store);
        self
    }

    pub fn build(self) -> Result<StoreClient> {
        let user_agent = HeaderValue::from_static(USER_AGENT_VALUE);

        let http = reqwest::blocking::Client::builder()
            .connect_timeout(CONNECT_TIMEOUT)
            .timeout(API_TIMEOUT)
            .tcp_keepalive(TCP_KEEPALIVE)
            .default_headers({
                let mut headers = reqwest::header::HeaderMap::new();
                headers.insert(USER_AGENT, user_agent.clone());
                headers
            })
            .build()
            .map_err(|e| Error::api(format!("Failed to create HTTP client: {e}")))?;

        // Upload client: generous overall timeout for large artifacts.
        // TCP keepalive prevents silent drops on long-lived upload connections.
        let upload_http = reqwest::blocking::Client::builder()
            .connect_timeout(CONNECT_TIMEOUT)
            .timeout(Duration::from_secs(30 * 60))
            .tcp_keepalive(TCP_KEEPALIVE)
            .default_headers({
                let mut headers = reqwest::header::HeaderMap::new();
                headers.insert(USER_AGENT, user_agent);
                headers
            })
            .build()
            .map_err(|e| Error::api(format!("Failed to create upload HTTP client: {e}")))?;

        let auth = self.token
            .map(Auth::Token)
            .or_else(|| {
                self.token_store
                    .as_ref()
                    .and_then(|s| s.load())
                    .map(Auth::Token)
            })
            .unwrap_or(Auth::None);

        Ok(StoreClient {
            base_url: self.base_url,
            http,
            upload_http,
            token_store: self.token_store,
            auth,
        })
    }
}
