//! Read-mostly access to the aggregated `contacts` table.
//!
//! Inserts and deletes happen as a side effect of `raw_contacts::insert` /
//! `raw_contacts::hard_delete`. This module only exposes the user-facing
//! reads (`get`, `list`) plus updates of fields that are owned by `contacts`
//! itself (`starred`, `times_contacted`, …) — properties not mirrored in
//! vCard, so they don't bump the change counter.

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

use crate::db::{ DbError, DbResult, now_millis };
use crate::model::contact::{ Contact, ContactPatch, ContactSummary };

const CONTACT_COLUMNS: &str = "
	_id, display_name, photo_id, times_contacted, last_time_contacted,
	starred, custom_ringtone, send_to_voicemail, created_at, updated_at
";

const NOT_FULLY_DELETED: &str = "
	EXISTS (
		SELECT 1 FROM raw_contacts r
		WHERE r.contact_id = contacts._id AND r.deleted = 0
	)
";

pub fn get( conn: &Connection, id: i64 ) -> DbResult<Contact>
{
	let sql = format!(
		"SELECT {CONTACT_COLUMNS} FROM contacts
		 WHERE _id = ?1 AND {NOT_FULLY_DELETED}"
	);
	conn.query_row( &sql, params![ id ], row_to_contact )
		.optional()?
		.ok_or( DbError::NotFound )
}

/// List contacts whose `display_name` contains `filter` (case-insensitive,
/// ASCII collation). Empty filter returns everything. Soft-deleted contacts
/// (those whose every raw_contact is deleted) are hidden.
pub fn list( conn: &Connection, filter: &str ) -> DbResult<Vec<Contact>>
{
	let pattern = format!( "%{}%", filter );
	let sql = format!(
		"SELECT {CONTACT_COLUMNS} FROM contacts
		 WHERE COALESCE( display_name, '' ) LIKE ?1 COLLATE NOCASE
		   AND {NOT_FULLY_DELETED}
		 ORDER BY display_name COLLATE NOCASE"
	);
	let mut stmt = conn.prepare( &sql )?;
	let rows: Vec<Contact> = stmt
		.query_map( params![ pattern ], row_to_contact )?
		.collect::<rusqlite::Result<_>>()?;
	Ok( rows )
}

/// Like [`list`], but each contact carries its primary phone number, type and
/// custom label. The phone is the contact's `is_super_primary`, else
/// `is_primary`, else lowest-`_id` `phone_v2` data row across its non-deleted
/// raw_contacts; contacts without a phone come back with `number = None`.
pub fn list_full( conn: &Connection, filter: &str ) -> DbResult<Vec<ContactSummary>>
{
	let pattern = format!( "%{}%", filter );
	let sql = format!(
		"SELECT
			contacts._id, contacts.display_name, contacts.photo_id, contacts.starred,
			p.data1, p.data2, p.data3
		 FROM contacts
		 LEFT JOIN data p ON p._id = (
			SELECT d._id FROM data d
			JOIN raw_contacts r ON r._id = d.raw_contact_id
			WHERE r.contact_id = contacts._id AND r.deleted = 0 AND d.mimetype = ?2
			ORDER BY d.is_super_primary DESC, d.is_primary DESC, d._id ASC
			LIMIT 1
		 )
		 WHERE COALESCE( contacts.display_name, '' ) LIKE ?1 COLLATE NOCASE
		   AND {NOT_FULLY_DELETED}
		 ORDER BY contacts.display_name COLLATE NOCASE"
	);
	let mut stmt = conn.prepare( &sql )?;
	let rows: Vec<ContactSummary> = stmt
		.query_map( params![ pattern, crate::model::mimetype::PHONE_V2 ], row_to_summary )?
		.collect::<rusqlite::Result<_>>()?;
	Ok( rows )
}

/// Update fields owned by `contacts`. `Some( v )` overwrites; `None` leaves
/// the column alone. Does NOT record a change_log entry: these fields are
/// not exposed via CardDAV, so consumers don't need a sync-token bump.
/// (D-Bus signals will still fire from the dbus dispatcher in Phase 2.)
pub fn update( conn: &Connection, id: i64, patch: ContactPatch ) -> DbResult<()>
{
	let now = now_millis();

	let mut set_parts: Vec<String> = Vec::new();
	let mut vals: Vec<Value> = Vec::new();

	if let Some( v ) = patch.display_name
	{
		set_parts.push( "display_name = ?".into() );
		vals.push( Value::Text( v ) );
	}
	if let Some( v ) = patch.photo_id
	{
		set_parts.push( "photo_id = ?".into() );
		vals.push( Value::Integer( v ) );
	}
	if let Some( v ) = patch.starred
	{
		set_parts.push( "starred = ?".into() );
		vals.push( Value::Integer( v as i64 ) );
	}
	if let Some( v ) = patch.custom_ringtone
	{
		set_parts.push( "custom_ringtone = ?".into() );
		vals.push( Value::Text( v ) );
	}
	if let Some( v ) = patch.send_to_voicemail
	{
		set_parts.push( "send_to_voicemail = ?".into() );
		vals.push( Value::Integer( v as i64 ) );
	}
	if let Some( v ) = patch.times_contacted
	{
		set_parts.push( "times_contacted = ?".into() );
		vals.push( Value::Integer( v ) );
	}
	if let Some( v ) = patch.last_time_contacted
	{
		set_parts.push( "last_time_contacted = ?".into() );
		vals.push( Value::Integer( v ) );
	}

	if set_parts.is_empty()
	{
		// No-op patch: confirm the contact exists, then return.
		let exists: Option<i64> = conn.query_row(
			"SELECT _id FROM contacts WHERE _id = ?1",
			params![ id ],
			|row| row.get( 0 ),
		).optional()?;
		return exists.map( |_| () ).ok_or( DbError::NotFound );
	}

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

	let sql = format!(
		"UPDATE contacts SET {} WHERE _id = ?",
		set_parts.join( ", " ),
	);
	let affected = conn.execute( &sql, params_from_iter( vals.iter() ) )?;
	if affected == 0
	{
		return Err( DbError::NotFound );
	}
	Ok( () )
}

fn row_to_summary( row: &rusqlite::Row<'_> ) -> rusqlite::Result<ContactSummary>
{
	let phone_type: Option<String> = row.get( 5 )?;
	Ok( ContactSummary {
		id:           row.get( 0 )?,
		display_name: row.get( 1 )?,
		photo_id:     row.get( 2 )?,
		starred:      row.get::<_, i64>( 3 )? != 0,
		number:       row.get( 4 )?,
		phone_type:   phone_type.and_then( |s| s.parse::<i32>().ok() ),
		phone_label:  row.get( 6 )?,
	} )
}

fn row_to_contact( row: &rusqlite::Row<'_> ) -> rusqlite::Result<Contact>
{
	Ok( Contact {
		id:                  row.get( 0 )?,
		display_name:        row.get( 1 )?,
		photo_id:            row.get( 2 )?,
		times_contacted:     row.get( 3 )?,
		last_time_contacted: row.get( 4 )?,
		starred:             row.get::<_, i64>( 5 )? != 0,
		custom_ringtone:     row.get( 6 )?,
		send_to_voicemail:   row.get::<_, i64>( 7 )? != 0,
		created_at:          row.get( 8 )?,
		updated_at:          row.get( 9 )?,
	} )
}
