//! HTTP listener loop and per-request dispatch.
//!
//! We implement `hyper::service::Service` directly rather than pull in
//! `hyper-util` for `service_fn`. The Service is small (one method body)
//! and avoiding the dep keeps the runtime story simple — we already drive
//! everything from smol.

use std::convert::Infallible;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

use bytes::Bytes;
use http_body_util::Full;
use hyper::body::Incoming;
use hyper::header::{
	ALLOW, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, ETAG, IF_NONE_MATCH,
	LAST_MODIFIED, WWW_AUTHENTICATE,
};
use hyper::service::Service;
use hyper::{ Method, Request, Response, StatusCode };

use crate::carddav::auth::{ self, AuthOutcome, Token };
use crate::carddav::etag;
use crate::carddav::io::SmolIo;
use crate::carddav::propfind::{ self, Depth };
use crate::carddav::report;
use crate::carddav::snapshot;
use crate::carddav::vcard_serialize::{ self, VCardInput };
use crate::carddav::xml as carddav_xml;
use crate::carddav::{ CardDavError, CardDavResult };
use crate::db::{ self, Database };
use http_body_util::BodyExt;

pub async fn run(
	listener: smol::net::TcpListener,
	db:       Arc<Database>,
	token:    Token,
) -> anyhow::Result<()>
{
	let state = Arc::new( HandlerState { db, token } );

	loop
	{
		let ( stream, peer ) = match listener.accept().await
		{
			Ok( pair ) => pair,
			Err( e ) =>
			{
				log::warn!( "carddav accept failed: {}", e );
				continue;
			}
		};
		let state = state.clone();
		smol::spawn( async move
		{
			if let Err( e ) = serve_connection( stream, state ).await
			{
				log::debug!( "carddav connection {} closed: {}", peer, e );
			}
		} ).detach();
	}
}

struct HandlerState
{
	db:    Arc<Database>,
	token: Token,
}

struct CardDavService
{
	state: Arc<HandlerState>,
}

impl Service<Request<Incoming>> for CardDavService
{
	type Response = Response<Full<Bytes>>;
	type Error    = Infallible;
	type Future   = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

	fn call( &self, req: Request<Incoming> ) -> Self::Future
	{
		let state = self.state.clone();
		Box::pin( async move
		{
			let response = match dispatch( req, state ).await
			{
				Ok( r ) => r,
				Err( CardDavError::NotFound ) => not_found_response(),
				Err( other ) =>
				{
					log::warn!( "carddav handler error: {}", other );
					internal_error_response()
				}
			};
			Ok( response )
		} )
	}
}

async fn serve_connection(
	stream: smol::net::TcpStream,
	state:  Arc<HandlerState>,
) -> anyhow::Result<()>
{
	let io = SmolIo::new( stream );
	let svc = CardDavService { state };
	hyper::server::conn::http1::Builder::new()
		.serve_connection( io, svc )
		.await
		.map_err( |e| anyhow::anyhow!( "hyper: {}", e ) )?;
	Ok( () )
}

