// @generated by `cargo run --manifest-path rust/Cargo.toml -p syncular-codegen --`
// Source: migrations/*.sql

use serde_json::{json, Map, Value};
pub use syncular_runtime::app_schema::{
    AppTableMetadata, ColumnMetadata, CrdtYjsFieldMetadata, EncryptedFieldMetadata, ScopeMetadata,
    ScopeSource,
};
#[allow(unused_imports)]
use syncular_runtime::client::{
    SubscriptionSpec, SyncChangedRow, SyncularClientConfig, SyncularCommandHistoryExecutor,
    SyncularEncryptedCrdtMutationExecutor, SyncularLeasedMutationExecutor,
    SyncularMutationExecutor,
};
use syncular_runtime::command_history::{
    CommandHistoryEntry, CommandHistoryReceipt, CommandHistoryRecord, CommandHistoryState,
};
#[allow(unused_imports)]
use syncular_runtime::crdt_yjs::{YjsUpdateEnvelope, YJS_PAYLOAD_KEY};
use syncular_runtime::encryption::FieldEncryptionRule;
use syncular_runtime::error::{Result, SyncularError};
use syncular_runtime::protocol::{
    random_syncular_id, IntoSyncularMutation, MutationCommit, MutationReceipt,
    PendingSyncularMutation, SyncOperation, SyncularMutationBatch, SyncularMutationKind,
};

pub const APP_TABLES: &[&str] = &["comments", "projects", "tasks"];

pub const COMMENTS_TABLE: &str = "comments";
pub const PROJECTS_TABLE: &str = "projects";
pub const TASKS_TABLE: &str = "tasks";

