//! `StateService` gRPC implementation.
//!
//! Provides editor state queries for v2 protocol clients.

// `Status` is tonic's standard error type - size is inherent to the library
#![allow(clippy::result_large_err)]

use std::sync::Arc;

use {
    reovim_protocol::v2::{
        CaptureRequestPayload, GetCursorRequest, GetCursorResponse, GetLayoutRequest,
        GetLayoutResponse, GetModeRequest, GetModeResponse, GetOptionsRequest, GetOptionsResponse,
        GetRegistersRequest, GetRegistersResponse, GetScreenContentRequest,
        GetScreenContentResponse, GetSelectionRequest, GetSelectionResponse,
        GetVisibleLinesRequest, GetVisibleLinesResponse, Notification, Position, RegisterEntry,
        SplitDirection, SubmitCaptureRequest, SubmitCaptureResponseReply, TabPageInfo, WindowLeaf,
        WindowNode, WindowRect, WindowSplit, notification::Payload,
        state_service_server::StateService, window_node::Node,
    },
    tonic::{Request, Response, Status},
};

use crate::{
    grpc::auth::resolve_target_client_id,
    session::{
        CaptureError, CaptureResult, ClientEventType, ClientId, Session, SessionId,
        SessionRegistry, wait_for_capture,
    },
};

/// Convert a `CaptureError` into a tonic `Status`.
fn capture_error_to_status(e: CaptureError) -> Status {
    match e {
        CaptureError::NoTuiClient => Status::unavailable(e.to_string()),
        CaptureError::Timeout => Status::deadline_exceeded(e.to_string()),
        CaptureError::Disconnected => Status::aborted(e.to_string()),
        CaptureError::InvalidResponse(msg) => Status::internal(msg),
    }
}

/// Convert a driver-layer `Window` to a proto `WindowLeaf`.
///
/// This conversion happens in the gRPC layer to keep the driver crate
/// free of protocol dependencies.
#[allow(clippy::cast_possible_truncation)]
fn window_to_leaf(window: &reovim_driver_session::Window) -> WindowLeaf {
    WindowLeaf {
        window_id: window.id.as_usize() as u64,
        // Phase #479: Use optional to eliminate ID ambiguity (0 vs "no buffer")
        buffer_id: window.buffer_id.map(|id| id.as_usize() as u64),
        rect: Some(WindowRect {
            x: 0, // Position calculated by client based on layout
            y: 0,
            width: u64::from(window.viewport.width),
            height: u64::from(window.viewport.height),
        }),
    }
}

/// Convert a kernel `OptionValue` to a proto `OptionValue`.
fn kernel_to_proto_option(
    value: &reovim_kernel::api::v1::OptionValue,
) -> reovim_protocol::v2::OptionValue {
    use reovim_protocol::v2::option_value::Value;

    let proto_value = match value {
        reovim_kernel::api::v1::OptionValue::Bool(b) => Some(Value::BoolValue(*b)),
        reovim_kernel::api::v1::OptionValue::Integer(i) => Some(Value::IntValue(*i)),
        reovim_kernel::api::v1::OptionValue::String(s) => Some(Value::StringValue(s.clone())),
        reovim_kernel::api::v1::OptionValue::Choice { value, .. } => {
            Some(Value::StringValue(value.clone()))
        }
    };

    reovim_protocol::v2::OptionValue { value: proto_value }
}

/// gRPC `StateService` implementation.
///
/// Bridges v2 protocol state queries to the session system.
/// Currently provides basic mode and cursor information.
pub struct StateServiceImpl {
    /// Shared session registry.
    sessions: Arc<SessionRegistry>,
    /// Default session ID to use when not specified.
    default_session_id: SessionId,
}

impl StateServiceImpl {
    /// Create a new `StateService` with access to the session registry.
    #[must_use]
    pub const fn new(sessions: Arc<SessionRegistry>, default_session_id: SessionId) -> Self {
        Self {
            sessions,
            default_session_id,
        }
    }

    /// Get the default session.
    fn get_session(&self) -> Result<Arc<Session>, Status> {
        self.sessions
            .get(&self.default_session_id)
            .ok_or_else(|| Status::not_found("No active session"))
    }
}

