//! Startup and construction of `Forge`: `new` (event loop, backend, protocol
//! globals, subsystem wiring), the socket / signal / display-source setup, the
//! init script and Xwayland/portal bring-up. Operates on `Forge`.

use super::*;

impl Forge {
    pub fn new(
        event_loop: LoopHandle<'static, Self>,
        backend: Backend,
        exec_override: Option<PathBuf>,
    ) -> Self {
        let config = ForgeConfig::load();
        crate::output::set_user_scale_override(config.display.scale);
        let display = Display::new().expect("wayland display");
        let display_handle = display.handle();

        Self::register_signal_handler(&event_loop);
        let socket_name = Self::register_socket(&event_loop, &display_handle);
        Self::register_display_source(&event_loop, display);

        let compositor_state = CompositorState::new_v6::<Self>(&display_handle);
        let xdg_shell_state = XdgShellState::new_with_capabilities::<Self>(
            &display_handle,
            [WmCapabilities::Fullscreen],
        );
        let layer_shell_state = WlrLayerShellState::new::<Self>(&display_handle);
        let shm_state = ShmState::new::<Self>(&display_handle, Vec::new());
        let dmabuf_state = DmabufState::new();

        OutputManagerState::new_with_xdg_output::<Self>(&display_handle);
        FractionalScaleManagerState::new::<Self>(&display_handle);
        ViewporterState::new::<Self>(&display_handle);
        XdgDecorationState::new::<Self>(&display_handle);
        ScreencopyManagerState::new::<Self>(&display_handle);
        let foreign_toplevel_state = ForeignToplevelState::new::<Self>(&display_handle);
        let ext_foreign_toplevel_list_state =
            ForeignToplevelListState::new::<Self>(&display_handle);
        let output_power_state = OutputPowerManagerState::new::<Self>(&display_handle);
        SinglePixelBufferState::new::<Self>(&display_handle);
        IdleInhibitManagerState::new::<Self>(&display_handle);
        TextInputManagerState::new::<Self>(&display_handle);
        CursorShapeManagerState::new::<Self>(&display_handle);
        PointerConstraintsState::new::<Self>(&display_handle);
        TabletManagerState::new::<Self>(&display_handle);
        AlphaModifierState::new::<Self>(&display_handle);
        smithay::wayland::content_type::ContentTypeState::new::<Self>(&display_handle);
        RelativePointerManagerState::new::<Self>(&display_handle);
        PointerGesturesState::new::<Self>(&display_handle);
        SecurityContextState::new::<Self, _>(&display_handle, |_| true);
        crate::protocols::image_copy_capture::ImageCopyCaptureState::new::<Self>(&display_handle);
        crate::protocols::gamma_control::GammaControlManagerState::new::<Self>(&display_handle);
        crate::protocols::output_management::OutputManagementState::new::<Self>(&display_handle);
        crate::protocols::export_dmabuf::ExportDmabufManagerState::new::<Self>(&display_handle);
        crate::protocols::input_inhibitor::InputInhibitorState::new::<Self>(&display_handle);
        crate::protocols::virtual_pointer::VirtualPointerManagerState::new::<Self>(&display_handle);
        let session_manager_state =
            crate::protocols::session_management::SessionManagerState::new::<Self>(&display_handle);

        let kde_decoration_state =
            KdeDecorationState::new::<Self>(&display_handle, ManagerMode::Server);

        PresentationState::new::<Self>(&display_handle, libc::CLOCK_MONOTONIC as u32);

        let idle_notifier_state = IdleNotifierState::new(&display_handle, event_loop.clone());
        let lock_state = SessionLockManagerState::new::<Self, _>(&display_handle, |_| true);
        let xdg_activation_state = XdgActivationState::new::<Self>(&display_handle);
        let primary_selection_state = PrimarySelectionState::new::<Self>(&display_handle);
        let keyboard_shortcuts_inhibit_state =
            KeyboardShortcutsInhibitState::new::<Self>(&display_handle);

        let seat_name = backend.seat_name();
        let mut seat_state = SeatState::new();
        let mut seat = seat_state.new_wl_seat(&display_handle, seat_name.clone());

        InputMethodManagerState::new::<Self, _>(&display_handle, |_| true);
        VirtualKeyboardManagerState::new::<Self, _>(&display_handle, |_| true);

        let data_device_state = DataDeviceState::new::<Self>(&display_handle);
        seat.add_keyboard(
            XkbConfig::default(),
            REPEAT_DELAY.as_millis() as i32,
            REPEAT_RATE.as_millis() as i32,
        )
        .expect("keyboard init");

        let data_control_state = DataControlState::new::<Self, _>(
            &display_handle,
            Some(&primary_selection_state),
            |_| true,
        );

        seat.add_pointer();
        seat.add_touch();
        let touch_state = TouchState::new(event_loop.clone());

        ipc_server::spawn_ipc_socket(&event_loop, &socket_name).expect("ipc socket");
        Self::clean_stale_thumbnails();

        let xwayland_shell_state =
            smithay::wayland::xwayland_shell::XWaylandShellState::new::<Self>(&display_handle);

        let mut windows = Windows::new(&display_handle);

        // Compile the user's `decoration.csd_apps` regexes once at startup.
        // Invalid patterns log a warning and are dropped — a typo in one
        // entry should not deny SSD suppression to the rest of the list.
        for pattern in &config.decoration.csd_apps {
            match forge_ipc::AppIdMatcher::try_from(pattern.clone()) {
                Ok(m) => windows.csd_apps.push(m),
                Err(e) => tracing::warn!("decoration.csd_apps: invalid regex {pattern:?}: {e}"),
            }
        }
        windows.snap_edge_fraction = config.windows.snap_edge_fraction.clamp(0.01, 0.5);

        let accel_token = Accelerometer::new().subscribe(&event_loop, |orientation, forge| {
            forge.handle_orientation(orientation);
        });

        if windows.orientation_locked() {
            trace_error!(event_loop.disable(&accel_token));
        }

        // Status bar clock + system state redraw timer.
        //
        // First fire is short (1 s) so the initial poll spawned in `new()` can
        // be applied to the topbar quickly — otherwise battery/bluetooth/wifi
        // stay hidden for ~60 s after startup (the bug Pedro reported: the
        // bar would render with `present=false`/`bluetooth_on=false` defaults
        // until the second 30 s tick managed to receive the first snapshot).
        // After the initial apply succeeds the cadence settles at 30 s.
        {
            use calloop::timer::{TimeoutAction, Timer};
            let timer = Timer::from_duration(Duration::from_secs(1));
            event_loop
                .insert_source(timer, |_, _, forge| {
                    // When display is off, only check lid switch (for wake) — skip
                    // all other work to save power. Wake on the closed->open
                    // transition only: the display may be off by idle policy
                    // with the lid open the whole time.
                    if !forge.session.display_on {
                        if let Some(closed) = forge.shell.sys_state.read_lid_state_only() {
                            let was_closed = forge.shell.sys_state.lid_closed == Some(true);
                            forge.shell.sys_state.lid_closed = Some(closed);
                            if was_closed && !closed {
                                info!("lid opened: turning display on");
                                forge.set_display_status(true);
                                forge.force_redraw(true);
                            }
                        }
                        return TimeoutAction::ToDuration(Duration::from_secs(30));
                    }

                    // Collect result from previous background poll if ready.
                    if let Some(rx) = &forge.shell.sys_state_rx {
                        if let Ok(new_state) = rx.try_recv() {
                            forge.shell.sys_state.apply(new_state);
                            forge.shell.sys_state_rx = None;
                        }
                    }

                    // Kick off a new background poll if none is in flight.
                    if forge.shell.sys_state_rx.is_none() && forge.shell.sys_state.should_poll() {
                        let (tx, rx) = mpsc::channel();
                        forge.shell.sys_state_rx = Some(rx);
                        std::thread::spawn(move || {
                            let _ = tx.send(SystemState::poll_snapshot());
                        });
                    }

                    // Handle lid switch: turn display off when closed, on when opened.
                    // Skip when an external monitor is connected — the user is
                    // likely using the laptop with the lid closed as a desktop.
                    if let Some(closed) = forge.shell.sys_state.lid_closed {
                        if closed && forge.session.display_on && !forge.backend.has_external_display() {
                            info!("lid closed: turning display off");
                            forge.set_display_status(false);
                            forge.lock_session();
                        } else if !closed && !forge.session.display_on {
                            forge.set_display_status(true);
                            forge.force_redraw(true);
                        }
                    }
                    forge.request_redraw();
                    // While a poll is still in flight (typically only the
                    // initial one at startup), retry quickly so apply() can
                    // run as soon as the snapshot is ready. Otherwise fall
                    // back to the normal 30 s cadence.
                    let next = if forge.shell.sys_state_rx.is_some() {
                        Duration::from_millis(500)
                    } else {
                        Duration::from_secs(30)
                    };
                    TimeoutAction::ToDuration(next)
                })
                .expect("status bar timer");
        }

        // Start MPRIS media listener
        MprisState::start_listener(&event_loop);

        // Start polkit authentication agent
        let (polkit_sender, polkit_pending) = PolkitAgent::start_agent(&event_loop);

        // Start auto-brightness polling
        AutoBrightness::start_polling(&event_loop);

        // Start captive portal detector
        PortalState::start_detector(&event_loop);

        // Start GNOME Shell D-Bus compatibility server
        crate::gnome_shell_dbus::start_server(&event_loop);

        // Start ScreenSaver D-Bus server (org.gnome.ScreenSaver)
        crate::screensaver_dbus::start_server(&event_loop);

        // Start Idle Monitor D-Bus server
        crate::idle_monitor_dbus::start_server(&event_loop);

        // Start USB mount monitor
        crate::mount_monitor::start_monitor(&event_loop);

        // Start accessibility text-scaling watcher
        crate::text_scale::start_watcher(&event_loop);

        // Start org.gnome.SessionManager D-Bus server
        crate::session_manager_dbus::start_server(&event_loop);

        // Start org.gnome.Mutter.DisplayConfig D-Bus server
        let display_state = crate::display_config_dbus::start_server(&event_loop);

        // Start night light (org.gnome.SettingsDaemon.Color) listener
        crate::night_light::start_listener(&event_loop);

        // Start airplane mode (rfkill) listener
        crate::rfkill_dbus::start_listener(&event_loop);

        // Start GeoClue2 location listener
        crate::geoclue_dbus::start_listener(&event_loop);

        // Start hostname listener
        crate::hostname_dbus::start_listener(&event_loop);

        // Start logind session listener (lock/sleep signals)
        crate::logind_dbus::start_listener(&event_loop);

        crate::powermanager_dbus::start(&event_loop);

        // Start xdg-desktop-portal backend
        crate::portal_dbus::start_server(&event_loop);

        // Read keyboard layouts from env (e.g., FORGE_XKB_LAYOUTS="us,it,de").
        // When unset, leave the list empty so the XKB_DEFAULT_* env vars
        // applied via XkbConfig::default() stand.
        let xkb_layouts: Vec<String> = env::var("FORGE_XKB_LAYOUTS")
            .ok()
            .map(|v| {
                v.split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect()
            })
            .unwrap_or_default();

        // Kick off the first system poll immediately so the topbar can be
        // populated within ~1 s of startup, instead of waiting for the
        // status-bar timer to spawn a poll on its second fire (~60 s).
        let initial_poll_rx = {
            let (tx, rx) = mpsc::channel();
            std::thread::spawn(move || {
                let _ = tx.send(SystemState::poll_snapshot());
            });
            rx
        };

        let pipewire = match crate::pw::PipeWire::new(&event_loop) {
            Ok(pw) => Some(pw),
            Err(e) => {
                warn!("pipewire unavailable, portal screencast disabled: {e}");
                None
            }
        };

        Self {
            protocol: ProtocolState {
                compositor_state,
                xdg_shell_state,
                shm_state,
                seat_state,
                data_device_state,
                primary_selection_state,
                data_control_state,
                layer_shell_state,
                xdg_activation_state,
                kde_decoration_state,
                keyboard_shortcuts_inhibit_state,
                lock_state,
                idle_notifier_state,
                dmabuf_state,
                foreign_toplevel_state,
                ext_foreign_toplevel_list_state,
                output_power_state,
                session_manager_state,
                xwayland_shell_state,
            },
            display_handle,
            event_loop,
            seat_name,
            accel_token,
            windows,
            backend,
            seat,
            render: RenderState {
                text_renderer: TextRenderer::new(),
                frame_pacer: FramePacer::default(),
                stalled: false,
                clipboard_png: None,
            },
            shell: ShellState {
                osd: Osd::new(),
                topbar: crate::topbar::Topbar::new(),
                app_switcher: crate::app_switcher::AppSwitcher::new(),
                mpris: MprisState::new(),
                polkit: PolkitAgent::new(polkit_sender, polkit_pending),
                portal: PortalState::new(),
                screenshot: crate::screenshot::ScreenshotSelector::new(),
                sys_state: SystemState::bootstrap(),
                sys_state_rx: Some(initial_poll_rx),
                pending_launcher_query: None,
            },
            display_state,
            popups: PopupManager::default(),
            screencast: None,
            pipewire,
            exec_override,
            init_done: false,
            session: SessionState {
                display_on: true,
                wake_display: true,
                greeter_mode: {
                    let greeter = crate::logind_dbus::is_greeter_session();
                    if greeter {
                        // The greeter must be visible on every screen; keep the
                        // whole greeter session mirrored. Runs before the DRM
                        // devices are enumerated, so the initial output setup
                        // already sees the override.
                        crate::output::set_mirror_forced(true);
                    }
                    greeter
                },
                kiosk: false,
                locker: None,
                active_lock: None,
                idle_inhibitors: Vec::new(),
                osk_visible: false,
                last_ti_active: false,
                has_keyboard: true,
                power_key_pressed_at: None,
                power_key_inhibitor: None,
                auto_brightness: AutoBrightness::new(),
            },
            input: InputState {
                key_bindings: Vec::new(),
                active_keys: HashSet::new(),
                touch_state,
                draw_cursor: false,
                cursor_position: None,
                cursor_status: CursorImageStatus::default_named(),
                gesture_recognizer: crate::gesture_recognizer::GestureRecognizer::new(
                    forge_ipc::gesture::GestureConfig::default(),
                ),
                corner_recognizer: {
                    let mut r = crate::corner_action::CornerRecognizer::new();
                    r.set_thresholds(config.corners.push_distance, config.corners.pin_radius);
                    r
                },
                edge_recognizer: crate::edge_action::BottomEdgeRecognizer::new(),
                ssd_last_press: None,
                armed_ssd: None,
                topbar_swipe: None,
                bottom_swipe: None,
                dwell_timer: None,
                dwell_anchor: None,
                xkb_layouts,
                current_layout: 0,
            },
            last_focus: None,
            terminated: false,

            xwm: None,
        }
    }