pub const COMMENTS_COLUMNS: &[ColumnMetadata] = &[
    ColumnMetadata {
        name: "id",
        type_family: "text",
        notnull_required: false,
        primary_key: true,
    },
    ColumnMetadata {
        name: "task_id",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "project_id",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "body",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "author_id",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "deleted",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "server_version",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
];

pub const COMMENTS_BLOB_COLUMNS: &[&str] = &[];

pub const COMMENTS_CRDT_YJS_FIELDS: &[CrdtYjsFieldMetadata] = &[];

pub const COMMENTS_ENCRYPTED_FIELDS: &[EncryptedFieldMetadata] = &[];

pub const COMMENTS_SCOPES: &[ScopeMetadata] = &[
    ScopeMetadata {
        name: "user_id",
        column: "author_id",
        source: ScopeSource::ActorId,
        required: true,
    },
    ScopeMetadata {
        name: "project_id",
        column: "project_id",
        source: ScopeSource::ProjectId,
        required: false,
    },
];

pub const COMMENTS_METADATA: AppTableMetadata = AppTableMetadata {
    name: "comments",
    primary_key_column: "id",
    server_version_column: "server_version",
    soft_delete_column: Some("deleted"),
    subscription_id: "sub-comments",
    columns: COMMENTS_COLUMNS,
    blob_columns: COMMENTS_BLOB_COLUMNS,
    crdt_yjs_fields: COMMENTS_CRDT_YJS_FIELDS,
    encrypted_fields: COMMENTS_ENCRYPTED_FIELDS,
    scopes: COMMENTS_SCOPES,
};

pub const PROJECTS_COLUMNS: &[ColumnMetadata] = &[
    ColumnMetadata {
        name: "id",
        type_family: "text",
        notnull_required: false,
        primary_key: true,
    },
    ColumnMetadata {
        name: "name",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "owner_id",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "archived",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "server_version",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
];

pub const PROJECTS_BLOB_COLUMNS: &[&str] = &[];

pub const PROJECTS_CRDT_YJS_FIELDS: &[CrdtYjsFieldMetadata] = &[];

pub const PROJECTS_ENCRYPTED_FIELDS: &[EncryptedFieldMetadata] = &[];

pub const PROJECTS_SCOPES: &[ScopeMetadata] = &[ScopeMetadata {
    name: "user_id",
    column: "owner_id",
    source: ScopeSource::ActorId,
    required: true,
}];

pub const PROJECTS_METADATA: AppTableMetadata = AppTableMetadata {
    name: "projects",
    primary_key_column: "id",
    server_version_column: "server_version",
    soft_delete_column: None,
    subscription_id: "sub-projects",
    columns: PROJECTS_COLUMNS,
    blob_columns: PROJECTS_BLOB_COLUMNS,
    crdt_yjs_fields: PROJECTS_CRDT_YJS_FIELDS,
    encrypted_fields: PROJECTS_ENCRYPTED_FIELDS,
    scopes: PROJECTS_SCOPES,
};

pub const TASKS_COLUMNS: &[ColumnMetadata] = &[
    ColumnMetadata {
        name: "id",
        type_family: "text",
        notnull_required: false,
        primary_key: true,
    },
    ColumnMetadata {
        name: "title",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "completed",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "user_id",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "project_id",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "server_version",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "image",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "title_yjs_state",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "description",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
];

pub const TASKS_BLOB_COLUMNS: &[&str] = &["image"];

pub const TASKS_CRDT_YJS_FIELDS: &[CrdtYjsFieldMetadata] = &[CrdtYjsFieldMetadata {
    field: "title",
    state_column: "title_yjs_state",
    container_key: "title",
    row_id_field: "id",
    kind: "text",
    sync_mode: "server-merge",
}];

pub const TASKS_ENCRYPTED_FIELDS: &[EncryptedFieldMetadata] = &[EncryptedFieldMetadata {
    field: "description",
    scope: "tasks",
    row_id_field: "id",
}];

pub const TASKS_SCOPES: &[ScopeMetadata] = &[
    ScopeMetadata {
        name: "user_id",
        column: "user_id",
        source: ScopeSource::ActorId,
        required: true,
    },
    ScopeMetadata {
        name: "project_id",
        column: "project_id",
        source: ScopeSource::ProjectId,
        required: false,
    },
];

pub const TASKS_METADATA: AppTableMetadata = AppTableMetadata {
    name: "tasks",
    primary_key_column: "id",
    server_version_column: "server_version",
    soft_delete_column: None,
    subscription_id: "sub-tasks",
    columns: TASKS_COLUMNS,
    blob_columns: TASKS_BLOB_COLUMNS,
    crdt_yjs_fields: TASKS_CRDT_YJS_FIELDS,
    encrypted_fields: TASKS_ENCRYPTED_FIELDS,
    scopes: TASKS_SCOPES,
};

pub const APP_TABLE_METADATA: &[AppTableMetadata] =
    &[COMMENTS_METADATA, PROJECTS_METADATA, TASKS_METADATA];

pub fn table_metadata(table: &str) -> Option<&'static AppTableMetadata> {
    APP_TABLE_METADATA
        .iter()
        .find(|metadata| metadata.name == table)
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CommentChangedFields {
    pub id: bool,
    pub task_id: bool,
    pub project_id: bool,
    pub body: bool,
    pub author_id: bool,
    pub deleted: bool,
    pub server_version: bool,
}

impl CommentChangedFields {
    pub fn from_columns(columns: &[String]) -> Self {
        Self {
            id: columns.iter().any(|column| column == "id"),
            task_id: columns.iter().any(|column| column == "task_id"),
            project_id: columns.iter().any(|column| column == "project_id"),
            body: columns.iter().any(|column| column == "body"),
            author_id: columns.iter().any(|column| column == "author_id"),
            deleted: columns.iter().any(|column| column == "deleted"),
            server_version: columns.iter().any(|column| column == "server_version"),
        }
    }

    pub fn contains(&self, column: &str) -> bool {
        match column {
            "id" => self.id,
            "task_id" => self.task_id,
            "project_id" => self.project_id,
            "body" => self.body,
            "author_id" => self.author_id,
            "deleted" => self.deleted,
            "server_version" => self.server_version,
            _ => false,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct CommentChangedRow<'a> {
    pub raw: &'a SyncChangedRow,
    pub changed: CommentChangedFields,
    pub crdt: CommentChangedFields,
}

impl<'a> CommentChangedRow<'a> {
    pub fn from_raw(row: &'a SyncChangedRow) -> Option<Self> {
        if row.table != "comments" {
            return None;
        }
        Some(Self {
            raw: row,
            changed: CommentChangedFields::from_columns(&row.changed_fields),
            crdt: CommentChangedFields::from_columns(&row.crdt_fields),
        })
    }

    pub fn row_id(&self) -> Option<&str> {
        self.raw.row_id.as_deref()
    }

    pub fn is_insert(&self) -> bool {
        self.raw.operation == "insert"
    }

    pub fn is_update(&self) -> bool {
        self.raw.operation == "update"
    }

    pub fn is_delete(&self) -> bool {
        self.raw.operation == "delete"
    }
}

pub fn comment_changed_rows<'a>(
    rows: impl IntoIterator<Item = &'a SyncChangedRow>,
) -> Vec<CommentChangedRow<'a>> {
    rows.into_iter()
        .filter_map(CommentChangedRow::from_raw)
        .collect()
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ProjectChangedFields {
    pub id: bool,
    pub name: bool,
    pub owner_id: bool,
    pub archived: bool,
    pub server_version: bool,
}

impl ProjectChangedFields {
    pub fn from_columns(columns: &[String]) -> Self {
        Self {
            id: columns.iter().any(|column| column == "id"),
            name: columns.iter().any(|column| column == "name"),
            owner_id: columns.iter().any(|column| column == "owner_id"),
            archived: columns.iter().any(|column| column == "archived"),
            server_version: columns.iter().any(|column| column == "server_version"),
        }
    }

    pub fn contains(&self, column: &str) -> bool {
        match column {
            "id" => self.id,
            "name" => self.name,
            "owner_id" => self.owner_id,
            "archived" => self.archived,
            "server_version" => self.server_version,
            _ => false,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct ProjectChangedRow<'a> {
    pub raw: &'a SyncChangedRow,
    pub changed: ProjectChangedFields,
    pub crdt: ProjectChangedFields,
}

impl<'a> ProjectChangedRow<'a> {
    pub fn from_raw(row: &'a SyncChangedRow) -> Option<Self> {
        if row.table != "projects" {
            return None;
        }
        Some(Self {
            raw: row,
            changed: ProjectChangedFields::from_columns(&row.changed_fields),
            crdt: ProjectChangedFields::from_columns(&row.crdt_fields),
        })
    }

    pub fn row_id(&self) -> Option<&str> {
        self.raw.row_id.as_deref()
    }

    pub fn is_insert(&self) -> bool {
        self.raw.operation == "insert"
    }

    pub fn is_update(&self) -> bool {
        self.raw.operation == "update"
    }

    pub fn is_delete(&self) -> bool {
        self.raw.operation == "delete"
    }
}

pub fn project_changed_rows<'a>(
    rows: impl IntoIterator<Item = &'a SyncChangedRow>,
) -> Vec<ProjectChangedRow<'a>> {
    rows.into_iter()
        .filter_map(ProjectChangedRow::from_raw)
        .collect()
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TaskChangedFields {
    pub id: bool,
    pub title: bool,
    pub completed: bool,
    pub user_id: bool,
    pub project_id: bool,
    pub server_version: bool,
    pub image: bool,
    pub title_yjs_state: bool,
    pub description: bool,
}

impl TaskChangedFields {
    pub fn from_columns(columns: &[String]) -> Self {
        Self {
            id: columns.iter().any(|column| column == "id"),
            title: columns.iter().any(|column| column == "title"),
            completed: columns.iter().any(|column| column == "completed"),
            user_id: columns.iter().any(|column| column == "user_id"),
            project_id: columns.iter().any(|column| column == "project_id"),
            server_version: columns.iter().any(|column| column == "server_version"),
            image: columns.iter().any(|column| column == "image"),
            title_yjs_state: columns.iter().any(|column| column == "title_yjs_state"),
            description: columns.iter().any(|column| column == "description"),
        }
    }

    pub fn contains(&self, column: &str) -> bool {
        match column {
            "id" => self.id,
            "title" => self.title,
            "completed" => self.completed,
            "user_id" => self.user_id,
            "project_id" => self.project_id,
            "server_version" => self.server_version,
            "image" => self.image,
            "title_yjs_state" => self.title_yjs_state,
            "description" => self.description,
            _ => false,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct TaskChangedRow<'a> {
    pub raw: &'a SyncChangedRow,
    pub changed: TaskChangedFields,
    pub crdt: TaskChangedFields,
}

impl<'a> TaskChangedRow<'a> {
    pub fn from_raw(row: &'a SyncChangedRow) -> Option<Self> {
        if row.table != "tasks" {
            return None;
        }
        Some(Self {
            raw: row,
            changed: TaskChangedFields::from_columns(&row.changed_fields),
            crdt: TaskChangedFields::from_columns(&row.crdt_fields),
        })
    }

    pub fn row_id(&self) -> Option<&str> {
        self.raw.row_id.as_deref()
    }

    pub fn is_insert(&self) -> bool {
        self.raw.operation == "insert"
    }

    pub fn is_update(&self) -> bool {
        self.raw.operation == "update"
    }

    pub fn is_delete(&self) -> bool {
        self.raw.operation == "delete"
    }
}

pub fn task_changed_rows<'a>(
    rows: impl IntoIterator<Item = &'a SyncChangedRow>,
) -> Vec<TaskChangedRow<'a>> {
    rows.into_iter()
        .filter_map(TaskChangedRow::from_raw)
        .collect()
}

pub fn generated_field_encryption_rules() -> Vec<FieldEncryptionRule> {
    vec![FieldEncryptionRule {
        scope: "tasks".to_string(),
        table: Some("tasks".to_string()),
        fields: vec!["description".to_string()],
        row_id_field: Some("id".to_string()),
    }]
}

pub fn default_subscriptions(config: &SyncularClientConfig) -> Vec<SubscriptionSpec> {
    vec![
        comments_subscription(&config.actor_id, config.project_id.as_deref()),
        projects_subscription(&config.actor_id),
        tasks_subscription(&config.actor_id, config.project_id.as_deref()),
    ]
}

pub fn default_subscriptions_with_bootstrap_phases(
    config: &SyncularClientConfig,
    bootstrap_phases: &[(&str, i64)],
) -> Vec<SubscriptionSpec> {
    let mut subscriptions = default_subscriptions(config);
    apply_bootstrap_phases(&mut subscriptions, bootstrap_phases);
    subscriptions
}

pub fn apply_bootstrap_phases(
    subscriptions: &mut [SubscriptionSpec],
    bootstrap_phases: &[(&str, i64)],
) {
    for subscription in subscriptions {
        if let Some((_, phase)) = bootstrap_phases
            .iter()
            .find(|(key, _)| *key == subscription.id || *key == subscription.table)
        {
            subscription.bootstrap_phase = *phase;
        }
    }
}

pub fn with_bootstrap_phase(
    mut subscription: SubscriptionSpec,
    bootstrap_phase: i64,
) -> SubscriptionSpec {
    subscription.bootstrap_phase = bootstrap_phase;
    subscription
}

pub fn comments_subscription(user_id: &str, project_id: Option<&str>) -> SubscriptionSpec {
    let mut scopes = Map::new();
    scopes.insert("user_id".to_string(), json!(user_id));
    if let Some(project_id) = project_id {
        scopes.insert("project_id".to_string(), json!(project_id));
    }
    let params = Map::new();

    SubscriptionSpec {
        id: "sub-comments".to_string(),
        table: "comments".to_string(),
        scopes,
        params,
        bootstrap_phase: 0,
    }
}

pub fn projects_subscription(user_id: &str) -> SubscriptionSpec {
    let mut scopes = Map::new();
    scopes.insert("user_id".to_string(), json!(user_id));
    let params = Map::new();

    SubscriptionSpec {
        id: "sub-projects".to_string(),
        table: "projects".to_string(),
        scopes,
        params,
        bootstrap_phase: 0,
    }
}

pub fn tasks_subscription(user_id: &str, project_id: Option<&str>) -> SubscriptionSpec {
    let mut scopes = Map::new();
    scopes.insert("user_id".to_string(), json!(user_id));
    if let Some(project_id) = project_id {
        scopes.insert("project_id".to_string(), json!(project_id));
    }
    let params = Map::new();

    SubscriptionSpec {
        id: "sub-tasks".to_string(),
        table: "tasks".to_string(),
        scopes,
        params,
        bootstrap_phase: 0,
    }
}

#[derive(Debug, Clone)]
pub struct NewComment {
    pub id: String,
    pub task_id: String,
    pub project_id: Option<String>,
    pub body: String,
    pub author_id: String,
    pub deleted: i32,
}

impl NewComment {
    pub fn new(
        id: &str,
        task_id: &str,
        project_id: Option<&str>,
        body: &str,
        author_id: &str,
    ) -> Self {
        Self {
            id: id.to_string(),
            task_id: task_id.to_string(),
            project_id: project_id.map(str::to_string),
            body: body.to_string(),
            author_id: author_id.to_string(),
            deleted: 0,
        }
    }

    pub fn with_generated_id(
        task_id: &str,
        project_id: Option<&str>,
        body: &str,
        author_id: &str,
    ) -> Self {
        Self {
            id: random_syncular_id(),
            task_id: task_id.to_string(),
            project_id: project_id.map(str::to_string),
            body: body.to_string(),
            author_id: author_id.to_string(),
            deleted: 0,
        }
    }

    pub fn row_json(&self) -> Value {
        let mut row = Map::new();
        row.insert("id".to_string(), json!(&self.id));
        row.insert("task_id".to_string(), json!(&self.task_id));
        if let Some(value) = &self.project_id {
            row.insert("project_id".to_string(), json!(value));
        }
        row.insert("body".to_string(), json!(&self.body));
        row.insert("author_id".to_string(), json!(&self.author_id));
        row.insert("deleted".to_string(), json!(&self.deleted));
        Value::Object(row)
    }

    pub fn sync_operation(&self) -> SyncOperation {
        let mut payload = Map::new();
        payload.insert("task_id".to_string(), json!(&self.task_id));
        if let Some(value) = &self.project_id {
            payload.insert("project_id".to_string(), json!(value));
        }
        payload.insert("body".to_string(), json!(&self.body));
        payload.insert("author_id".to_string(), json!(&self.author_id));
        payload.insert("deleted".to_string(), json!(&self.deleted));

        SyncOperation {
            table: "comments".to_string(),
            row_id: self.id.clone(),
            op: "upsert".to_string(),
            payload: Some(Value::Object(payload)),
            base_version: Some(0),
        }
    }
}

impl IntoSyncularMutation for NewComment {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        let row_id = self.id.clone();
        PendingSyncularMutation {
            kind: SyncularMutationKind::Insert,
            table: "comments".to_string(),
            row_id,
            payload: self.sync_operation().payload,
            base_version: None,
            local_row: Some(self.row_json()),
        }
    }
}

#[derive(Debug, Clone)]
pub struct CommentPatch {
    row_id: String,
    base_version: Option<i64>,
    task_id: Option<String>,
    project_id: Option<Option<String>>,
    body: Option<String>,
    author_id: Option<String>,
    deleted: Option<i32>,
}

impl CommentPatch {
    pub fn new(row_id: &str) -> Self {
        Self {
            row_id: row_id.to_string(),
            base_version: None,
            task_id: None,
            project_id: None,
            body: None,
            author_id: None,
            deleted: None,
        }
    }

    pub fn base_version(mut self, base_version: i64) -> Self {
        self.base_version = Some(base_version);
        self
    }

    pub fn task_id(mut self, task_id: &str) -> Self {
        self.task_id = Some(task_id.to_string());
        self
    }

    pub fn project_id(mut self, project_id: Option<&str>) -> Self {
        self.project_id = Some(project_id.map(str::to_string));
        self
    }

    pub fn body(mut self, body: &str) -> Self {
        self.body = Some(body.to_string());
        self
    }

    pub fn author_id(mut self, author_id: &str) -> Self {
        self.author_id = Some(author_id.to_string());
        self
    }

    pub fn deleted(mut self, deleted: i32) -> Self {
        self.deleted = Some(deleted);
        self
    }

    pub fn payload_json(&self) -> Value {
        let mut payload = Map::new();
        if let Some(value) = &self.task_id {
            payload.insert("task_id".to_string(), json!(value));
        }
        if let Some(value) = &self.project_id {
            payload.insert("project_id".to_string(), json!(value));
        }
        if let Some(value) = &self.body {
            payload.insert("body".to_string(), json!(value));
        }
        if let Some(value) = &self.author_id {
            payload.insert("author_id".to_string(), json!(value));
        }
        if let Some(value) = &self.deleted {
            payload.insert("deleted".to_string(), json!(value));
        }
        Value::Object(payload)
    }

    pub fn sync_operation(&self) -> SyncOperation {
        SyncOperation {
            table: "comments".to_string(),
            row_id: self.row_id.clone(),
            op: "upsert".to_string(),
            payload: Some(self.payload_json()),
            base_version: self.base_version,
        }
    }
}

impl IntoSyncularMutation for CommentPatch {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        let payload = self.payload_json();
        PendingSyncularMutation {
            kind: SyncularMutationKind::Update,
            table: "comments".to_string(),
            row_id: self.row_id,
            payload: Some(payload),
            base_version: self.base_version,
            local_row: None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct DeleteComment {
    row_id: String,
    base_version: Option<i64>,
}

impl DeleteComment {
    pub fn new(row_id: &str) -> Self {
        Self {
            row_id: row_id.to_string(),
            base_version: None,
        }
    }

    pub fn base_version(mut self, base_version: i64) -> Self {
        self.base_version = Some(base_version);
        self
    }

    pub fn sync_operation(&self) -> SyncOperation {
        SyncOperation {
            table: "comments".to_string(),
            row_id: self.row_id.clone(),
            op: "upsert".to_string(),
            payload: Some(json!({ "deleted": 1 })),
            base_version: self.base_version,
        }
    }
}

impl IntoSyncularMutation for DeleteComment {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        PendingSyncularMutation {
            kind: SyncularMutationKind::Update,
            table: "comments".to_string(),
            row_id: self.row_id,
            payload: Some(json!({ "deleted": 1 })),
            base_version: self.base_version,
            local_row: None,
        }
    }
}

pub fn delete_comment(row_id: &str, base_version: Option<i64>) -> SyncOperation {
    SyncOperation {
        table: "comments".to_string(),
        row_id: row_id.to_string(),
        op: "upsert".to_string(),
        payload: Some(json!({ "deleted": 1 })),
        base_version,
    }
}

#[derive(Debug, Clone)]
pub struct NewProject {
    pub id: String,
    pub name: String,
    pub owner_id: String,
    pub archived: i32,
}

impl NewProject {
    pub fn new(id: &str, name: &str, owner_id: &str) -> Self {
        Self {
            id: id.to_string(),
            name: name.to_string(),
            owner_id: owner_id.to_string(),
            archived: 0,
        }
    }

    pub fn with_generated_id(name: &str, owner_id: &str) -> Self {
        Self {
            id: random_syncular_id(),
            name: name.to_string(),
            owner_id: owner_id.to_string(),
            archived: 0,
        }
    }

    pub fn row_json(&self) -> Value {
        let mut row = Map::new();
        row.insert("id".to_string(), json!(&self.id));
        row.insert("name".to_string(), json!(&self.name));
        row.insert("owner_id".to_string(), json!(&self.owner_id));
        row.insert("archived".to_string(), json!(&self.archived));
        Value::Object(row)
    }

    pub fn sync_operation(&self) -> SyncOperation {
        let mut payload = Map::new();
        payload.insert("name".to_string(), json!(&self.name));
        payload.insert("owner_id".to_string(), json!(&self.owner_id));
        payload.insert("archived".to_string(), json!(&self.archived));

        SyncOperation {
            table: "projects".to_string(),
            row_id: self.id.clone(),
            op: "upsert".to_string(),
            payload: Some(Value::Object(payload)),
            base_version: Some(0),
        }
    }
}

impl IntoSyncularMutation for NewProject {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        let row_id = self.id.clone();
        PendingSyncularMutation {
            kind: SyncularMutationKind::Insert,
            table: "projects".to_string(),
            row_id,
            payload: self.sync_operation().payload,
            base_version: None,
            local_row: Some(self.row_json()),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ProjectPatch {
    row_id: String,
    base_version: Option<i64>,
    name: Option<String>,
    owner_id: Option<String>,
    archived: Option<i32>,
}

impl ProjectPatch {
    pub fn new(row_id: &str) -> Self {
        Self {
            row_id: row_id.to_string(),
            base_version: None,
            name: None,
            owner_id: None,
            archived: None,
        }
    }

    pub fn base_version(mut self, base_version: i64) -> Self {
        self.base_version = Some(base_version);
        self
    }

    pub fn name(mut self, name: &str) -> Self {
        self.name = Some(name.to_string());
        self
    }

    pub fn owner_id(mut self, owner_id: &str) -> Self {
        self.owner_id = Some(owner_id.to_string());
        self
    }

    pub fn archived(mut self, archived: i32) -> Self {
        self.archived = Some(archived);
        self
    }

    pub fn payload_json(&self) -> Value {
        let mut payload = Map::new();
        if let Some(value) = &self.name {
            payload.insert("name".to_string(), json!(value));
        }
        if let Some(value) = &self.owner_id {
            payload.insert("owner_id".to_string(), json!(value));
        }
        if let Some(value) = &self.archived {
            payload.insert("archived".to_string(), json!(value));
        }
        Value::Object(payload)
    }

    pub fn sync_operation(&self) -> SyncOperation {
        SyncOperation {
            table: "projects".to_string(),
            row_id: self.row_id.clone(),
            op: "upsert".to_string(),
            payload: Some(self.payload_json()),
            base_version: self.base_version,
        }
    }
}

impl IntoSyncularMutation for ProjectPatch {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        let payload = self.payload_json();
        PendingSyncularMutation {
            kind: SyncularMutationKind::Update,
            table: "projects".to_string(),
            row_id: self.row_id,
            payload: Some(payload),
            base_version: self.base_version,
            local_row: None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct DeleteProject {
    row_id: String,
    base_version: Option<i64>,
}

impl DeleteProject {
    pub fn new(row_id: &str) -> Self {
        Self {
            row_id: row_id.to_string(),
            base_version: None,
        }
    }

    pub fn base_version(mut self, base_version: i64) -> Self {
        self.base_version = Some(base_version);
        self
    }

    pub fn sync_operation(&self) -> SyncOperation {
        SyncOperation {
            table: "projects".to_string(),
            row_id: self.row_id.clone(),
            op: "delete".to_string(),
            payload: None,
            base_version: self.base_version,
        }
    }
}

impl IntoSyncularMutation for DeleteProject {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        PendingSyncularMutation {
            kind: SyncularMutationKind::Delete,
            table: "projects".to_string(),
            row_id: self.row_id,
            payload: None,
            base_version: self.base_version,
            local_row: None,
        }
    }
}

pub fn delete_project(row_id: &str, base_version: Option<i64>) -> SyncOperation {
    SyncOperation {
        table: "projects".to_string(),
        row_id: row_id.to_string(),
        op: "delete".to_string(),
        payload: None,
        base_version,
    }
}

#[derive(Debug, Clone)]
pub struct NewTask {
    pub id: String,
    pub title: String,
    pub completed: i32,
    pub user_id: String,
    pub project_id: Option<String>,
    pub image: Option<String>,
    pub description: Option<String>,
    yjs_updates: Map<String, Value>,
}

impl NewTask {
    pub fn new(id: &str, title: &str, user_id: &str, project_id: Option<&str>) -> Self {
        Self {
            id: id.to_string(),
            title: title.to_string(),
            completed: 0,
            user_id: user_id.to_string(),
            project_id: project_id.map(str::to_string),
            image: None,
            description: None,
            yjs_updates: Map::new(),
        }
    }

    pub fn with_generated_id(title: &str, user_id: &str, project_id: Option<&str>) -> Self {
        Self {
            id: random_syncular_id(),
            title: title.to_string(),
            completed: 0,
            user_id: user_id.to_string(),
            project_id: project_id.map(str::to_string),
            image: None,
            description: None,
            yjs_updates: Map::new(),
        }
    }

    pub fn title_yjs_update(mut self, update: YjsUpdateEnvelope) -> Self {
        self.yjs_updates.insert("title".to_string(), json!(update));
        self
    }

    pub fn title_yjs_updates(mut self, updates: Vec<YjsUpdateEnvelope>) -> Self {
        self.yjs_updates.insert("title".to_string(), json!(updates));
        self
    }

    pub fn row_json(&self) -> Value {
        let mut row = Map::new();
        row.insert("id".to_string(), json!(&self.id));
        row.insert("title".to_string(), json!(&self.title));
        row.insert("completed".to_string(), json!(&self.completed));
        row.insert("user_id".to_string(), json!(&self.user_id));
        if let Some(value) = &self.project_id {
            row.insert("project_id".to_string(), json!(value));
        }
        if let Some(value) = &self.image {
            row.insert("image".to_string(), json!(value));
        }
        if let Some(value) = &self.description {
            row.insert("description".to_string(), json!(value));
        }
        Value::Object(row)
    }

    pub fn sync_operation(&self) -> SyncOperation {
        let mut payload = Map::new();
        if !self.yjs_updates.contains_key("title") {
            payload.insert("title".to_string(), json!(&self.title));
        }
        payload.insert("completed".to_string(), json!(&self.completed));
        payload.insert("user_id".to_string(), json!(&self.user_id));
        if let Some(value) = &self.project_id {
            payload.insert("project_id".to_string(), json!(value));
        }
        if let Some(value) = &self.image {
            payload.insert("image".to_string(), json!(value));
        }
        if let Some(value) = &self.description {
            payload.insert("description".to_string(), json!(value));
        }
        if !self.yjs_updates.is_empty() {
            payload.insert(
                YJS_PAYLOAD_KEY.to_string(),
                Value::Object(self.yjs_updates.clone()),
            );
        }

        SyncOperation {
            table: "tasks".to_string(),
            row_id: self.id.clone(),
            op: "upsert".to_string(),
            payload: Some(Value::Object(payload)),
            base_version: Some(0),
        }
    }
}

impl IntoSyncularMutation for NewTask {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        let row_id = self.id.clone();
        PendingSyncularMutation {
            kind: SyncularMutationKind::Insert,
            table: "tasks".to_string(),
            row_id,
            payload: self.sync_operation().payload,
            base_version: None,
            local_row: Some(self.row_json()),
        }
    }
}

#[derive(Debug, Clone)]
pub struct TaskPatch {
    row_id: String,
    base_version: Option<i64>,
    title: Option<String>,
    completed: Option<i32>,
    user_id: Option<String>,
    project_id: Option<Option<String>>,
    image: Option<Option<String>>,
    description: Option<Option<String>>,
    yjs_updates: Map<String, Value>,
}

impl TaskPatch {
    pub fn new(row_id: &str) -> Self {
        Self {
            row_id: row_id.to_string(),
            base_version: None,
            title: None,
            completed: None,
            user_id: None,
            project_id: None,
            image: None,
            description: None,
            yjs_updates: Map::new(),
        }
    }

    pub fn base_version(mut self, base_version: i64) -> Self {
        self.base_version = Some(base_version);
        self
    }

    pub fn title(mut self, title: &str) -> Self {
        self.title = Some(title.to_string());
        self
    }

    pub fn completed(mut self, completed: i32) -> Self {
        self.completed = Some(completed);
        self
    }

    pub fn user_id(mut self, user_id: &str) -> Self {
        self.user_id = Some(user_id.to_string());
        self
    }

    pub fn project_id(mut self, project_id: Option<&str>) -> Self {
        self.project_id = Some(project_id.map(str::to_string));
        self
    }

    pub fn image(mut self, image: Option<&str>) -> Self {
        self.image = Some(image.map(str::to_string));
        self
    }

    pub fn description(mut self, description: Option<&str>) -> Self {
        self.description = Some(description.map(str::to_string));
        self
    }

    pub fn title_yjs_update(mut self, update: YjsUpdateEnvelope) -> Self {
        self.yjs_updates.insert("title".to_string(), json!(update));
        self
    }

    pub fn title_yjs_updates(mut self, updates: Vec<YjsUpdateEnvelope>) -> Self {
        self.yjs_updates.insert("title".to_string(), json!(updates));
        self
    }

    pub fn payload_json(&self) -> Value {
        let mut payload = Map::new();
        if !self.yjs_updates.contains_key("title") {
            if let Some(value) = &self.title {
                payload.insert("title".to_string(), json!(value));
            }
        }
        if let Some(value) = &self.completed {
            payload.insert("completed".to_string(), json!(value));
        }
        if let Some(value) = &self.user_id {
            payload.insert("user_id".to_string(), json!(value));
        }
        if let Some(value) = &self.project_id {
            payload.insert("project_id".to_string(), json!(value));
        }
        if let Some(value) = &self.image {
            payload.insert("image".to_string(), json!(value));
        }
        if let Some(value) = &self.description {
            payload.insert("description".to_string(), json!(value));
        }
        if !self.yjs_updates.is_empty() {
            payload.insert(
                YJS_PAYLOAD_KEY.to_string(),
                Value::Object(self.yjs_updates.clone()),
            );
        }
        Value::Object(payload)
    }

    pub fn sync_operation(&self) -> SyncOperation {
        SyncOperation {
            table: "tasks".to_string(),
            row_id: self.row_id.clone(),
            op: "upsert".to_string(),
            payload: Some(self.payload_json()),
            base_version: self.base_version,
        }
    }
}

impl IntoSyncularMutation for TaskPatch {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        let payload = self.payload_json();
        PendingSyncularMutation {
            kind: SyncularMutationKind::Update,
            table: "tasks".to_string(),
            row_id: self.row_id,
            payload: Some(payload),
            base_version: self.base_version,
            local_row: None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct DeleteTask {
    row_id: String,
    base_version: Option<i64>,
}

impl DeleteTask {
    pub fn new(row_id: &str) -> Self {
        Self {
            row_id: row_id.to_string(),
            base_version: None,
        }
    }

    pub fn base_version(mut self, base_version: i64) -> Self {
        self.base_version = Some(base_version);
        self
    }

    pub fn sync_operation(&self) -> SyncOperation {
        SyncOperation {
            table: "tasks".to_string(),
            row_id: self.row_id.clone(),
            op: "delete".to_string(),
            payload: None,
            base_version: self.base_version,
        }
    }
}

impl IntoSyncularMutation for DeleteTask {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        PendingSyncularMutation {
            kind: SyncularMutationKind::Delete,
            table: "tasks".to_string(),
            row_id: self.row_id,
            payload: None,
            base_version: self.base_version,
            local_row: None,
        }
    }
}

pub fn delete_task(row_id: &str, base_version: Option<i64>) -> SyncOperation {
    SyncOperation {
        table: "tasks".to_string(),
        row_id: row_id.to_string(),
        op: "delete".to_string(),
        payload: None,
        base_version,
    }
}

#[derive(Debug, Clone)]
pub struct InsertReceipt {
    pub id: String,
    pub commit: MutationReceipt,
}

#[derive(Debug, Clone)]
pub struct InsertManyReceipt {
    pub ids: Vec<String>,
    pub commit: MutationReceipt,
}

pub trait SyncularGeneratedMutationsExt: SyncularMutationExecutor {
    fn mutations(&mut self) -> SyncularAppMutations<'_, Self>
    where
        Self: Sized,
    {
        SyncularAppMutations { client: self }
    }

    fn leased_mutations(&mut self) -> SyncularAppLeasedMutations<'_, Self>
    where
        Self: Sized + SyncularLeasedMutationExecutor,
    {
        SyncularAppLeasedMutations { client: self }
    }

    fn command_history(&mut self) -> SyncularAppCommandHistory<'_, Self>
    where
        Self: Sized + SyncularCommandHistoryExecutor,
    {
        SyncularAppCommandHistory { client: self }
    }

    fn commit<R>(
        &mut self,
        f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
    ) -> Result<MutationCommit<R>>
    where
        Self: Sized,
    {
        let mut batch = SyncularMutationBatch::new();
        let result = {
            let mut tx = SyncularAppMutationTx { batch: &mut batch };
            f(&mut tx)?
        };
        let commit = self.apply_mutation_batch(batch)?;
        Ok(MutationCommit { result, commit })
    }

    fn commit_with_history<R>(
        &mut self,
        f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
    ) -> Result<MutationCommit<R>>
    where
        Self: Sized + SyncularCommandHistoryExecutor,
    {
        syncular_commit_with_history(self, "mutations", f)
    }

    fn commit_leased<R>(
        &mut self,
        f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
    ) -> Result<MutationCommit<R>>
    where
        Self: Sized + SyncularLeasedMutationExecutor,
    {
        let mut batch = SyncularMutationBatch::new();
        let result = {
            let mut tx = SyncularAppMutationTx { batch: &mut batch };
            f(&mut tx)?
        };
        let commit = self.apply_leased_mutation_batch(batch)?;
        Ok(MutationCommit { result, commit })
    }

    fn commit_leased_with_history<R>(
        &mut self,
        f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
    ) -> Result<MutationCommit<R>>
    where
        Self: Sized + SyncularCommandHistoryExecutor,
    {
        syncular_commit_with_history(self, "leasedMutations", f)
    }
}

impl<C> SyncularGeneratedMutationsExt for C where C: SyncularMutationExecutor {}

pub struct SyncularAppMutations<'a, C: SyncularMutationExecutor + ?Sized> {
    client: &'a mut C,
}

pub struct SyncularAppLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
    client: &'a mut C,
}

pub struct SyncularAppMutationTx<'a> {
    batch: &'a mut SyncularMutationBatch,
}

pub struct SyncularAppCommandHistory<'a, C: SyncularCommandHistoryExecutor + ?Sized> {
    client: &'a mut C,
}

impl<C> SyncularAppCommandHistory<'_, C>
where
    C: SyncularCommandHistoryExecutor + ?Sized,
{
    pub fn can_undo(&mut self) -> Result<bool> {
        Ok(self
            .client
            .command_history_latest(CommandHistoryState::Done)?
            .is_some())
    }

    pub fn can_redo(&mut self) -> Result<bool> {
        Ok(self
            .client
            .command_history_latest(CommandHistoryState::Undone)?
            .is_some())
    }

    pub fn latest_undo(&mut self) -> Result<Option<CommandHistoryRecord>> {
        self.client
            .command_history_latest(CommandHistoryState::Done)
    }

    pub fn latest_redo(&mut self) -> Result<Option<CommandHistoryRecord>> {
        self.client
            .command_history_latest(CommandHistoryState::Undone)
    }

    pub fn undo_last(&mut self) -> Result<CommandHistoryReceipt> {
        syncular_replay_command_history(self.client, CommandHistoryState::Done)
    }

    pub fn redo_last(&mut self) -> Result<CommandHistoryReceipt> {
        syncular_replay_command_history(self.client, CommandHistoryState::Undone)
    }
}

fn syncular_commit_with_history<C, R>(
    client: &mut C,
    mutation_scope: &str,
    f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
) -> Result<MutationCommit<R>>
where
    C: SyncularCommandHistoryExecutor + ?Sized,
{
    let mut batch = SyncularMutationBatch::new();
    let result = {
        let mut tx = SyncularAppMutationTx { batch: &mut batch };
        f(&mut tx)?
    };
    let commit = client.apply_command_history_tracked_batch(mutation_scope, batch)?;
    Ok(MutationCommit { result, commit })
}

fn syncular_replay_command_history<C>(
    client: &mut C,
    state: CommandHistoryState,
) -> Result<CommandHistoryReceipt>
where
    C: SyncularCommandHistoryExecutor + ?Sized,
{
    let command = client.command_history_latest(state)?.ok_or_else(|| {
        syncular_command_history_error(
            "sync.command_history_empty",
            "there is no Syncular command to replay",
        )
    })?;
    syncular_assert_command_history_safe(&command)?;
    let undo = state == CommandHistoryState::Done;
    syncular_assert_command_history_current_rows(client, &command, undo)?;

    let mut batch = SyncularMutationBatch::new();
    if undo {
        for entry in command.entries.iter().rev() {
            batch.push(syncular_command_history_mutation_for_snapshot(
                &entry.table,
                &entry.row_id,
                &entry.before,
            )?);
        }
    } else {
        for entry in &command.entries {
            batch.push(syncular_command_history_mutation_for_snapshot(
                &entry.table,
                &entry.row_id,
                &entry.after,
            )?);
        }
    }

    let commit = client.apply_command_history_batch(&command.mutation_scope, batch)?;
    let next_state = if undo {
        CommandHistoryState::Undone
    } else {
        CommandHistoryState::Done
    };
    client.command_history_mark(&command.id, next_state, &commit)?;
    Ok(CommandHistoryReceipt {
        command_id: command.id,
        commit,
    })
}

fn syncular_assert_command_history_current_rows<C>(
    client: &mut C,
    command: &CommandHistoryRecord,
    undo: bool,
) -> Result<()>
where
    C: SyncularCommandHistoryExecutor + ?Sized,
{
    for entry in &command.entries {
        let current = client.command_history_current_row_json(&entry.table, &entry.row_id)?;
        let expected = if undo { &entry.after } else { &entry.before };
        if &current != expected {
            return Err(syncular_command_history_error(
                "sync.command_history_conflict",
                &format!(
                    "cannot replay Syncular command {} because {}.{} changed since it was recorded",
                    command.id, entry.table, entry.row_id
                ),
            ));
        }
    }
    Ok(())
}

fn syncular_assert_command_history_safe(command: &CommandHistoryRecord) -> Result<()> {
    for entry in &command.entries {
        let unsafe_fields = syncular_command_history_unsafe_fields(entry)?;
        if !unsafe_fields.is_empty() {
            return Err(syncular_command_history_error(
                "sync.command_history_unsafe_field",
                &format!(
                    "cannot replay Syncular command {} because {}.{} changed unsafe fields: {}",
                    command.id,
                    entry.table,
                    entry.row_id,
                    unsafe_fields.join(", ")
                ),
            ));
        }
    }
    Ok(())
}

fn syncular_command_history_mutation_for_snapshot(
    table: &str,
    row_id: &str,
    snapshot: &Option<Value>,
) -> Result<PendingSyncularMutation> {
    match snapshot {
        None => Ok(PendingSyncularMutation {
            kind: SyncularMutationKind::Delete,
            table: table.to_string(),
            row_id: row_id.to_string(),
            payload: None,
            base_version: None,
            local_row: None,
        }),
        Some(snapshot) => Ok(PendingSyncularMutation {
            kind: SyncularMutationKind::Upsert,
            table: table.to_string(),
            row_id: row_id.to_string(),
            payload: Some(syncular_command_history_payload_for_snapshot(
                table, snapshot,
            )?),
            base_version: None,
            local_row: None,
        }),
    }
}

fn syncular_command_history_payload_for_snapshot(table: &str, snapshot: &Value) -> Result<Value> {
    let Some(object) = snapshot.as_object() else {
        return Err(syncular_command_history_error(
            "sync.command_history_conflict",
            "command history snapshot must be a JSON object",
        ));
    };
    let mut payload: Map<String, Value> = object.clone();
    match table {
        "comments" => {
            payload.remove("id");
            payload.remove("server_version");
            Ok(())
        }
        "projects" => {
            payload.remove("id");
            payload.remove("server_version");
            Ok(())
        }
        "tasks" => {
            payload.remove("id");
            payload.remove("server_version");
            payload.remove("title_yjs_state");
            Ok(())
        }
        _ => Err(syncular_command_history_error(
            "sync.command_history_table_unsupported",
            &format!("cannot replay undo history for unsupported table {table}"),
        )),
    }?;
    Ok(Value::Object(payload))
}

fn syncular_command_history_unsafe_fields(
    entry: &CommandHistoryEntry,
) -> Result<Vec<&'static str>> {
    #[allow(unused_mut)]
    let mut fields = Vec::new();
    match entry.table.as_str() {
        "comments" => Ok(fields),
        "projects" => Ok(fields),
        "tasks" => {
            syncular_command_history_push_unsafe_field(&mut fields, entry, "image");
            syncular_command_history_push_unsafe_field(&mut fields, entry, "description");
            syncular_command_history_push_unsafe_field(&mut fields, entry, "title");
            syncular_command_history_push_unsafe_field(&mut fields, entry, "title_yjs_state");
            Ok(fields)
        }
        _ => Err(syncular_command_history_error(
            "sync.command_history_table_unsupported",
            &format!(
                "cannot replay undo history for unsupported table {}",
                entry.table
            ),
        )),
    }
}

#[allow(dead_code)]
fn syncular_command_history_push_unsafe_field(
    fields: &mut Vec<&'static str>,
    entry: &CommandHistoryEntry,
    field: &'static str,
) {
    if syncular_command_history_snapshot_field(&entry.before, field)
        != syncular_command_history_snapshot_field(&entry.after, field)
        && !fields.contains(&field)
    {
        fields.push(field);
    }
}

#[allow(dead_code)]
fn syncular_command_history_snapshot_field<'a>(
    snapshot: &'a Option<Value>,
    field: &str,
) -> Option<&'a Value> {
    snapshot.as_ref()?.as_object()?.get(field)
}

fn syncular_command_history_error(code: &str, message: &str) -> SyncularError {
    SyncularError::config(format!("{code}: {message}"))
}

impl<'a, C> SyncularAppMutations<'a, C>
where
    C: SyncularMutationExecutor + ?Sized,
{
    pub fn comments(self) -> CommentMutations<'a, C> {
        CommentMutations {
            client: self.client,
        }
    }
}

