//! `ApplyBatch` handler — runs a sequence of mutations atomically inside
//! a single SQL transaction, with one shared `change_counter` bump and
//! one `BatchChanged` D-Bus signal at the end.
//!
//! Wire shapes (see `docs/dbus-api.md`):
//!
//! - input  `a(s sa{sv})` — list of `( kind, target, args )`
//! - output `a(s a{sv})`  — parallel list of `( result_kind, payload )`
//!
//! Op kinds:
//!
//! - `"insert_raw"`   target ignored,            args = [`NewRawContact`-like]
//! - `"update_raw"`   target = raw_id or `"@N"`, args = [`RawContactPatch`-like]
//! - `"delete_raw"`   target = raw_id or `"@N"`, args = `{}`
//! - `"add_data"`     target = raw_id or `"@N"`, args = `{ mimetype: s, …per-mimetype keys }`
//! - `"update_data"`  target = data_id (decimal), args = `{ …per-mimetype keys }`
//! - `"delete_data"`  target = data_id,          args = `{}`
//!
//! Back-references: `"@N"` resolves to the raw_contact id returned by
//! the N-th preceding `insert_raw` in the same batch (zero-based).

use std::collections::HashMap;

use rusqlite::{ Connection, Transaction };
use serde::{ Deserialize, Serialize };
use zbus::zvariant::{ OwnedValue, Type, Value };

use crate::db::{ self, ChangeKind, DbError, now_millis };
use crate::dbus::codec::{ dict_to_field_map, owned_to_field_value };
use crate::dbus::error::Error;
use crate::model::data_fields::{ self, FieldValue };
use crate::model::raw_contact::{ NewRawContact, RawContactPatch };

#[ derive( Debug, Serialize, Deserialize, Type ) ]
pub struct BatchOp
{
	pub kind: String,
	pub target: String,
	pub args: HashMap<String, OwnedValue>,
}

#[ derive( Debug, Serialize, Deserialize, Type ) ]
pub struct BatchResult
{
	pub kind: String,
	pub payload: HashMap<String, OwnedValue>,
}

impl BatchResult
{
	fn ok() -> Self
	{
		Self { kind: "ok".into(), payload: HashMap::new() }
	}

	fn id( id: u32 ) -> Result<Self, Error>
	{
		let mut p = HashMap::new();
		let v = Value::from( id ).try_to_owned()
			.map_err( |e| Error::Internal( format!( "encoding id: {e}" ) ) )?;
		p.insert( "id".into(), v );
		Ok( Self { kind: "id".into(), payload: p } )
	}
}

/// Apply a batch in a single transaction. Returns the per-op results in
/// order. On any error the transaction is rolled back (via Drop) and a
/// typed `Error` carrying the failing index is returned.
pub fn apply(
	conn: &mut Connection,
	ops: Vec<BatchOp>,
) -> Result<Vec<BatchResult>, Error>
{
	let now = now_millis();
	let tx = conn.transaction().map_err( DbError::from ).map_err( Error::from )?;

	// `new_raw_ids[N]` = the raw_contact_id returned by the N-th
	// `insert_raw` in this batch — drives `"@N"` back-reference resolution.
	let mut new_raw_ids: Vec<i64> = Vec::new();

	// Coalesce per-raw_contact change records: Delete dominates Put.
	let mut log_kinds: HashMap<i64, ChangeKind> = HashMap::new();

	let mut results: Vec<BatchResult> = Vec::with_capacity( ops.len() );

	for ( idx, op ) in ops.into_iter().enumerate()
	{
		let r = apply_one( &tx, op, idx, &mut new_raw_ids, &mut log_kinds, now )?;
		results.push( r );
	}

	// Sort affected raw_contacts by id for deterministic change_log ordering.
	let mut affected: Vec<( i64, ChangeKind )> = log_kinds.into_iter().collect();
	affected.sort_by_key( |( id, _ )| *id );
	db::sync_state::record_batch_changes( &tx, &affected )
		.map_err( DbError::from ).map_err( Error::from )?;

	tx.commit().map_err( DbError::from ).map_err( Error::from )?;

	Ok( results )
}