    fn register_signal_handler(event_loop: &LoopHandle<'static, Self>) {
        let signals = Signals::new(&[Signal::SIGTERM]).expect("signal mask");
        event_loop
            .insert_source(signals, |_, _, forge| forge.terminated = true)
            .expect("signal source");
    }

    fn register_socket(event_loop: &LoopHandle<'static, Self>, dh: &DisplayHandle) -> String {
        unsafe fn set_var_if_unset(key: &str, value: &str) {
            unsafe {
                if env::var_os(key).is_none() {
                    env::set_var(key, value);
                }
            }
        }

        let source = ListeningSocketSource::new_auto().expect("wayland socket");
        let name = source.socket_name().to_string_lossy().into_owned();

        event_loop
            .insert_source(source, {
                let mut dh = dh.clone();
                move |stream, _, _| {
                    let _ = dh.insert_client(stream, Arc::new(ClientState::default()));
                }
            })
            .expect("socket source");

        // Set session environment variables before any child processes are spawned.
        // SAFETY: called once during init before any threads read these variables.
        // Respect values the display manager / session wrapper already set
        // (XDG_CURRENT_DESKTOP, XDG_SESSION_DESKTOP) so the right portal
        // backend is selected.
        unsafe {
            env::set_var("WAYLAND_DISPLAY", &name);
            set_var_if_unset("XDG_CURRENT_DESKTOP", "Forge:GNOME");
            set_var_if_unset("XDG_SESSION_TYPE", "wayland");
            set_var_if_unset("XDG_SESSION_DESKTOP", "forge");
            env::set_var("GDK_BACKEND", "wayland");
            env::set_var("QT_QPA_PLATFORM", "wayland");
            env::set_var("SDL_VIDEODRIVER", "wayland");
            env::set_var("CLUTTER_BACKEND", "wayland");
            env::set_var("MOZ_ENABLE_WAYLAND", "1");
        }
        info!("Wayland socket: {name}");

        // Export WAYLAND_DISPLAY and session variables to systemd user manager and D-Bus
        // so that apps launched via D-Bus activation or systemd can find the display.
        Self::export_session_environment();

        name
    }

