//! Small key/value store and the change_counter / change_log helpers.
//!
//! `change_counter` is a strictly monotonic 64-bit value bumped exactly once
//! per externally-observable mutation (a single ApplyBatch counts as one
//! bump, not one per operation). The value drives the CardDAV `sync-token`.
//!
//! `change_log` is an append-only history feeding `sync-collection`; it
//! records which raw_contact changed and how, keyed by the counter value
//! emitted at that mutation.

use std::collections::HashSet;

use rusqlite::{ Connection, OptionalExtension, params };

use crate::db::{ ChangeKind, DbResult, now_millis };

pub fn get_value( conn: &Connection, key: &str ) -> DbResult<Option<String>>
{
	let v = conn.query_row(
		"SELECT value FROM sync_state WHERE key = ?1",
		params![ key ],
		|row| row.get( 0 ),
	).optional()?;
	Ok( v )
}

pub fn set_value( conn: &Connection, key: &str, value: &str ) -> DbResult<()>
{
	conn.execute(
		"INSERT INTO sync_state( key, value ) VALUES( ?1, ?2 )
		 ON CONFLICT( key ) DO UPDATE SET value = excluded.value",
		params![ key, value ],
	)?;
	Ok( () )
}

/// Read the current `change_counter` without mutating anything.
pub fn change_counter( conn: &Connection ) -> DbResult<i64>
{
	Ok( get_value( conn, "change_counter" )?
		.and_then( |s| s.parse::<i64>().ok() )
		.unwrap_or( 0 ) )
}

/// Atomically `+= 1` the counter and return the new value.
///
/// Uses `UPDATE ... RETURNING` so the read and the increment are a single
/// statement. The `change_counter` row is initialised to '0' by the v1
/// migration so the UPDATE always finds a row to bump.
fn bump_counter( conn: &Connection ) -> DbResult<i64>
{
	let new_value: String = conn.query_row(
		"UPDATE sync_state
		 SET value = CAST( CAST( value AS INTEGER ) + 1 AS TEXT )
		 WHERE key = 'change_counter'
		 RETURNING value",
		[],
		|row| row.get( 0 ),
	)?;
	Ok( new_value.parse::<i64>().unwrap_or( 0 ) )
}

/// Bump the counter and append a `change_log` row.
///
/// Callers pass the `raw_contact_id` whose serialisation has changed (for
/// data-level mutations, this is the parent raw_contact). CardDAV
/// `sync-collection` reads `change_log` to answer "what changed since
/// counter N".
pub fn record_change(
	conn: &Connection,
	raw_contact_id: i64,
	kind: ChangeKind,
) -> DbResult<i64>
{
	let counter = bump_counter( conn )?;
	conn.execute(
		"INSERT INTO change_log( counter, raw_contact_id, change_kind, happened_at )
		 VALUES( ?1, ?2, ?3, ?4 )",
		params![ counter, raw_contact_id, kind.as_str(), now_millis() ],
	)?;
	Ok( counter )
}

/// Bump the counter once and append one `change_log` row per
/// `(raw_contact_id, kind)` entry. Used by `ApplyBatch` so a whole batch
/// shares a single sync-token bump even when many raw_contacts were
/// touched. Schema migration v3 dropped the `counter PRIMARY KEY`
/// constraint precisely so this can write multiple rows under the same
/// counter value.
pub fn record_batch_changes(
	conn: &Connection,
	affected: &[ ( i64, ChangeKind ) ],
) -> DbResult<i64>
{
	let counter = bump_counter( conn )?;
	if affected.is_empty()
	{
		return Ok( counter );
	}
	let now = now_millis();
	let mut stmt = conn.prepare(
		"INSERT INTO change_log( counter, raw_contact_id, change_kind, happened_at )
		 VALUES( ?1, ?2, ?3, ?4 )",
	)?;
	for ( raw_id, kind ) in affected
	{
		stmt.execute( params![ counter, raw_id, kind.as_str(), now ] )?;
	}
	Ok( counter )
}

/// One entry produced by [`changes_since`].
///
/// `kind` is *current state*, not the literal change_log event:
/// - `Put`     — the resource exists right now and clients should re-fetch.
/// - `Delete`  — the resource is soft-deleted; clients should drop it.
#[ derive( Debug, Clone, PartialEq, Eq ) ]
pub struct SyncEntry
{
	pub raw_contact_id: i64,
	pub uid: String,
	pub kind: ChangeKind,
}

