//! Versioned persistence for compiled matchers.
//!
//! Every persisted matcher envelope carries a stable format identifier and a
//! format version. Readers dispatch explicitly on the version, migrate
//! supported older versions deterministically, and reject unsupported future
//! versions with a typed error *before* interpreting the payload.
//!
//! Construction configuration ([`MatcherOptions`]) is stored inside the
//! envelope and is re-applied when the matcher is rebuilt on load, so a
//! save-load round trip preserves observable matching behaviour exactly.

use std::io::Read;

use serde::{Deserialize, Serialize};
use terraphim_types::NormalizedTerm;

use crate::compiled::{CompiledMatcher, MatcherBuilder, MatcherOptions};
use crate::{Result, TerraphimAutomataError};

/// Stable identifier for the compiled-matcher persisted format.
pub const MATCHER_FORMAT_ID: &str = "terraphim.automata.matcher";

/// Current format version written by this crate.
pub const MATCHER_FORMAT_VERSION: u32 = 1;

/// Construction configuration plus source patterns (format version 1).
///
/// The compiled automaton is deliberately not stored; it is rebuilt from this
/// data on load, applying the persisted options.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PersistedMatcherV1 {
    /// Behaviour-affecting construction options; re-applied on rebuild.
    pub options: MatcherOptions,
    /// Source patterns paired with their normalized terms.
    pub patterns: Vec<(String, NormalizedTerm)>,
}

/// Explicitly dispatched payload versions.
///
/// Adding `V2` later means adding a variant plus a deterministic one-way
/// migration from `V1`; readers never guess.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "version")]
pub enum PersistedMatcherPayload {
    /// Initial versioned format.
    #[serde(rename = "1")]
    V1(PersistedMatcherV1),
}

/// The persisted envelope: format identity, version and payload.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PersistedMatcherEnvelope {
    /// Stable format identifier; see [`MATCHER_FORMAT_ID`].
    pub format_id: String,
    /// Payload, dispatched by explicit version tag.
    #[serde(flatten)]
    pub payload: PersistedMatcherPayload,
}

impl From<&CompiledMatcher> for PersistedMatcherEnvelope {
    fn from(matcher: &CompiledMatcher) -> Self {
        Self {
            format_id: MATCHER_FORMAT_ID.to_string(),
            payload: PersistedMatcherPayload::V1(PersistedMatcherV1 {
                options: matcher.options().clone(),
                patterns: matcher.patterns().to_vec(),
            }),
        }
    }
}

impl PersistedMatcherEnvelope {
    /// Build an envelope from an existing compiled matcher.
    pub fn from_matcher(matcher: &CompiledMatcher) -> Self {
        Self::from(matcher)
    }

    /// Rebuild a [`CompiledMatcher`] from the envelope, applying the stored
    /// construction options.
    ///
    /// Returns [`TerraphimAutomataError::ConfigurationMismatch`] when the
    /// stored patterns are not valid under the stored options (for example a
    /// pattern shorter than the persisted minimum length).
    pub fn to_matcher(&self) -> Result<CompiledMatcher> {
        let PersistedMatcherPayload::V1(data) = &self.payload;
        let mut builder = MatcherBuilder::new(data.options.clone());
        for (pattern, term) in &data.patterns {
            builder.insert(pattern.clone(), term.clone())?;
        }
        builder.build()
    }

    /// Serialise the envelope to pretty JSON.
    pub fn to_json(&self) -> Result<String> {
        serde_json::to_string_pretty(self).map_err(Into::into)
    }