impl<'a, C> SyncularAppLeasedMutations<'a, C>
where
    C: SyncularLeasedMutationExecutor + ?Sized,
{
    pub fn comments(self) -> CommentLeasedMutations<'a, C> {
        CommentLeasedMutations {
            client: self.client,
        }
    }
}

impl<'a> SyncularAppMutationTx<'a> {
    pub fn comments(&mut self) -> CommentMutationTx<'_> {
        CommentMutationTx { batch: self.batch }
    }
}

pub struct CommentMutations<'a, C: SyncularMutationExecutor + ?Sized> {
    client: &'a mut C,
}

impl<C> CommentMutations<'_, C>
where
    C: SyncularMutationExecutor + ?Sized,
{
    pub fn insert(self, row: NewComment) -> Result<InsertReceipt> {
        let id = row.id.clone();
        let commit = self.client.apply_mutation(row)?;
        Ok(InsertReceipt { id, commit })
    }

    pub fn insert_many(
        self,
        rows: impl IntoIterator<Item = NewComment>,
    ) -> Result<InsertManyReceipt> {
        let mut batch = SyncularMutationBatch::new();
        let mut ids = Vec::new();
        for row in rows {
            ids.push(row.id.clone());
            batch.push(row);
        }
        let commit = self.client.apply_mutation_batch(batch)?;
        Ok(InsertManyReceipt { ids, commit })
    }

    pub fn update(self, patch: CommentPatch) -> Result<MutationReceipt> {
        self.client.apply_mutation(patch)
    }

    pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
        self.client.apply_mutation(DeleteComment::new(row_id))
    }
}

