// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Captive-portal sign-in window: an ltk-webkit WebView pointed at a plain
//! HTTP probe URL that the portal hijacks with its login page. Closes itself
//! as soon as NetworkManager reports full connectivity (login completed), or
//! on Escape / the titlebar close button.

use std::sync::Arc;
use std::sync::atomic::{ AtomicBool, Ordering };
use std::time::Duration;

use ltk::{ App, CursorShape, Element, Keysym };
use ltk_webkit::WebView;

const DEFAULT_URL: &str = "http://network-test.debian.org/nm";
const CONNECTIVITY_FULL: u32 = 4;

/// Input trace enabled by `EYDOS_PORTAL_DEBUG` — logs every pointer and
/// touch callback ltk delivers, to bracket where coordinates go stale.
fn dbg_log( msg: &str )
{
	static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
	if *ON.get_or_init( || std::env::var_os( "EYDOS_PORTAL_DEBUG" ).is_some() )
	{
		eprintln!( "[portal dbg] {}", msg );
	}
}

#[ derive( Clone ) ]
enum Msg
{
	Quit,
	Tick,
}

struct PortalApp
{
	webview:  WebView,
	quit:     bool,
	pressed:  bool,
	signed_in: Arc<AtomicBool>,
}

/// Re-probe connectivity every few seconds; flip the flag once the portal
/// login went through so the window can dismiss itself like on a phone.
fn spawn_connectivity_watch( signed_in: Arc<AtomicBool> )
{
	std::thread::spawn( move ||
	{
		let Ok( conn ) = zbus::blocking::Connection::system() else { return };
		let Ok( proxy ) = zbus::blocking::Proxy::new(
			&conn,
			"org.freedesktop.NetworkManager",
			"/org/freedesktop/NetworkManager",
			"org.freedesktop.NetworkManager",
		) else { return };
		loop
		{
			std::thread::sleep( Duration::from_secs( 5 ) );
			let state = proxy.call::<_, _, u32>( "CheckConnectivity", &() )
				.or_else( |_| proxy.get_property::<u32>( "Connectivity" ) )
				.unwrap_or( 0 );
			if state == CONNECTIVITY_FULL
			{
				signed_in.store( true, Ordering::Relaxed );
				return;
			}
		}
	});
}

impl App for PortalApp
{
	type Message = Msg;

	fn app_id( &self ) -> &str { "eydos-portal" }
	fn save_state( &self ) -> Option<Vec<u8>> { None }
	fn restore_state( &mut self, _state: Vec<u8> ) {}

	fn view( &self ) -> Element<Msg>
	{
		ltk::column().push( self.webview.element::<Msg>() ).into()
	}

	fn update( &mut self, msg: Msg )
	{
		match msg
		{
			Msg::Quit => self.quit = true,
			Msg::Tick => {}
		}
	}

	fn invalidate_after( &self, msg: &Msg ) -> ltk::InvalidationScope
	{
		match msg
		{
			Msg::Quit => ltk::InvalidationScope::Only( Vec::new() ),
			Msg::Tick =>
			{
				if self.webview.take_redraw_request()
				{
					ltk::InvalidationScope::All
				} else {
					ltk::InvalidationScope::Only( Vec::new() )
				}
			}
		}
	}

	fn on_resize( &mut self, width: u32, height: u32 )
	{
		dbg_log( &format!( "resize {width}x{height}" ) );
		self.webview.resize( width as f32, height as f32 );
	}

	fn on_pointer_button( &mut self, x: f32, y: f32, pressed: bool )
	{
		dbg_log( &format!( "pointer_button ({x:.0},{y:.0}) pressed={pressed}" ) );
		if pressed
		{
			self.pressed = true;
			self.webview.pointer_press( x as f64, y as f64 );
		} else if self.pressed {
			self.pressed = false;
			self.webview.pointer_release( x as f64, y as f64 );
		}
	}

	fn on_pointer_move( &mut self, x: f32, y: f32 )
	{
		dbg_log( &format!( "pointer_move ({x:.0},{y:.0})" ) );
		self.webview.pointer_move( x as f64, y as f64 );
	}

	fn on_raw_key( &mut self, keysym: Keysym, keycode: u32, pressed: bool, ctrl: bool, shift: bool )
	{
		self.webview.send_key( keysym.raw(), keycode, pressed, ctrl, shift );
	}

	// Touchscreen path: take every finger raw and let WebKit's own
	// gesture recognition turn the stream into taps, kinetic scrolls
	// and pinch-zoom.
	fn claims_raw_touch( &self ) -> bool { true }

	fn on_touch_down( &mut self, id: i64, x: f32, y: f32 )
	{
		dbg_log( &format!( "touch_down id={id} ({x:.0},{y:.0})" ) );
		self.webview.touch_down( id as u32, x as f64, y as f64 );
	}

	fn on_touch_move( &mut self, id: i64, x: f32, y: f32 )
	{
		dbg_log( &format!( "touch_move id={id} ({x:.0},{y:.0})" ) );
		self.webview.touch_move( id as u32, x as f64, y as f64 );
	}

	fn on_touch_up( &mut self, id: i64, x: f32, y: f32 )
	{
		dbg_log( &format!( "touch_up id={id} ({x:.0},{y:.0})" ) );
		self.webview.touch_up( id as u32, x as f64, y as f64 );
	}

	fn on_pointer_axis( &mut self, x: f32, y: f32, dx: f32, dy: f32 )
	{
		// Same wheel scaling and sign flip as ltk-webkit's reference app:
		// ltk pre-multiplies ticks ×10 and uses the opposite sign convention.
		let scale = -1.0_f32 / 3.0;
		self.webview.scroll( x as f64, y as f64, ( dx * scale ) as f64, ( dy * scale ) as f64 );
	}

	fn cursor_override( &self ) -> Option<CursorShape>
	{
		Some( self.webview.cursor_shape() )
	}

	fn poll_external( &mut self ) -> Vec<Msg>
	{
		self.webview.tick();
		if self.signed_in.load( Ordering::Relaxed )
		{
			return vec![ Msg::Quit ];
		}
		vec![ Msg::Tick ]
	}

	fn poll_interval( &self ) -> Option<Duration>
	{
		Some( Duration::from_millis( 16 ) )
	}

	fn on_key( &mut self, keysym: Keysym ) -> Option<Msg>
	{
		if keysym == Keysym::Escape { Some( Msg::Quit ) } else { None }
	}

	fn requested_exit( &self ) -> bool { self.quit }

	fn window_config( &self ) -> Option<( &str, &str )>
	{
		Some( ( "Network sign-in", "eydos-portal" ) )
	}

	fn window_size_hint( &self ) -> Option<( u32, u32 )>
	{
		Some( ( 480, 800 ) )
	}
}

fn main() -> Result<(), String>
{
	tracing_subscriber::fmt()
		.with_env_filter( tracing_subscriber::EnvFilter::from_default_env() )
		.init();

	// An explicit URL means a manual / test session: skip the auto-close
	// watch, which would dismiss the window as soon as the device already
	// has full connectivity.
	let url_arg = std::env::args().nth( 1 );
	let watch   = url_arg.is_none();
	let url     = url_arg.unwrap_or_else( || DEFAULT_URL.to_string() );
	let webview = WebView::new( 480.0, 800.0, &url )?;
	let signed_in: Arc<AtomicBool> = Arc::default();
	if watch
	{
		spawn_connectivity_watch( signed_in.clone() );
	}
	ltk::run( PortalApp
	{
		webview,
		quit:    false,
		pressed: false,
		signed_in,
	} );
	Ok(())
}
