vstorage 0.8.1

Common API for various icalendar/vcard storages.
Documentation
// Copyright 2023-2026 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: EUPL-1.2

use std::sync::Arc;

use crate::{
    property::Property,
    sync::{
        items::SideState, mapping::ResolvedMapping, mode::Mode, operation::StatusAction,
        status::StatusVersions,
    },
};

use super::{
    operation::{
        DeleteItem, ItemOp, MappingUidSource, PropertyOp, PropertyOpKind, StatusOnly, StatusWrite,
        StorageWrite, WriteItem, resource_name,
    },
    ordering::{DeletionBarrier, DeletionCompletionHandle},
    status::{ItemPairStatus, MappingUid, Side},
};

/// One-way synchronisation mode (A -> B).
///
/// Never conflicts occur as A always wins.
pub struct OneWaySync;

impl Mode for OneWaySync {
    fn decide_item_action(
        &self,
        side_a: Option<SideState>,
        side_b: Option<SideState>,
        previous: Option<ItemPairStatus>,
        mapping: &Arc<ResolvedMapping>,
        mapping_uid_source: MappingUidSource,
        on_complete: Option<&DeletionBarrier>,
    ) -> Option<ItemOp> {
        let on_complete_handle = on_complete.map(DeletionBarrier::completion_handle);

        match previous {
            None => Some(Self::plan_new_item(
                side_a,
                side_b,
                mapping,
                mapping_uid_source,
                on_complete_handle,
            )),
            Some(status) => {
                Self::plan_existing_item(side_a, side_b, status, mapping, on_complete_handle)
            }
        }
    }

    fn decide_property_action(
        &self,
        property: Property,
        value_a: Option<String>,
        value_b: Option<String>,
        previous: Option<String>,
        mapping: &Arc<ResolvedMapping>,
        mapping_uid: MappingUid,
        on_complete: Option<&DeletionBarrier>,
    ) -> Option<PropertyOp> {
        let on_complete = on_complete.map(DeletionBarrier::completion_handle);
        let mapping = mapping.clone();
        let mapping_uid = MappingUidSource::Immediate(mapping_uid);

        let kind = match (value_a, value_b, previous) {
            (None, None, None) => unreachable!("property must exist somewhere"),
            (None, None, Some(_)) => PropertyOpKind::ClearStatus,
            (Some(value_a), None, _) => PropertyOpKind::Write {
                value: value_a,
                side: Side::B,
            },
            (None, Some(_), _) => PropertyOpKind::Delete { side: Side::B },
            (Some(value_a), Some(value_b), previous) => {
                if value_a == value_b {
                    return match previous {
                        Some(prev) if prev == value_a => None, // No change needed
                        _ => Some(PropertyOp {
                            property,
                            mapping,
                            mapping_uid,
                            on_complete,
                            kind: PropertyOpKind::UpdateStatus { value: value_a },
                        }),
                    };
                }
                PropertyOpKind::Write {
                    value: value_a,
                    side: Side::B,
                }
            }
        };

        Some(PropertyOp {
            property,
            mapping,
            mapping_uid,
            on_complete,
            kind,
        })
    }
}

impl OneWaySync {
    /// Plan operations for a new item (no previous status entry).
    fn plan_new_item(
        side_a: Option<SideState>,
        side_b: Option<SideState>,
        mapping: &Arc<ResolvedMapping>,
        mapping_uid_source: MappingUidSource,
        on_complete: Option<DeletionCompletionHandle>,
    ) -> ItemOp {
        match (side_a, side_b) {
            (None, None) => unreachable!("item does not exist anywhere"),
            (Some(a), None) => {
                let name = resource_name(&a.state().version.href);
                ItemOp::Write(WriteItem {
                    source: a.into(),
                    target_side: Side::B,
                    storage_write: StorageWrite::Create {
                        collection: mapping.b().href().clone(),
                        resource_name: Some(name),
                    },
                    status_write: StatusWrite::Insert,
                    mapping_uid: mapping_uid_source,
                    on_complete,
                })
            }
            (None, Some(b)) => ItemOp::Delete(DeleteItem {
                uid: b.state().uid.clone(),
                target: b.into(),
                side: Side::B,
                mapping_uid: mapping_uid_source,
                on_complete,
            }),
            (Some(a), Some(b)) => {
                if a.state().hash == b.state().hash {
                    ItemOp::StatusOnly(StatusOnly {
                        action: StatusAction::Insert {
                            uid: a.state().uid.clone(),
                            hash: a.state().hash.clone(),
                            versions: StatusVersions {
                                a: a.into(),
                                b: b.into(),
                            },
                        },
                        mapping_uid: mapping_uid_source,
                        on_complete,
                    })
                } else {
                    ItemOp::Write(WriteItem {
                        source: a.into(),
                        target_side: Side::B,
                        storage_write: StorageWrite::Update { target: b.into() },
                        status_write: StatusWrite::Insert,
                        mapping_uid: mapping_uid_source,
                        on_complete,
                    })
                }
            }
        }
    }

