//! CardDAV REPORT handlers (RFC 6352 + RFC 6578).
//!
//! Three reports are supported in v1:
//!
//! - `addressbook-query` — return every non-deleted contact (filter
//!   elements are intentionally ignored in v1; see `docs/carddav.md`).
//! - `addressbook-multiget` — fetch a client-supplied list of hrefs;
//!   missing/soft-deleted resources surface as 404 entries.
//! - `sync-collection` (RFC 6578) — incremental delta keyed by the
//!   `data:,<change_counter>` sync-token, emitted with a fresh token
//!   suffix at the end of the multistatus body.

use std::sync::Arc;

use crate::carddav::propfind::MultistatusBody;
use crate::carddav::snapshot::{ self, ContactSnapshot };
use crate::carddav::xml::{ self, ReportKind, ReportRequest };
use crate::carddav::{ CardDavError, CardDavResult };
use crate::db::{ self, ChangeKind, Database };

const NS_DECL: &str = concat!(
	r#" xmlns:D="DAV:""#,
	r#" xmlns:CARD="urn:ietf:params:xml:ns:carddav""#,
	r#" xmlns:CS="http://calendarserver.org/ns/""#,
);

pub async fn handle(
	request: ReportRequest,
	db: Arc<Database>,
) -> CardDavResult<MultistatusBody>
{
	let kind = request.kind.clone().ok_or( CardDavError::NotFound )?;
	match kind
	{
		ReportKind::AddressbookQuery    => addressbook_query( request, db ).await,
		ReportKind::AddressbookMultiget => addressbook_multiget( request, db ).await,
		ReportKind::SyncCollection      => sync_collection( request, db ).await,
		ReportKind::Unknown( _ )        => Err( CardDavError::NotFound ),
	}
}

// -- addressbook-query --------------------------------------------------------

async fn addressbook_query(
	request: ReportRequest,
	db: Arc<Database>,
) -> CardDavResult<MultistatusBody>
{
	let snapshot = snapshot::full_addressbook( db ).await?;
	let want_body = request.want_address_data;

	let mut body = String::new();
	body.push_str( &multistatus_open() );
	for entry in &snapshot.contacts
	{
		body.push_str( &resource_response( entry, want_body ) );
	}
	body.push_str( &multistatus_close( None ) );

	Ok( MultistatusBody { bytes: body.into_bytes() } )
}

// -- addressbook-multiget -----------------------------------------------------

async fn addressbook_multiget(
	request: ReportRequest,
	db: Arc<Database>,
) -> CardDavResult<MultistatusBody>
{
	let want_body = request.want_address_data;

	let mut body = String::new();
	body.push_str( &multistatus_open() );

	for href in &request.multiget_hrefs
	{
		let trimmed = href.trim();
		match xml::href_to_carddav_uid( trimmed )
		{
			Some( uid ) =>
			{
				match snapshot::by_uid( db.clone(), uid.to_owned() ).await
				{
					Ok( entry )                   => body.push_str( &resource_response( &entry, want_body ) ),
					Err( CardDavError::NotFound ) => body.push_str( &not_found_response( trimmed ) ),
					Err( e )                      => return Err( e ),
				}
			}
			None =>
			{
				body.push_str( &not_found_response( trimmed ) );
			}
		}
	}

	body.push_str( &multistatus_close( None ) );
	Ok( MultistatusBody { bytes: body.into_bytes() } )
}

// -- sync-collection ----------------------------------------------------------

