//! Per-mimetype mapping between the raw `DataRow` array and named-key dicts.
//!
//! Used by both the `dbus` layer (for `a{sv}` field encoding/decoding) and,
//! later, by the `carddav` layer (which needs typed access to phone numbers,
//! email addresses, photo blobs, etc. when emitting vCards).
//!
//! The mapping is data-driven: each first-class mimetype declares a static
//! list of `KeySpec` entries. Unknown mimetypes round-trip as raw
//! `data1`..`data14` text keys + an optional `blob` byte-string.

use std::collections::HashMap;

use thiserror::Error;

use crate::model::data_item::{ DataPatch, DataRow, NewDataRow, TEXT_FIELDS };
use crate::model::mimetype;

#[ derive( Debug, Clone, PartialEq, Eq ) ]
pub enum FieldValue
{
	Text( String ),
	Int32( i32 ),
	Bytes( Vec<u8> ),
}

impl FieldValue
{
	pub fn ty( &self ) -> FieldType
	{
		match self
		{
			FieldValue::Text( _ )  => FieldType::Text,
			FieldValue::Int32( _ ) => FieldType::Int32,
			FieldValue::Bytes( _ ) => FieldType::Bytes,
		}
	}
}

#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum FieldType
{
	Text,
	Int32,
	Bytes,
}

#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum Slot
{
	/// 1-based: `data1` .. `data14` (text columns).
	Data( usize ),
	/// `data15` BLOB column.
	Blob,
}

pub struct KeySpec
{
	pub name: &'static str,
	pub slot: Slot,
	pub ty: FieldType,
}

#[ derive( Debug, Error ) ]
pub enum CodecError
{
	#[ error( "type mismatch for key {key}: expected {expected:?}, got {actual:?}" ) ]
	TypeMismatch
	{
		key: String,
		expected: FieldType,
		actual: FieldType,
	},

	#[ error( "invalid raw fallback key {key:?} (must be data1..data14 or blob)" ) ]
	InvalidRawKey
	{
		key: String,
	},
}

const NAME_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "display_name",    slot: Slot::Data( 1 ), ty: FieldType::Text },
	KeySpec { name: "given_name",      slot: Slot::Data( 2 ), ty: FieldType::Text },
	KeySpec { name: "family_name",     slot: Slot::Data( 3 ), ty: FieldType::Text },
	KeySpec { name: "prefix",          slot: Slot::Data( 4 ), ty: FieldType::Text },
	KeySpec { name: "middle_name",     slot: Slot::Data( 5 ), ty: FieldType::Text },
	KeySpec { name: "suffix",          slot: Slot::Data( 6 ), ty: FieldType::Text },
	KeySpec { name: "phonetic_given",  slot: Slot::Data( 7 ), ty: FieldType::Text },
	KeySpec { name: "phonetic_middle", slot: Slot::Data( 8 ), ty: FieldType::Text },
	KeySpec { name: "phonetic_family", slot: Slot::Data( 9 ), ty: FieldType::Text },
];

const PHONE_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "number",            slot: Slot::Data( 1 ), ty: FieldType::Text  },
	KeySpec { name: "type",              slot: Slot::Data( 2 ), ty: FieldType::Int32 },
	KeySpec { name: "label",             slot: Slot::Data( 3 ), ty: FieldType::Text  },
	KeySpec { name: "normalized_number", slot: Slot::Data( 4 ), ty: FieldType::Text  },
];

const EMAIL_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "address",      slot: Slot::Data( 1 ), ty: FieldType::Text  },
	KeySpec { name: "type",         slot: Slot::Data( 2 ), ty: FieldType::Int32 },
	KeySpec { name: "label",        slot: Slot::Data( 3 ), ty: FieldType::Text  },
	KeySpec { name: "display_name", slot: Slot::Data( 4 ), ty: FieldType::Text  },
];

const POSTAL_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "formatted_address", slot: Slot::Data( 1 ),  ty: FieldType::Text  },
	KeySpec { name: "type",              slot: Slot::Data( 2 ),  ty: FieldType::Int32 },
	KeySpec { name: "label",             slot: Slot::Data( 3 ),  ty: FieldType::Text  },
	KeySpec { name: "street",            slot: Slot::Data( 4 ),  ty: FieldType::Text  },
	KeySpec { name: "pobox",             slot: Slot::Data( 5 ),  ty: FieldType::Text  },
	KeySpec { name: "neighborhood",      slot: Slot::Data( 6 ),  ty: FieldType::Text  },
	KeySpec { name: "city",              slot: Slot::Data( 7 ),  ty: FieldType::Text  },
	KeySpec { name: "region",            slot: Slot::Data( 8 ),  ty: FieldType::Text  },
	KeySpec { name: "postcode",          slot: Slot::Data( 9 ),  ty: FieldType::Text  },
	KeySpec { name: "country",           slot: Slot::Data( 10 ), ty: FieldType::Text  },
];

