//! CardDAV (RFC 6352) endpoint over HTTP loopback.
//!
//! Phase 3 surface: OPTIONS, PROPFIND, GET/HEAD, plus 403 stubs for PUT and
//! DELETE. REPORT verbs land in Phase 4 once `sync-collection` is wired.
//!
//! The listener binds `127.0.0.1:0` and the kernel-assigned port is
//! advertised via the runtime port file (and, eventually, the EDS
//! `.source` written by Phase 5).

use std::path::Path;
use std::sync::Arc;

use anyhow::{ Context, Result };
use thiserror::Error;

use crate::db::Database;

pub mod auth;
pub mod etag;
pub mod io;
pub mod propfind;
pub mod report;
pub mod server;
pub mod snapshot;
pub mod vcard_serialize;
pub mod xml;

/// Errors that can short-circuit a CardDAV handler. Internal to this
/// module — the request-dispatcher converts them to HTTP statuses.
#[ derive( Debug, Error ) ]
pub enum CardDavError
{
	#[ error( "not found" ) ]
	NotFound,

	#[ error( "db: {0}" ) ]
	Db( crate::db::DbError ),

	#[ error( "io: {0}" ) ]
	Io( #[ from ] std::io::Error ),
}

impl From<crate::db::DbError> for CardDavError
{
	fn from( e: crate::db::DbError ) -> Self
	{
		match e
		{
			crate::db::DbError::NotFound => CardDavError::NotFound,
			other                        => CardDavError::Db( other ),
		}
	}
}

pub type CardDavResult<T> = Result<T, CardDavError>;

/// Configuration consumed by `bind`. The bind step is split from the
/// accept loop so callers (main.rs in Phase 5) can register the bound
/// port with EDS / Secret Service before launching the loop, which never
/// returns under normal operation.
pub struct BindConfig<'a>
{
	pub token:     auth::Token,
	pub port_file: Option<&'a Path>,
}

/// Result of a successful `bind`: owns the listener, exposes the bound
/// address. Pass to `Bound::run` to start the accept loop.
pub struct Bound
{
	listener: smol::net::TcpListener,
	pub addr: std::net::SocketAddr,
	token:    auth::Token,
}

impl Bound
{
	/// Run the accept loop forever. Returns only on a fatal accept error.
	pub async fn run( self, db: Arc<Database> ) -> Result<()>
	{
		log::info!(
			"CardDAV listening on http://{}/  (Basic user=liberux)",
			self.addr,
		);
		server::run( self.listener, db, self.token ).await
	}
}

/// Bind the loopback listener and write the runtime port file. Does **not**
/// start serving; the caller is expected to perform any side-channel
/// registration (EDS, Secret Service) and then call `Bound::run`.
pub async fn bind( cfg: BindConfig<'_> ) -> Result<Bound>
{
	let listener = smol::net::TcpListener::bind( "127.0.0.1:0" ).await
		.context( "binding 127.0.0.1:0" )?;
	let addr = listener.local_addr().context( "reading bound port" )?;

	if let Some( path ) = cfg.port_file
	{
		write_port_file( path, addr.port() )
			.with_context( || format!( "writing port file {}", path.display() ) )?;
	}

	Ok( Bound { listener, addr, token: cfg.token } )
}

fn write_port_file( path: &Path, port: u16 ) -> std::io::Result<()>
{
	use std::io::Write;
	use std::os::unix::fs::{ OpenOptionsExt, PermissionsExt };

	if let Some( parent ) = path.parent()
	{
		std::fs::create_dir_all( parent )?;
		std::fs::set_permissions(
			parent,
			std::fs::Permissions::from_mode( 0o700 ),
		)?;
	}
	let tmp = path.with_extension( "port.tmp" );
	{
		let mut f = std::fs::OpenOptions::new()
			.create( true )
			.write( true )
			.truncate( true )
			.mode( 0o600 )
			.open( &tmp )?;
		writeln!( f, "{}", port )?;
		f.sync_all()?;
	}
	std::fs::rename( &tmp, path )?;
	std::fs::set_permissions( path, std::fs::Permissions::from_mode( 0o600 ) )?;
	Ok( () )
}