    /// Parse an envelope from JSON.
    ///
    /// The format identifier is validated first; an unknown identifier or an
    /// unsupported (future) version fails with a typed error before any
    /// payload bytes are interpreted as the current type.
    pub fn from_json(json: &str) -> Result<Self> {
        #[derive(Deserialize)]
        struct Probe {
            format_id: String,
        }
        let probe: Probe = serde_json::from_str(json).map_err(|err| {
            TerraphimAutomataError::MalformedPersisted {
                format_id: MATCHER_FORMAT_ID.to_string(),
                reason: err.to_string(),
            }
        })?;
        if probe.format_id != MATCHER_FORMAT_ID {
            return Err(TerraphimAutomataError::UnsupportedFormat {
                found: probe.format_id,
                expected: MATCHER_FORMAT_ID.to_string(),
            });
        }
        let envelope: Self = serde_json::from_str(json).map_err(|err| {
            if err.to_string().contains("unknown variant") {
                TerraphimAutomataError::UnsupportedVersion {
                    format_id: MATCHER_FORMAT_ID.to_string(),
                    found: extract_reported_version(err.to_string()),
                    supported: supported_versions(),
                }
            } else {
                TerraphimAutomataError::MalformedPersisted {
                    format_id: MATCHER_FORMAT_ID.to_string(),
                    reason: err.to_string(),
                }
            }
        })?;
        Ok(envelope)
    }

    /// Write the envelope to a writer.
    pub fn to_writer<W: std::io::Write>(&self, writer: W) -> Result<()> {
        serde_json::to_writer_pretty(writer, self).map_err(Into::into)
    }

    /// Read an envelope from a reader, with the same validation as
    /// [`PersistedMatcherEnvelope::from_json`].
    pub fn from_reader<R: Read>(mut reader: R) -> Result<Self> {
        let mut buf = String::new();
        reader
            .read_to_string(&mut buf)
            .map_err(TerraphimAutomataError::Io)?;
        Self::from_json(&buf)
    }
}

/// Sorted list of payload versions this reader supports.
fn supported_versions() -> Vec<u32> {
    vec![MATCHER_FORMAT_VERSION]
}

