//! PROPFIND multistatus response generation.
//!
//! v1 semantics: we always reply as if `<D:allprop/>` was requested. The
//! request body is parsed only enough to extract the depth and to detect a
//! `<D:propname/>` request (which we do NOT support in v1; we still return
//! values). Phase 4 adds proper request parsing.
//!
//! Properties returned per resource type are documented in `docs/carddav.md`
//! ("PROPFIND" section).

use std::sync::Arc;

use crate::carddav::snapshot::{ self, ContactSnapshot };
use crate::carddav::{ CardDavError, CardDavResult };
use crate::db::{ self, Database };

/// Logical depth for the PROPFIND request. CardDAV only ever uses 0 or 1
/// in practice; we coerce anything else to `One`.
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum Depth
{
	Zero,
	One,
}

impl Depth
{
	pub fn parse( header_value: &str ) -> Self
	{
		match header_value.trim()
		{
			"0" => Depth::Zero,
			_   => Depth::One,
		}
	}
}

/// Resource a PROPFIND request can target.
#[ derive( Debug ) ]
pub enum Resource<'a>
{
	Root,
	PrincipalCollection,
	Principal,
	HomeSet,
	Addressbook,
	VCard( &'a str ),
	NotFound,
}

pub fn classify( path: &str ) -> Resource<'_>
{
	let p = path.trim_end_matches( '/' );
	match p
	{
		"" | "/" => Resource::Root,
		"/principals"          => Resource::PrincipalCollection,
		"/principals/liberux"  => Resource::Principal,
		"/addressbook"         => Resource::HomeSet,
		"/addressbook/main"    => Resource::Addressbook,
		other =>
		{
			if let Some( rest ) = other.strip_prefix( "/addressbook/main/" )
			{
				if let Some( uuid ) = rest.strip_suffix( ".vcf" )
				{
					return Resource::VCard( uuid );
				}
			}
			Resource::NotFound
		}
	}
}

/// Output of a successful PROPFIND. The caller wraps this body in a 207
/// Multi-Status HTTP response.
pub struct MultistatusBody
{
	pub bytes: Vec<u8>,
}

pub async fn build(
	resource: Resource<'_>,
	depth: Depth,
	db: Arc<Database>,
) -> CardDavResult<MultistatusBody>
{
	match resource
	{
		Resource::Root                => Ok( render_root() ),
		Resource::PrincipalCollection => Ok( render_principal_collection() ),
		Resource::Principal           => Ok( render_principal() ),
		Resource::HomeSet             => Ok( render_home_set() ),
		Resource::Addressbook =>
		{
			match depth
			{
				Depth::Zero => render_addressbook_collection_only( db ).await,
				Depth::One  => render_addressbook_collection_and_resources( db ).await,
			}
		}
		Resource::VCard( uid ) => render_single_vcard( db, uid.to_owned() ).await,
		Resource::NotFound     => Err( CardDavError::NotFound ),
	}
}

// -- per-resource renderers ---------------------------------------------------

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/""#,
);

