use std::{
    collections::{HashMap, HashSet},
    path::PathBuf,
    rc::Rc,
    sync::{
        Arc, Once,
        atomic::{AtomicBool, Ordering},
    },
};

use dpi::PhysicalSize;
use ely_domain::{ColorScheme, ProfileId, TabId, WebViewId};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use servo::{
    DevicePoint, DeviceVector2D, Opts, Scroll, Servo, ServoBuilder, Theme, WebViewBuilder,
    WebViewPoint, WebViewVector,
};

#[path = "runtime_context.rs"]
mod runtime_context;
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
#[path = "runtime_hardware.rs"]
mod runtime_hardware;
#[path = "runtime_paint.rs"]
mod runtime_paint;
#[path = "runtime_preferences.rs"]
mod runtime_preferences;

use runtime_context::hidpi_scale_from_factor;
pub use runtime_context::{RenderingContextKind, ServoSurfaceSize};
use runtime_preferences::ely_servo_preferences;
use url::Url;

use crate::{
    ColorSchemeRequest, ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest,
    MouseClickRequest, MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest,
    PermissionSnapshotRequest, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost,
    ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState,
    runtime_input::{
        send_keyboard_text, send_mouse_click, send_mouse_drag, send_mouse_hover, send_touch_tap,
    },
    runtime_permissions::{
        PermissionStore, drain_consumed_permissions, replace_permission_decisions,
    },
    runtime_waker::ServoWakeFlag,
    runtime_webview::{HostWebView, HostWebViewDelegate},
};

static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
static RUSTLS_PROVIDER: Once = Once::new();

pub struct SoftwareServoHost {
    servo: Servo,
    default_surface_size: ServoSurfaceSize,
    rendering_context_kind: RenderingContextKind,
    webviews: HashMap<WebViewId, HostWebView>,
    permissions: PermissionStore,
    wake_requested: Arc<AtomicBool>,
    last_rendered_frame: Option<RenderedFrame>,
}

impl SoftwareServoHost {
    pub fn new(size: ServoSurfaceSize) -> Result<Self, ServoHostError> {
        Self::new_with_config_dir_and_kind(size, None, RenderingContextKind::Software)
    }

    pub fn new_with_config_dir(
        size: ServoSurfaceSize,
        config_dir: Option<PathBuf>,
    ) -> Result<Self, ServoHostError> {
        Self::new_with_config_dir_and_kind(size, config_dir, RenderingContextKind::Software)
    }

    /// Construct the host with an explicit [`RenderingContextKind`].
    pub fn new_with_config_dir_and_kind(
        size: ServoSurfaceSize,
        config_dir: Option<PathBuf>,
        rendering_context_kind: RenderingContextKind,
    ) -> Result<Self, ServoHostError> {
        if SERVO_RUNTIME_STARTED
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
            .is_err()
        {
            return Err(ServoHostError::RuntimeAlreadyStarted);
        }

        let host = Self::new_started(size, config_dir, rendering_context_kind);
        if host.is_err() {
            SERVO_RUNTIME_STARTED.store(false, Ordering::Release);
        }
        host
    }

    pub fn create_webview_with_size(
        &mut self,
        tab_id: TabId,
        profile_id: ProfileId,
        size: ServoSurfaceSize,
    ) -> Result<WebViewId, ServoHostError> {
        let handles = self.new_rendering_context(size)?;
        self.create_webview_in_context(tab_id, profile_id, handles)
    }

    pub fn create_webview_with_native_surface<S>(
        &mut self,
        tab_id: TabId,
        profile_id: ProfileId,
        size: ServoSurfaceSize,
        native_surface: &S,
    ) -> Result<WebViewId, ServoHostError>
    where
        S: HasDisplayHandle + HasWindowHandle + ?Sized,
    {
        let handles = self.new_rendering_context_for_native_surface(size, native_surface)?;
        self.create_webview_in_context(tab_id, profile_id, handles)
    }

    pub fn close_webview(&mut self, webview_id: &WebViewId) -> bool {
        self.webviews.remove(webview_id).is_some()
    }

