//! Quick Open Provider System
//!
//! A unified prompt system with prefix-based routing to different providers.
//! Inspired by VSCode's Quick Open (Ctrl+P) which supports:
//! - Empty prefix: file finder
//! - `>`: command palette
//! - `#`: symbol finder (future)
//! - `@`: go to symbol in file (future)
//! - `:`: go to line
//!
//! Providers are registered with a prefix and handle suggestion generation
//! and selection for their domain.

pub mod providers;

pub use providers::{BufferProvider, CommandProvider, FileProvider, GotoLineProvider};

use crate::input::commands::Suggestion;
use crate::input::keybindings::Action;
use std::collections::HashMap;

/// Result of confirming a selection in a provider
#[derive(Debug, Clone)]
pub enum QuickOpenResult {
    /// Execute an editor action
    ExecuteAction(Action),
    /// Open a file at optional line/column
    OpenFile {
        path: String,
        line: Option<usize>,
        column: Option<usize>,
    },
    /// Show a buffer by ID
    ShowBuffer(usize),
    /// Go to a line in the current buffer
    GotoLine(GotoLineTarget),
    /// Do nothing (provider handled it internally)
    None,
    /// Show an error message
    Error(String),
}

/// A parsed goto-line target. The presence of an explicit sign in the user's
/// input chooses between absolute and relative jumps independently of the
/// `relative_line_numbers` display setting.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GotoLineTarget {
    /// Absolute 1-based line number (input had no leading sign).
    Absolute(usize),
    /// Signed offset from the current cursor line (input had `+`/`-` prefix).
    Relative(isize),
}

/// Parse a goto-line input string.
///
/// - `"500"` → `Absolute(500)`
/// - `"+3"` → `Relative(3)`
/// - `"-3"` → `Relative(-3)`
/// - `"0"`, `"+0"`, `"-0"`, `""`, `"abc"` → `None`
///
/// Whitespace around the input is ignored. The leading-sign convention is
/// independent of any display setting: the user's literal input decides.
pub fn parse_goto_line_input(input: &str) -> Option<GotoLineTarget> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return None;
    }
    if let Some(rest) = trimmed
        .strip_prefix('+')
        .or_else(|| trimmed.strip_prefix('-'))
    {
        // Reject bare "+"/"-" and avoid double signs like "++3" or "+-3".
        if rest.is_empty() || rest.starts_with('+') || rest.starts_with('-') {
            return None;
        }
        let delta = trimmed.parse::<isize>().ok()?;
        if delta == 0 {
            return None;
        }
        Some(GotoLineTarget::Relative(delta))
    } else {
        let n = trimmed.parse::<usize>().ok()?;
        if n == 0 {
            return None;
        }
        Some(GotoLineTarget::Absolute(n))
    }
}

/// Context provided to providers when generating suggestions
#[derive(Debug, Clone)]
pub struct QuickOpenContext {
    /// Current working directory
    pub cwd: String,
    /// List of open buffer paths
    pub open_buffers: Vec<BufferInfo>,
    /// Active buffer ID
    pub active_buffer_id: usize,
    /// Active buffer path (if any)
    pub active_buffer_path: Option<String>,
    /// Whether there's an active selection
    pub has_selection: bool,
    /// Current key context
    pub key_context: crate::input::keybindings::KeyContext,
    /// Active custom contexts (for command filtering)
    pub custom_contexts: std::collections::HashSet<String>,
    /// Active buffer mode (e.g., "vi_normal")
    pub buffer_mode: Option<String>,
    /// Whether the active buffer's language has an LSP server configured
    pub has_lsp_config: bool,
    /// What the active buffer can be asked to do (save, revert, accept edits).
    /// Shared with the menus so both surfaces gate on the same answers.
    pub buffer_caps: crate::app::buffer_capabilities::BufferCapabilities,
    /// Whether relative line numbers are enabled
    pub relative_line_numbers: bool,
}

/// Information about an open buffer
#[derive(Debug, Clone)]
pub struct BufferInfo {
    pub id: usize,
    pub path: String,
    pub name: String,
    pub modified: bool,
    /// Whether this is a virtual buffer (a plugin panel like `*blame:…*` or
    /// `*Git Log*`) with no backing file path. Virtual buffers are listed in
    /// the `#` switcher even though `path` is empty.
    pub is_virtual: bool,
}

