//! Manifest parsing for remote index.

use crate::error::{NxvError, Result};
use serde::{Deserialize, Serialize};
use std::io::Cursor;

/// Embedded public key for verifying manifest signatures.
/// This is the minisign public key used to sign official nxv manifests.
/// Loaded at compile time from `keys/nxv.pub`.
///
/// To generate a new keypair (requires `--features indexer`):
/// ```sh
/// nxv keygen --secret-key nxv.key --public-key nxv.pub
/// ```
///
/// To sign a manifest during publish:
/// ```sh
/// nxv publish --sign --secret-key nxv.key <index.db>
/// ```
pub const MANIFEST_PUBLIC_KEY: &str = include_str!("../../keys/nxv.pub");

/// Remote index manifest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
    /// Manifest format version (currently 1).
    /// This is the manifest structure version, not the database schema version.
    /// See `fetch_manifest()` for version validation.
    pub version: u32,
    /// Minimum database schema version required to read this index.
    /// When omitted, clients fall back to checking the database's schema_version.
    /// Set this to allow older clients to read indexes with additive schema changes
    /// (e.g., new columns that don't break backward compatibility).
    ///
    /// Note: This is checked before downloading. The database also stores
    /// `min_schema_version` in its meta table for local validation.
    #[serde(default)]
    pub min_version: Option<u32>,
    pub latest_commit: String,
    pub latest_commit_date: String,
    pub full_index: IndexFile,
    pub bloom_filter: IndexFile,
    #[serde(default)]
    pub deltas: Vec<DeltaFile>,
}

/// A downloadable file with metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexFile {
    pub url: String,
    pub size_bytes: u64,
    pub sha256: String,
}

/// A delta pack file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeltaFile {
    pub from_commit: String,
    pub to_commit: String,
    pub url: String,
    pub size_bytes: u64,
    pub sha256: String,
}

impl Manifest {
    /// Parse a manifest from JSON content.
    pub fn parse(json: &str) -> Result<Self> {
        let manifest: Manifest = serde_json::from_str(json)?;
        Ok(manifest)
    }

    /// Parse and verify a manifest from JSON content and its signature.
    ///
    /// The signature should be in minisign format.
    pub fn parse_and_verify(json: &str, signature: &str) -> Result<Self> {
        // Verify signature first
        verify_manifest_signature(json.as_bytes(), signature)?;

        // Then parse the manifest
        Self::parse(json)
    }

    /// Find a delta that can update from the given commit.
    pub fn find_delta(&self, from_commit: &str) -> Option<&DeltaFile> {
        self.deltas.iter().find(|d| d.from_commit == from_commit)
    }
}

/// Verify a manifest signature using the embedded public key.
pub fn verify_manifest_signature(manifest_data: &[u8], signature_str: &str) -> Result<()> {
    // Parse the public key using from_box (expects comment + base64 format)
    let pk_box = minisign::PublicKeyBox::from_string(MANIFEST_PUBLIC_KEY)
        .map_err(|_| NxvError::InvalidManifestSignature)?;
    let pk =
        minisign::PublicKey::from_box(pk_box).map_err(|_| NxvError::InvalidManifestSignature)?;

    // Parse the signature
    let sig_box = minisign::SignatureBox::from_string(signature_str)
        .map_err(|_| NxvError::InvalidManifestSignature)?;

    // Verify
    let mut cursor = Cursor::new(manifest_data);
    minisign::verify(&pk, &sig_box, &mut cursor, true, false, true)
        .map_err(|_| NxvError::InvalidManifestSignature)?;

    Ok(())
}

/// Normalize a public key string to minisign format.
///
/// If the key is just raw base64 (starts with "RW"), prepends a default comment.
/// If already in full format (starts with "untrusted comment:"), returns as-is.
fn normalize_public_key(key: &str) -> String {
    let trimmed = key.trim();
    if trimmed.starts_with("untrusted comment:") {
        trimmed.to_string()
    } else if trimmed.starts_with("RW") {
        format!("untrusted comment: minisign public key\n{}", trimmed)
    } else {
        // Return as-is, let the parser give a proper error
        trimmed.to_string()
    }
}

