//! Minimal XML parsing for CardDAV REPORT request bodies.
//!
//! v1 only inspects the structure to extract:
//! - which REPORT was requested (root element name);
//! - whether `<C:address-data/>` appeared inside `<D:prop>` (so handlers
//!   know to inline the vCard body);
//! - the `<D:sync-token>` text for `sync-collection`;
//! - the list of `<D:href>` entries for `addressbook-multiget`.
//!
//! Filter elements (`<C:filter>...`) are intentionally ignored — Phase 4
//! returns the full collection regardless. A future v1.x revision can
//! parse `prop-filter` once a real consumer needs it.

use thiserror::Error;

use quick_xml::Reader;
use quick_xml::events::Event;
use quick_xml::name::QName;

/// Top-level REPORT type, as identified by the request body's root element.
#[ derive( Debug, Clone, PartialEq, Eq ) ]
pub enum ReportKind
{
	AddressbookQuery,
	AddressbookMultiget,
	SyncCollection,
	Unknown( String ),
}

/// Parsed REPORT request. All fields are optional and the handler picks
/// the ones it needs.
#[ derive( Debug, Default ) ]
pub struct ReportRequest
{
	pub kind: Option<ReportKind>,
	pub want_address_data: bool,
	/// `Some( "" )` indicates an explicit empty `<sync-token/>` (initial
	/// sync); `None` means the element was absent.
	pub sync_token: Option<String>,
	pub multiget_hrefs: Vec<String>,
}

#[ derive( Debug, Error ) ]
pub enum ParseError
{
	#[ error( "xml: {0}" ) ]
	Xml( String ),

	#[ error( "non-utf8 element name" ) ]
	Encoding,

	#[ error( "empty body or missing root element" ) ]
	Empty,
}

impl From<quick_xml::Error> for ParseError
{
	fn from( e: quick_xml::Error ) -> Self
	{
		ParseError::Xml( e.to_string() )
	}
}

pub fn parse_report( body: &[ u8 ] ) -> Result<ReportRequest, ParseError>
{
	let mut reader = Reader::from_reader( body );
	let mut buf: Vec<u8> = Vec::with_capacity( 1024 );

	let mut req = ReportRequest::default();
	let mut depth: usize = 0;
	let mut in_prop      = false;
	let mut in_sync      = false;
	let mut in_href      = false;
	let mut current_text = String::new();

	loop
	{
		let event = reader.read_event_into( &mut buf )?;
		match event
		{
			Event::Start( e ) =>
			{
				let local = local_name( &e.name() )?;
				if depth == 0
				{
					req.kind = Some( classify_root( &local ) );
				} else if local == "prop" {
					in_prop = true;
				} else if local == "address-data" && in_prop {
					req.want_address_data = true;
				} else if local == "sync-token" {
					in_sync = true;
					current_text.clear();
				} else if local == "href" {
					in_href = true;
					current_text.clear();
				}
				depth += 1;
			}
			Event::Empty( e ) =>
			{
				let local = local_name( &e.name() )?;
				if depth == 0
				{
					// Self-closing root element (e.g. `<D:expand-property/>`) —
					// quick-xml emits a single Empty event with no Start/End
					// pair, so we classify here too. `depth` does not change
					// for Empty, so the rest of the state machine is
					// unaffected.
					req.kind = Some( classify_root( &local ) );
				} else if local == "address-data" && in_prop {
					req.want_address_data = true;
				} else if local == "sync-token" && depth >= 1 {
					req.sync_token = Some( String::new() );
				}
			}
			Event::Text( e ) =>
			{
				if in_sync || in_href
				{
					let unescaped = e.unescape().map_err( ParseError::from )?;
					current_text.push_str( unescaped.as_ref() );
				}
			}
			Event::End( e ) =>
			{
				let local = local_name( &e.name() )?;
				if local == "prop"
				{
					in_prop = false;
				} else if local == "sync-token" {
					in_sync = false;
					req.sync_token = Some( current_text.trim().to_owned() );
					current_text.clear();
				} else if local == "href" {
					in_href = false;
					let trimmed = current_text.trim().to_owned();
					if !trimmed.is_empty()
					{
						req.multiget_hrefs.push( trimmed );
					}
					current_text.clear();
				}
				depth = depth.saturating_sub( 1 );
			}
			Event::Eof => break,
			_ => {}
		}
		buf.clear();
	}

	if req.kind.is_none()
	{
		return Err( ParseError::Empty );
	}
	Ok( req )
}

fn classify_root( local: &str ) -> ReportKind
{
	match local
	{
		"addressbook-query"    => ReportKind::AddressbookQuery,
		"addressbook-multiget" => ReportKind::AddressbookMultiget,
		"sync-collection"      => ReportKind::SyncCollection,
		other                  => ReportKind::Unknown( other.to_owned() ),
	}
}