pub struct CommentLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
    client: &'a mut C,
}

impl<C> CommentLeasedMutations<'_, C>
where
    C: SyncularLeasedMutationExecutor + ?Sized,
{
    pub fn insert(self, row: NewComment) -> Result<InsertReceipt> {
        let id = row.id.clone();
        let commit = self.client.apply_leased_mutation(row)?;
        Ok(InsertReceipt { id, commit })
    }

    pub fn insert_many(
        self,
        rows: impl IntoIterator<Item = NewComment>,
    ) -> Result<InsertManyReceipt> {
        let mut batch = SyncularMutationBatch::new();
        let mut ids = Vec::new();
        for row in rows {
            ids.push(row.id.clone());
            batch.push(row);
        }
        let commit = self.client.apply_leased_mutation_batch(batch)?;
        Ok(InsertManyReceipt { ids, commit })
    }

    pub fn update(self, patch: CommentPatch) -> Result<MutationReceipt> {
        self.client.apply_leased_mutation(patch)
    }

    pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
        self.client
            .apply_leased_mutation(DeleteComment::new(row_id))
    }
}

pub struct CommentMutationTx<'a> {
    batch: &'a mut SyncularMutationBatch,
}

impl CommentMutationTx<'_> {
    pub fn insert(self, row: NewComment) -> Result<String> {
        let id = row.id.clone();
        self.batch.push(row);
        Ok(id)
    }

    pub fn insert_many(self, rows: impl IntoIterator<Item = NewComment>) -> Result<Vec<String>> {
        let mut ids = Vec::new();
        for row in rows {
            ids.push(row.id.clone());
            self.batch.push(row);
        }
        Ok(ids)
    }

    pub fn update(self, patch: CommentPatch) -> Result<()> {
        self.batch.push(patch);
        Ok(())
    }

    pub fn delete(self, row_id: &str) -> Result<()> {
        self.batch.push(DeleteComment::new(row_id));
        Ok(())
    }
}