    fn export_session_environment() {
        let env_vars = [
            "WAYLAND_DISPLAY",
            "XDG_CURRENT_DESKTOP",
            "XDG_SESSION_TYPE",
            "XDG_SESSION_DESKTOP",
            "GDK_BACKEND",
            "QT_QPA_PLATFORM",
            "MOZ_ENABLE_WAYLAND",
        ];

        let mut args = vec!["--user", "import-environment"];
        args.extend_from_slice(&env_vars);
        let _ = std::process::Command::new("systemctl")
            .args(&args)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .and_then(|mut c| c.wait());

        let _ = std::process::Command::new("dbus-update-activation-environment")
            .args(&env_vars)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .and_then(|mut c| c.wait());

        Self::sd_notify_ready();
    }

    // sd_notify in-process so sender PID == MainPID; required because the
    // unit uses NotifyAccess=main to reject stray STOPPING=1 from
    // autostart-condition helpers leaking into our cgroup.
    fn sd_notify_ready() {
        use std::os::unix::net::UnixDatagram;
        let Ok(path) = std::env::var("NOTIFY_SOCKET") else {
            return;
        };
        if path.starts_with('@') {
            return;
        }
        let Ok(sock) = UnixDatagram::unbound() else {
            return;
        };
        let _ = sock.send_to(b"READY=1\n", &path);
    }

