//! CRUD for `raw_contacts`. Each insert also creates the matching `contacts`
//! aggregate row (v1 aggregation is 1:1).
//!
//! Mutations record an entry in `change_log` so CardDAV `sync-collection` can
//! tell clients what changed.

use rusqlite::{ Connection, OptionalExtension, params, params_from_iter };
use rusqlite::types::Value;

use crate::db::{ ChangeKind, DbError, DbResult, now_millis, sync_state };
use crate::model::raw_contact::{ NewRawContact, RawContact, RawContactPatch };

/// Insert a raw_contact along with its aggregated `contacts` row (v1 1:1
/// aggregation). Returns `( raw_contact_id, contact_id )` since callers
/// almost always need both — e.g. the dbus dispatcher emits the
/// `Changed` signal carrying `contact_id`.
pub fn insert( conn: &mut Connection, new: NewRawContact ) -> DbResult<( i64, i64 )>
{
	let now = now_millis();
	let tx = conn.transaction()?;
	let ( raw_id, contact_id ) = insert_in_tx( &tx, &new, now )?;
	sync_state::record_change( &tx, raw_id, ChangeKind::Put )?;
	tx.commit()?;
	Ok( ( raw_id, contact_id ) )
}

/// In-transaction variant: writes the rows but does **not** open a
/// transaction or record a change. Used by `ApplyBatch` so a whole batch
/// shares one transaction and bumps `change_counter` exactly once.
pub fn insert_in_tx(
	conn: &Connection,
	new: &NewRawContact,
	now: i64,
) -> DbResult<( i64, i64 )>
{
	conn.execute(
		"INSERT INTO contacts( display_name, created_at, updated_at )
		 VALUES( ?1, ?2, ?2 )",
		params![ new.display_name, now ],
	)?;
	let contact_id = conn.last_insert_rowid();

	conn.execute(
		"INSERT INTO raw_contacts(
			contact_id, account_name, account_type, display_name,
			sourceid, created_at, updated_at
		 ) VALUES( ?1, ?2, ?3, ?4, ?5, ?6, ?6 )",
		params![
			contact_id,
			new.account_name,
			new.account_type,
			new.display_name,
			new.sourceid,
			now,
		],
	)?;
	let raw_id = conn.last_insert_rowid();
	Ok( ( raw_id, contact_id ) )
}

/// Resolve `raw_contact_id` to its parent `contact_id`. Returns
/// `NotFound` if the raw_contact does not exist (regardless of whether it
/// is soft-deleted — sync engines need this lookup even for deleted rows).
pub fn contact_id_of( conn: &Connection, raw_contact_id: i64 ) -> DbResult<i64>
{
	conn.query_row(
		"SELECT contact_id FROM raw_contacts WHERE _id = ?1",
		params![ raw_contact_id ],
		|row| row.get( 0 ),
	).optional()?.ok_or( DbError::NotFound )
}

/// Return the stable CardDAV UUID for a raw_contact. The column starts NULL
/// and is populated the first time we serve the raw_contact over CardDAV;
/// subsequent calls return the persisted value. Includes soft-deleted rows
/// because sync clients need the URI to ack a deletion.
pub fn ensure_carddav_uid( conn: &mut Connection, raw_contact_id: i64 ) -> DbResult<String>
{
	let tx = conn.transaction()?;

	let existing: Option<Option<String>> = tx.query_row(
		"SELECT carddav_uid FROM raw_contacts WHERE _id = ?1",
		params![ raw_contact_id ],
		|row| row.get::<_, Option<String>>( 0 ),
	).optional()?;

	let uid = match existing
	{
		None              => return Err( DbError::NotFound ),
		Some( Some( u ) ) => u,
		Some( None )      =>
		{
			let new_uid = uuid::Uuid::new_v4().to_string();
			tx.execute(
				"UPDATE raw_contacts SET carddav_uid = ?2 WHERE _id = ?1",
				params![ raw_contact_id, &new_uid ],
			)?;
			new_uid
		}
	};

	tx.commit()?;
	Ok( uid )
}