    fn new_started(
        size: ServoSurfaceSize,
        config_dir: Option<PathBuf>,
        rendering_context_kind: RenderingContextKind,
    ) -> Result<Self, ServoHostError> {
        install_rustls_provider();
        let wake_requested = Arc::new(AtomicBool::new(false));
        let mut builder = ServoBuilder::default()
            .preferences(ely_servo_preferences())
            .event_loop_waker(Box::new(ServoWakeFlag::new(wake_requested.clone())));
        if let Some(config_dir) = config_dir {
            builder = builder.opts(Opts { config_dir: Some(config_dir), ..Opts::default() });
        }
        let servo = builder.build();

        Ok(Self {
            servo,
            default_surface_size: size,
            rendering_context_kind,
            webviews: HashMap::new(),
            permissions: PermissionStore::default(),
            wake_requested,
            last_rendered_frame: None,
        })
    }

    /// Returns the current snapshot and acknowledges metadata-only
    /// updates without clearing Servo's frame-ready signal.
    pub fn snapshot_and_mark_metadata_observed(
        &self,
        webview_id: &WebViewId,
    ) -> Result<WebViewSnapshot, ServoHostError> {
        let webview = self.webview(webview_id)?;
        let snapshot = webview.snapshot(webview_id);
        webview.delegate.mark_metadata_observed();
        Ok(snapshot)
    }

    fn drain_after_webview_close(&self) {
        for _ in 0..16 {
            self.servo.spin_event_loop();
        }
    }
}

fn install_rustls_provider() {
    RUSTLS_PROVIDER.call_once(|| {
        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    });
}

impl ServoHost for SoftwareServoHost {
    fn create_webview(
        &mut self,
        tab_id: TabId,
        profile_id: ProfileId,
    ) -> Result<WebViewId, ServoHostError> {
        let handles = self.new_rendering_context(self.default_surface_size)?;
        self.create_webview_in_context(tab_id, profile_id, handles)
    }

    fn navigate(&mut self, request: NavigationRequest) -> Result<(), ServoHostError> {
        let url = Url::parse(request.url.as_str()).map_err(|_| {
            ServoHostError::InvalidNavigationUrl { value: request.url.as_str().to_string() }
        })?;
        self.servo.spin_event_loop();
        let servo = self.servo.clone();
        let webview = self
            .webviews
            .get_mut(&request.webview_id)
            .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;

        let requested_url = url.to_string();
        let current_url = webview.current_url();
        let has_loaded_page =
            matches!(current_url.as_deref(), Some(value) if value != "about:blank");
        let should_create_initial_document = webview.requested_url.is_none() && !has_loaded_page;
        // A same-URL reload does not fire a URL change, so only a navigation
        // to a different URL arms the pending-navigation hold; reloads defer
        // to Servo's own load-status transitions.
        let navigates_to_new_url = current_url.as_deref() != Some(requested_url.as_str());

        webview.delegate.set_state(WebViewState::Loading);
        if should_create_initial_document {
            let hidpi_scale_factor = webview.webview.hidpi_scale_factor();
            let replacement = WebViewBuilder::new(&servo, webview.rendering_context.clone())
                .delegate(webview.delegate.clone())
                .url(url)
                // The live path pushes DPR before first navigation. Preserve that scale when
                // replacing the about:blank WebView so CSS viewport = physical surface / DPR.
                .hidpi_scale_factor(hidpi_scale_factor)
                .build();
            replacement.notify_theme_change(servo_theme(webview.color_scheme));
            webview.webview = replacement;
            // The input-accepting invariant lives in `webview_for_input`.
            webview.webview.show();
            webview.webview.focus();
        } else {
            webview.webview.load(url);
        }
        // Hold `Loading` until Servo reports the URL actually changed, so a
        // redirect reconciles instead of pinning the surface (a stale
        // previous-page `Complete` cannot clear it).
        if navigates_to_new_url {
            webview.delegate.arm_pending_navigation();
        }
        webview.requested_url = Some(requested_url);
        Ok(())
    }