#[tonic::async_trait]
impl StateService for StateServiceImpl {
    /// Get the current editor mode.
    ///
    /// Returns the mode from the client's per-client mode stack.
    ///
    /// # Per-client state (#471): Per-client mode isolation
    ///
    /// Each client has its own mode stack. This method returns the mode
    /// for the specified client only.
    ///
    /// # Errors
    ///
    /// - `Unauthenticated`: No session token (#483)
    /// - `NotFound`: Client with given ID not found in session
    async fn get_mode(
        &self,
        request: Request<GetModeRequest>,
    ) -> Result<Response<GetModeResponse>, Status> {
        // #483 Phase 5: Token for auth, body client_id for targeting
        let token_client_id = request.extensions().get::<ClientId>().copied();
        let req = request.into_inner();
        let session = self.get_session()?;

        let client_id = resolve_target_client_id(token_client_id, req.client_id)?;

        // Per-client mode lookup - now required (no fallback to shared state)
        let mode = session.client_current_mode(client_id).ok_or_else(|| {
            // Log to ring buffer before returning error
            session.with_client_ring_buffer(client_id, |rb| {
                rb.log_event(
                    ClientEventType::Error,
                    format!("CLIENT_NOT_FOUND: get_mode client_id={}", req.client_id),
                );
            });
            Status::not_found(format!("Client {} not found", req.client_id))
        })?;

        let name = mode.name().to_string();
        let display = name.to_uppercase();
        let is_insert = name.contains("insert") || name.contains("cmdline");

        Ok(Response::new(GetModeResponse {
            name,
            display,
            is_insert,
        }))
    }

    /// Get cursor position in the active window/buffer.
    ///
    /// # Phase #471: Per-client cursor isolation
    ///
    /// Returns the cursor from the client's per-client window layout.
    ///
    /// # Errors
    ///
    /// - `InvalidArgument`: `client_id=0` is reserved (like PID 1)
    /// - `NotFound`: Client with given ID not found, or no active window
    #[allow(clippy::cast_possible_truncation)]
    async fn get_cursor(
        &self,
        request: Request<GetCursorRequest>,
    ) -> Result<Response<GetCursorResponse>, Status> {
        // #483 Phase 5: Token for auth, body client_id for targeting
        let token_client_id = request.extensions().get::<ClientId>().copied();
        let req = request.into_inner();
        let session = self.get_session()?;

        let client_id = resolve_target_client_id(token_client_id, req.client_id)?;

        // Per-client state lookup - now required (no fallback to shared state)
        let state = session.client_state(client_id).ok_or_else(|| {
            session.with_client_ring_buffer(client_id, |rb| {
                rb.log_event(
                    ClientEventType::Error,
                    format!("CLIENT_NOT_FOUND: get_cursor client_id={}", req.client_id),
                );
            });
            Status::not_found(format!("Client {} not found", req.client_id))
        })?;

        let window = state
            .windows
            .active()
            .ok_or_else(|| Status::not_found("No active window"))?;

        let cursor = &window.cursor;
        Ok(Response::new(GetCursorResponse {
            window_id: window.id.as_usize() as u64,
            position: Some(Position {
                line: cursor.line as u64,
                column: cursor.column as u64,
            }),
        }))
    }

    /// Get editor options.
    ///
    /// Returns the current values of requested options. If `names` is empty,
    /// returns all registered options. Values are resolved at global scope.
    ///
    /// # Errors
    ///
    /// - `NotFound`: No active session
    #[allow(clippy::cast_possible_truncation)]
    async fn get_options(
        &self,
        request: Request<GetOptionsRequest>,
    ) -> Result<Response<GetOptionsResponse>, Status> {
        let req = request.into_inner();
        let session = self.get_session()?;

        let options = session
            .with_state(|state| {
                use reovim_kernel::api::v1::OptionScopeId;

                let registry = &state.app.kernel.options;
                let names: Vec<String> = if req.names.is_empty() {
                    registry.list_all()
                } else {
                    req.names
                        .iter()
                        .filter_map(|n| registry.resolve_name(n))
                        .collect()
                };

                let mut map = std::collections::HashMap::new();
                for name in &names {
                    if let Some(value) = registry.get(name, OptionScopeId::Global) {
                        map.insert(name.clone(), kernel_to_proto_option(&value));
                    }
                }
                map
            })
            .await;

        Ok(Response::new(GetOptionsResponse { options }))
    }