    /// Plan operations for an existing item (has previous status entry).
    /// One-way sync: A always wins.
    fn plan_existing_item(
        side_a: Option<SideState>,
        side_b: Option<SideState>,
        status: ItemPairStatus,
        mapping: &Arc<ResolvedMapping>,
        on_complete: Option<DeletionCompletionHandle>,
    ) -> Option<ItemOp> {
        let mapping_uid = MappingUidSource::Immediate(status.mapping_uid);

        match (side_a, side_b) {
            (None, None) => Some(ItemOp::StatusOnly(StatusOnly {
                action: StatusAction::Clear { uid: status.uid },
                mapping_uid,
                on_complete,
            })),
            (Some(a), None) => {
                let name = resource_name(&a.state().version.href);
                Some(ItemOp::Write(WriteItem {
                    source: a.into(),
                    target_side: Side::B,
                    storage_write: StorageWrite::Create {
                        collection: mapping.b().href().clone(),
                        resource_name: Some(name),
                    },
                    status_write: StatusWrite::Update { old: status.into() },
                    mapping_uid,
                    on_complete,
                }))
            }
            (None, Some(b)) => Some(ItemOp::Delete(DeleteItem {
                uid: b.state().uid.clone(),
                target: b.into(),
                side: Side::B,
                mapping_uid,
                on_complete,
            })),
            (Some(a), Some(b)) => {
                Self::plan_existing_on_both(a, b, status, mapping_uid, on_complete)
            }
        }
    }

    /// Plan for item existing on both sides with status.
    fn plan_existing_on_both(
        a: SideState,
        b: SideState,
        status: ItemPairStatus,
        mapping_uid: MappingUidSource,
        on_complete: Option<DeletionCompletionHandle>,
    ) -> Option<ItemOp> {
        match (&a, b) {
            // A unchanged, B unchanged: no-op if B version matches status
            (SideState::Unchanged { .. }, SideState::Unchanged { state: b_state }) => {
                if b_state.version == status.b {
                    None
                } else {
                    let old: StatusVersions = status.into();
                    let target = old.b.clone();
                    Some(ItemOp::Write(WriteItem {
                        source: a.into(),
                        target_side: Side::B,
                        storage_write: StorageWrite::Update { target },
                        status_write: StatusWrite::Update { old },
                        mapping_uid,
                        on_complete,
                    }))
                }
            }
            // A changed, same hash as B: status-only update
            (
                SideState::Changed { state: a_state, .. },
                SideState::Changed { state: b_state, .. } | SideState::Unchanged { state: b_state },
            ) if a_state.hash == b_state.hash => Some(ItemOp::StatusOnly(StatusOnly {
                action: StatusAction::Update {
                    hash: a_state.hash.clone(),
                    old: status.into(),
                    new: StatusVersions {
                        a: a_state.version.clone(),
                        b: b_state.version,
                    },
                },
                mapping_uid,
                on_complete,
            })),
            // B changed: overwrite B with A
            (
                SideState::Unchanged { .. } | SideState::Changed { .. },
                SideState::Changed { state: b_state, .. },
            ) => Some(ItemOp::Write(WriteItem {
                source: a.into(),
                target_side: Side::B,
                storage_write: StorageWrite::Update {
                    target: b_state.version,
                },
                status_write: StatusWrite::Update { old: status.into() },
                mapping_uid,
                on_complete,
            })),
            // A changed, B unchanged (different hash)
            (SideState::Changed { .. }, SideState::Unchanged { .. }) => {
                let old: StatusVersions = status.into();
                let target = old.b.clone();
                Some(ItemOp::Write(WriteItem {
                    source: a.into(),
                    target_side: Side::B,
                    storage_write: StorageWrite::Update { target },
                    status_write: StatusWrite::Update { old },
                    mapping_uid,
                    on_complete,
                }))
            }
        }
    }
}