/// Verify a manifest signature with a custom public key.
///
/// Used for self-hosted indexes with custom signing keys.
/// Accepts either:
/// - Full minisign format with comment (untrusted comment: ...\nRW...)
/// - Raw base64 key only (RW...)
pub fn verify_manifest_signature_with_key(
    manifest_data: &[u8],
    signature_str: &str,
    public_key_str: &str,
) -> Result<()> {
    // Normalize the key: add default comment if only raw base64 provided
    let normalized_key = normalize_public_key(public_key_str);

    // Parse the public key using from_box (expects comment + base64 format)
    let pk_box = minisign::PublicKeyBox::from_string(&normalized_key)
        .map_err(|e| NxvError::PublicKey(format!("failed to parse public key: {}", e)))?;
    let pk = minisign::PublicKey::from_box(pk_box)
        .map_err(|e| NxvError::PublicKey(format!("failed to parse public key: {}", e)))?;

    // Parse the signature
    let sig_box = minisign::SignatureBox::from_string(signature_str)
        .map_err(|e| NxvError::PublicKey(format!("failed to parse signature: {}", e)))?;

    // Verify
    let mut cursor = Cursor::new(manifest_data);
    minisign::verify(&pk, &sig_box, &mut cursor, true, false, true)
        .map_err(|e| NxvError::PublicKey(format!("signature verification failed: {}", e)))?;

    Ok(())
}

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

    #[test]
    fn test_embedded_public_key_parses() {
        // The embedded public key should be parseable using from_box
        let pk_box = minisign::PublicKeyBox::from_string(MANIFEST_PUBLIC_KEY);
        assert!(
            pk_box.is_ok(),
            "Embedded public key box should parse: {:?}",
            pk_box.err()
        );

        let pk = minisign::PublicKey::from_box(pk_box.unwrap());
        assert!(
            pk.is_ok(),
            "Embedded public key should parse: {:?}",
            pk.err()
        );
    }

    #[test]
    fn test_normalize_public_key_raw_base64() {
        let raw_key = "RWRBBg3BOtB6V0YjlII702+pQeXUsdIk3mzZRmopwRlutFAW9Bl/naMV";
        let normalized = normalize_public_key(raw_key);

        // Should add a default comment
        assert!(normalized.starts_with("untrusted comment:"));
        assert!(normalized.contains(raw_key));

        // Should be parseable by minisign
        let pk_box = minisign::PublicKeyBox::from_string(&normalized);
        assert!(
            pk_box.is_ok(),
            "Normalized key should parse: {:?}",
            pk_box.err()
        );
    }

    #[test]
    fn test_normalize_public_key_full_format() {
        let full_key =
            "untrusted comment: my key\nRWRBBg3BOtB6V0YjlII702+pQeXUsdIk3mzZRmopwRlutFAW9Bl/naMV";
        let normalized = normalize_public_key(full_key);

        // Should preserve the original format
        assert_eq!(normalized, full_key);
    }

    #[test]
    fn test_normalize_public_key_with_whitespace() {
        let key_with_whitespace = "  RWRBBg3BOtB6V0YjlII702+pQeXUsdIk3mzZRmopwRlutFAW9Bl/naMV  \n";
        let normalized = normalize_public_key(key_with_whitespace);

        // Should trim and add comment
        assert!(normalized.starts_with("untrusted comment:"));
        assert!(!normalized.starts_with("  ")); // No leading whitespace
    }

    #[test]
    fn test_manifest_parse() {
        let json = r#"{
            "version": 2,
            "latest_commit": "abc123def456",
            "latest_commit_date": "2024-01-15T12:00:00Z",
            "full_index": {
                "url": "https://example.com/index.db.zst",
                "size_bytes": 150000000,
                "sha256": "abc123"
            },
            "bloom_filter": {
                "url": "https://example.com/index.bloom",
                "size_bytes": 1200000,
                "sha256": "def456"
            },
            "deltas": []
        }"#;

        let manifest = Manifest::parse(json).unwrap();
        assert_eq!(manifest.version, 2);
        assert_eq!(manifest.latest_commit, "abc123def456");
    }

    #[test]
    fn test_manifest_find_delta() {
        let json = r#"{
            "version": 1,
            "latest_commit": "def456",
            "latest_commit_date": "2024-01-15T12:00:00Z",
            "full_index": {
                "url": "https://example.com/index.db.zst",
                "size_bytes": 150000000,
                "sha256": "abc123"
            },
            "bloom_filter": {
                "url": "https://example.com/index.bloom",
                "size_bytes": 1200000,
                "sha256": "def456"
            },
            "deltas": [
                {
                    "from_commit": "abc123",
                    "to_commit": "def456",
                    "url": "https://example.com/delta.sql.zst",
                    "size_bytes": 1000,
                    "sha256": "delta123"
                }
            ]
        }"#;

        let manifest = Manifest::parse(json).unwrap();
        let delta = manifest.find_delta("abc123");
        assert!(delta.is_some());
        assert_eq!(delta.unwrap().to_commit, "def456");

        let no_delta = manifest.find_delta("unknown");
        assert!(no_delta.is_none());
    }

    #[test]
    fn test_signature_verification_fails_on_tampered_manifest() {
        // This tests that verification fails with an invalid signature
        let manifest = r#"{"version":1,"latest_commit":"abc123"}"#;

        // A fake/invalid signature
        let fake_signature = "untrusted comment: fake signature\nRUTHy8Hb+LSqSJNRMBXPzXl8J5F5WTWmYu5J0CxmZWQ3z8rLnVJk9ABCAAAA\ntrusted comment: fake\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

        let result = verify_manifest_signature(manifest.as_bytes(), fake_signature);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            NxvError::InvalidManifestSignature
        ));
    }

    #[test]
    fn test_signature_verification_fails_on_invalid_format() {
        let manifest = r#"{"version":1}"#;
        let invalid_sig = "not a valid signature format";

        let result = verify_manifest_signature(manifest.as_bytes(), invalid_sig);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            NxvError::InvalidManifestSignature
        ));
    }

    #[test]
    fn test_parse_and_verify_fails_on_bad_signature() {
        let json = r#"{
            "version": 1,
            "latest_commit": "abc123",
            "latest_commit_date": "2024-01-15T12:00:00Z",
            "full_index": {
                "url": "https://example.com/index.db.zst",
                "size_bytes": 100,
                "sha256": "abc"
            },
            "bloom_filter": {
                "url": "https://example.com/index.bloom",
                "size_bytes": 100,
                "sha256": "def"
            },
            "deltas": []
        }"#;

        let bad_sig = "invalid signature";
        let result = Manifest::parse_and_verify(json, bad_sig);
        assert!(result.is_err());
    }
}