    /// Get window layout tree.
    ///
    /// Returns the current window layout as a tree structure. Each window
    /// is represented as a `WindowLeaf` with its buffer, position, and size.
    /// Multiple windows are wrapped in a `WindowSplit` container.
    ///
    /// # Layout Model
    ///
    /// For Phase 11, windows are stored in a flat list. The proto tree is
    /// constructed as:
    /// - 0 windows: empty response (no root)
    /// - 1 window: single `WindowLeaf`
    /// - N windows: `WindowSplit` with N leaf children (vertical split)
    ///
    /// Phase 12 (`LayoutService`) will add proper nested split support.
    ///
    /// # Phase #471: Per-client layout isolation
    ///
    /// Returns the layout from the client's per-client window layout.
    ///
    /// # Errors
    ///
    /// - `InvalidArgument`: `client_id=0` is reserved (like PID 1)
    /// - `NotFound`: Client with given ID not found in session
    #[allow(clippy::cast_possible_truncation)]
    async fn get_layout(
        &self,
        request: Request<GetLayoutRequest>,
    ) -> Result<Response<GetLayoutResponse>, Status> {
        // #483 Phase 5: Token for auth, body client_id for targeting
        let token_client_id = request.extensions().get::<ClientId>().copied();
        let req = request.into_inner();
        let session = self.get_session()?;

        let client_id = resolve_target_client_id(token_client_id, req.client_id)?;

        // Per-client state lookup - now required (no fallback to shared state)
        let state = session.client_state(client_id).ok_or_else(|| {
            session.with_client_ring_buffer(client_id, |rb| {
                rb.log_event(
                    ClientEventType::Error,
                    format!("CLIENT_NOT_FOUND: get_layout client_id={}", req.client_id),
                );
            });
            Status::not_found(format!("Client {} not found", req.client_id))
        })?;

        let layout = &state.windows;
        // Phase #479: Use optional to eliminate ID ambiguity (0 vs "no focus")
        let focused_id = layout.active_id().map(|id| id.as_usize() as u64);

        let root = match layout.len() {
            0 => None,
            1 => layout.active().map(|w| WindowNode {
                node: Some(Node::Leaf(window_to_leaf(w))),
            }),
            _ => {
                let children: Vec<WindowNode> = layout
                    .windows
                    .iter()
                    .map(|w| WindowNode {
                        node: Some(Node::Leaf(window_to_leaf(w))),
                    })
                    .collect();

                Some(WindowNode {
                    node: Some(Node::Split(WindowSplit {
                        direction: SplitDirection::Vertical.into(),
                        children,
                    })),
                })
            }
        };

        // #401 Phase 5: Populate tab info from per-client TabPageSet
        let tab_set = &state.tabs;
        let active_tab_id = Some(tab_set.active_tab_id().as_usize() as u64);
        let tabs_info: Vec<TabPageInfo> = tab_set
            .tab_info()
            .into_iter()
            .map(|(id, label, is_active)| TabPageInfo {
                tab_id: id.as_usize() as u64,
                label: label.to_string(),
                active: is_active,
            })
            .collect();

        Ok(Response::new(GetLayoutResponse {
            root,
            focused_window_id: focused_id,
            active_tab_id,
            tabs: tabs_info,
        }))
    }

    /// Get visible line range for a window.
    ///
    /// Returns the first and last visible line indices based on the window's
    /// viewport scroll position and height.
    ///
    /// # Arguments
    ///
    /// - `window_id`: Optional window ID. If not specified, uses the focused window.
    /// - `client_id`: Required client ID for per-client viewport
    ///
    /// # Returns
    ///
    /// - `first_line`: First visible line (0-indexed, based on `scroll_top`)
    /// - `last_line`: Last visible line (0-indexed, `scroll_top` + height - 1)
    /// - `viewport_height`: Number of visible lines
    ///
    /// # Phase #471: Per-client viewport isolation
    ///
    /// Returns visible lines from the client's per-client window layout.
    ///
    /// # Errors
    ///
    /// - `InvalidArgument`: `client_id=0` is reserved (like PID 1)
    /// - `NotFound`: Client with given ID not found, or window not found
    #[allow(clippy::cast_possible_truncation)]
    async fn get_visible_lines(
        &self,
        request: Request<GetVisibleLinesRequest>,
    ) -> Result<Response<GetVisibleLinesResponse>, Status> {
        // #483 Phase 5: Token for auth, body client_id for targeting
        let token_client_id = request.extensions().get::<ClientId>().copied();
        let req = request.into_inner();
        let requested_window_id = req.window_id;
        let session = self.get_session()?;

        let client_id = resolve_target_client_id(token_client_id, req.client_id)?;

        // Per-client state lookup - now required (no fallback to shared state)
        let state = session.client_state(client_id).ok_or_else(|| {
            session.with_client_ring_buffer(client_id, |rb| {
                rb.log_event(
                    ClientEventType::Error,
                    format!("CLIENT_NOT_FOUND: get_visible_lines client_id={}", req.client_id),
                );
            });
            Status::not_found(format!("Client {} not found", req.client_id))
        })?;

        let layout = &state.windows;

        let window = requested_window_id.map_or_else(
            || layout.active(),
            |id| layout.windows.iter().find(|w| w.id.as_usize() as u64 == id),
        );

        let w = window.ok_or_else(|| Status::not_found("Window not found"))?;
        let viewport = &w.viewport;

        Ok(Response::new(GetVisibleLinesResponse {
            window_id: w.id.as_usize() as u64,
            first_line: viewport.scroll_top as u64,
            last_line: viewport.last_visible_line() as u64,
            viewport_height: u64::from(viewport.height),
        }))
    }