/// Look up the raw_contact id whose `carddav_uid` matches. Soft-deleted
/// rows are visible — CardDAV `GET` on a deleted resource needs to return
/// `404`, not pretend the row never existed (the difference matters for
/// sync semantics).
pub fn id_by_carddav_uid( conn: &Connection, uid: &str ) -> DbResult<i64>
{
	conn.query_row(
		"SELECT _id FROM raw_contacts WHERE carddav_uid = ?1",
		params![ uid ],
		|row| row.get( 0 ),
	).optional()?.ok_or( DbError::NotFound )
}

/// List all raw_contacts that the CardDAV layer should expose. Soft-deleted
/// rows are excluded. Returns `( raw_contact_id, carddav_uid, updated_at )`
/// triples sorted by `_id`, with `carddav_uid` lazily populated for any row
/// that still had it NULL. `updated_at` is read from the listing snapshot
/// (assigning a UUID does not bump the timestamp, so the original value is
/// still authoritative).
pub fn list_for_carddav( conn: &mut Connection ) -> DbResult<Vec<( i64, String, i64 )>>
{
	let snapshot: Vec<( i64, Option<String>, i64 )> =
	{
		let mut stmt = conn.prepare(
			"SELECT _id, carddav_uid, updated_at
			 FROM raw_contacts
			 WHERE deleted = 0
			 ORDER BY _id"
		)?;
		let rows = stmt.query_map( [], |row|
		{
			let id:         i64            = row.get( 0 )?;
			let uid:        Option<String> = row.get( 1 )?;
			let updated_at: i64            = row.get( 2 )?;
			Ok( ( id, uid, updated_at ) )
		} )?;
		rows.collect::<rusqlite::Result<Vec<_>>>()?
	};

	let mut out = Vec::with_capacity( snapshot.len() );
	for ( id, uid, updated_at ) in snapshot
	{
		let uid = match uid
		{
			Some( u ) => u,
			None      => ensure_carddav_uid( conn, id )?,
		};
		out.push( ( id, uid, updated_at ) );
	}
	Ok( out )
}

pub fn get( conn: &Connection, id: i64 ) -> DbResult<RawContact>
{
	conn.query_row(
		"SELECT _id, contact_id, account_name, account_type, display_name,
				deleted, dirty, version, sourceid, created_at, updated_at
		 FROM raw_contacts
		 WHERE _id = ?1 AND deleted = 0",
		params![ id ],
		row_to_raw_contact,
	)
		.optional()?
		.ok_or( DbError::NotFound )
}

/// Apply a tri-state patch. Bumps `version` and `updated_at` unconditionally
/// (any call to `update` is observable). Records a `Put` change.
pub fn update( conn: &mut Connection, id: i64, patch: RawContactPatch ) -> DbResult<()>
{
	let now = now_millis();
	let tx = conn.transaction()?;
	update_in_tx( &tx, id, &patch, now )?;
	sync_state::record_change( &tx, id, ChangeKind::Put )?;
	tx.commit()?;
	Ok( () )
}

