// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use typed_builder::TypedBuilder;

/// Type of changes in this snapshot.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub enum CommitKind {
    /// Changes flushed from the mem table.
    APPEND,

    /// Changes by compacting existing data files.
    COMPACT,

    /// Changes that clear up the whole partition and then add new records.
    OVERWRITE,

    /// Collect statistics.
    ANALYZE,
}

impl std::fmt::Display for CommitKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::APPEND => write!(f, "APPEND"),
            Self::COMPACT => write!(f, "COMPACT"),
            Self::OVERWRITE => write!(f, "OVERWRITE"),
            Self::ANALYZE => write!(f, "ANALYZE"),
        }
    }
}

/// Snapshot for paimon.
///
/// Impl Reference: <https://github.com/apache/paimon/blob/release-0.8.2/paimon-core/src/main/java/org/apache/paimon/Snapshot.java#L68>.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, TypedBuilder)]
#[serde(rename_all = "camelCase")]
pub struct Snapshot {
    /// version of snapshot
    version: i32,
    id: i64,
    schema_id: i64,
    /// a manifest list recording all changes from the previous snapshots
    base_manifest_list: String,
    /// a manifest list recording all new changes occurred in this snapshot
    delta_manifest_list: String,
    /// a manifest list recording all changelog produced in this snapshot
    #[builder(default = None)]
    #[serde(skip_serializing_if = "Option::is_none")]
    changelog_manifest_list: Option<String>,
    /// byte size of changelog manifest list file
    #[builder(default = None)]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    changelog_manifest_list_size: Option<i64>,
    /// a manifest recording all index files of this table
    #[builder(default = None)]
    #[serde(skip_serializing_if = "Option::is_none")]
    index_manifest: Option<String>,
    /// user who committed this snapshot
    commit_user: String,
    /// Mainly for snapshot deduplication.
    ///
    /// If multiple snapshots have the same commitIdentifier, reading from any of these snapshots
    /// must produce the same table.
    ///
    /// If snapshot A has a smaller commitIdentifier than snapshot B, then snapshot A must be
    /// committed before snapshot B, and thus snapshot A must contain older records than snapshot B.
    commit_identifier: i64,
    commit_kind: CommitKind,
    /// timestamp of this snapshot
    time_millis: u64,
    /// log offsets of all changes occurred in this snapshot
    #[builder(default = None)]
    #[serde(skip_serializing_if = "Option::is_none")]
    log_offsets: Option<HashMap<i32, i64>>,
    /// record count of all changes occurred in this snapshot
    #[builder(default = None)]
    total_record_count: Option<i64>,
    /// record count of all new changes occurred in this snapshot
    #[builder(default = None)]
    delta_record_count: Option<i64>,
    /// record count of all changelog produced in this snapshot
    #[builder(default = None)]
    #[serde(skip_serializing_if = "Option::is_none")]
    changelog_record_count: Option<i64>,
    /// watermark for input records
    #[builder(default = None)]
    #[serde(skip_serializing_if = "Option::is_none")]
    watermark: Option<i64>,
    /// stats file name for statistics of this table
    #[builder(default = None)]
    #[serde(skip_serializing_if = "Option::is_none")]
    statistics: Option<String>,
    /// user-facing properties of this snapshot
    #[builder(default = None)]
    #[serde(skip_serializing_if = "Option::is_none")]
    properties: Option<HashMap<String, String>>,
    /// next row id for row tracking
    #[builder(default = None)]
    #[serde(skip_serializing_if = "Option::is_none")]
    next_row_id: Option<i64>,
}

impl Snapshot {
    /// Get the version of this snapshot.
    #[inline]
    pub fn version(&self) -> i32 {
        self.version
    }

    /// Get the id of this snapshot.
    #[inline]
    pub fn id(&self) -> i64 {
        self.id
    }

    /// Get the schema id of this snapshot.
    #[inline]
    pub fn schema_id(&self) -> i64 {
        self.schema_id
    }

    /// Get the base manifest list of this snapshot.
    #[inline]
    pub fn base_manifest_list(&self) -> &str {
        &self.base_manifest_list
    }

    /// Get the delta manifest list of this snapshot.
    #[inline]
    pub fn delta_manifest_list(&self) -> &str {
        &self.delta_manifest_list
    }

    /// Get the changelog manifest list of this snapshot.
    #[inline]
    pub fn changelog_manifest_list(&self) -> Option<&str> {
        self.changelog_manifest_list.as_deref()
    }

    /// Get the byte size of changelog manifest list of this snapshot.
    #[inline]
    pub fn changelog_manifest_list_size(&self) -> Option<i64> {
        self.changelog_manifest_list_size
    }

    /// Get the index manifest of this snapshot.
    #[inline]
    pub fn index_manifest(&self) -> Option<&str> {
        self.index_manifest.as_deref()
    }

    /// Get the commit user of this snapshot.
    #[inline]
    pub fn commit_user(&self) -> &str {
        &self.commit_user
    }