impl<'a, C> SyncularAppMutations<'a, C>
where
    C: SyncularMutationExecutor + ?Sized,
{
    pub fn projects(self) -> ProjectMutations<'a, C> {
        ProjectMutations {
            client: self.client,
        }
    }
}

impl<'a, C> SyncularAppLeasedMutations<'a, C>
where
    C: SyncularLeasedMutationExecutor + ?Sized,
{
    pub fn projects(self) -> ProjectLeasedMutations<'a, C> {
        ProjectLeasedMutations {
            client: self.client,
        }
    }
}

impl<'a> SyncularAppMutationTx<'a> {
    pub fn projects(&mut self) -> ProjectMutationTx<'_> {
        ProjectMutationTx { batch: self.batch }
    }
}

pub struct ProjectMutations<'a, C: SyncularMutationExecutor + ?Sized> {
    client: &'a mut C,
}

impl<C> ProjectMutations<'_, C>
where
    C: SyncularMutationExecutor + ?Sized,
{
    pub fn insert(self, row: NewProject) -> Result<InsertReceipt> {
        let id = row.id.clone();
        let commit = self.client.apply_mutation(row)?;
        Ok(InsertReceipt { id, commit })
    }

    pub fn insert_many(
        self,
        rows: impl IntoIterator<Item = NewProject>,
    ) -> Result<InsertManyReceipt> {
        let mut batch = SyncularMutationBatch::new();
        let mut ids = Vec::new();
        for row in rows {
            ids.push(row.id.clone());
            batch.push(row);
        }
        let commit = self.client.apply_mutation_batch(batch)?;
        Ok(InsertManyReceipt { ids, commit })
    }

    pub fn update(self, patch: ProjectPatch) -> Result<MutationReceipt> {
        self.client.apply_mutation(patch)
    }

    pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
        self.client.apply_mutation(DeleteProject::new(row_id))
    }
}

