//! Pinned registry artifacts used by local test servers.

use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::{Arc, OnceLock};

use anyhow::{Context, Result, anyhow, bail};
use sha2::{Digest, Sha256};

use uv_configuration::TrustedHost;
use uv_fs::{LockedFile, LockedFileMode, persist_with_retry_sync, tempfile_in};
use uv_static::EnvVars;

use crate::TestContext;

pub(crate) struct VendorArtifact {
    pub(crate) filename: &'static str,
    url: &'static str,
    pub(crate) sha256: &'static str,
    bytes: OnceLock<Arc<[u8]>>,
}

impl VendorArtifact {
    pub(crate) fn bytes(&self) -> Result<Arc<[u8]>> {
        if let Some(bytes) = self.bytes.get() {
            return Ok(Arc::clone(bytes));
        }

        let bytes = load_vendor_file(self)?;
        Ok(Arc::clone(self.bytes.get_or_init(|| bytes)))
    }

    #[cfg(test)]
    pub(crate) fn is_loaded(&self) -> bool {
        self.bytes.get().is_some()
    }
}

static VENDOR_ARTIFACTS: [VendorArtifact; 17] = [
    VendorArtifact {
        filename: "calver-2022.6.26-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/f7/39/e421c06f42ca00fa9cf8929c2466e58a837e8e97b8ab3ff4f4ff9a15e33e/calver-2022.6.26-py3-none-any.whl",
        sha256: "a1d7fcdd67797afc52ee36ffb8c8adf6643173864306547bfd1380cbce6310a0",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "editables-0.5-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/6b/be/0f2f4a5e8adc114a02b63d92bf8edbfa24db6fc602fca83c885af2479e0e/editables-0.5-py3-none-any.whl",
        sha256: "61e5ffa82629e0d8bfe09bc44a07db3c1ab8ed1ce78a6980732870f19b5e7d4c",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "flit_core-3.9.0-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/38/45/618e84e49a6c51e5dd15565ec2fcd82ab273434f236b8f108f065ded517a/flit_core-3.9.0-py3-none-any.whl",
        sha256: "7aada352fb0c7f5538c4fafeddf314d3a6a92ee8e2b1de70482329e42de70301",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "futzed_bz2-0.1.0-py3-none-any.whl",
        url: "https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_bz2-0.1.0-py3-none-any.whl",
        sha256: "3c106ea0433a90f767271c322a8cb8bf5145f210adf8a357152796475b00b316",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "futzed_lzma-0.1.0-py3-none-any.whl",
        url: "https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_lzma-0.1.0-py3-none-any.whl",
        sha256: "9f39b4686926ffbed723450d700cca524001af259949eb5e4b973e7846df23d2",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "hatchling-1.20.0-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/87/4a/7d22a92b55809c579d8deafb0dabeb102b411a0ef9439949cccef3071527/hatchling-1.20.0-py3-none-any.whl",
        sha256: "872c63aa7e8aca85e8dba07b05c6a9b28d5a149fe00638f1a47e36930197248f",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "packaging-23.2-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl",
        sha256: "8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "packaging-23.2.tar.gz",
        url: "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz",
        sha256: "048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "pathspec-0.12.1-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl",
        sha256: "a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "pip-24.0-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl",
        sha256: "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "pluggy-1.3.0-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/05/b8/42ed91898d4784546c5f06c60506400548db3f7a4b3fb441cba4e5c17952/pluggy-1.3.0-py3-none-any.whl",
        sha256: "d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "setuptools-69.0.2-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/bb/e1/ed2dd0850446b8697ad28d118df885ad04140c64ace06c4bd559f7c8a94f/setuptools-69.0.2-py3-none-any.whl",
        sha256: "1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "setuptools_scm-8.0.4-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/0e/a3/b9a8b0adfe672bf0df5901707aa929d30a97ee390ba651910186776746d2/setuptools_scm-8.0.4-py3-none-any.whl",
        sha256: "b47844cd2a84b83b3187a5782c71128c28b4c94cad8bfb871da2784a5cb54c4f",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "tomli-2.0.1-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl",
        sha256: "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "trove_classifiers-2023.11.29-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/f8/50/e223fe762fe21fefb7a3f37c10e9693ea5f63cb54b5ae39daa876b780abc/trove_classifiers-2023.11.29-py3-none-any.whl",
        sha256: "02307750cbbac2b3d13078662f8a5bf077732bf506e9c33c97204b7f68f3699e",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "typing_extensions-4.9.0-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/b7/f4/6a90020cd2d93349b442bfcb657d0dc91eee65491600b2cb1d388bc98e6b/typing_extensions-4.9.0-py3-none-any.whl",
        sha256: "af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd",
        bytes: OnceLock::new(),
    },
    VendorArtifact {
        filename: "wheel-0.42.0-py3-none-any.whl",
        url: "https://files.pythonhosted.org/packages/c7/c3/55076fc728723ef927521abaa1955213d094933dc36d4a2008d5101e1af5/wheel-0.42.0-py3-none-any.whl",
        sha256: "177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d",
        bytes: OnceLock::new(),
    },
];

pub(crate) fn vendor_artifacts() -> &'static [VendorArtifact] {
    &VENDOR_ARTIFACTS
}

fn vendor_cache_dir() -> PathBuf {
    TestContext::test_bucket_dir().join("vendor")
}