    fn reload(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> {
        self.servo.spin_event_loop();
        let webview = self
            .webviews
            .get_mut(webview_id)
            .ok_or_else(|| ServoHostError::WebViewNotFound { id: webview_id.clone() })?;
        webview.delegate.set_state(WebViewState::Loading);
        webview.webview.reload();
        Ok(())
    }

    fn scroll(&mut self, request: ScrollRequest) -> Result<(), ServoHostError> {
        if request.delta_x == 0 && request.delta_y == 0 {
            return Ok(());
        }

        let webview = self.webview_for_input(&request.webview_id)?;
        webview.webview.notify_scroll_event(
            Scroll::Delta(WebViewVector::Device(DeviceVector2D::new(
                request.delta_x as f32,
                request.delta_y as f32,
            ))),
            WebViewPoint::Device(DevicePoint::new(request.point_x as f32, request.point_y as f32)),
        );
        Ok(())
    }

    fn resize(&mut self, request: ResizeRequest) -> Result<(), ServoHostError> {
        let webview = self
            .webviews
            .get(&request.webview_id)
            .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;

        // `WebView::resize` is the single entry point — it routes through
        // `paint().resize_rendering_context`, which:
        //   1. early-returns if `rendering_context.size() == new_size`;
        //   2. calls `rendering_context.resize` itself;
        //   3. calls `webview_renderer.set_rect(new_viewport_rect)` so the
        //      compositor relays out the page at the new size; and
        //   4. sends `transaction.set_document_view(...)` so WebRender's
        //      document viewport matches the surface.
        //
        // Calling `rendering_context.resize` *ourselves* before that path
        // is what produced the original bug: surfman saw the new size,
        // Servo's early-return then skipped steps 3 and 4, and the page
        // stayed laid out at the original size while we presented a
        // larger surface — content collapsed to the top-left of a giant
        // viewport (StableLance) or rendered nothing at all (Google).
        // The Servo `winit_minimal` reference embedder calls
        // `webview.resize` and nothing else; mirror that exactly.
        let size = PhysicalSize::new(request.width, request.height);
        webview.webview.resize(size);
        Ok(())
    }

    fn set_page_zoom(&mut self, request: PageZoomRequest) -> Result<(), ServoHostError> {
        let webview = self
            .webviews
            .get(&request.webview_id)
            .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;

        webview.webview.set_page_zoom(request.zoom_factor);
        Ok(())
    }

    fn set_hidpi_scale(&mut self, request: HidpiScaleRequest) -> Result<(), ServoHostError> {
        let webview = self
            .webviews
            .get(&request.webview_id)
            .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;

        let scale = hidpi_scale_from_factor(request.scale_factor);
        webview.webview.set_hidpi_scale_factor(scale);
        Ok(())
    }

    fn set_color_scheme(&mut self, request: ColorSchemeRequest) -> Result<(), ServoHostError> {
        let webview = self
            .webviews
            .get_mut(&request.webview_id)
            .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
        if webview.color_scheme != request.color_scheme {
            webview.color_scheme = request.color_scheme;
            webview.webview.notify_theme_change(servo_theme(request.color_scheme));
        }
        Ok(())
    }

    fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError> {
        let webview = self.webview_for_input(&request.webview_id)?;
        send_mouse_hover(&webview.webview, request.x, request.y);
        Ok(())
    }

    fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError> {
        let webview = self.webview_for_input(&request.webview_id)?;
        send_mouse_click(&webview.webview, request.x, request.y);
        Ok(())
    }

    fn drag(&mut self, request: MouseDragRequest) -> Result<(), ServoHostError> {
        let webview = self.webview_for_input(&request.webview_id)?;
        send_mouse_drag(
            &webview.webview,
            request.from_x,
            request.from_y,
            request.to_x,
            request.to_y,
        );
        Ok(())
    }

    fn touch_tap(&mut self, request: TouchTapRequest) -> Result<(), ServoHostError> {
        let webview = self.webview_for_input(&request.webview_id)?;
        send_touch_tap(&webview.webview, request.x, request.y);
        Ok(())
    }

    fn type_text(&mut self, request: KeyboardTextRequest) -> Result<(), ServoHostError> {
        let webview = self.webview_for_input(&request.webview_id)?;
        send_keyboard_text(&webview.webview, &request.text);
        Ok(())
    }

    fn replace_permissions(
        &mut self,
        request: PermissionSnapshotRequest,
    ) -> Result<(), ServoHostError> {
        let webview = self
            .webviews
            .get(&request.webview_id)
            .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
        if webview.profile_id != request.profile_id {
            return Err(ServoHostError::PermissionProfileMismatch {
                webview_id: request.webview_id,
                expected: webview.profile_id.clone(),
                actual: request.profile_id,
            });
        }
        let mut keys = HashSet::new();
        for entry in &request.entries {
            if !keys.insert((entry.origin.clone(), entry.feature)) {
                return Err(ServoHostError::DuplicatePermissionSnapshotEntry {
                    profile_id: request.profile_id,
                    origin: entry.origin.clone(),
                    feature: entry.feature,
                });
            }
        }
        replace_permission_decisions(
            &self.permissions,
            &request.profile_id,
            request.generation,
            request.entries,
        );
        Ok(())
    }

    fn take_consumed_permissions(&mut self) -> Vec<ConsumedPermission> {
        drain_consumed_permissions(&self.permissions)
    }

    fn state(&self, webview_id: &WebViewId) -> Result<WebViewState, ServoHostError> {
        Ok(self.webview(webview_id)?.state())
    }

    fn snapshot(&self, webview_id: &WebViewId) -> Result<WebViewSnapshot, ServoHostError> {
        self.webview(webview_id).map(|webview| webview.snapshot(webview_id))
    }

    fn tick(&mut self) -> bool {
        let requested = self.wake_requested.swap(false, Ordering::AcqRel);
        self.servo.spin_event_loop();
        requested
    }

    fn paint(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> {
        self.paint_with_readback(webview_id)
    }

    fn last_rendered_frame(&self) -> Result<RenderedFrame, ServoHostError> {
        self.last_rendered_frame.clone().ok_or(ServoHostError::RenderedFrameUnavailable)
    }
}

impl Drop for SoftwareServoHost {
    fn drop(&mut self) {
        self.webviews.clear();
        self.drain_after_webview_close();
        SERVO_RUNTIME_STARTED.store(false, Ordering::Release);
    }
}

impl SoftwareServoHost {
    fn create_webview_in_context(
        &mut self,
        tab_id: TabId,
        profile_id: ProfileId,
        handles: runtime_context::RenderingContextHandles,
    ) -> Result<WebViewId, ServoHostError> {
        let webview_id = WebViewId::new();
        let delegate =
            Rc::new(HostWebViewDelegate::new(profile_id.clone(), self.permissions.clone()));
        let webview = WebViewBuilder::new(&self.servo, handles.rendering_context.clone())
            .delegate(delegate.clone())
            .build();
        // Cosmetic: makes the first frame paint into the rendering
        // context. The input-accepting invariant is owned by
        // `webview_for_input`, which re-asserts show/focus on every
        // dispatch — so a sibling tab's later creation (which would
        // steal focus here) cannot break input on this WebView.
        webview.show();
        webview.focus();

        self.webviews.insert(
            webview_id.clone(),
            HostWebView {
                tab_id,
                profile_id,
                rendering_context: handles.rendering_context,
                #[cfg(feature = "hardware-render")]
                hardware_context: handles.hardware_context,
                webview,
                delegate,
                requested_url: None,
                color_scheme: ColorScheme::Light,
            },
        );

        Ok(webview_id)
    }

    fn webview(&self, webview_id: &WebViewId) -> Result<&HostWebView, ServoHostError> {
        self.webviews
            .get(webview_id)
            .ok_or_else(|| ServoHostError::WebViewNotFound { id: webview_id.clone() })
    }

    /// Returns a WebView guaranteed to accept input.
    ///
    /// Servo's hit-test silently absorbs `notify_input_event` on a
    /// hidden or unfocused WebView. The create + first-navigate paths
    /// call `show()`/`focus()` for first-frame visibility, but every
    /// later operation that creates a sibling WebView (multi-tab)
    /// calls `focus()` on the new one, silently stealing focus from
    /// the foreground tab. `load()` (later navigates), `resize`,
    /// `set_page_zoom`, and `paint` do not re-focus, so by the time a
    /// click arrives the visible tab's WebView is unreachable.
    /// Re-asserting per dispatch keeps the invariant on the dispatch
    /// path instead of spread across creation, navigation, and
    /// tab-switching.
    fn webview_for_input(&self, webview_id: &WebViewId) -> Result<&HostWebView, ServoHostError> {
        let webview = self.webview(webview_id)?;
        webview.webview.show();
        webview.webview.focus();
        Ok(webview)
    }
}

fn servo_theme(color_scheme: ColorScheme) -> Theme {
    match color_scheme {
        ColorScheme::Light => Theme::Light,
        ColorScheme::Dark => Theme::Dark,
    }
}