    /// Get selection state.
    ///
    /// Returns the current visual selection bounds and mode if active.
    /// Selection is active when in visual mode (character, line, or block).
    ///
    /// # Phase #471: Per-client selection isolation
    ///
    /// Returns the selection from the client's per-client window layout.
    ///
    /// # Errors
    ///
    /// - `InvalidArgument`: `client_id=0` is reserved (like PID 1)
    /// - `NotFound`: Client with given ID not found, or no active window
    #[allow(clippy::cast_possible_truncation)]
    async fn get_selection(
        &self,
        request: Request<GetSelectionRequest>,
    ) -> Result<Response<GetSelectionResponse>, Status> {
        use reovim_protocol::v2::Selection;

        // #483 Phase 5: Token for auth, body client_id for targeting
        let token_client_id = request.extensions().get::<ClientId>().copied();
        let req = request.into_inner();
        let session = self.get_session()?;

        let client_id = resolve_target_client_id(token_client_id, req.client_id)?;

        // Per-client state lookup - now required (no fallback to shared state)
        let state = session.client_state(client_id).ok_or_else(|| {
            session.with_client_ring_buffer(client_id, |rb| {
                rb.log_event(
                    ClientEventType::Error,
                    format!("CLIENT_NOT_FOUND: get_selection client_id={}", req.client_id),
                );
            });
            Status::not_found(format!("Client {} not found", req.client_id))
        })?;

        // Get selection from per-client windows
        let layout = &state.windows;

        // Get active window from per-client layout
        let window = layout
            .active()
            .ok_or_else(|| Status::not_found("No active window"))?;

        if let Some(ref sel) = window.selection {
            use reovim_driver_session::SelectionMode;

            let mode_str = match sel.mode {
                SelectionMode::Character => "char",
                SelectionMode::Line => "line",
                SelectionMode::Block => "block",
            };

            return Ok(Response::new(GetSelectionResponse {
                has_selection: true,
                selection: Some(Selection {
                    start: Some(Position {
                        line: sel.start.line as u64,
                        column: sel.start.column as u64,
                    }),
                    end: Some(Position {
                        line: sel.end.line as u64,
                        column: sel.end.column as u64,
                    }),
                }),
                visual_mode: Some(mode_str.to_string()),
            }));
        }

        // Window exists but no selection
        Ok(Response::new(GetSelectionResponse {
            has_selection: false,
            selection: None,
            visual_mode: None,
        }))
    }