/// Walk `change_log` entries with `counter > since_counter`, coalesce
/// events per raw_contact, and resolve each affected raw_contact to its
/// current state. Returns `( new_counter, entries )` where `new_counter`
/// is the current `change_counter`. Entries are sorted by `raw_contact_id`
/// for stable wire ordering.
///
/// Entries we silently drop:
/// - raw_contacts that have been hard-deleted (orphan log rows);
/// - raw_contacts that were never assigned a `carddav_uid` AND are now
///   soft-deleted — clients never observed the resource, so signalling a
///   delete is meaningless.
///
/// For affected raw_contacts that still need a `carddav_uid` (created via
/// D-Bus but never served via CardDAV before), one is generated lazily.
pub fn changes_since(
	conn: &mut Connection,
	since_counter: i64,
) -> DbResult<( i64, Vec<SyncEntry> )>
{
	let mut affected: HashSet<i64> = HashSet::new();
	{
		let mut stmt = conn.prepare(
			"SELECT raw_contact_id FROM change_log WHERE counter > ?1"
		)?;
		let rows = stmt.query_map( params![ since_counter ], |row| row.get::<_, i64>( 0 ) )?;
		for r in rows
		{
			affected.insert( r? );
		}
	}

	let mut entries: Vec<SyncEntry> = Vec::with_capacity( affected.len() );
	for rid in affected
	{
		let info: Option<( Option<String>, i64 )> = conn.query_row(
			"SELECT carddav_uid, deleted FROM raw_contacts WHERE _id = ?1",
			params![ rid ],
			|row|
			{
				let uid:     Option<String> = row.get( 0 )?;
				let deleted: i64            = row.get( 1 )?;
				Ok( ( uid, deleted ) )
			},
		).optional()?;

		let Some( ( uid_opt, deleted_flag ) ) = info else
		{
			// raw_contact was hard-deleted; the change_log row is an orphan.
			continue;
		};
		let is_deleted = deleted_flag != 0;

		let uid = match uid_opt
		{
			Some( u )                   => u,
			None if is_deleted          => continue,
			None                        => crate::db::raw_contacts::ensure_carddav_uid( conn, rid )?,
		};

		let kind = if is_deleted { ChangeKind::Delete } else { ChangeKind::Put };
		entries.push( SyncEntry
		{
			raw_contact_id: rid,
			uid,
			kind,
		} );
	}

	entries.sort_by_key( |e| e.raw_contact_id );
	let new_counter = change_counter( conn )?;
	Ok( ( new_counter, entries ) )
}

/// Stable per-database UUID. Generated on first read.
pub fn instance_uuid( conn: &Connection ) -> DbResult<String>
{
	if let Some( v ) = get_value( conn, "instance_uuid" )?
	{
		Ok( v )
	} else {
		let new_uuid = uuid::Uuid::new_v4().to_string();
		set_value( conn, "instance_uuid", &new_uuid )?;
		Ok( new_uuid )
	}
}

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

	#[ test ]
	fn counter_starts_at_zero_and_bumps()
	{
		let db = Database::open_in_memory().unwrap();
		db.with_conn( |c| {
			assert_eq!( change_counter( c ).unwrap(), 0 );
			let n1 = record_change( c, 1, ChangeKind::Put ).unwrap();
			let n2 = record_change( c, 1, ChangeKind::Put ).unwrap();
			assert_eq!( n1, 1 );
			assert_eq!( n2, 2 );
			assert_eq!( change_counter( c ).unwrap(), 2 );
		} );
	}

	#[ test ]
	fn instance_uuid_is_stable()
	{
		let db = Database::open_in_memory().unwrap();
		let ( a, b ) = db.with_conn( |c| {
			let a = instance_uuid( c ).unwrap();
			let b = instance_uuid( c ).unwrap();
			( a, b )
		} );
		assert_eq!( a, b );
		assert!( !a.is_empty() );
	}

	#[ test ]
	fn changes_since_coalesces_and_resolves_uids()
	{
		use crate::db::raw_contacts;
		use crate::model::raw_contact::NewRawContact;

		let db = Database::open_in_memory().unwrap();

		let ( ada_id, _ ) = db.with_conn( |c|
		{
			raw_contacts::insert( c, NewRawContact
			{
				display_name: Some( "Ada".into() ),
				..Default::default()
			} )
		} ).unwrap();

		let ( _, bea_id, _ada_uid_first ) = db.with_conn( |c|
		{
			let ( bea_id, _ ) = raw_contacts::insert( c, NewRawContact
			{
				display_name: Some( "Bea".into() ),
				..Default::default()
			} ).unwrap();
			let ada_uid = raw_contacts::ensure_carddav_uid( c, ada_id ).unwrap();
			( (), bea_id, ada_uid )
		} );

		// Caller sees changes since 0 → both contacts as Put.
		let ( counter, entries ) = db.with_conn( |c|
			changes_since( c, 0 )
		).unwrap();
		assert!( counter >= 2 );
		assert_eq!( entries.len(), 2 );
		assert!( entries.iter().all( |e| matches!( e.kind, ChangeKind::Put ) ) );

		// Soft-delete Bea, then ask for changes since `counter`.
		let counter_after_inserts = counter;
		db.with_conn( |c| raw_contacts::soft_delete( c, bea_id ) ).unwrap();

		let ( _, deltas ) = db.with_conn( |c|
			changes_since( c, counter_after_inserts )
		).unwrap();
		assert_eq!( deltas.len(), 1 );
		assert_eq!( deltas[ 0 ].raw_contact_id, bea_id );
		assert!( matches!( deltas[ 0 ].kind, ChangeKind::Delete ) );
	}
}