pub struct ProjectLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
    client: &'a mut C,
}

impl<C> ProjectLeasedMutations<'_, C>
where
    C: SyncularLeasedMutationExecutor + ?Sized,
{
    pub fn insert(self, row: NewProject) -> Result<InsertReceipt> {
        let id = row.id.clone();
        let commit = self.client.apply_leased_mutation(row)?;
        Ok(InsertReceipt { id, commit })
    }

    pub fn insert_many(
        self,
        rows: impl IntoIterator<Item = NewProject>,
    ) -> Result<InsertManyReceipt> {
        let mut batch = SyncularMutationBatch::new();
        let mut ids = Vec::new();
        for row in rows {
            ids.push(row.id.clone());
            batch.push(row);
        }
        let commit = self.client.apply_leased_mutation_batch(batch)?;
        Ok(InsertManyReceipt { ids, commit })
    }

    pub fn update(self, patch: ProjectPatch) -> Result<MutationReceipt> {
        self.client.apply_leased_mutation(patch)
    }

    pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
        self.client
            .apply_leased_mutation(DeleteProject::new(row_id))
    }
}

pub struct ProjectMutationTx<'a> {
    batch: &'a mut SyncularMutationBatch,
}

impl ProjectMutationTx<'_> {
    pub fn insert(self, row: NewProject) -> Result<String> {
        let id = row.id.clone();
        self.batch.push(row);
        Ok(id)
    }

    pub fn insert_many(self, rows: impl IntoIterator<Item = NewProject>) -> Result<Vec<String>> {
        let mut ids = Vec::new();
        for row in rows {
            ids.push(row.id.clone());
            self.batch.push(row);
        }
        Ok(ids)
    }

    pub fn update(self, patch: ProjectPatch) -> Result<()> {
        self.batch.push(patch);
        Ok(())
    }

    pub fn delete(self, row_id: &str) -> Result<()> {
        self.batch.push(DeleteProject::new(row_id));
        Ok(())
    }
}

