//! Session management for the agent
//!
//! Handles the lifecycle of a remote session including:
//! - Connection to server
//! - Idle mode (heartbeat only, minimal resources)
//! - Active/streaming mode (capture and send frames)
//! - Input event handling

#[cfg(windows)]
use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};

use crate::capture::{self, Capturer};
use crate::chat::{ChatController, ChatMessage as ChatMsg};
use crate::config::Config;
use crate::encoder::{self, Encoder};
use crate::input::InputController;

/// Show the debug console window (Windows only)
#[cfg(windows)]
fn show_debug_console() {
    unsafe {
        let hwnd = GetConsoleWindow();
        if hwnd.0.is_null() {
            let _ = AllocConsole();
            tracing::info!("Debug console window opened");
        } else {
            let _ = ShowWindow(hwnd, SW_SHOW);
            tracing::info!("Debug console window shown");
        }
    }
}

#[cfg(not(windows))]
fn show_debug_console() {
    // No-op on non-Windows platforms
}

use crate::proto::{message, AgentStatus, ChatMessage, Heartbeat, HeartbeatAck, Message};
use crate::transport::WebSocketTransport;
use crate::tray::{TrayAction, TrayController};
use anyhow::Result;
use std::time::{Duration, Instant};

// Heartbeat interval (30 seconds)
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
// Status report interval (60 seconds)
const STATUS_INTERVAL: Duration = Duration::from_secs(60);
// Update check interval (1 hour)
const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(3600);

/// Session manager handles the remote control session
pub struct SessionManager {
    config: Config,
    transport: Option<WebSocketTransport>,
    state: SessionState,
    // Lazy-initialized streaming resources
    capturer: Option<Box<dyn Capturer>>,
    encoder: Option<Box<dyn Encoder>>,
    input: Option<InputController>,
    // Streaming state
    current_viewer_id: Option<String>,
    // Codec negotiated by the server for the current stream (Task 7). Set from
    // StartStream.video_codec; the encoder is built from it (guarded by the
    // agent's own hardware capability, with raw as the safe fallback).
    negotiated_codec: crate::proto::VideoCodec,
    // System info for status reports
    hostname: String,
    is_elevated: bool,
    start_time: Instant,
}

#[derive(Debug, Clone, PartialEq)]
enum SessionState {
    Disconnected,
    Connecting,
    Idle,      // Connected but not streaming - minimal resource usage
    Streaming, // Actively capturing and sending frames
}

impl SessionManager {
    /// Create a new session manager
    pub fn new(config: Config, is_elevated: bool) -> Self {
        let hostname = config.hostname();
        Self {
            config,
            transport: None,
            state: SessionState::Disconnected,
            capturer: None,
            encoder: None,
            input: None,
            current_viewer_id: None,
            // Default to RAW until the server negotiates otherwise (StartStream).
            negotiated_codec: crate::proto::VideoCodec::Raw,
            hostname,
            is_elevated,
            start_time: Instant::now(),
        }
    }

    /// Connect to the server
    pub async fn connect(&mut self) -> Result<()> {
        self.state = SessionState::Connecting;

        // Deterministic, recomputable identity reported alongside agent_id
        // (v2 stable-identity Task 1). Cached after the first call.
        let machine_uid = crate::identity::machine_uid();
        let transport = WebSocketTransport::connect(
            &self.config.server_url,
            &self.config.agent_id,
            &self.config.api_key,
            Some(&self.hostname),
            self.config.support_code.as_deref(),
            Some(&machine_uid),
        )
        .await?;

        self.transport = Some(transport);
        self.state = SessionState::Idle; // Start in idle mode

        tracing::info!("Connected to server, entering idle mode");

        Ok(())
    }