fn render_root() -> MultistatusBody
{
	let body = format!( r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus{ns}>
  <D:response>
    <D:href>/</D:href>
    <D:propstat>
      <D:prop>
        <D:resourcetype><D:collection/></D:resourcetype>
        <D:displayname>Liberux</D:displayname>
        <D:current-user-principal><D:href>/principals/liberux/</D:href></D:current-user-principal>
        <D:principal-URL><D:href>/principals/liberux/</D:href></D:principal-URL>
        <CARD:addressbook-home-set><D:href>/addressbook/</D:href></CARD:addressbook-home-set>
      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
</D:multistatus>
"#, ns = NS_DECL );
	MultistatusBody { bytes: body.into_bytes() }
}

fn render_principal_collection() -> MultistatusBody
{
	let body = format!( r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus{ns}>
  <D:response>
    <D:href>/principals/</D:href>
    <D:propstat>
      <D:prop>
        <D:resourcetype><D:collection/></D:resourcetype>
        <D:displayname>Principals</D:displayname>
      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
</D:multistatus>
"#, ns = NS_DECL );
	MultistatusBody { bytes: body.into_bytes() }
}

fn render_principal() -> MultistatusBody
{
	let body = format!( r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus{ns}>
  <D:response>
    <D:href>/principals/liberux/</D:href>
    <D:propstat>
      <D:prop>
        <D:resourcetype><D:collection/><D:principal/></D:resourcetype>
        <D:displayname>liberux</D:displayname>
        <D:principal-URL><D:href>/principals/liberux/</D:href></D:principal-URL>
        <D:current-user-principal><D:href>/principals/liberux/</D:href></D:current-user-principal>
        <CARD:addressbook-home-set><D:href>/addressbook/</D:href></CARD:addressbook-home-set>
      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
</D:multistatus>
"#, ns = NS_DECL );
	MultistatusBody { bytes: body.into_bytes() }
}

fn render_home_set() -> MultistatusBody
{
	let body = format!( r#"<?xml version="1.0" encoding="utf-8"?>
<D:multistatus{ns}>
  <D:response>
    <D:href>/addressbook/</D:href>
    <D:propstat>
      <D:prop>
        <D:resourcetype><D:collection/></D:resourcetype>
        <D:displayname>Liberux address books</D:displayname>
        <D:current-user-principal><D:href>/principals/liberux/</D:href></D:current-user-principal>
      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
  <D:response>
    <D:href>/addressbook/main/</D:href>
    <D:propstat>
      <D:prop>
        <D:resourcetype><D:collection/><CARD:addressbook/></D:resourcetype>
        <D:displayname>Liberux Contacts</D:displayname>
      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
</D:multistatus>
"#, ns = NS_DECL );
	MultistatusBody { bytes: body.into_bytes() }
}

async fn render_addressbook_collection_only( db: Arc<Database> ) -> CardDavResult<MultistatusBody>
{
	let counter = smol::unblock( move ||
	{
		db.with_conn( |c| db::sync_state::change_counter( c ) )
	} ).await?;

	let body = collection_response( counter );
	let xml = format!(
		"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus{ns}>\n{body}</D:multistatus>\n",
		ns = NS_DECL,
	);
	Ok( MultistatusBody { bytes: xml.into_bytes() } )
}

async fn render_addressbook_collection_and_resources(
	db: Arc<Database>,
) -> CardDavResult<MultistatusBody>
{
	let snapshot = snapshot::full_addressbook( db ).await?;

	let mut body = String::new();
	body.push_str( &collection_response( snapshot.change_counter ) );

	for entry in &snapshot.contacts
	{
		body.push_str( &resource_response( entry ) );
	}

	let xml = format!(
		"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus{ns}>\n{body}</D:multistatus>\n",
		ns = NS_DECL,
	);
	Ok( MultistatusBody { bytes: xml.into_bytes() } )
}

async fn render_single_vcard(
	db: Arc<Database>,
	uid: String,
) -> CardDavResult<MultistatusBody>
{
	let entry = snapshot::by_uid( db, uid ).await?;

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

	let xml = format!(
		"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus{ns}>\n{body}</D:multistatus>\n",
		ns = NS_DECL,
	);
	Ok( MultistatusBody { bytes: xml.into_bytes() } )
}

fn collection_response( change_counter: i64 ) -> String
{
	format!( r#"  <D:response>
    <D:href>/addressbook/main/</D:href>
    <D:propstat>
      <D:prop>
        <D:resourcetype><D:collection/><CARD:addressbook/></D:resourcetype>
        <D:displayname>Liberux Contacts</D:displayname>
        <CARD:addressbook-description>Liberux unified contacts</CARD:addressbook-description>
        <CARD:supported-address-data>
          <CARD:address-data-type content-type="text/vcard" version="3.0"/>
        </CARD:supported-address-data>
        <CARD:max-resource-size>1048576</CARD:max-resource-size>
        <CS:getctag>{ctag}</CS:getctag>
        <D:sync-token>data:,{counter}</D:sync-token>
        <D:supported-report-set>
          <D:supported-report><D:report><CARD:addressbook-query/></D:report></D:supported-report>
          <D:supported-report><D:report><CARD:addressbook-multiget/></D:report></D:supported-report>
          <D:supported-report><D:report><D:sync-collection/></D:report></D:supported-report>
        </D:supported-report-set>
        <D:current-user-privilege-set>
          <D:privilege><D:read/></D:privilege>
          <D:privilege><D:read-current-user-privilege-set/></D:privilege>
          <D:privilege><D:read-acl/></D:privilege>
        </D:current-user-privilege-set>
      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
"#,
		ctag = change_counter,
		counter = change_counter,
	)
}

fn resource_response( entry: &ContactSnapshot ) -> String
{
	format!( r#"  <D:response>
    <D:href>/addressbook/main/{uid}.vcf</D:href>
    <D:propstat>
      <D:prop>
        <D:resourcetype/>
        <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>
      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
"#,
		uid = escape_xml( &entry.uid ),
		etag = escape_xml_attr_value( &entry.etag ),
		len = entry.vcard_body.len(),
		last_mod = escape_xml( &entry.last_modified ),
	)
}

// -- formatting helpers -------------------------------------------------------

fn escape_xml( 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
}