impl<'a, C> SyncularAppMutations<'a, C>
where
    C: SyncularMutationExecutor + ?Sized,
{
    pub fn tasks(self) -> TaskMutations<'a, C> {
        TaskMutations {
            client: self.client,
        }
    }
}

impl<'a, C> SyncularAppLeasedMutations<'a, C>
where
    C: SyncularLeasedMutationExecutor + ?Sized,
{
    pub fn tasks(self) -> TaskLeasedMutations<'a, C> {
        TaskLeasedMutations {
            client: self.client,
        }
    }
}

impl<'a> SyncularAppMutationTx<'a> {
    pub fn tasks(&mut self) -> TaskMutationTx<'_> {
        TaskMutationTx { batch: self.batch }
    }
}

pub struct TaskMutations<'a, C: SyncularMutationExecutor + ?Sized> {
    client: &'a mut C,
}

impl<C> TaskMutations<'_, C>
where
    C: SyncularMutationExecutor + ?Sized,
{
    pub fn insert(self, row: NewTask) -> Result<InsertReceipt> {
        let id = row.id.clone();
        let commit = self.client.apply_mutation(row)?;
        Ok(InsertReceipt { id, commit })
    }

    pub fn insert_many(self, rows: impl IntoIterator<Item = NewTask>) -> Result<InsertManyReceipt> {
        let mut batch = SyncularMutationBatch::new();
        let mut ids = Vec::new();
        for row in rows {
            ids.push(row.id.clone());
            batch.push(row);
        }
        let commit = self.client.apply_mutation_batch(batch)?;
        Ok(InsertManyReceipt { ids, commit })
    }

    pub fn update(self, patch: TaskPatch) -> Result<MutationReceipt> {
        self.client.apply_mutation(patch)
    }

    pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
        self.client.apply_mutation(DeleteTask::new(row_id))
    }
}