/// Parse a `path:line:col` string into its components.
///
/// Supports formats like `file.rs:10`, `file.rs:10:5`, and Windows paths with drive prefixes.
pub fn parse_path_line_col(input: &str) -> (String, Option<usize>, Option<usize>) {
    use std::path::{Component, Path};

    let trimmed = input.trim();
    if trimmed.is_empty() {
        return (String::new(), None, None);
    }

    // Skip past Windows drive prefix (e.g., "C:") when looking for :line:col
    let has_drive = Path::new(trimmed)
        .components()
        .next()
        .is_some_and(|c| matches!(c, Component::Prefix(_)));
    let search_start = if has_drive {
        trimmed.find(':').map(|i| i + 1).unwrap_or(0)
    } else {
        0
    };

    let suffix = &trimmed[search_start..];
    let parts: Vec<&str> = suffix.rsplitn(3, ':').collect();

    // Reconstruct the path portion, re-attaching the drive prefix if needed
    let rebuild_path = |rest: &str| {
        if has_drive {
            format!("{}{}", &trimmed[..search_start], rest)
        } else {
            rest.to_string()
        }
    };

    // Try path:line:col, then path:line
    let parsed = match parts.as_slice() {
        [col_s, line_s, rest] if !rest.is_empty() => col_s
            .parse::<usize>()
            .ok()
            .filter(|&c| c > 0)
            .zip(line_s.parse::<usize>().ok().filter(|&l| l > 0))
            .map(|(col, line)| (rebuild_path(rest), Some(line), Some(col))),
        [line_s, rest] if !rest.is_empty() => line_s
            .parse::<usize>()
            .ok()
            .filter(|&l| l > 0)
            .map(|line| (rebuild_path(rest), Some(line), None)),
        _ => None,
    };

    parsed.unwrap_or_else(|| (trimmed.to_string(), None, None))
}

/// Trait for quick open providers
///
/// Each provider handles a specific prefix and provides suggestions
/// for that domain (files, commands, buffers, etc.)
pub trait QuickOpenProvider: Send + Sync {
    /// The prefix that triggers this provider (e.g., ">", "#", ":")
    /// Empty string means this is the default provider (no prefix)
    fn prefix(&self) -> &str;

    /// Generate suggestions for the given query
    ///
    /// The query has already had the prefix stripped.
    fn suggestions(&self, query: &str, context: &QuickOpenContext) -> Vec<Suggestion>;

    /// Handle selection of a suggestion
    ///
    /// `suggestion` is the currently selected suggestion (already resolved by the caller).
    /// `query` is the original query (without prefix).
    fn on_select(
        &self,
        suggestion: Option<&Suggestion>,
        query: &str,
        context: &QuickOpenContext,
    ) -> QuickOpenResult;

    /// Downcast support for concrete provider access (e.g., updating cache).
    fn as_any(&self) -> &dyn std::any::Any;

    /// Mutable downcast support, for re-pointing concrete provider state
    /// (e.g. swapping the `FileProvider`'s filesystem + process spawner when
    /// the active authority changes).
    fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
}

/// Registry for quick open providers
pub struct QuickOpenRegistry {
    /// Providers indexed by their prefix
    providers: HashMap<String, Box<dyn QuickOpenProvider>>,
}

impl QuickOpenRegistry {
    pub fn new() -> Self {
        Self {
            providers: HashMap::new(),
        }
    }

    /// Register a provider
    ///
    /// If a provider with the same prefix exists, it will be replaced.
    pub fn register(&mut self, provider: Box<dyn QuickOpenProvider>) {
        let prefix = provider.prefix().to_string();
        self.providers.insert(prefix, provider);
    }

    /// Re-point the file provider's backends at the active authority's
    /// filesystem + process spawner. The `FileProvider` captures these at
    /// construction, so an *in-place* authority swap (`set_boot_authority` /
    /// `set_session_authority`, used by the test harness and devcontainer
    /// attach, which simulate the restart inline) would otherwise leave
    /// quick-open's `git ls-files` fast path running against the old backend —
    /// listing the previous authority's files. Call this whenever the
    /// authority changes without a full reconstruction.
    pub fn set_file_backends(
        &mut self,
        filesystem: std::sync::Arc<dyn crate::model::filesystem::FileSystem + Send + Sync>,
        process_spawner: std::sync::Arc<dyn crate::services::remote::ProcessSpawner>,
    ) {
        for provider in self.providers.values_mut() {
            if let Some(fp) = provider.as_any_mut().downcast_mut::<FileProvider>() {
                fp.set_backends(filesystem, process_spawner);
                return;
            }
        }
    }