fn load_vendor_file(artifact: &VendorArtifact) -> Result<Arc<[u8]>> {
    // HTTP responders run in a Tokio runtime; load on another thread instead of nesting runtimes.
    let bytes = std::thread::scope(|scope| {
        scope
            .spawn(|| load_vendor_file_async(artifact))
            .join()
            .map_err(|_| anyhow!("vendor artifact loader panicked"))
    })??;

    Ok(bytes.into())
}

#[tokio::main(flavor = "current_thread")]
async fn load_vendor_file_async(artifact: &VendorArtifact) -> Result<Vec<u8>> {
    let cache_dir = vendor_cache_dir();
    fs_err::create_dir_all(&cache_dir)
        .with_context(|| format!("failed to create vendor cache at `{}`", cache_dir.display()))?;
    let path = cache_dir.join(artifact.filename);
    ensure_cached_artifact(artifact, &path).await?;
    let bytes = fs_err::read(&path)
        .with_context(|| format!("failed to read cached vendor artifact `{}`", path.display()))?;
    verify_bytes(artifact, &bytes)?;
    Ok(bytes)
}

async fn ensure_cached_artifact(artifact: &VendorArtifact, path: &Path) -> Result<()> {
    if cached_artifact_matches(artifact, path) {
        return Ok(());
    }

    let lock_path = artifact_lock_path(path)?;
    let _lock = LockedFile::acquire(
        &lock_path,
        LockedFileMode::Exclusive,
        format_args!("vendor artifact `{}`", artifact.filename),
    )
    .await
    .with_context(|| {
        format!(
            "failed to lock cached vendor artifact `{}`",
            artifact.filename
        )
    })?;

    if cached_artifact_matches(artifact, path) {
        return Ok(());
    }

    let trusted_hosts = std::env::var(EnvVars::UV_INSECURE_HOST)
        .unwrap_or_default()
        .split(' ')
        .filter(|host| !host.is_empty())
        .map(TrustedHost::from_str)
        .collect::<std::result::Result<Vec<_>, _>>()?;
    let client = uv_client::BaseClientBuilder::default()
        .allow_insecure_host(trusted_hosts)
        .build()
        .context("failed to build vendor artifact client")?;

    let url = artifact
        .url
        .parse()
        .with_context(|| format!("invalid vendor artifact URL `{}`", artifact.url))?;
    let response = client
        .for_host(&url)
        .get(reqwest::Url::from(url))
        .send()
        .await
        .with_context(|| format!("failed to download `{}`", artifact.url))?
        .error_for_status()
        .with_context(|| format!("failed to download `{}`", artifact.url))?;
    let bytes = response
        .bytes()
        .await
        .with_context(|| format!("failed to read `{}`", artifact.url))?;
    verify_bytes(artifact, &bytes)?;

    let parent = path
        .parent()
        .context("vendor artifact cache path should have a parent")?;
    let mut temp = tempfile_in(parent).with_context(|| {
        format!(
            "failed to create vendor temp file in `{}`",
            parent.display()
        )
    })?;
    temp.write_all(&bytes)
        .with_context(|| format!("failed to write `{}`", artifact.filename))?;
    temp.as_file_mut()
        .sync_all()
        .with_context(|| format!("failed to sync `{}`", artifact.filename))?;

    match persist_with_retry_sync(temp, path) {
        Ok(()) => Ok(()),
        Err(error) => {
            if let Ok(bytes) = fs_err::read(path)
                && verify_bytes(artifact, &bytes).is_ok()
            {
                Ok(())
            } else {
                Err(error).with_context(|| {
                    format!(
                        "failed to persist cached vendor artifact `{}`",
                        path.display()
                    )
                })
            }
        }
    }
}

fn cached_artifact_matches(artifact: &VendorArtifact, path: &Path) -> bool {
    fs_err::read(path).is_ok_and(|bytes| verify_bytes(artifact, &bytes).is_ok())
}

fn artifact_lock_path(path: &Path) -> Result<PathBuf> {
    let parent = path
        .parent()
        .context("vendor artifact cache path should have a parent")?;
    let mut filename = path
        .file_name()
        .context("vendor artifact cache path should have a filename")?
        .to_os_string();
    filename.push(".lock");
    Ok(parent.join(filename))
}

fn verify_bytes(artifact: &VendorArtifact, bytes: &[u8]) -> Result<()> {
    let actual = format!("{:x}", Sha256::digest(bytes));
    if actual == artifact.sha256 {
        Ok(())
    } else {
        bail!(
            "hash mismatch for cached vendor artifact `{}`: expected `{}`, found `{}`",
            artifact.filename,
            artifact.sha256,
            actual
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn artifact_lock_path_is_per_artifact() {
        let path = Path::new("vendor").join("example-1.0.0-py3-none-any.whl");

        assert_eq!(
            artifact_lock_path(&path).expect("path should have a parent"),
            Path::new("vendor").join("example-1.0.0-py3-none-any.whl.lock")
        );
    }

    #[test]
    fn cached_bytes_are_held_per_artifact() {
        let bytes = OnceLock::new();
        bytes
            .set(Arc::from(b"available".as_slice()))
            .expect("artifact should not already have cached bytes");
        let loaded = VendorArtifact {
            filename: "available.whl",
            url: "unused",
            sha256: "unused",
            bytes,
        };
        let unloaded = VendorArtifact {
            filename: "unrequested.whl",
            url: "unused",
            sha256: "unused",
            bytes: OnceLock::new(),
        };

        assert_eq!(
            loaded
                .bytes()
                .expect("preloaded bytes should be returned")
                .as_ref(),
            b"available"
        );
        assert!(unloaded.bytes.get().is_none());
    }
}