fn apply_one(
	tx: &Transaction<'_>,
	op: BatchOp,
	idx: usize,
	new_raw_ids: &mut Vec<i64>,
	log_kinds: &mut HashMap<i64, ChangeKind>,
	now: i64,
) -> Result<BatchResult, Error>
{
	match op.kind.as_str()
	{
		"insert_raw" =>
		{
			let new = decode_new_raw_contact( op.args, idx )?;
			let ( raw_id, _contact_id ) = db::raw_contacts::insert_in_tx( tx, &new, now )
				.map_err( |e| op_err( idx, "insert_raw", e ) )?;
			new_raw_ids.push( raw_id );
			mark( log_kinds, raw_id, ChangeKind::Put );
			BatchResult::id( raw_id as u32 )
		}
		"update_raw" =>
		{
			let id = resolve_target( &op.target, new_raw_ids, idx )?;
			let patch = decode_raw_contact_patch( op.args, idx )?;
			db::raw_contacts::update_in_tx( tx, id, &patch, now )
				.map_err( |e| op_err( idx, "update_raw", e ) )?;
			mark( log_kinds, id, ChangeKind::Put );
			Ok( BatchResult::ok() )
		}
		"delete_raw" =>
		{
			let id = resolve_target( &op.target, new_raw_ids, idx )?;
			db::raw_contacts::soft_delete_in_tx( tx, id, now )
				.map_err( |e| op_err( idx, "delete_raw", e ) )?;
			mark( log_kinds, id, ChangeKind::Delete );
			Ok( BatchResult::ok() )
		}
		"add_data" =>
		{
			let raw_id = resolve_target( &op.target, new_raw_ids, idx )?;
			let mimetype = take_string_arg( &op.args, "mimetype", idx )?;
			let mut args = op.args;
			args.remove( "mimetype" );
			let fmap = dict_to_field_map( args ).map_err( Error::from )?;
			let new = data_fields::new_data_row_from_fields( raw_id, &mimetype, fmap )
				.map_err( Error::from )?;
			let data_id = db::data::insert_in_tx( tx, &new, now )
				.map_err( |e| op_err( idx, "add_data", e ) )?;
			mark( log_kinds, raw_id, ChangeKind::Put );
			BatchResult::id( data_id as u32 )
		}
		"update_data" =>
		{
			let id = parse_id( &op.target, idx )?;
			let mimetype = db::data::mimetype_of( tx, id )
				.map_err( |e| op_err( idx, "update_data", e ) )?;
			let fmap = dict_to_field_map( op.args ).map_err( Error::from )?;
			let patch = data_fields::data_patch_from_fields( &mimetype, fmap )
				.map_err( Error::from )?;
			let raw_id = db::data::update_in_tx( tx, id, &patch, now )
				.map_err( |e| op_err( idx, "update_data", e ) )?;
			mark( log_kinds, raw_id, ChangeKind::Put );
			Ok( BatchResult::ok() )
		}
		"delete_data" =>
		{
			let id = parse_id( &op.target, idx )?;
			let raw_id = db::data::delete_in_tx( tx, id, now )
				.map_err( |e| op_err( idx, "delete_data", e ) )?;
			mark( log_kinds, raw_id, ChangeKind::Put );
			Ok( BatchResult::ok() )
		}
		other => Err( Error::InvalidArgument(
			format!( "operation #{idx}: unknown kind {:?}", other ),
		) ),
	}
}

fn mark( log_kinds: &mut HashMap<i64, ChangeKind>, raw_id: i64, kind: ChangeKind )
{
	let entry = log_kinds.entry( raw_id ).or_insert( kind );
	if matches!( kind, ChangeKind::Delete )
	{
		*entry = ChangeKind::Delete;
	}
}

fn op_err( idx: usize, kind: &str, e: DbError ) -> Error
{
	match e
	{
		DbError::NotFound => Error::NotFound( format!(
			"operation #{idx} ({kind}): target row not found",
		) ),
		DbError::InvalidArgument( msg ) => Error::InvalidArgument( format!(
			"operation #{idx} ({kind}): {msg}",
		) ),
		DbError::Sqlite( inner ) => Error::Internal( format!(
			"operation #{idx} ({kind}): sqlite: {inner}",
		) ),
	}
}

fn resolve_target( target: &str, new_raw_ids: &[ i64 ], idx: usize ) -> Result<i64, Error>
{
	if let Some( rest ) = target.strip_prefix( '@' )
	{
		let n: usize = rest.parse().map_err( |_| Error::InvalidArgument(
			format!( "operation #{idx}: invalid back-reference {:?}", target ),
		) )?;
		new_raw_ids.get( n ).copied().ok_or_else( || Error::InvalidArgument(
			format!(
				"operation #{idx}: back-reference @{n} but only {} insert_raw ops have run",
				new_raw_ids.len(),
			),
		) )
	} else {
		parse_id( target, idx )
	}
}

fn parse_id( target: &str, idx: usize ) -> Result<i64, Error>
{
	target.parse::<i64>().map_err( |_| Error::InvalidArgument(
		format!( "operation #{idx}: invalid id {:?}", target ),
	) )
}

fn decode_new_raw_contact(
	dict: HashMap<String, OwnedValue>,
	idx: usize,
) -> Result<NewRawContact, Error>
{
	let mut new = NewRawContact::default();
	for ( key, value ) in dict
	{
		let s = extract_string( &value, &key, idx )?;
		match key.as_str()
		{
			"account_name" => new.account_name = Some( s ),
			"account_type" => new.account_type = Some( s ),
			"display_name" => new.display_name = Some( s ),
			"sourceid"     => new.sourceid     = Some( s ),
			_              => {}
		}
	}
	if new.account_type.is_none()
	{
		new.account_type = Some( "org.liberux.local".to_owned() );
	}
	Ok( new )
}

fn decode_raw_contact_patch(
	dict: HashMap<String, OwnedValue>,
	idx: usize,
) -> Result<RawContactPatch, Error>
{
	let mut patch = RawContactPatch::default();
	for ( key, value ) in dict
	{
		let s = extract_string( &value, &key, idx )?;
		match key.as_str()
		{
			"account_name" => patch.account_name = Some( Some( s ) ),
			"account_type" => patch.account_type = Some( Some( s ) ),
			"display_name" => patch.display_name = Some( Some( s ) ),
			"sourceid"     => patch.sourceid     = Some( Some( s ) ),
			_              => {}
		}
	}
	Ok( patch )
}

fn take_string_arg(
	dict: &HashMap<String, OwnedValue>,
	key: &str,
	idx: usize,
) -> Result<String, Error>
{
	let v = dict.get( key ).ok_or_else( || Error::InvalidArgument(
		format!( "operation #{idx}: missing required arg {:?}", key ),
	) )?;
	extract_string( v, key, idx )
}

fn extract_string( v: &OwnedValue, key: &str, idx: usize ) -> Result<String, Error>
{
	match owned_to_field_value( v ).map_err( Error::from )?
	{
		FieldValue::Text( s ) => Ok( s ),
		other => Err( Error::InvalidArgument( format!(
			"operation #{idx}: key {:?}: expected string, got {:?}",
			key, other.ty(),
		) ) ),
	}
}