    /// Get the provider for a given input
    ///
    /// Returns (provider, query_without_prefix)
    pub fn get_provider_for_input<'a>(
        &'a self,
        input: &'a str,
    ) -> Option<(&'a dyn QuickOpenProvider, &'a str)> {
        // Try prefixes in order (longest first to handle overlapping prefixes)
        let mut prefixes: Vec<_> = self.providers.keys().collect();
        prefixes.sort_by_key(|b| std::cmp::Reverse(b.len()));

        for prefix in prefixes {
            if prefix.is_empty() {
                continue; // Handle default provider last
            }
            if input.starts_with(prefix.as_str()) {
                let query = &input[prefix.len()..];
                return self.providers.get(prefix).map(|p| (p.as_ref(), query));
            }
        }

        // Fall back to default provider (empty prefix)
        self.providers.get("").map(|p| (p.as_ref(), input))
    }
}

impl Default for QuickOpenRegistry {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct TestProvider {
        prefix: String,
    }

    impl QuickOpenProvider for TestProvider {
        fn prefix(&self) -> &str {
            &self.prefix
        }

        fn suggestions(&self, _query: &str, _context: &QuickOpenContext) -> Vec<Suggestion> {
            vec![]
        }

        fn on_select(
            &self,
            _suggestion: Option<&Suggestion>,
            _query: &str,
            _context: &QuickOpenContext,
        ) -> QuickOpenResult {
            QuickOpenResult::None
        }

        fn as_any(&self) -> &dyn std::any::Any {
            self
        }

        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
            self
        }
    }

    #[test]
    fn parse_goto_line_input_absolute() {
        assert_eq!(
            parse_goto_line_input("500"),
            Some(GotoLineTarget::Absolute(500))
        );
        assert_eq!(
            parse_goto_line_input("  42 "),
            Some(GotoLineTarget::Absolute(42))
        );
    }

    #[test]
    fn parse_goto_line_input_relative() {
        assert_eq!(
            parse_goto_line_input("+3"),
            Some(GotoLineTarget::Relative(3))
        );
        assert_eq!(
            parse_goto_line_input("-3"),
            Some(GotoLineTarget::Relative(-3))
        );
        assert_eq!(
            parse_goto_line_input(" -10 "),
            Some(GotoLineTarget::Relative(-10))
        );
    }

    #[test]
    fn parse_goto_line_input_rejects_invalid() {
        assert_eq!(parse_goto_line_input(""), None);
        assert_eq!(parse_goto_line_input("   "), None);
        assert_eq!(parse_goto_line_input("0"), None);
        assert_eq!(parse_goto_line_input("+0"), None);
        assert_eq!(parse_goto_line_input("-0"), None);
        assert_eq!(parse_goto_line_input("+"), None);
        assert_eq!(parse_goto_line_input("-"), None);
        assert_eq!(parse_goto_line_input("++3"), None);
        assert_eq!(parse_goto_line_input("+-3"), None);
        assert_eq!(parse_goto_line_input("abc"), None);
        assert_eq!(parse_goto_line_input("3a"), None);
    }

    #[test]
    fn test_provider_routing() {
        let mut registry = QuickOpenRegistry::new();

        registry.register(Box::new(TestProvider {
            prefix: "".to_string(),
        }));
        registry.register(Box::new(TestProvider {
            prefix: ">".to_string(),
        }));
        registry.register(Box::new(TestProvider {
            prefix: "#".to_string(),
        }));

        // Default provider for no prefix
        let (provider, query) = registry.get_provider_for_input("hello").unwrap();
        assert_eq!(provider.prefix(), "");
        assert_eq!(query, "hello");

        // Command provider
        let (provider, query) = registry.get_provider_for_input(">save").unwrap();
        assert_eq!(provider.prefix(), ">");
        assert_eq!(query, "save");

        // Buffer provider
        let (provider, query) = registry.get_provider_for_input("#main").unwrap();
        assert_eq!(provider.prefix(), "#");
        assert_eq!(query, "main");
    }
}
