//! Pattern and project sharing via compact text strings.
//!
//! Export: data → MessagePack → Brotli → base64 URL-safe → prefix
//! Import: strip prefix → base64 decode → Brotli decompress → MessagePack → data

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;

use crate::{Bank, Pattern};

const PATTERN_PREFIX: &str = "cgr:";
const BANK_PREFIX: &str = "cgrb:";

/// Error during pattern or bank import/export.
#[derive(Debug)]
pub enum ShareError {
    InvalidPrefix,
    Base64(base64::DecodeError),
    Decompress(std::io::Error),
    Deserialize(rmp_serde::decode::Error),
    Serialize(rmp_serde::encode::Error),
    Compress(std::io::Error),
}

impl std::fmt::Display for ShareError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidPrefix => write!(f, "missing cgr:/cgrb: prefix"),
            Self::Base64(e) => write!(f, "base64: {e}"),
            Self::Decompress(e) => write!(f, "decompress: {e}"),
            Self::Deserialize(e) => write!(f, "deserialize: {e}"),
            Self::Serialize(e) => write!(f, "serialize: {e}"),
            Self::Compress(e) => write!(f, "compress: {e}"),
        }
    }
}

fn compress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
    let mut output = Vec::new();
    let params = brotli::enc::BrotliEncoderParams {
        quality: 11,
        lgwin: 22,
        lgblock: 0,
        ..Default::default()
    };
    brotli::BrotliCompress(&mut &data[..], &mut output, &params).map_err(ShareError::Compress)?;
    Ok(output)
}

fn decompress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
    let mut output = Vec::new();
    brotli::BrotliDecompress(&mut &data[..], &mut output).map_err(ShareError::Decompress)?;
    Ok(output)
}

fn encode<T: serde::Serialize>(value: &T, prefix: &str) -> Result<String, ShareError> {
    let packed = rmp_serde::to_vec_named(value).map_err(ShareError::Serialize)?;
    let compressed = compress(&packed)?;
    let encoded = URL_SAFE_NO_PAD.encode(&compressed);
    Ok(format!("{prefix}{encoded}"))
}

fn decode<T: serde::de::DeserializeOwned>(text: &str, prefix: &str) -> Result<T, ShareError> {
    let text = text.trim();
    let payload = text.strip_prefix(prefix).ok_or(ShareError::InvalidPrefix)?;
    let compressed = URL_SAFE_NO_PAD.decode(payload).map_err(ShareError::Base64)?;
    let packed = decompress(&compressed)?;
    rmp_serde::from_slice(&packed).map_err(ShareError::Deserialize)
}

/// Encode a pattern as a shareable `cgr:` string.
pub fn export(pattern: &Pattern) -> Result<String, ShareError> {
    encode(pattern, PATTERN_PREFIX)
}

/// Decode a `cgr:` string back into a pattern.
pub fn import(text: &str) -> Result<Pattern, ShareError> {
    decode(text, PATTERN_PREFIX)
}

/// Encode a bank as a shareable `cgrb:` string.
pub fn export_bank(bank: &Bank) -> Result<String, ShareError> {
    encode(bank, BANK_PREFIX)
}

/// Decode a `cgrb:` string back into a bank.
pub fn import_bank(text: &str) -> Result<Bank, ShareError> {
    decode(text, BANK_PREFIX)
}

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

    #[test]
    fn roundtrip_empty() {
        let pattern = Pattern::default();
        let encoded = export(&pattern).unwrap();
        assert!(encoded.starts_with("cgr:"));
        let decoded = import(&encoded).unwrap();
        assert_eq!(decoded.length, pattern.length);
        assert_eq!(decoded.steps.len(), pattern.steps.len());
    }

    #[test]
    fn roundtrip_with_steps() {
        let mut pattern = Pattern::default();
        pattern.steps[0] = Step {
            active: true,
            script: "kick 60 note".to_string(),
            source: None,
            name: Some("kick".to_string()),
        };
        pattern.steps[1] = Step {
            active: false,
            script: "snare".to_string(),
            source: None,
            name: None,
        };
        pattern.steps[3] = Step {
            active: true,
            script: String::new(),
            source: Some(0),
            name: None,
        };
        pattern.length = 8;
        pattern.name = Some("Test".to_string());

        let encoded = export(&pattern).unwrap();
        let decoded = import(&encoded).unwrap();

        assert_eq!(decoded.length, 8);
        assert_eq!(decoded.name.as_deref(), Some("Test"));
        assert_eq!(decoded.steps[0].script, "kick 60 note");
        assert_eq!(decoded.steps[0].name.as_deref(), Some("kick"));
        assert!(!decoded.steps[1].active);
        assert_eq!(decoded.steps[1].script, "snare");
        assert_eq!(decoded.steps[3].source, Some(0));
    }

    #[test]
    fn bad_prefix() {
        assert!(matches!(import("xxx:abc"), Err(ShareError::InvalidPrefix)));
    }

    #[test]
    fn bad_base64() {
        assert!(matches!(import("cgr:!!!"), Err(ShareError::Base64(_))));
    }

    #[test]
    fn whitespace_trimming() {
        let pattern = Pattern::default();
        let encoded = export(&pattern).unwrap();
        let padded = format!("  {encoded}  \n");
        let decoded = import(&padded).unwrap();
        assert_eq!(decoded.length, pattern.length);
    }

    #[test]
    fn msgpack_brotli_smaller_than_json_deflate() {
        let mut pattern = Pattern::default();
        for i in 0..16 {
            pattern.steps[i] = Step {
                active: true,
                script: format!("kick {i} note 0.5 dur"),
                source: None,
                name: Some(format!("step_{i}")),
            };
        }
        pattern.length = 16;

        // Current (msgpack+brotli)
        let new_encoded = export(&pattern).unwrap();

        // Old pipeline (json+deflate) for comparison
        use std::io::Write;
        let json = serde_json::to_vec(&pattern).unwrap();
        let mut encoder =
            flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best());
        encoder.write_all(&json).unwrap();
        let old_compressed = encoder.finish().unwrap();
        let old_encoded = format!("cgr:{}", URL_SAFE_NO_PAD.encode(&old_compressed));

        assert!(
            new_encoded.len() < old_encoded.len(),
            "msgpack+brotli ({}) should be smaller than json+deflate ({})",
            new_encoded.len(),
            old_encoded.len()
        );
    }

    #[test]
    fn roundtrip_bank() {
        let mut bank = Bank::default();
        bank.patterns[0].steps[0] = Step {
            active: true,
            script: "kick 60 note".to_string(),
            source: None,
            name: Some("kick".to_string()),
        };
        bank.patterns[0].length = 8;
        bank.name = Some("Drums".to_string());

        let encoded = export_bank(&bank).unwrap();
        assert!(encoded.starts_with("cgrb:"));
        let decoded = import_bank(&encoded).unwrap();

        assert_eq!(decoded.name.as_deref(), Some("Drums"));
        assert_eq!(decoded.patterns[0].length, 8);
        assert_eq!(decoded.patterns[0].steps[0].script, "kick 60 note");
    }
}