const PHOTO_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "photo_file_id", slot: Slot::Data( 14 ), ty: FieldType::Int32 },
	KeySpec { name: "photo",         slot: Slot::Blob,       ty: FieldType::Bytes },
];

const ORGANIZATION_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "company",         slot: Slot::Data( 1 ), ty: FieldType::Text  },
	KeySpec { name: "type",            slot: Slot::Data( 2 ), ty: FieldType::Int32 },
	KeySpec { name: "label",           slot: Slot::Data( 3 ), ty: FieldType::Text  },
	KeySpec { name: "title",           slot: Slot::Data( 4 ), ty: FieldType::Text  },
	KeySpec { name: "department",      slot: Slot::Data( 5 ), ty: FieldType::Text  },
	KeySpec { name: "job_description", slot: Slot::Data( 6 ), ty: FieldType::Text  },
	KeySpec { name: "symbol",          slot: Slot::Data( 7 ), ty: FieldType::Text  },
	KeySpec { name: "phonetic_name",   slot: Slot::Data( 8 ), ty: FieldType::Text  },
	KeySpec { name: "office_location", slot: Slot::Data( 9 ), ty: FieldType::Text  },
];

const NOTE_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "note", slot: Slot::Data( 1 ), ty: FieldType::Text },
];

const EVENT_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "start_date", slot: Slot::Data( 1 ), ty: FieldType::Text  },
	KeySpec { name: "type",       slot: Slot::Data( 2 ), ty: FieldType::Int32 },
	KeySpec { name: "label",      slot: Slot::Data( 3 ), ty: FieldType::Text  },
];

const IM_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "data",            slot: Slot::Data( 1 ), ty: FieldType::Text  },
	KeySpec { name: "type",            slot: Slot::Data( 2 ), ty: FieldType::Int32 },
	KeySpec { name: "label",           slot: Slot::Data( 3 ), ty: FieldType::Text  },
	KeySpec { name: "protocol",        slot: Slot::Data( 5 ), ty: FieldType::Int32 },
	KeySpec { name: "custom_protocol", slot: Slot::Data( 6 ), ty: FieldType::Text  },
];

const WEBSITE_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "url",   slot: Slot::Data( 1 ), ty: FieldType::Text  },
	KeySpec { name: "type",  slot: Slot::Data( 2 ), ty: FieldType::Int32 },
	KeySpec { name: "label", slot: Slot::Data( 3 ), ty: FieldType::Text  },
];

const NICKNAME_KEYS: &[ KeySpec ] = &[
	KeySpec { name: "name",  slot: Slot::Data( 1 ), ty: FieldType::Text  },
	KeySpec { name: "type",  slot: Slot::Data( 2 ), ty: FieldType::Int32 },
	KeySpec { name: "label", slot: Slot::Data( 3 ), ty: FieldType::Text  },
];

pub fn keymap_for( mimetype: &str ) -> Option<&'static [ KeySpec ]>
{
	match mimetype
	{
		mimetype::NAME         => Some( NAME_KEYS ),
		mimetype::PHONE_V2     => Some( PHONE_KEYS ),
		mimetype::EMAIL_V2     => Some( EMAIL_KEYS ),
		mimetype::POSTAL_V2    => Some( POSTAL_KEYS ),
		mimetype::PHOTO        => Some( PHOTO_KEYS ),
		mimetype::ORGANIZATION => Some( ORGANIZATION_KEYS ),
		mimetype::NOTE         => Some( NOTE_KEYS ),
		mimetype::EVENT        => Some( EVENT_KEYS ),
		mimetype::IM           => Some( IM_KEYS ),
		mimetype::WEBSITE      => Some( WEBSITE_KEYS ),
		mimetype::NICKNAME     => Some( NICKNAME_KEYS ),
		_                      => None,
	}
}