async fn dispatch(
	req:   Request<Incoming>,
	state: Arc<HandlerState>,
) -> CardDavResult<Response<Full<Bytes>>>
{
	if let AuthOutcome::MissingOrInvalid =
		auth::authenticate( req.headers().get( AUTHORIZATION ), &state.token )
	{
		return Ok( unauthorized_response() );
	}

	let method = req.method().clone();
	let path   = req.uri().path().to_owned();

	let depth = req.headers()
		.get( "depth" )
		.and_then( |v| v.to_str().ok() )
		.map( Depth::parse );

	let if_none_match = req.headers()
		.get( IF_NONE_MATCH )
		.and_then( |v| v.to_str().ok() )
		.map( str::to_owned );

	match method.as_str()
	{
		"OPTIONS" => Ok( options_response() ),
		"PROPFIND" =>
		{
			let resource = propfind::classify( &path );
			let body = propfind::build(
				resource,
				depth.unwrap_or( Depth::Zero ),
				state.db.clone(),
			).await?;
			Ok( multistatus_response( body ) )
		}
		"REPORT" =>
		{
			let body_bytes = match req.into_body().collect().await
			{
				Ok( c )  => c.to_bytes(),
				Err( e ) =>
				{
					log::warn!( "carddav: failed to read REPORT body: {}", e );
					return Ok( bad_request_response( "could not read request body" ) );
				}
			};
			match carddav_xml::parse_report( &body_bytes )
			{
				Ok( parsed ) =>
				{
					if matches!( parsed.kind, Some( carddav_xml::ReportKind::Unknown( _ ) ) )
					{
						return Ok( forbidden_response(
							"This server only implements addressbook-query, addressbook-multiget and sync-collection.\n",
						) );
					}
					let body = report::handle( parsed, state.db.clone() ).await?;
					Ok( multistatus_response( body ) )
				}
				Err( e ) =>
				{
					log::debug!( "carddav: malformed REPORT body: {}", e );
					Ok( bad_request_response( "malformed REPORT body" ) )
				}
			}
		}
		"GET" | "HEAD" =>
		{
			if let Some( uuid ) = parse_vcard_path( &path )
			{
				handle_vcard_get(
					state.db.clone(),
					uuid.to_owned(),
					method == Method::HEAD,
					if_none_match,
				).await
			} else if path == "/" || path.starts_with( "/.well-known/carddav" ) {
				Ok( plain_text_response(
					"Liberux CardDAV endpoint. PROPFIND /addressbook/main/ to discover.\n",
				) )
			} else {
				Ok( not_found_response() )
			}
		}
		"PUT" | "DELETE" => Ok( forbidden_readonly_response() ),
		_                => Ok( method_not_allowed_response() ),
	}
}

fn parse_vcard_path( path: &str ) -> Option<&str>
{
	let rest = path.strip_prefix( "/addressbook/main/" )?;
	rest.strip_suffix( ".vcf" )
}

async fn handle_vcard_get(
	db:            Arc<Database>,
	uid:           String,
	head_only:     bool,
	if_none_match: Option<String>,
) -> CardDavResult<Response<Full<Bytes>>>
{
	let ( body, updated_at ) = smol::unblock( move ||
	{
		db.with_conn( |conn| -> db::DbResult<( Vec<u8>, i64 )>
		{
			let raw_id    = db::raw_contacts::id_by_carddav_uid( conn, &uid )?;
			let raw       = db::raw_contacts::get( conn, raw_id )?;
			let data_rows = db::data::query( conn, raw_id, None )?;
			let body = vcard_serialize::serialize( VCardInput
			{
				uid:         &uid,
				raw_contact: &raw,
				data_rows:   &data_rows,
			} );
			Ok( ( body, raw.updated_at ) )
		} )
	} ).await?;

	let etag_value = etag::etag_for( &body );
	let last_mod   = snapshot::format_http_date( updated_at );

	if let Some( inm ) = &if_none_match
	{
		if inm == &etag_value || inm == "*"
		{
			return Ok( not_modified_response( &etag_value, &last_mod ) );
		}
	}

	let body_len  = body.len();
	let resp_body = if head_only
	{
		Full::new( Bytes::new() )
	} else {
		Full::new( Bytes::from( body ) )
	};

	let resp = Response::builder()
		.status( StatusCode::OK )
		.header( CONTENT_TYPE,   "text/vcard; charset=utf-8" )
		.header( ETAG,           etag_value )
		.header( LAST_MODIFIED,  last_mod )
		.header( CONTENT_LENGTH, body_len.to_string() )
		.header( "DAV",          "1, 3, addressbook" )
		.body( resp_body )
		.expect( "static response builder" );
	Ok( resp )
}

// -- response builders --------------------------------------------------------

fn options_response() -> Response<Full<Bytes>>
{
	Response::builder()
		.status( StatusCode::OK )
		.header( "DAV",          "1, 3, addressbook" )
		.header( ALLOW,          "OPTIONS, GET, HEAD, PROPFIND, REPORT" )
		.header( CONTENT_LENGTH, "0" )
		.body( Full::new( Bytes::new() ) )
		.expect( "static response builder" )
}

fn unauthorized_response() -> Response<Full<Bytes>>
{
	let realm = format!( "Basic realm=\"{}\", charset=\"UTF-8\"", auth::REALM );
	Response::builder()
		.status( StatusCode::UNAUTHORIZED )
		.header( WWW_AUTHENTICATE, realm )
		.header( "DAV",            "1, 3, addressbook" )
		.header( CONTENT_LENGTH,   "0" )
		.body( Full::new( Bytes::new() ) )
		.expect( "static response builder" )
}