    fn register_display_source(event_loop: &LoopHandle<'static, Self>, display: Display<Self>) {
        event_loop
            .insert_source(
                Generic::new(display, Interest::READ, TriggerMode::Level),
                |_, display, forge| Ok(forge.dispatch_clients(display)),
            )
            .expect("display source");
    }

    fn dispatch_clients(&mut self, display: &mut NoIoDrop<Display<Self>>) -> PostAction {
        unsafe {
            display
                .get_mut()
                .dispatch_clients(self)
                .expect("wayland dispatch")
        };
        self.sync_osk();
        PostAction::Continue
    }

    pub fn run_init_if_needed(&mut self) {
        if self.init_done {
            return;
        }
        self.init_done = true;

        self.session.has_keyboard = Self::has_physical_keyboard();

        // If FORGE_MOBILE was set externally before launch, use it as override.
        if let Ok(val) = env::var("FORGE_MOBILE") {
            match val.as_str() {
                "0" => self.windows.set_mobile_override(Some(false)),
                "1" => self.windows.set_mobile_override(Some(true)),
                _ => {}
            }
        }

        let is_mobile = self.windows.canvas().is_mobile();
        // SAFETY: called once during init before any threads read this variable.
        unsafe { env::set_var("FORGE_MOBILE", if is_mobile { "1" } else { "0" }) };

        // Client-side cursor and X11-toolkit scaling. Wayland-native
        // toolkits follow wp_fractional_scale / preferred_buffer_scale on
        // their own, but client-drawn cursors read XCURSOR_SIZE and
        // Xwayland GTK apps read the integer GDK_SCALE.
        let scale = self.windows.canvas().scale();
        // SAFETY: same single-threaded init window as FORGE_MOBILE above.
        unsafe {
            env::set_var("XCURSOR_SIZE", ((24.0 * scale).round() as i32).to_string());
            let gdk = scale.round() as i32;
            if gdk > 1 {
                env::set_var("GDK_SCALE", gdk.to_string());
            }
        }

        // Reserve space for the built-in topbar (28 logical px).
        self.windows
            .configure_topbar(crate::topbar::Topbar::height());

        self.update_display_state();

        // Apply the initial keyboard layout
        if let Some(layout) = self.input.xkb_layouts.first() {
            let layout = layout.clone();
            self.set_xkb_config(XkbConfig {
                layout: &layout,
                ..Default::default()
            });
        }

        // Set `DISPLAY` before spawning the session so children inherit it.
        self.start_xwayland();

        self.exec_init_script();
        Self::start_desktop_portals();
        self.ensure_greeter_osk();
    }