pub struct TaskLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
    client: &'a mut C,
}

impl<C> TaskLeasedMutations<'_, C>
where
    C: SyncularLeasedMutationExecutor + ?Sized,
{
    pub fn insert(self, row: NewTask) -> Result<InsertReceipt> {
        let id = row.id.clone();
        let commit = self.client.apply_leased_mutation(row)?;
        Ok(InsertReceipt { id, commit })
    }

    pub fn insert_many(self, rows: impl IntoIterator<Item = NewTask>) -> Result<InsertManyReceipt> {
        let mut batch = SyncularMutationBatch::new();
        let mut ids = Vec::new();
        for row in rows {
            ids.push(row.id.clone());
            batch.push(row);
        }
        let commit = self.client.apply_leased_mutation_batch(batch)?;
        Ok(InsertManyReceipt { ids, commit })
    }

    pub fn update(self, patch: TaskPatch) -> Result<MutationReceipt> {
        self.client.apply_leased_mutation(patch)
    }

    pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
        self.client.apply_leased_mutation(DeleteTask::new(row_id))
    }
}

pub struct TaskMutationTx<'a> {
    batch: &'a mut SyncularMutationBatch,
}

impl TaskMutationTx<'_> {
    pub fn insert(self, row: NewTask) -> Result<String> {
        let id = row.id.clone();
        self.batch.push(row);
        Ok(id)
    }

    pub fn insert_many(self, rows: impl IntoIterator<Item = NewTask>) -> Result<Vec<String>> {
        let mut ids = Vec::new();
        for row in rows {
            ids.push(row.id.clone());
            self.batch.push(row);
        }
        Ok(ids)
    }

    pub fn update(self, patch: TaskPatch) -> Result<()> {
        self.batch.push(patch);
        Ok(())
    }

    pub fn delete(self, row_id: &str) -> Result<()> {
        self.batch.push(DeleteTask::new(row_id));
        Ok(())
    }
}

pub mod prelude {
    pub use super::{
        CommentLeasedMutations, CommentMutationTx, CommentMutations, CommentPatch, DeleteComment,
        DeleteProject, DeleteTask, InsertManyReceipt, InsertReceipt, NewComment, NewProject,
        NewTask, ProjectLeasedMutations, ProjectMutationTx, ProjectMutations, ProjectPatch,
        SyncularAppCommandHistory, SyncularAppLeasedMutations, SyncularAppMutationTx,
        SyncularAppMutations, SyncularGeneratedMutationsExt, TaskLeasedMutations, TaskMutationTx,
        TaskMutations, TaskPatch,
    };
}