fn forbidden_readonly_response() -> Response<Full<Bytes>>
{
	let body: &'static [ u8 ] = b"This address book is read-only via CardDAV. Use the org.liberux.Contacts1 D-Bus API to modify contacts.\n";
	Response::builder()
		.status( StatusCode::FORBIDDEN )
		.header( CONTENT_TYPE,   "text/plain; charset=utf-8" )
		.header( CONTENT_LENGTH, body.len().to_string() )
		.header( "DAV",          "1, 3, addressbook" )
		.body( Full::new( Bytes::from_static( body ) ) )
		.expect( "static response builder" )
}

fn method_not_allowed_response() -> Response<Full<Bytes>>
{
	Response::builder()
		.status( StatusCode::METHOD_NOT_ALLOWED )
		.header( ALLOW,          "OPTIONS, GET, HEAD, PROPFIND, REPORT" )
		.header( CONTENT_LENGTH, "0" )
		.body( Full::new( Bytes::new() ) )
		.expect( "static response builder" )
}

fn not_found_response() -> Response<Full<Bytes>>
{
	Response::builder()
		.status( StatusCode::NOT_FOUND )
		.header( CONTENT_LENGTH, "0" )
		.body( Full::new( Bytes::new() ) )
		.expect( "static response builder" )
}

fn bad_request_response( message: &str ) -> Response<Full<Bytes>>
{
	let body = format!( "{}\n", message ).into_bytes();
	let len  = body.len();
	Response::builder()
		.status( StatusCode::BAD_REQUEST )
		.header( CONTENT_TYPE,   "text/plain; charset=utf-8" )
		.header( CONTENT_LENGTH, len.to_string() )
		.body( Full::new( Bytes::from( body ) ) )
		.expect( "static response builder" )
}

fn forbidden_response( message: &str ) -> Response<Full<Bytes>>
{
	let body = message.as_bytes().to_vec();
	let len  = body.len();
	Response::builder()
		.status( StatusCode::FORBIDDEN )
		.header( CONTENT_TYPE,   "text/plain; charset=utf-8" )
		.header( CONTENT_LENGTH, len.to_string() )
		.header( "DAV",          "1, 3, addressbook" )
		.body( Full::new( Bytes::from( body ) ) )
		.expect( "static response builder" )
}

fn internal_error_response() -> Response<Full<Bytes>>
{
	Response::builder()
		.status( StatusCode::INTERNAL_SERVER_ERROR )
		.header( CONTENT_LENGTH, "0" )
		.body( Full::new( Bytes::new() ) )
		.expect( "static response builder" )
}

fn not_modified_response( etag_value: &str, last_mod: &str ) -> Response<Full<Bytes>>
{
	Response::builder()
		.status( StatusCode::NOT_MODIFIED )
		.header( ETAG,           etag_value )
		.header( LAST_MODIFIED,  last_mod )
		.header( CONTENT_LENGTH, "0" )
		.body( Full::new( Bytes::new() ) )
		.expect( "static response builder" )
}

fn plain_text_response( body: &'static str ) -> Response<Full<Bytes>>
{
	let bytes = body.as_bytes();
	Response::builder()
		.status( StatusCode::OK )
		.header( CONTENT_TYPE,   "text/plain; charset=utf-8" )
		.header( CONTENT_LENGTH, bytes.len().to_string() )
		.body( Full::new( Bytes::from_static( bytes ) ) )
		.expect( "static response builder" )
}

fn multistatus_response( body: propfind::MultistatusBody ) -> Response<Full<Bytes>>
{
	let len = body.bytes.len();
	Response::builder()
		.status( StatusCode::from_u16( 207 ).expect( "207 is a valid status" ) )
		.header( CONTENT_TYPE,   "application/xml; charset=utf-8" )
		.header( CONTENT_LENGTH, len.to_string() )
		.header( "DAV",          "1, 3, addressbook" )
		.body( Full::new( Bytes::from( body.bytes ) ) )
		.expect( "static response builder" )
}