fn local_name( name: &QName<'_> ) -> Result<String, ParseError>
{
	let bytes = name.local_name();
	std::str::from_utf8( bytes.as_ref() )
		.map( str::to_owned )
		.map_err( |_| ParseError::Encoding )
}

/// Extract the carddav resource UUID from an `href` like
/// `/addressbook/main/<uuid>.vcf`. Tolerates both absolute paths and full
/// URLs (some clients send `http://host/...`).
pub fn href_to_carddav_uid( href: &str ) -> Option<&str>
{
	let path = if let Some( rest ) = href.find( "://" )
	{
		// Skip scheme://host
		let after_scheme = &href[ rest + 3.. ];
		match after_scheme.find( '/' )
		{
			Some( i ) => &after_scheme[ i.. ],
			None      => return None,
		}
	} else {
		href
	};
	let rest = path.strip_prefix( "/addressbook/main/" )?;
	rest.strip_suffix( ".vcf" )
}

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

	#[ test ]
	fn parse_addressbook_query_with_address_data()
	{
		let body = br#"<?xml version="1.0"?>
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
  <D:prop>
    <D:getetag/>
    <C:address-data/>
  </D:prop>
  <C:filter>
    <C:prop-filter name="FN"/>
  </C:filter>
</C:addressbook-query>"#;
		let req = parse_report( body ).unwrap();
		assert_eq!( req.kind, Some( ReportKind::AddressbookQuery ) );
		assert!( req.want_address_data );
		assert!( req.sync_token.is_none() );
		assert!( req.multiget_hrefs.is_empty() );
	}

	#[ test ]
	fn parse_addressbook_multiget()
	{
		let body = br#"<?xml version="1.0"?>
<C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
  <D:prop><D:getetag/><C:address-data/></D:prop>
  <D:href>/addressbook/main/aaa.vcf</D:href>
  <D:href>/addressbook/main/bbb.vcf</D:href>
</C:addressbook-multiget>"#;
		let req = parse_report( body ).unwrap();
		assert_eq!( req.kind, Some( ReportKind::AddressbookMultiget ) );
		assert!( req.want_address_data );
		assert_eq!( req.multiget_hrefs, vec![
			"/addressbook/main/aaa.vcf".to_owned(),
			"/addressbook/main/bbb.vcf".to_owned(),
		] );
	}

	#[ test ]
	fn parse_sync_collection_with_token()
	{
		let body = br#"<?xml version="1.0"?>
<D:sync-collection xmlns:D="DAV:">
  <D:sync-token>data:,42</D:sync-token>
  <D:sync-level>1</D:sync-level>
  <D:prop><D:getetag/></D:prop>
</D:sync-collection>"#;
		let req = parse_report( body ).unwrap();
		assert_eq!( req.kind, Some( ReportKind::SyncCollection ) );
		assert_eq!( req.sync_token.as_deref(), Some( "data:,42" ) );
		assert!( !req.want_address_data );
	}

	#[ test ]
	fn parse_sync_collection_initial()
	{
		let body = br#"<?xml version="1.0"?>
<D:sync-collection xmlns:D="DAV:">
  <D:sync-token/>
  <D:prop><D:getetag/></D:prop>
</D:sync-collection>"#;
		let req = parse_report( body ).unwrap();
		assert_eq!( req.kind, Some( ReportKind::SyncCollection ) );
		assert_eq!( req.sync_token.as_deref(), Some( "" ) );
	}

	#[ test ]
	fn parse_self_closed_unknown_root_classifies_as_unknown()
	{
		// Regression: a self-closing root element used to leave req.kind
		// = None and surface as ParseError::Empty (HTTP 400). Now it must
		// classify as Unknown so the dispatcher can return 403.
		let body = br#"<?xml version="1.0"?><D:expand-property xmlns:D="DAV:"/>"#;
		let req = parse_report( body ).unwrap();
		assert_eq!(
			req.kind,
			Some( ReportKind::Unknown( "expand-property".to_owned() ) ),
		);
	}

	#[ test ]
	fn href_to_uid_handles_absolute_and_full_urls()
	{
		assert_eq!(
			href_to_carddav_uid( "/addressbook/main/abcd.vcf" ),
			Some( "abcd" ),
		);
		assert_eq!(
			href_to_carddav_uid( "http://127.0.0.1:1234/addressbook/main/efgh.vcf" ),
			Some( "efgh" ),
		);
		assert_eq!( href_to_carddav_uid( "/principals/" ), None );
	}
}