/// Encode a stored `DataRow` as a named-key dict suitable for the D-Bus
/// `a{sv}` representation. Unknown mimetypes round-trip as raw `dataN` keys
/// plus an optional `blob`.
pub fn row_to_fields( row: &DataRow ) -> HashMap<String, FieldValue>
{
	let mut out = HashMap::new();

	if let Some( spec ) = keymap_for( &row.mimetype )
	{
		for entry in spec
		{
			if let Some( v ) = read_slot( row, entry.slot, entry.ty )
			{
				out.insert( entry.name.into(), v );
			}
		}
	} else {
		for ( i, slot ) in row.fields.iter().enumerate()
		{
			if let Some( v ) = slot
			{
				out.insert( format!( "data{}", i + 1 ), FieldValue::Text( v.clone() ) );
			}
		}
		if let Some( b ) = &row.blob
		{
			out.insert( "blob".into(), FieldValue::Bytes( b.clone() ) );
		}
	}

	out
}

/// Build a `NewDataRow` from a named-key dict.
pub fn new_data_row_from_fields(
	raw_contact_id: i64,
	mimetype: &str,
	fields: HashMap<String, FieldValue>,
) -> Result<NewDataRow, CodecError>
{
	let resolved = resolve( mimetype, fields )?;
	let mut row = NewDataRow::new( raw_contact_id, mimetype );
	for ResolvedField { slot, value } in resolved
	{
		match ( slot, value )
		{
			( Slot::Data( n ), FieldValue::Text( s ) )  => row.fields[ n - 1 ] = Some( s ),
			( Slot::Data( n ), FieldValue::Int32( i ) ) => row.fields[ n - 1 ] = Some( i.to_string() ),
			( Slot::Blob,      FieldValue::Bytes( b ) ) => row.blob = Some( b ),
			// Type was verified by `resolve`, so other combinations are unreachable.
			_ => unreachable!( "resolve() should have filtered mismatched (slot, value) pairs" ),
		}
	}
	Ok( row )
}

/// Build a tri-state `DataPatch` from a named-key dict. Keys present become
/// `Some( Some( v ) )` (set); absent keys stay `None`. To clear a column to
/// NULL via D-Bus there is no dict form yet — internal callers can build a
/// `DataPatch` directly with `Some( None )`.
pub fn data_patch_from_fields(
	mimetype: &str,
	fields: HashMap<String, FieldValue>,
) -> Result<DataPatch, CodecError>
{
	let resolved = resolve( mimetype, fields )?;
	let mut patch = DataPatch::default();
	for ResolvedField { slot, value } in resolved
	{
		match ( slot, value )
		{
			( Slot::Data( n ), FieldValue::Text( s ) )  => patch.fields[ n - 1 ] = Some( Some( s ) ),
			( Slot::Data( n ), FieldValue::Int32( i ) ) => patch.fields[ n - 1 ] = Some( Some( i.to_string() ) ),
			( Slot::Blob,      FieldValue::Bytes( b ) ) => patch.blob = Some( Some( b ) ),
			_ => unreachable!( "resolve() should have filtered mismatched (slot, value) pairs" ),
		}
	}
	Ok( patch )
}

struct ResolvedField
{
	slot: Slot,
	value: FieldValue,
}

/// Validate the dict against the mimetype's keymap and return a flat list of
/// `( slot, value )` pairs ready to be applied. Unknown keys for a known
/// mimetype are silently dropped (forward compatibility); typed mismatches
/// are rejected.
fn resolve(
	mimetype: &str,
	fields: HashMap<String, FieldValue>,
) -> Result<Vec<ResolvedField>, CodecError>
{
	let mut out = Vec::with_capacity( fields.len() );

	if let Some( spec ) = keymap_for( mimetype )
	{
		for ( key, value ) in fields
		{
			let Some( entry ) = spec.iter().find( |s| s.name == key ) else
			{
				continue;
			};
			ensure_type( &key, entry.ty, value.ty() )?;
			out.push( ResolvedField { slot: entry.slot, value } );
		}
	} else {
		for ( key, value ) in fields
		{
			let slot = if key == "blob"
			{
				ensure_type( &key, FieldType::Bytes, value.ty() )?;
				Slot::Blob
			} else if let Some( n ) = parse_data_key( &key ) {
				if matches!( value, FieldValue::Bytes( _ ) )
				{
					return Err( CodecError::TypeMismatch
					{
						key,
						expected: FieldType::Text,
						actual: FieldType::Bytes,
					} );
				}
				Slot::Data( n )
			} else {
				return Err( CodecError::InvalidRawKey { key } );
			};
			out.push( ResolvedField { slot, value } );
		}
	}

	Ok( out )
}