    /// Get the commit time of this snapshot.
    #[inline]
    pub fn time_millis(&self) -> u64 {
        self.time_millis
    }

    /// Get the commit identifier of this snapshot.
    #[inline]
    pub fn commit_identifier(&self) -> i64 {
        self.commit_identifier
    }

    /// Get the log offsets of this snapshot.
    #[inline]
    pub fn log_offsets(&self) -> Option<&HashMap<i32, i64>> {
        self.log_offsets.as_ref()
    }

    /// Get the total record count of this snapshot.
    #[inline]
    pub fn total_record_count(&self) -> Option<i64> {
        self.total_record_count
    }

    /// Get the delta record count of this snapshot.
    #[inline]
    pub fn delta_record_count(&self) -> Option<i64> {
        self.delta_record_count
    }

    /// Get the changelog record count of this snapshot.
    #[inline]
    pub fn changelog_record_count(&self) -> Option<i64> {
        self.changelog_record_count
    }

    /// Get the watermark of this snapshot.
    #[inline]
    pub fn watermark(&self) -> Option<i64> {
        self.watermark
    }

    /// Get the statistics of this snapshot.
    #[inline]
    pub fn statistics(&self) -> Option<&str> {
        self.statistics.as_deref()
    }

    /// Get the properties of this snapshot.
    #[inline]
    pub fn properties(&self) -> Option<&HashMap<String, String>> {
        self.properties.as_ref()
    }

    /// Get the next row id of this snapshot.
    #[inline]
    pub fn next_row_id(&self) -> Option<i64> {
        self.next_row_id
    }

    /// Get the commit kind of this snapshot.
    #[inline]
    pub fn commit_kind(&self) -> &CommitKind {
        &self.commit_kind
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;
    use serde_json;
    use std::env::current_dir;

    fn load_fixture(name: &str) -> String {
        let path = current_dir()
            .unwrap_or_else(|err| panic!("current_dir must exist: {err}"))
            .join(format!("tests/fixtures/snapshot/{name}.json"));
        let bytes = std::fs::read(&path)
            .unwrap_or_else(|err| panic!("fixtures {path:?} load failed: {err}"));
        String::from_utf8(bytes).expect("fixtures content must be valid utf8")
    }

    fn test_cases() -> Vec<(&'static str, Snapshot)> {
        vec![
            (
                "snapshot-v3",
                Snapshot::builder()
                    .version(3)
                    .id(2)
                    .schema_id(0)
                    .base_manifest_list(
                        "manifest-list-ea4b892d-edc8-4ee7-9eee-7068b83a947b-0".to_string(),
                    )
                    .delta_manifest_list(
                        "manifest-list-ea4b892d-edc8-4ee7-9eee-7068b83a947b-1".to_string(),
                    )
                    .commit_user("abbaac9e-4a17-43e3-b135-2269da263e3a".to_string())
                    .commit_identifier(9223372036854775807)
                    .changelog_manifest_list(Some(
                        "manifest-list-ea4b892d-edc8-4ee7-9eee-7068b83a947b-2".to_string(),
                    ))
                    .commit_kind(CommitKind::APPEND)
                    .time_millis(1724509030368)
                    .log_offsets(Some(HashMap::default()))
                    .total_record_count(Some(4))
                    .delta_record_count(Some(2))
                    .changelog_record_count(Some(2))
                    .statistics(Some("statistics_string".to_string()))
                    .properties(Some(HashMap::from([(
                        "scan.snapshot-id".to_string(),
                        "2".to_string(),
                    )])))
                    .build(),
            ),
            (
                "snapshot-v3-none-field",
                Snapshot::builder()
                    .version(3)
                    .id(2)
                    .schema_id(0)
                    .base_manifest_list(
                        "manifest-list-ea4b892d-edc8-4ee7-9eee-7068b83a947b-0".to_string(),
                    )
                    .delta_manifest_list(
                        "manifest-list-ea4b892d-edc8-4ee7-9eee-7068b83a947b-1".to_string(),
                    )
                    .commit_user("abbaac9e-4a17-43e3-b135-2269da263e3a".to_string())
                    .commit_identifier(9223372036854775807)
                    .changelog_manifest_list(None)
                    .commit_kind(CommitKind::APPEND)
                    .time_millis(1724509030368)
                    .log_offsets(Some(HashMap::default()))
                    .total_record_count(Some(4))
                    .delta_record_count(Some(2))
                    .changelog_record_count(Some(2))
                    .build(),
            ),
        ]
    }

    #[test]
    fn test_snapshot_serialization_deserialization() {
        for (name, expect) in test_cases() {
            let content = load_fixture(name);
            let snapshot: Snapshot =
                serde_json::from_str(content.as_str()).expect("Failed to deserialize Snapshot");
            assert_eq!(snapshot, expect);
            let serialized =
                serde_json::to_string(&snapshot).expect("Failed to serialize Snapshot");

            let deserialized: Snapshot = serde_json::from_str(&serialized)
                .expect("Failed to deserialize serialized Snapshot");

            assert_eq!(snapshot, deserialized);
        }
    }
}