    /// Get screen content via TUI capture relay.
    ///
    /// This implements the CLI→Server→TUI→Server→CLI capture flow:
    /// 1. Creates a pending capture request with unique ID
    /// 2. Sends `capture_request` notification to connected TUI client
    /// 3. Waits for TUI to respond with `capture_response` notification
    /// 4. Returns the captured frame content to the CLI
    ///
    /// Requires a connected TUI client. Returns an error if no TUI is connected
    /// or if the capture times out.
    async fn get_screen_content(
        &self,
        request: Request<GetScreenContentRequest>,
    ) -> Result<Response<GetScreenContentResponse>, Status> {
        let req = request.into_inner();
        let session = self.get_session()?;

        // Validate format
        let format = if req.format.is_empty() {
            "raw_ansi".to_string() // Default to ANSI
        } else {
            match req.format.as_str() {
                "plain_text" | "raw_ansi" | "cell_grid" => req.format,
                _ => {
                    return Err(Status::invalid_argument(format!(
                        "Invalid format '{}'. Use 'plain_text', 'raw_ansi', or 'cell_grid'",
                        req.format
                    )));
                }
            }
        };

        // Get target client ID
        let target_client_id = req.client_id;

        // Create pending capture request
        let (request_id, rx) = session.capture_tracker().create_pending();

        // Build and emit capture_request notification
        let notification = Notification {
            event_type: "capture_request".to_string(),
            // Phase #479: System time before UNIX_EPOCH is a serious system error
            #[allow(clippy::cast_possible_truncation)] // Timestamp won't overflow u64
            timestamp_ms: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .expect("system time before UNIX_EPOCH")
                .as_millis() as u64,
            payload: Some(Payload::CaptureRequest(CaptureRequestPayload {
                request_id,
                format: format.clone(),
                target_client_id,
            })),
        };

        session.emit_notification(notification);
        tracing::debug!(request_id, target_client_id, format, "Sent capture_request notification");

        // Wait for TUI response with timeout
        let result = wait_for_capture(rx).await.map_err(|e| {
            // Cancel the pending request on error
            session.capture_tracker().cancel(request_id);
            capture_error_to_status(e)
        })?;

        tracing::debug!(
            request_id,
            width = result.width,
            height = result.height,
            "Received capture response"
        );

        Ok(Response::new(GetScreenContentResponse {
            width: result.width,
            height: result.height,
            format: result.format,
            content: result.content,
        }))
    }

    /// Submit captured screen content (TUI → Server part of capture relay).
    ///
    /// Called by TUI in response to a `capture_request` notification.
    /// Delivers the captured frame to the pending `GetScreenContent` request.
    async fn submit_capture_response(
        &self,
        request: Request<SubmitCaptureRequest>,
    ) -> Result<Response<SubmitCaptureResponseReply>, Status> {
        let req = request.into_inner();
        let session = self.get_session()?;

        let result = CaptureResult {
            width: req.width,
            height: req.height,
            format: req.format,
            content: req.content,
        };

        let ok = session
            .capture_tracker()
            .deliver_response(req.request_id, result);
        tracing::debug!(request_id = req.request_id, ok, "Delivered capture response");

        Ok(Response::new(SubmitCaptureResponseReply { ok }))
    }

    /// Get register contents from per-client state (#515).
    ///
    /// If names are specified, returns only those registers.
    /// If names is empty, returns all non-empty registers.
    async fn get_registers(
        &self,
        request: Request<GetRegistersRequest>,
    ) -> Result<Response<GetRegistersResponse>, Status> {
        use reovim_kernel::api::v1::YankType;

        let token_client_id = request.extensions().get::<ClientId>().copied();
        let req = request.into_inner();
        let session = self.get_session()?;

        let client_id = resolve_target_client_id(token_client_id, req.client_id)?;

        let client_state = session.client_state(client_id).ok_or_else(|| {
            Status::not_found(format!("Client {} not found", client_id.as_usize()))
        })?;

        let bank = &client_state.registers;

        let registers = if req.names.is_empty() {
            // Return all non-empty registers
            bank.iter_non_empty()
                .map(|(name, content)| {
                    let yank_type_str = match content.yank_type {
                        YankType::Characterwise => "char",
                        YankType::Linewise => "line",
                    };
                    RegisterEntry {
                        name: name.to_string(),
                        content_type: "text".to_string(),
                        content: content.text.clone(),
                        yank_type: yank_type_str.to_string(),
                    }
                })
                .collect::<Vec<_>>()
        } else {
            // Return only requested registers
            req.names
                .iter()
                .filter_map(|name| {
                    let name_char = name.chars().next()?;
                    let content = bank.get_by_name(Some(name_char))?;
                    if content.is_empty() {
                        return None;
                    }
                    let yank_type_str = match content.yank_type {
                        YankType::Characterwise => "char",
                        YankType::Linewise => "line",
                    };
                    Some(RegisterEntry {
                        name: name_char.to_string(),
                        content_type: "text".to_string(),
                        content: content.text.clone(),
                        yank_type: yank_type_str.to_string(),
                    })
                })
                .collect()
        };

        Ok(Response::new(GetRegistersResponse { registers }))
    }
}

#[cfg(test)]
#[path = "state_tests.rs"]
mod tests;