    /// Initialize streaming resources (capturer, encoder, input)
    fn init_streaming(&mut self) -> Result<()> {
        if self.capturer.is_some() {
            return Ok(()); // Already initialized
        }

        tracing::info!("Initializing streaming resources...");
        tracing::info!(
            "Capture config: use_dxgi={}, gdi_fallback={}, fps={}",
            self.config.capture.use_dxgi,
            self.config.capture.gdi_fallback,
            self.config.capture.fps
        );

        // Get primary display with panic protection
        tracing::debug!("Enumerating displays...");
        let primary_display = match std::panic::catch_unwind(capture::primary_display) {
            Ok(result) => result?,
            Err(e) => {
                tracing::error!("Panic during display enumeration: {:?}", e);
                return Err(anyhow::anyhow!("Display enumeration panicked"));
            }
        };
        tracing::info!(
            "Using display: {} ({}x{})",
            primary_display.name,
            primary_display.width,
            primary_display.height
        );

        // Create capturer with panic protection
        // Force GDI mode if DXGI fails or panics
        tracing::debug!(
            "Creating capturer (DXGI={})...",
            self.config.capture.use_dxgi
        );
        let capturer = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            capture::create_capturer(
                primary_display.clone(),
                self.config.capture.use_dxgi,
                self.config.capture.gdi_fallback,
            )
        })) {
            Ok(result) => result?,
            Err(e) => {
                tracing::error!("Panic during capturer creation: {:?}", e);
                // Try GDI-only as last resort
                tracing::warn!("Attempting GDI-only capture after DXGI panic...");
                capture::create_capturer(primary_display.clone(), false, false)?
            }
        };
        self.capturer = Some(capturer);
        tracing::info!("Capturer created successfully");

        // Create encoder from the NEGOTIATED codec (Task 7), guarded by the
        // agent's own hardware capability. `create_encoder_for` selects the H.264
        // encoder only if it can actually be constructed, otherwise it returns a
        // working raw encoder — so this never breaks the session.
        let chosen =
            encoder::select_codec(self.negotiated_codec, encoder::supports_hardware_h264());
        tracing::debug!(
            "Creating encoder (negotiated={:?}, chosen={:?}, quality={})...",
            self.negotiated_codec,
            chosen,
            self.config.encoding.quality
        );
        let encoder = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            encoder::create_encoder_for(chosen, self.config.encoding.quality)
        })) {
            Ok(result) => result?,
            Err(e) => {
                tracing::error!("Panic during encoder creation: {:?}", e);
                return Err(anyhow::anyhow!("Encoder creation panicked"));
            }
        };
        self.encoder = Some(encoder);
        tracing::info!("Encoder created successfully");

        // Create input controller with panic protection
        tracing::debug!("Creating input controller...");
        let input = match std::panic::catch_unwind(InputController::new) {
            Ok(result) => result?,
            Err(e) => {
                tracing::error!("Panic during input controller creation: {:?}", e);
                return Err(anyhow::anyhow!("Input controller creation panicked"));
            }
        };
        self.input = Some(input);

        tracing::info!("Streaming resources initialized successfully");
        Ok(())
    }

    /// Release streaming resources to save CPU/memory when idle
    fn release_streaming(&mut self) {
        if self.capturer.is_some() {
            tracing::info!("Releasing streaming resources");
            self.capturer = None;
            self.encoder = None;
            self.input = None;
            self.current_viewer_id = None;
        }
    }

    /// Get display count for status reports
    fn get_display_count(&self) -> i32 {
        capture::enumerate_displays()
            .map(|d| d.len() as i32)
            .unwrap_or(1)
    }

    /// Send agent status to server
    async fn send_status(&mut self) -> Result<()> {
        let status = AgentStatus {
            hostname: self.hostname.clone(),
            os_version: std::env::consts::OS.to_string(),
            is_elevated: self.is_elevated,
            uptime_secs: self.start_time.elapsed().as_secs() as i64,
            display_count: self.get_display_count(),
            is_streaming: self.state == SessionState::Streaming,
            agent_version: crate::build_info::short_version(),
            organization: self.config.company.clone().unwrap_or_default(),
            site: self.config.site.clone().unwrap_or_default(),
            tags: self.config.tags.clone(),
            // Advertise hardware H.264 capability so the server can negotiate the
            // codec (Task 7). Detected once and cached by the encoder module.
            supports_h264: encoder::supports_hardware_h264(),
            // Deterministic, recomputable hardware identity (v2 stable-identity
            // Task 1). Reported alongside the unchanged random agent_id; cached
            // after the first (registry) read.
            machine_uid: crate::identity::machine_uid(),
        };

        let msg = Message {
            payload: Some(message::Payload::AgentStatus(status)),
        };

        if let Some(transport) = self.transport.as_mut() {
            transport.send(msg).await?;
        }

        Ok(())
    }

    /// Send heartbeat to server
    async fn send_heartbeat(&mut self) -> Result<()> {
        let heartbeat = Heartbeat {
            timestamp: chrono::Utc::now().timestamp_millis(),
        };

        let msg = Message {
            payload: Some(message::Payload::Heartbeat(heartbeat)),
        };

        if let Some(transport) = self.transport.as_mut() {
            transport.send(msg).await?;
        }

        Ok(())
    }

    /// Run the session main loop with tray and chat event processing
    pub async fn run_with_tray(
        &mut self,
        tray: Option<&TrayController>,
        chat: Option<&ChatController>,
    ) -> Result<()> {
        if self.transport.is_none() {
            anyhow::bail!("Not connected");
        }

        // Send initial status
        self.send_status().await?;

        // Timing for heartbeat and status
        let mut last_heartbeat = Instant::now();
        let mut last_status = Instant::now();
        let mut last_frame_time = Instant::now();
        let mut last_update_check = Instant::now();
        let frame_interval = Duration::from_millis(1000 / self.config.capture.fps as u64);

        // Main loop
        loop {
            // Process tray events
            if let Some(t) = tray {
                if let Some(action) = t.process_events() {
                    match action {
                        TrayAction::EndSession => {
                            tracing::info!("User requested session end via tray");
                            return Err(anyhow::anyhow!("USER_EXIT: Session ended by user"));
                        }
                        TrayAction::ShowDetails => {
                            tracing::info!("User requested details (not yet implemented)");
                        }
                        TrayAction::ShowDebugWindow => {
                            show_debug_console();
                        }
                    }
                }

                if t.exit_requested() {
                    tracing::info!("Exit requested via tray");
                    return Err(anyhow::anyhow!("USER_EXIT: Exit requested by user"));
                }
            }

            // Process incoming messages
            let messages: Vec<Message> = {
                let transport = self.transport.as_mut().unwrap();
                let mut msgs = Vec::new();
                while let Some(msg) = transport.try_recv()? {
                    msgs.push(msg);
                }
                msgs
            };

            for msg in messages {
                // Handle chat messages specially
                if let Some(message::Payload::ChatMessage(chat_msg)) = &msg.payload {
                    if let Some(c) = chat {
                        c.add_message(ChatMsg {
                            id: chat_msg.id.clone(),
                            sender: chat_msg.sender.clone(),
                            content: chat_msg.content.clone(),
                            timestamp: chat_msg.timestamp,
                        });
                    }
                    continue;
                }

                // Handle control messages that affect state
                if let Some(ref payload) = msg.payload {
                    match payload {
                        message::Payload::StartStream(start) => {
                            tracing::info!("StartStream received from viewer: {}", start.viewer_id);
                            // Apply the server-negotiated codec (Task 7) BEFORE
                            // building the encoder. An older server that omits the
                            // field sends 0 = VIDEO_CODEC_RAW, preserving the raw
                            // default. `select_codec` (in init_streaming) re-guards
                            // against missing hardware.
                            self.negotiated_codec =
                                crate::proto::VideoCodec::try_from(start.video_codec)
                                    .unwrap_or(crate::proto::VideoCodec::Raw);
                            tracing::info!("Server negotiated codec: {:?}", self.negotiated_codec);
                            if let Err(e) = self.init_streaming() {
                                tracing::error!("Failed to init streaming: {}", e);
                            } else {
                                self.state = SessionState::Streaming;
                                self.current_viewer_id = Some(start.viewer_id.clone());
                                tracing::info!("Now streaming to viewer {}", start.viewer_id);
                            }
                            continue;
                        }
                        message::Payload::StopStream(stop) => {
                            tracing::info!("StopStream received for viewer: {}", stop.viewer_id);
                            // Only stop if it matches current viewer
                            if self.current_viewer_id.as_ref() == Some(&stop.viewer_id) {
                                self.release_streaming();
                                self.state = SessionState::Idle;
                                tracing::info!("Stopped streaming, returning to idle mode");
                            }
                            continue;
                        }
                        message::Payload::Heartbeat(hb) => {
                            // Respond to server heartbeat with ack
                            let ack = HeartbeatAck {
                                client_timestamp: hb.timestamp,
                                server_timestamp: chrono::Utc::now().timestamp_millis(),
                            };
                            let ack_msg = Message {
                                payload: Some(message::Payload::HeartbeatAck(ack)),
                            };
                            if let Some(transport) = self.transport.as_mut() {
                                let _ = transport.send(ack_msg).await;
                            }
                            continue;
                        }
                        message::Payload::ConsentRequest(req) => {
                            // ATTENDED-MODE CONSENT (Task 5). The server is holding
                            // this session in `consent_state = pending` and will not
                            // surface it to the technician until we reply. Show the
                            // end user a native dialog and return their decision; the
                            // dialog blocks, so run it off the async runtime. If the
                            // user closes it / no choice is made, `prompt_consent`
                            // returns false (deny).
                            self.handle_consent_request(req.clone()).await;
                            continue;
                        }
                        _ => {}
                    }
                }

                // Handle other messages (input events, disconnect, etc.)
                self.handle_message(msg).await?;
            }

            // Check for outgoing chat messages
            if let Some(c) = chat {
                if let Some(outgoing) = c.poll_outgoing() {
                    let chat_proto = ChatMessage {
                        id: outgoing.id,
                        sender: "client".to_string(),
                        content: outgoing.content,
                        timestamp: outgoing.timestamp,
                    };
                    let msg = Message {
                        payload: Some(message::Payload::ChatMessage(chat_proto)),
                    };
                    let transport = self.transport.as_mut().unwrap();
                    transport.send(msg).await?;
                }
            }

            // State-specific behavior
            match self.state {
                SessionState::Idle => {
                    // In idle mode, just send heartbeats and status periodically
                    if last_heartbeat.elapsed() >= HEARTBEAT_INTERVAL {
                        last_heartbeat = Instant::now();
                        if let Err(e) = self.send_heartbeat().await {
                            tracing::warn!("Failed to send heartbeat: {}", e);
                        }
                    }

                    if last_status.elapsed() >= STATUS_INTERVAL {
                        last_status = Instant::now();
                        if let Err(e) = self.send_status().await {
                            tracing::warn!("Failed to send status: {}", e);
                        }
                    }

                    // Periodic update check (only for persistent agents, not support sessions)
                    if self.config.support_code.is_none()
                        && last_update_check.elapsed() >= UPDATE_CHECK_INTERVAL
                    {
                        last_update_check = Instant::now();
                        let server_url = self
                            .config
                            .server_url
                            .replace("/ws/agent", "")
                            .replace("wss://", "https://")
                            .replace("ws://", "http://");
                        match crate::update::check_for_update(&server_url).await {
                            Ok(Some(version_info)) => {
                                tracing::info!(
                                    "Update available: {} -> {}",
                                    crate::build_info::VERSION,
                                    version_info.latest_version
                                );
                                if let Err(e) = crate::update::perform_update(&version_info).await {
                                    tracing::error!("Auto-update failed: {}", e);
                                }
                            }
                            Ok(None) => {
                                tracing::debug!("No update available");
                            }
                            Err(e) => {
                                tracing::debug!("Update check failed: {}", e);
                            }
                        }
                    }

                    // Longer sleep in idle mode to reduce CPU usage
                    tokio::time::sleep(Duration::from_millis(100)).await;
                }
                SessionState::Streaming => {
                    // In streaming mode, capture and send frames
                    if last_frame_time.elapsed() >= frame_interval {
                        last_frame_time = Instant::now();

                        if let (Some(capturer), Some(encoder)) =
                            (self.capturer.as_mut(), self.encoder.as_mut())
                        {
                            if let Ok(Some(frame)) = capturer.capture() {
                                if let Ok(encoded) = encoder.encode(&frame) {
                                    if encoded.size > 0 {
                                        let msg = Message {
                                            payload: Some(message::Payload::VideoFrame(
                                                encoded.frame,
                                            )),
                                        };
                                        let transport = self.transport.as_mut().unwrap();
                                        if let Err(e) = transport.send(msg).await {
                                            tracing::warn!("Failed to send frame: {}", e);
                                        }
                                    }
                                }
                            }
                        }
                    }

                    // Short sleep in streaming mode
                    tokio::time::sleep(Duration::from_millis(1)).await;
                }
                _ => {
                    // Disconnected or connecting - shouldn't be in main loop
                    tokio::time::sleep(Duration::from_millis(100)).await;
                }
            }

            // Check if still connected
            if let Some(transport) = self.transport.as_ref() {
                if !transport.is_connected() {
                    tracing::warn!("Connection lost");
                    break;
                }
            } else {
                tracing::warn!("Transport is None");
                break;
            }
        }

        self.release_streaming();
        self.state = SessionState::Disconnected;
        Ok(())
    }

    /// Handle an attended-mode `ConsentRequest` from the server (Task 5).
    ///
    /// Shows the end user a native consent dialog (off the async runtime, since
    /// it blocks) and sends a `ConsentResponse` carrying their decision. A
    /// closed dialog / unavailable surface is treated as a DENY. The server
    /// gates the whole session on this reply, so we always send a response (even
    /// on send failure the server's consent timeout will deny).
    async fn handle_consent_request(&mut self, req: crate::proto::ConsentRequest) {
        use crate::consent::{prompt_consent, ConsentAccessMode};
        use crate::proto::ConsentResponse;

        let session_id = req.session_id.clone();
        let technician_name = req.technician_name.clone();
        let access = ConsentAccessMode::from_proto(req.access_mode);

        tracing::info!(
            "Consent requested for session {} by '{}' ({:?}); prompting end user",
            session_id,
            technician_name,
            access
        );

        // The MessageBox blocks the calling thread, so it runs on the blocking
        // pool to avoid stalling the tokio runtime. Note, however, that the main
        // session loop `.await`s this method (see the ConsentRequest arm), so
        // the loop is SUSPENDED for the user's entire think-time and does NOT
        // process or respond to server heartbeats while the dialog is open.
        // This is safe because CONSENT_TIMEOUT_SECS (60s, server-side) is within
        // the server's 90s HEARTBEAT_TIMEOUT_SECS: the prompt resolves before the
        // server would consider the agent dead, so the session is not torn down.
        let granted = tokio::task::spawn_blocking(move || prompt_consent(&technician_name, access))
            .await
            .unwrap_or_else(|e| {
                // The blocking task panicked — fail closed (deny).
                tracing::error!("Consent dialog task failed: {}; denying", e);
                false
            });

        tracing::info!(
            "End user {} consent for session {}",
            if granted { "GRANTED" } else { "DENIED" },
            session_id
        );

        let response = Message {
            payload: Some(message::Payload::ConsentResponse(ConsentResponse {
                session_id,
                granted,
                reason: if granted {
                    String::new()
                } else {
                    "user_declined".to_string()
                },
            })),
        };

        if let Some(transport) = self.transport.as_mut() {
            if let Err(e) = transport.send(response).await {
                tracing::error!("Failed to send ConsentResponse: {}", e);
            }
        }
    }

    /// Handle incoming message from server
    async fn handle_message(&mut self, msg: Message) -> Result<()> {
        match msg.payload {
            Some(message::Payload::MouseEvent(mouse)) => {
                if let Some(input) = self.input.as_mut() {
                    use crate::input::MouseButton;
                    use crate::proto::MouseEventType;

                    match MouseEventType::try_from(mouse.event_type)
                        .unwrap_or(MouseEventType::MouseMove)
                    {
                        MouseEventType::MouseMove => {
                            input.mouse_move(mouse.x, mouse.y)?;
                        }
                        MouseEventType::MouseDown => {
                            input.mouse_move(mouse.x, mouse.y)?;
                            if let Some(ref buttons) = mouse.buttons {
                                if buttons.left {
                                    input.mouse_click(MouseButton::Left, true)?;
                                }
                                if buttons.right {
                                    input.mouse_click(MouseButton::Right, true)?;
                                }
                                if buttons.middle {
                                    input.mouse_click(MouseButton::Middle, true)?;
                                }
                            }
                        }
                        MouseEventType::MouseUp => {
                            if let Some(ref buttons) = mouse.buttons {
                                if buttons.left {
                                    input.mouse_click(MouseButton::Left, false)?;
                                }
                                if buttons.right {
                                    input.mouse_click(MouseButton::Right, false)?;
                                }
                                if buttons.middle {
                                    input.mouse_click(MouseButton::Middle, false)?;
                                }
                            }
                        }
                        MouseEventType::MouseWheel => {
                            input.mouse_scroll(mouse.wheel_delta_x, mouse.wheel_delta_y)?;
                        }
                    }
                }
            }

            Some(message::Payload::KeyEvent(key)) => {
                if let Some(input) = self.input.as_mut() {
                    // Full-fidelity scan-code injection: pass the viewer-captured
                    // scan code and extended-key flag through. A scan_code of 0 (older
                    // viewers / synthesized events) makes the agent derive it from the VK.
                    input.key_event_full(
                        key.vk_code as u16,
                        key.scan_code as u16,
                        key.is_extended,
                        key.down,
                    )?;
                }
            }

            Some(message::Payload::SpecialKey(special)) => {
                if let Some(input) = self.input.as_mut() {
                    use crate::proto::SpecialKey;
                    if let Ok(SpecialKey::CtrlAltDel) = SpecialKey::try_from(special.key) {
                        input.send_ctrl_alt_del()?;
                    }
                }
            }

            Some(message::Payload::AdminCommand(cmd)) => {
                use crate::proto::AdminCommandType;
                tracing::info!("Admin command received: {:?} - {}", cmd.command, cmd.reason);

                match AdminCommandType::try_from(cmd.command).ok() {
                    Some(AdminCommandType::AdminUninstall) => {
                        tracing::warn!("Uninstall command received from server");
                        // Return special error to trigger uninstall in main loop
                        return Err(anyhow::anyhow!("ADMIN_UNINSTALL: {}", cmd.reason));
                    }
                    Some(AdminCommandType::AdminRestart) => {
                        tracing::info!("Restart command received from server");
                        // For now, just disconnect - the auto-restart logic will handle it
                        return Err(anyhow::anyhow!("ADMIN_RESTART: {}", cmd.reason));
                    }
                    Some(AdminCommandType::AdminUpdate) => {
                        tracing::info!("Update command received from server: {}", cmd.reason);
                        // Trigger update check and perform update if available
                        // The server URL is derived from the config
                        let server_url = self
                            .config
                            .server_url
                            .replace("/ws/agent", "")
                            .replace("wss://", "https://")
                            .replace("ws://", "http://");
                        match crate::update::check_for_update(&server_url).await {
                            Ok(Some(version_info)) => {
                                tracing::info!(
                                    "Update available: {} -> {}",
                                    crate::build_info::VERSION,
                                    version_info.latest_version
                                );
                                if let Err(e) = crate::update::perform_update(&version_info).await {
                                    tracing::error!("Update failed: {}", e);
                                }
                                // If we get here, the update failed (perform_update exits on success)
                            }
                            Ok(None) => {
                                tracing::info!("Already running latest version");
                            }
                            Err(e) => {
                                tracing::error!("Failed to check for updates: {}", e);
                            }
                        }
                    }
                    None => {
                        tracing::warn!("Unknown admin command: {}", cmd.command);
                    }
                }
            }

            Some(message::Payload::Disconnect(disc)) => {
                tracing::info!("Disconnect requested: {}", disc.reason);
                if disc.reason.contains("cancelled") {
                    return Err(anyhow::anyhow!("SESSION_CANCELLED: {}", disc.reason));
                }
                if disc.reason.contains("administrator") || disc.reason.contains("Disconnected") {
                    return Err(anyhow::anyhow!("ADMIN_DISCONNECT: {}", disc.reason));
                }
                return Err(anyhow::anyhow!("Disconnect: {}", disc.reason));
            }

            _ => {
                // Ignore unknown messages
            }
        }

        Ok(())
    }
}
