//! SQLite persistence layer.
//!
//! Operations are synchronous (rusqlite is sync). Async callers wrap them
//! with `smol::unblock` to avoid stalling the executor. The module exposes a
//! thin `Database` handle holding a single locked connection — one per
//! daemon instance is enough for our write rates, and WAL mode keeps reads
//! lock-free against in-flight writers.

use std::path::Path;
use std::sync::{ Mutex, MutexGuard };
use std::time::{ SystemTime, UNIX_EPOCH };

use rusqlite::Connection;
use thiserror::Error;

pub mod schema;
pub mod sync_state;
pub mod raw_contacts;
pub mod contacts;
pub mod data;
pub mod groups;

#[ derive( Debug, Error ) ]
pub enum DbError
{
	#[ error( "sqlite: {0}" ) ]
	Sqlite( #[ from ] rusqlite::Error ),

	#[ error( "not found" ) ]
	NotFound,

	#[ error( "invalid argument: {0}" ) ]
	InvalidArgument( String ),
}

pub type DbResult<T> = Result<T, DbError>;

pub struct Database
{
	conn: Mutex<Connection>,
}

impl Database
{
	pub fn open( path: &Path ) -> DbResult<Self>
	{
		let mut conn = Connection::open( path )?;
		configure_pragmas( &conn )?;
		schema::apply_migrations( &mut conn )?;
		Ok( Self { conn: Mutex::new( conn ) } )
	}

	pub fn open_in_memory() -> DbResult<Self>
	{
		let mut conn = Connection::open_in_memory()?;
		configure_pragmas( &conn )?;
		schema::apply_migrations( &mut conn )?;
		Ok( Self { conn: Mutex::new( conn ) } )
	}

	/// Borrow the underlying connection for the duration of `f`.
	///
	/// The closure runs while the mutex is held — keep it short. For
	/// transactional writes use `Connection::transaction()` inside `f`.
	pub fn with_conn<F, R>( &self, f: F ) -> R
	where
		F: FnOnce( &mut Connection ) -> R,
	{
		let mut guard = self.lock();
		f( &mut guard )
	}

	fn lock( &self ) -> MutexGuard<'_, Connection>
	{
		// A poisoned mutex means a previous holder panicked while inside
		// `with_conn`. The DB itself is not corrupted (rusqlite uses its
		// own locking), so we recover the inner guard rather than abort.
		match self.conn.lock()
		{
			Ok( g )  => g,
			Err( p ) => p.into_inner(),
		}
	}
}

fn configure_pragmas( conn: &Connection ) -> DbResult<()>
{
	conn.pragma_update( None, "journal_mode", "WAL" )?;
	conn.pragma_update( None, "synchronous",  "NORMAL" )?;
	conn.pragma_update( None, "foreign_keys", "ON" )?;
	Ok( () )
}

/// Milliseconds since the Unix epoch. Used everywhere for `*_at` columns.
pub fn now_millis() -> i64
{
	SystemTime::now()
		.duration_since( UNIX_EPOCH )
		.map( |d| d.as_millis() as i64 )
		.unwrap_or( 0 )
}

/// Kind of change recorded in `change_log`. `Put` covers both inserts and
/// updates from the CardDAV viewpoint — clients re-fetch the resource either
/// way. `Delete` removes the resource on the client.
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum ChangeKind
{
	Put,
	Delete,
}

impl ChangeKind
{
	pub fn as_str( self ) -> &'static str
	{
		match self
		{
			ChangeKind::Put    => "put",
			ChangeKind::Delete => "delete",
		}
	}
}

#[ cfg( test ) ]
mod tests
{
	use super::*;
	use crate::model::data_item::NewDataRow;
	use crate::model::mimetype;
	use crate::model::raw_contact::{ NewRawContact, RawContactPatch };

	#[ test ]
	fn full_roundtrip()
	{
		let db = Database::open_in_memory().expect( "open" );

		let ( raw_id, contact_id, name_id, phone_id ) = db.with_conn( |c| -> DbResult<_> {
			let ( raw_id, contact_id ) = raw_contacts::insert( c, NewRawContact {
				account_name: Some( "liberux-local".into() ),
				account_type: Some( "org.liberux.local".into() ),
				display_name: Some( "Ada Lovelace".into() ),
				sourceid: None,
			} )?;

			let raw = raw_contacts::get( c, raw_id )?;
			assert_eq!( raw.display_name.as_deref(), Some( "Ada Lovelace" ) );
			assert_eq!( raw.contact_id, contact_id );

			let name_id = data::insert( c, NewDataRow::new( raw_id, mimetype::NAME )
				.with_data( 1, "Ada Lovelace" )
				.with_data( 2, "Ada" )
				.with_data( 3, "Lovelace" ) )?;

			let phone_id = data::insert( c, NewDataRow::new( raw_id, mimetype::PHONE_V2 )
				.with_data( 1, "+44 20 7946 0958" )
				.with_data( 2, "2" )
				.with_data( 4, "+442079460958" ) )?;

			Ok( ( raw_id, contact_id, name_id, phone_id ) )
		} ).expect( "insert path" );

		db.with_conn( |c| -> DbResult<()> {
			let phone_rows = data::query( c, raw_id, Some( mimetype::PHONE_V2 ) )?;
			assert_eq!( phone_rows.len(), 1 );
			assert_eq!( phone_rows[ 0 ].id, phone_id );
			assert_eq!( phone_rows[ 0 ].fields[ 0 ].as_deref(), Some( "+44 20 7946 0958" ) );

			let all_rows = data::query( c, raw_id, None )?;
			assert_eq!( all_rows.len(), 2 );

			let by_phone = data::search_by_normalized_number( c, "+442079460958" )?;
			assert_eq!( by_phone, vec![ raw_id ] );

			let listing = contacts::list( c, "" )?;
			assert_eq!( listing.len(), 1 );
			assert_eq!( listing[ 0 ].id, contact_id );

			raw_contacts::update( c, raw_id, RawContactPatch {
				display_name: Some( Some( "Augusta Ada Lovelace".into() ) ),
				..Default::default()
			} )?;
			assert_eq!(
				raw_contacts::contact_id_of( c, raw_id )?,
				contact_id,
				"contact_id_of must match the id returned by insert",
			);
			let raw = raw_contacts::get( c, raw_id )?;
			assert_eq!( raw.display_name.as_deref(), Some( "Augusta Ada Lovelace" ) );
			assert_eq!( raw.version, 2 );

			let counter_after_writes = sync_state::change_counter( c )?;
			assert!( counter_after_writes >= 4, "counter = {}", counter_after_writes );

			let _name = data::get( c, name_id )?;

			data::delete( c, phone_id )?;
			assert!( matches!( data::get( c, phone_id ), Err( DbError::NotFound ) ) );

			raw_contacts::soft_delete( c, raw_id )?;
			assert!( matches!( raw_contacts::get( c, raw_id ), Err( DbError::NotFound ) ) );

			Ok( () )
		} ).expect( "query/update/delete path" );
	}
}