#[cfg(test)]
mod proptests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        /// Manifest::parse should never panic on arbitrary input.
        #[test]
        fn parse_never_panics(s in "\\PC*") {
            // Should return Ok or Err, never panic
            let _ = Manifest::parse(&s);
        }

        /// Valid JSON that doesn't match schema should return Err, not panic.
        #[test]
        fn parse_rejects_wrong_schema(
            version in any::<i32>(),
            commit in "[a-f0-9]{12}",
        ) {
            // Missing required fields
            let json = format!(r#"{{"version": {}, "latest_commit": "{}"}}"#, version, commit);
            let result = Manifest::parse(&json);
            assert!(result.is_err());
        }

        /// Malformed URLs in manifest should parse but be detectable.
        #[test]
        fn parse_accepts_any_url_string(
            url in ".*",
            commit in "[a-f0-9]{12}",
        ) {
            let json = format!(
                r#"{{
                    "version": 1,
                    "latest_commit": "{}",
                    "latest_commit_date": "2024-01-15T12:00:00Z",
                    "full_index": {{"url": "{}", "size_bytes": 100, "sha256": "abc"}},
                    "bloom_filter": {{"url": "{}", "size_bytes": 100, "sha256": "def"}},
                    "deltas": []
                }}"#,
                commit,
                url.replace('\\', "\\\\").replace('"', "\\\""),
                url.replace('\\', "\\\\").replace('"', "\\\"")
            );
            // Should parse without panicking (URL validation is done elsewhere)
            let _ = Manifest::parse(&json);
        }

        /// find_delta should handle any commit string.
        #[test]
        fn find_delta_handles_any_commit(commit in ".*") {
            let json = r#"{
                "version": 1,
                "latest_commit": "def456",
                "latest_commit_date": "2024-01-15T12:00:00Z",
                "full_index": {"url": "https://example.com/index.db.zst", "size_bytes": 100, "sha256": "abc"},
                "bloom_filter": {"url": "https://example.com/bloom.bin", "size_bytes": 100, "sha256": "def"},
                "deltas": [
                    {"from_commit": "abc123", "to_commit": "def456", "url": "https://example.com/delta.sql.zst", "size_bytes": 100, "sha256": "ghi"}
                ]
            }"#;

            let manifest = Manifest::parse(json).unwrap();
            // Should never panic
            let _ = manifest.find_delta(&commit);
        }

        /// Signature verification should reject random data without panicking.
        #[test]
        fn verify_rejects_random_signature(
            manifest in "[\\x00-\\x7F]{10,100}",
            signature in "[\\x00-\\x7F]{10,100}",
        ) {
            let result = verify_manifest_signature(manifest.as_bytes(), &signature);
            // Should return Err, not panic
            assert!(result.is_err());
        }
    }
}