/// In-transaction variant of [`update`]. See `insert_in_tx` for the
/// rationale.
pub fn update_in_tx(
	conn: &Connection,
	id: i64,
	patch: &RawContactPatch,
	now: i64,
) -> DbResult<()>
{
	let mut set_parts: Vec<String> = Vec::new();
	let mut vals: Vec<Value> = Vec::new();

	let mut push_str = | slot: &Option<Option<String>>, col: &str |
	{
		if let Some( v ) = slot
		{
			set_parts.push( format!( "{} = ?", col ) );
			vals.push( match v
			{
				Some( s ) => Value::Text( s.clone() ),
				None      => Value::Null,
			} );
		}
	};

	push_str( &patch.account_name, "account_name" );
	push_str( &patch.account_type, "account_type" );
	push_str( &patch.display_name, "display_name" );
	push_str( &patch.sourceid,     "sourceid"     );

	set_parts.push( "version = version + 1".into() );
	set_parts.push( "updated_at = ?".into() );
	vals.push( Value::Integer( now ) );

	vals.push( Value::Integer( id ) );

	let sql = format!(
		"UPDATE raw_contacts SET {} WHERE _id = ? AND deleted = 0",
		set_parts.join( ", " ),
	);

	let affected = conn.execute( &sql, params_from_iter( vals.iter() ) )?;
	if affected == 0
	{
		return Err( DbError::NotFound );
	}

	conn.execute(
		"UPDATE contacts SET updated_at = ?2
		 WHERE _id = ( SELECT contact_id FROM raw_contacts WHERE _id = ?1 )",
		params![ id, now ],
	)?;
	Ok( () )
}

/// Mark the raw_contact as deleted but keep the row. Sync engines and
/// CardDAV clients need to observe the deletion before we hard-delete.
///
/// In v1 (1:1 aggregation), the parent `contacts` row is left in place;
/// `contacts::list` filters it out via an EXISTS subquery on non-deleted
/// raw_contacts. v2 will revisit when multi-source aggregation lands.
pub fn soft_delete( conn: &mut Connection, id: i64 ) -> DbResult<()>
{
	let now = now_millis();
	let tx = conn.transaction()?;
	soft_delete_in_tx( &tx, id, now )?;
	sync_state::record_change( &tx, id, ChangeKind::Delete )?;
	tx.commit()?;
	Ok( () )
}

/// In-transaction variant of [`soft_delete`].
pub fn soft_delete_in_tx( conn: &Connection, id: i64, now: i64 ) -> DbResult<()>
{
	let affected = conn.execute(
		"UPDATE raw_contacts SET deleted = 1, updated_at = ?2 WHERE _id = ?1 AND deleted = 0",
		params![ id, now ],
	)?;
	if affected == 0
	{
		return Err( DbError::NotFound );
	}
	Ok( () )
}

/// Hard-delete a soft-deleted raw_contact. Cascades to `data` rows; clears
/// orphan `contacts` rows. Intended for sync engines that have acknowledged
/// the deletion. Not exposed via D-Bus in v1.
pub fn hard_delete( conn: &mut Connection, id: i64 ) -> DbResult<()>
{
	let tx = conn.transaction()?;

	let contact_id: Option<i64> = tx.query_row(
		"SELECT contact_id FROM raw_contacts WHERE _id = ?1 AND deleted = 1",
		params![ id ],
		|row| row.get( 0 ),
	).optional()?;
	let contact_id = contact_id.ok_or( DbError::NotFound )?;

	tx.execute( "DELETE FROM raw_contacts WHERE _id = ?1", params![ id ] )?;

	// If no other raw_contact points at this contact, clean it up too.
	let surviving: i64 = tx.query_row(
		"SELECT COUNT(*) FROM raw_contacts WHERE contact_id = ?1",
		params![ contact_id ],
		|row| row.get( 0 ),
	)?;
	if surviving == 0
	{
		tx.execute( "DELETE FROM contacts WHERE _id = ?1", params![ contact_id ] )?;
	}

	tx.commit()?;
	Ok( () )
}

fn row_to_raw_contact( row: &rusqlite::Row<'_> ) -> rusqlite::Result<RawContact>
{
	Ok( RawContact {
		id:           row.get( 0 )?,
		contact_id:   row.get( 1 )?,
		account_name: row.get( 2 )?,
		account_type: row.get( 3 )?,
		display_name: row.get( 4 )?,
		deleted:      row.get::<_, i64>( 5 )? != 0,
		dirty:        row.get::<_, i64>( 6 )? != 0,
		version:      row.get( 7 )?,
		sourceid:     row.get( 8 )?,
		created_at:   row.get( 9 )?,
		updated_at:   row.get( 10 )?,
	} )
}