    fn start_xwayland(&mut self) {
        use smithay::xwayland::xwm::X11Wm;
        use smithay::xwayland::{XWayland, XWaylandEvent};
        use std::process::Stdio;

        let dh = self.display_handle.clone();
        match XWayland::spawn(
            &dh,
            None,
            std::iter::empty::<(String, String)>(),
            true,
            Stdio::null(),
            Stdio::null(),
            |_| {},
        ) {
            Ok((xwayland, client)) => {
                let display_num = xwayland.display_number();
                info!("XWayland started on display :{display_num}");
                unsafe {
                    env::set_var("DISPLAY", format!(":{display_num}"));
                }

                // `take_socket()` returns `Ok(None)` until XWayland writes
                // readiness over its displayfd pipe. Wait for the event.
                let mut client = Some(client);
                if let Err(e) = self
                    .event_loop
                    .insert_source(xwayland, move |event, _, forge| match event {
                        XWaylandEvent::Ready {
                            x11_socket,
                            display_number: _,
                        } => {
                            let Some(client) = client.take() else {
                                return;
                            };
                            match X11Wm::start_wm(
                                forge.event_loop.clone(),
                                &forge.display_handle,
                                x11_socket,
                                client,
                            ) {
                                Ok(wm) => {
                                    forge.xwm = Some(wm);
                                    info!("X11 window manager started");
                                }
                                Err(e) => error!("Failed to start X11 WM: {e}"),
                            }
                        }
                        XWaylandEvent::Error => {
                            error!("XWayland exited unexpectedly during startup");
                        }
                    })
                {
                    error!("Failed to register XWayland event source: {e}");
                }
            }
            Err(e) => warn!("Failed to start XWayland: {e}"),
        }
    }