/// Best-effort extraction of the offending version tag from a serde
/// "unknown variant" message, for diagnostics only.
fn extract_reported_version(message: String) -> Option<String> {
    message
        .split("unknown variant `")
        .nth(1)
        .and_then(|rest| rest.split('`').next())
        .map(str::to_string)
}

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

    fn term(id: &str) -> NormalizedTerm {
        NormalizedTerm::with_auto_id(NormalizedTermValue::from(id.to_string()))
    }

    fn sample_matcher() -> Result<CompiledMatcher> {
        let options = MatcherOptions {
            case_insensitive: false,
            min_pattern_length: 4,
        };
        let mut builder = MatcherBuilder::new(options);
        builder.insert("AlphaMatch".to_string(), term("a1"))?;
        builder.insert("Beta Pattern".to_string(), term("b2"))?;
        builder.build()
    }

    #[test]
    fn round_trip_preserves_options_and_behaviour()
    -> std::result::Result<(), Box<dyn std::error::Error>> {
        let matcher = sample_matcher()?;
        let envelope = PersistedMatcherEnvelope::from_matcher(&matcher);
        let json = envelope.to_json()?;

        let restored = PersistedMatcherEnvelope::from_json(&json)?.to_matcher()?;
        assert_eq!(matcher.options(), restored.options());
        assert_eq!(matcher.len(), restored.len());

        let text = "AlphaMatch then beta pattern then AlphaMatch";
        let before = matcher
            .find_matches(text, true)?
            .into_iter()
            .map(|m| (m.term, m.pos))
            .collect::<Vec<_>>();
        let after = restored
            .find_matches(text, true)?
            .into_iter()
            .map(|m| (m.term, m.pos))
            .collect::<Vec<_>>();
        let names = |ms: &[(String, Option<(usize, usize)>)]| ms.to_vec();
        let before = names(&before);
        let after = names(&after);
        assert_eq!(before, after);
        // Case-insensitivity stayed off: the lowercase 'beta pattern' does not
        // match, so only the two case-exact occurrences hit.
        assert_eq!(before.len(), 2);
        assert!(
            before
                .iter()
                .all(|(term, _)| term == "AlphaMatch" || term == "Beta Pattern")
        );
        Ok(())
    }

    #[test]
    fn envelope_carries_format_identity_and_version()
    -> std::result::Result<(), Box<dyn std::error::Error>> {
        let matcher = sample_matcher()?;
        let json = PersistedMatcherEnvelope::from_matcher(&matcher).to_json()?;
        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["format_id"], MATCHER_FORMAT_ID);
        assert_eq!(value["version"], "1");
        Ok(())
    }

    #[test]
    fn future_version_is_rejected_before_payload_use()
    -> std::result::Result<(), Box<dyn std::error::Error>> {
        let matcher = sample_matcher()?;
        let json = PersistedMatcherEnvelope::from_matcher(&matcher).to_json()?;
        let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
        value["version"] = serde_json::Value::String("99".to_string());
        let future = serde_json::to_string(&value).unwrap();

        let err = PersistedMatcherEnvelope::from_json(&future).unwrap_err();
        match err {
            TerraphimAutomataError::UnsupportedVersion {
                format_id,
                found,
                supported,
            } => {
                assert_eq!(format_id, MATCHER_FORMAT_ID);
                assert_eq!(found.as_deref(), Some("99"));
                assert_eq!(supported, vec![1]);
            }
            other => panic!("expected UnsupportedVersion, got: {other}"),
        }
        Ok(())
    }

    #[test]
    fn unknown_format_id_is_rejected() -> std::result::Result<(), Box<dyn std::error::Error>> {
        let matcher = sample_matcher()?;
        let json = PersistedMatcherEnvelope::from_matcher(&matcher).to_json()?;
        let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
        value["format_id"] = serde_json::Value::String("someone.else.format".to_string());
        let other = serde_json::to_string(&value).unwrap();

        let err = PersistedMatcherEnvelope::from_json(&other).unwrap_err();
        assert!(matches!(
            err,
            TerraphimAutomataError::UnsupportedFormat { .. }
        ));
        Ok(())
    }

    #[test]
    fn malformed_envelope_is_rejected_with_typed_error()
    -> std::result::Result<(), Box<dyn std::error::Error>> {
        let err = PersistedMatcherEnvelope::from_json("{ not json").unwrap_err();
        assert!(matches!(
            err,
            TerraphimAutomataError::MalformedPersisted { .. }
        ));

        // Missing version tag entirely.
        let matcher = sample_matcher()?;
        let json = PersistedMatcherEnvelope::from_matcher(&matcher).to_json()?;
        let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
        value.as_object_mut().unwrap().remove("version");
        let tagless = serde_json::to_string(&value).unwrap();
        let err = PersistedMatcherEnvelope::from_json(&tagless).unwrap_err();
        assert!(matches!(
            err,
            TerraphimAutomataError::UnsupportedVersion { .. }
                | TerraphimAutomataError::MalformedPersisted { .. }
        ));
        Ok(())
    }

    #[test]
    fn configuration_mismatch_is_reported_on_rebuild()
    -> std::result::Result<(), Box<dyn std::error::Error>> {
        // A persisted pattern shorter than the persisted minimum length can
        // never be rebuilt; the load must fail rather than silently adjust.
        let data = PersistedMatcherV1 {
            options: MatcherOptions {
                case_insensitive: true,
                min_pattern_length: 10,
            },
            patterns: vec![("short".to_string(), term("s"))],
        };
        let envelope = PersistedMatcherEnvelope {
            format_id: MATCHER_FORMAT_ID.to_string(),
            payload: PersistedMatcherPayload::V1(data),
        };
        let err = envelope.to_matcher().unwrap_err();
        assert!(matches!(err, TerraphimAutomataError::InvalidPattern { .. }));
        Ok(())
    }

    #[test]
    fn reader_and_writer_round_trip() -> std::result::Result<(), Box<dyn std::error::Error>> {
        let matcher = sample_matcher()?;
        let envelope = PersistedMatcherEnvelope::from_matcher(&matcher);
        let mut buf = Vec::new();
        envelope.to_writer(&mut buf).unwrap();
        let restored = PersistedMatcherEnvelope::from_reader(&buf[..]).unwrap();
        assert_eq!(envelope, restored);
        Ok(())
    }
}