async fn sync_collection(
	request: ReportRequest,
	db: Arc<Database>,
) -> CardDavResult<MultistatusBody>
{
	let want_body = request.want_address_data;
	// Forgiving parse: a missing, empty, or unrecognised sync-token is
	// treated as initial sync (since=0). RFC 6578 also allows returning
	// 403 with `<valid-sync-token/>`, but in v1 we'd rather over-deliver
	// than refuse — clients re-sync correctly either way.
	let since: i64 = request.sync_token.as_deref()
		.and_then( parse_sync_token )
		.unwrap_or( 0 );

	let ( new_counter, deltas ) = collect_deltas( db.clone(), since ).await?;

	let mut body = String::new();
	body.push_str( &multistatus_open() );

	for entry in &deltas
	{
		match entry.kind
		{
			ChangeKind::Put =>
			{
				match snapshot::by_uid( db.clone(), entry.uid.clone() ).await
				{
					Ok( snap )                    => body.push_str( &resource_response( &snap, want_body ) ),
					Err( CardDavError::NotFound ) =>
					{
						// Race: the resource was deleted between us collecting
						// the deltas and rendering. Surface as a delete entry.
						body.push_str( &not_found_response( &href_for_uid( &entry.uid ) ) );
					}
					Err( e ) => return Err( e ),
				}
			}
			ChangeKind::Delete =>
			{
				body.push_str( &not_found_response( &href_for_uid( &entry.uid ) ) );
			}
		}
	}

	body.push_str( &multistatus_close( Some( new_counter ) ) );
	Ok( MultistatusBody { bytes: body.into_bytes() } )
}

async fn collect_deltas(
	db: Arc<Database>,
	since: i64,
) -> CardDavResult<( i64, Vec<db::sync_state::SyncEntry> )>
{
	let outcome = smol::unblock( move ||
	{
		db.with_conn( |c| db::sync_state::changes_since( c, since ) )
	} ).await?;
	Ok( outcome )
}

fn parse_sync_token( raw: &str ) -> Option<i64>
{
	let n = raw.trim().strip_prefix( "data:," )?;
	n.parse::<i64>().ok()
}

fn href_for_uid( uid: &str ) -> String
{
	format!( "/addressbook/main/{}.vcf", uid )
}

// -- response building --------------------------------------------------------

fn multistatus_open() -> String
{
	format!(
		"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus{ns}>\n",
		ns = NS_DECL,
	)
}

/// `sync_token` is `Some( counter )` for `sync-collection` (per RFC 6578),
/// `None` for query/multiget.
fn multistatus_close( sync_token: Option<i64> ) -> String
{
	match sync_token
	{
		Some( n ) => format!( "  <D:sync-token>data:,{n}</D:sync-token>\n</D:multistatus>\n" ),
		None      => "</D:multistatus>\n".to_owned(),
	}
}

fn resource_response( entry: &ContactSnapshot, include_body: bool ) -> String
{
	let address_data_block = if include_body
	{
		let xml_body = escape_xml_text( std::str::from_utf8( &entry.vcard_body ).unwrap_or( "" ) );
		format!( "        <CARD:address-data>{}</CARD:address-data>\n", xml_body )
	} else {
		String::new()
	};

	format!( r#"  <D:response>
    <D:href>/addressbook/main/{uid}.vcf</D:href>
    <D:propstat>
      <D:prop>
        <D:getetag>{etag}</D:getetag>
        <D:getcontenttype>text/vcard; charset=utf-8</D:getcontenttype>
        <D:getcontentlength>{len}</D:getcontentlength>
        <D:getlastmodified>{last_mod}</D:getlastmodified>
{address_data}      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
"#,
		uid = escape_xml_text( &entry.uid ),
		etag = escape_xml_attr_value( &entry.etag ),
		len = entry.vcard_body.len(),
		last_mod = escape_xml_text( &entry.last_modified ),
		address_data = address_data_block,
	)
}

fn not_found_response( href: &str ) -> String
{
	format!( r#"  <D:response>
    <D:href>{href}</D:href>
    <D:status>HTTP/1.1 404 Not Found</D:status>
  </D:response>
"#,
		href = escape_xml_text( href ),
	)
}

fn escape_xml_text( s: &str ) -> String
{
	let mut out = String::with_capacity( s.len() );
	for ch in s.chars()
	{
		match ch
		{
			'<'  => out.push_str( "&lt;" ),
			'>'  => out.push_str( "&gt;" ),
			'&'  => out.push_str( "&amp;" ),
			other => out.push( other ),
		}
	}
	out
}

fn escape_xml_attr_value( s: &str ) -> String
{
	let mut out = String::with_capacity( s.len() );
	for ch in s.chars()
	{
		match ch
		{
			'<'  => out.push_str( "&lt;" ),
			'>'  => out.push_str( "&gt;" ),
			'&'  => out.push_str( "&amp;" ),
			'"'  => out.push_str( "&quot;" ),
			'\'' => out.push_str( "&apos;" ),
			other => out.push( other ),
		}
	}
	out
}