fn read_slot( row: &DataRow, slot: Slot, ty: FieldType ) -> Option<FieldValue>
{
	match slot
	{
		Slot::Data( n ) =>
		{
			let raw = row.fields.get( n - 1 )?.as_ref()?;
			match ty
			{
				FieldType::Text  => Some( FieldValue::Text( raw.clone() ) ),
				FieldType::Int32 => raw.parse::<i32>().ok().map( FieldValue::Int32 ),
				// A Bytes-typed key on a text slot is a schema bug; ignore.
				FieldType::Bytes => None,
			}
		}
		Slot::Blob => row.blob.as_ref().map( |b| FieldValue::Bytes( b.clone() ) ),
	}
}

fn parse_data_key( key: &str ) -> Option<usize>
{
	let n = key.strip_prefix( "data" )?.parse::<usize>().ok()?;
	if ( 1..=TEXT_FIELDS ).contains( &n ) { Some( n ) } else { None }
}

fn ensure_type( key: &str, expected: FieldType, actual: FieldType ) -> Result<(), CodecError>
{
	if expected == actual
	{
		Ok( () )
	} else {
		Err( CodecError::TypeMismatch
		{
			key: key.to_owned(),
			expected,
			actual,
		} )
	}
}

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

	fn make_row( mimetype: &str, fields: [ Option<&str>; TEXT_FIELDS ] ) -> DataRow
	{
		DataRow
		{
			id: 1,
			raw_contact_id: 1,
			mimetype: mimetype.into(),
			fields: fields.map( |o| o.map( |s| s.to_owned() ) ),
			blob: None,
			is_primary: false,
			is_super_primary: false,
			data_version: 1,
			sync: Default::default(),
			created_at: 0,
			updated_at: 0,
		}
	}

	#[ test ]
	fn phone_row_to_fields_uses_named_keys()
	{
		let mut fields: [ Option<&str>; TEXT_FIELDS ] = Default::default();
		fields[ 0 ] = Some( "+44 20 7946 0958" );
		fields[ 1 ] = Some( "2" );
		fields[ 3 ] = Some( "+442079460958" );
		let row = make_row( mimetype::PHONE_V2, fields );
		let dict = row_to_fields( &row );
		assert_eq!( dict.get( "number" ), Some( &FieldValue::Text( "+44 20 7946 0958".into() ) ) );
		assert_eq!( dict.get( "type" ),   Some( &FieldValue::Int32( 2 ) ) );
		assert_eq!( dict.get( "normalized_number" ), Some( &FieldValue::Text( "+442079460958".into() ) ) );
		assert!( dict.get( "label" ).is_none(), "absent slots are not emitted" );
	}

	#[ test ]
	fn phone_fields_to_new_data_row_round_trips()
	{
		let mut fields = HashMap::new();
		fields.insert( "number".into(),            FieldValue::Text( "+44 20 7946 0958".into() ) );
		fields.insert( "type".into(),              FieldValue::Int32( 2 ) );
		fields.insert( "normalized_number".into(), FieldValue::Text( "+442079460958".into() ) );

		let row = new_data_row_from_fields( 7, mimetype::PHONE_V2, fields ).unwrap();
		assert_eq!( row.raw_contact_id, 7 );
		assert_eq!( row.fields[ 0 ].as_deref(), Some( "+44 20 7946 0958" ) );
		assert_eq!( row.fields[ 1 ].as_deref(), Some( "2" ) );
		assert_eq!( row.fields[ 3 ].as_deref(), Some( "+442079460958" ) );
	}

	#[ test ]
	fn type_mismatch_is_rejected()
	{
		let mut fields = HashMap::new();
		// `type` expects Int32; passing Text must error.
		fields.insert( "type".into(), FieldValue::Text( "mobile".into() ) );
		let err = new_data_row_from_fields( 1, mimetype::PHONE_V2, fields ).unwrap_err();
		assert!( matches!( err, CodecError::TypeMismatch { .. } ) );
	}

	#[ test ]
	fn unknown_mimetype_passes_raw_keys_through()
	{
		let mut fields = HashMap::new();
		fields.insert( "data1".into(), FieldValue::Text( "raw".into() ) );
		fields.insert( "data3".into(), FieldValue::Int32( 7 ) );
		fields.insert( "blob".into(),  FieldValue::Bytes( vec![ 1, 2, 3 ] ) );

		let row = new_data_row_from_fields( 1, "vnd.android.cursor.item/relation", fields ).unwrap();
		assert_eq!( row.fields[ 0 ].as_deref(), Some( "raw" ) );
		assert_eq!( row.fields[ 2 ].as_deref(), Some( "7" ) );
		assert_eq!( row.blob.as_deref(), Some( &[ 1u8, 2, 3 ][..] ) );
	}
}