    fn exec_init_script(&self) {
        if let Some(path) = &self.exec_override {
            if !path.exists() {
                error!("--exec path does not exist: {path:?}");
                return;
            }
            info!("Executing --exec target: {path:?}");
            let path = path.clone();
            std::thread::spawn(move || {
                let result = std::process::Command::new(&path)
                    .stdin(Stdio::null())
                    .stdout(Stdio::inherit())
                    .stderr(Stdio::inherit())
                    .spawn();
                match result {
                    Ok(mut child) => {
                        let _ = child.wait();
                        info!("--exec target {path:?} exited; terminating forge");
                        unsafe {
                            libc::kill(libc::getpid(), libc::SIGTERM);
                        }
                    }
                    Err(e) => error!("--exec target failed to spawn: {e}"),
                }
            });
            return;
        }

        let candidates = [
            dirs::config_dir().map(|d| d.join("forge").join(INIT_SCRIPT)),
            Some(PathBuf::from(format!("/etc/forge/{INIT_SCRIPT}"))),
        ];

        for path in candidates.into_iter().flatten() {
            if path.exists() {
                info!("Executing init script: {path:?}");
                if let Err(e) = daemon(path.as_os_str(), std::iter::empty()) {
                    error!("Init script failed: {e}");
                }
                return;
            }
        }

        info!("No init script found");
    }

    /// Start xdg-desktop-portal backends for screenshot, screencast, file chooser.
    fn start_desktop_portals() {
        // Try xdg-desktop-portal-wlr first (more compatible with wlroots-based compositors),
        // fall back to xdg-desktop-portal-gnome
        let portals = [
            ("xdg-desktop-portal-wlr", &[] as &[&str]),
            ("xdg-desktop-portal-gnome", &[]),
        ];

        for (portal, args) in portals {
            if which_exists(portal) {
                info!("Starting {portal}");
                let _ = daemon(portal, args.iter().copied());
                break;
            }
        }

        // Start the main xdg-desktop-portal daemon
        if which_exists("xdg-desktop-portal") {
            let _ = daemon("xdg-desktop-portal", std::iter::empty::<&str>());
        }
    }
}
