use std::sync::Arc;

use crate::terminal::mock_data::load_mock_financial_data;
use crate::terminal::yahoo_finance::{
    SecurityLookup, SecurityLookupError, SecurityMatch, YahooFinanceLookup,
};
use crate::terminal::{
    ChatCommandRequest, ExecuteTerminalCommandRequest, MockFinancialData, PanelPayload,
    TerminalCommandResponse,
};

/// Executes supported slash commands against live search plus shared local fixture data.
pub struct TerminalCommandService {
    mock_data: MockFinancialData,
    security_lookup: Arc<dyn SecurityLookup>,
}

impl Default for TerminalCommandService {
    fn default() -> Self {
        Self::with_dependencies(load_mock_financial_data(), Arc::new(YahooFinanceLookup::default()))
    }
}

impl TerminalCommandService {
    /// Creates a terminal command service with a custom security lookup backend.
    pub fn new(security_lookup: Arc<dyn SecurityLookup>) -> Self {
        Self::with_dependencies(load_mock_financial_data(), security_lookup)
    }

    fn with_dependencies(
        mock_data: MockFinancialData,
        security_lookup: Arc<dyn SecurityLookup>,
    ) -> Self {
        Self {
            mock_data,
            security_lookup,
        }
    }

    /// Resolves a slash command into either a text reply or a structured panel payload.
    pub async fn execute(&self, request: ExecuteTerminalCommandRequest) -> TerminalCommandResponse {
        let command = parse_command(&request.input);

        match command.command.as_str() {
            "/search" => self.search(command.args.join(" ").trim()).await,
            "/portfolio" => TerminalCommandResponse::Panel {
                panel: PanelPayload::Portfolio {
                    data: self.mock_data.portfolio.clone(),
                },
            },
            "/news" => self.news(command.args.first().map(String::as_str)),
            "/analyze" => self.analyze(command.args.first().map(String::as_str)),
            "/help" => help_response(),
            _ => TerminalCommandResponse::Text {
                content: format!("Unknown command: {}\n\n{}", command.command, help_text()),
            },
        }
    }

    async fn search(&self, query: &str) -> TerminalCommandResponse {
        if query.is_empty() {
            return TerminalCommandResponse::Text {
                content: "Usage: /search [ticker or company name]".to_string(),
            };
        }

        let matches = match self.security_lookup.search(query).await {
            Ok(matches) => matches
                .into_iter()
                .filter(|security_match| security_match.kind.is_supported())
                .collect::<Vec<_>>(),
            Err(SecurityLookupError::SearchUnavailable) => {
                return TerminalCommandResponse::Text {
                    content: format!("Live search failed for \"{query}\"."),
                };
            }
            Err(SecurityLookupError::DetailUnavailable { .. }) => {
                return TerminalCommandResponse::Text {
                    content: format!("Live search failed for \"{query}\"."),
                };
            }
        };

        if matches.is_empty() {
            return TerminalCommandResponse::Text {
                content: format!("No live results found for \"{query}\"."),
            };
        }

        if let Some(selected_match) = select_exact_symbol_match(query, &matches) {
            let selected_symbol = selected_match.symbol.clone();
            return match self.security_lookup.load_company(&selected_match).await {
                Ok(company) => TerminalCommandResponse::Panel {
                    panel: PanelPayload::Company { data: company },
                },
                Err(SecurityLookupError::DetailUnavailable { symbol }) => {
                    TerminalCommandResponse::Text {
                        content: format!("Live security data unavailable for \"{symbol}\"."),
                    }
                }
                Err(SecurityLookupError::SearchUnavailable) => TerminalCommandResponse::Text {
                    content: format!("Live security data unavailable for \"{selected_symbol}\"."),
                },
            };
        }

        TerminalCommandResponse::Text {
            content: format!(
                "Multiple matches found for \"{query}\":\n{}",
                format_search_matches(&matches)
            ),
        }
    }

    fn news(&self, ticker: Option<&str>) -> TerminalCommandResponse {
        let normalized_ticker = ticker.map(|value| value.trim().to_uppercase());
        let news_items = match normalized_ticker.as_deref() {
            Some(ticker) if !ticker.is_empty() => self
                .mock_data
                .news_items
                .iter()
                .filter(|item| {
                    item.related_tickers
                        .iter()
                        .any(|related| related.eq_ignore_ascii_case(ticker))
                })
                .cloned()
                .collect(),
            _ => self.mock_data.news_items.clone(),
        };

        TerminalCommandResponse::Panel {
            panel: PanelPayload::News {
                data: news_items,
                ticker: normalized_ticker.filter(|value| !value.is_empty()),
            },
        }
    }

    fn analyze(&self, ticker: Option<&str>) -> TerminalCommandResponse {
        let Some(ticker) = ticker.map(str::trim).filter(|value| !value.is_empty()) else {
            return TerminalCommandResponse::Text {
                content: "Usage: /analyze [ticker]".to_string(),
            };
        };

        match self.mock_data.analyses.get(&ticker.to_uppercase()) {
            Some(analysis) => TerminalCommandResponse::Panel {
                panel: PanelPayload::Analysis {
                    data: analysis.clone(),
                },
            },
            None => TerminalCommandResponse::Text {
                content: format!("Analysis not available for \"{ticker}\"."),
            },
        }
    }
}

fn select_exact_symbol_match(query: &str, matches: &[SecurityMatch]) -> Option<SecurityMatch> {
    matches
        .iter()
        .enumerate()
        .filter(|(_, security_match)| security_match.symbol.eq_ignore_ascii_case(query))
        .min_by_key(|(index, security_match)| {
            (
                exchange_priority(security_match.exchange.as_deref()),
                *index,
            )
        })
        .map(|(_, security_match)| security_match.clone())
}

fn exchange_priority(exchange: Option<&str>) -> usize {
    match exchange {
        Some("NASDAQ") => 0,
        Some("NYSE") => 1,
        Some("AMEX") => 2,
        Some("BATS") => 3,
        _ => 4,
    }
}

fn format_search_matches(matches: &[SecurityMatch]) -> String {
    matches
        .iter()
        .map(|security_match| {
            let name = security_match.name.as_deref().unwrap_or("Unknown");
            let exchange = security_match.exchange.as_deref().unwrap_or("N/A");
            format!(
                "  {}  {}  {}  {}",
                security_match.symbol,
                name,
                exchange,
                security_match.kind.label()
            )
        })
        .collect::<Vec<_>>()
        .join("\n")
}

/// Parses raw slash-command input into a normalized command plus positional arguments.
fn parse_command(input: &str) -> ChatCommandRequest {
    let trimmed = input.trim();
    let mut parts = trimmed.split_whitespace();
    let command = parts.next().unwrap_or_default().to_ascii_lowercase();
    let args = parts.map(ToString::to_string).collect();

    ChatCommandRequest {
        command,
        args,
        raw_input: trimmed.to_string(),
    }
}

/// Human-readable help text returned for `/help` and unknown commands.
fn help_text() -> &'static str {
    "Available Commands:\n\n  /search [ticker]     - Search live security data\n  /portfolio           - Show your portfolio\n  /news [ticker?]      - Show market news\n  /analyze [ticker]    - Get stock analysis\n  /help                - Show this help text\n  /clear               - Clear terminal locally"
}

/// Wraps the shared help text into the terminal command response envelope.
fn help_response() -> TerminalCommandResponse {
    TerminalCommandResponse::Text {
        content: help_text().to_string(),
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use futures::future::BoxFuture;

    use super::TerminalCommandService;
    use crate::terminal::mock_data::load_mock_financial_data;
    use crate::terminal::yahoo_finance::{
        SecurityKind, SecurityLookup, SecurityLookupError, SecurityMatch,
    };
    use crate::terminal::{Company, ExecuteTerminalCommandRequest, PanelPayload, TerminalCommandResponse};

    struct FakeSecurityLookup {
        search_result: Result<Vec<SecurityMatch>, SecurityLookupError>,
        fail_detail: bool,
    }

    impl FakeSecurityLookup {
        fn successful(matches: Vec<SecurityMatch>) -> Self {
            Self {
                search_result: Ok(matches),
                fail_detail: false,
            }
        }
    }

    impl SecurityLookup for FakeSecurityLookup {
        fn search<'a>(
            &'a self,
            _query: &'a str,
        ) -> BoxFuture<'a, Result<Vec<SecurityMatch>, SecurityLookupError>> {
            Box::pin(async move { self.search_result.clone() })
        }

        fn load_company<'a>(
            &'a self,
            security_match: &'a SecurityMatch,
        ) -> BoxFuture<'a, Result<Company, SecurityLookupError>> {
            Box::pin(async move {
                if self.fail_detail {
                    return Err(SecurityLookupError::DetailUnavailable {
                        symbol: security_match.symbol.clone(),
                    });
                }

                Ok(Company {
                    symbol: security_match.symbol.clone(),
                    name: security_match.exchange.clone().unwrap_or_else(|| "N/A".to_string()),
                    price: 100.0,
                    change: 1.0,
                    change_percent: 1.0,
                    market_cap: 1_000_000.0,
                    volume: 10_000,
                    pe: Some(20.0),
                    eps: Some(2.0),
                    high52_week: Some(110.0),
                    low52_week: Some(80.0),
                })
            })
        }
    }

    fn build_service(search_result: Result<Vec<SecurityMatch>, SecurityLookupError>) -> TerminalCommandService {
        TerminalCommandService::with_dependencies(
            load_mock_financial_data(),
            Arc::new(FakeSecurityLookup {
                search_result,
                fail_detail: false,
            }),
        )
    }

    fn execute(service: &TerminalCommandService, input: &str) -> TerminalCommandResponse {
        futures::executor::block_on(service.execute(ExecuteTerminalCommandRequest {
            workspace_id: "workspace-1".to_string(),
            input: input.to_string(),
        }))
    }

    #[test]
    fn returns_company_panel_for_exact_search_match() {
        let service = build_service(Ok(vec![SecurityMatch {
            symbol: "AAPL".to_string(),
            name: Some("Apple Inc.".to_string()),
            exchange: Some("NASDAQ".to_string()),
            kind: SecurityKind::Equity,
        }]));

        let response = execute(&service, "/search AAPL");

        match response {
            TerminalCommandResponse::Panel {
                panel: PanelPayload::Company { data },
            } => assert_eq!(data.symbol, "AAPL"),
            other => panic!("expected company panel, got {other:?}"),
        }
    }

    #[test]
    fn returns_text_list_for_name_search() {
        let service = build_service(Ok(vec![
            SecurityMatch {
                symbol: "AAPL".to_string(),
                name: Some("Apple Inc.".to_string()),
                exchange: Some("NASDAQ".to_string()),
                kind: SecurityKind::Equity,
            },
            SecurityMatch {
                symbol: "APLE".to_string(),
                name: Some("Apple Hospitality REIT".to_string()),
                exchange: Some("NYSE".to_string()),
                kind: SecurityKind::Equity,
            },
        ]));

        let response = execute(&service, "/search apple");

        match response {
            TerminalCommandResponse::Text { content } => {
                assert!(content.contains("Multiple matches found for \"apple\""));
                assert!(content.contains("AAPL"));
                assert!(content.contains("NASDAQ"));
                assert!(content.contains("Equity"));
            }
            other => panic!("expected text response, got {other:?}"),
        }
    }

    #[test]
    fn filters_unsupported_assets() {
        let service = build_service(Ok(vec![SecurityMatch {
            symbol: "BTC-USD".to_string(),
            name: Some("Bitcoin USD".to_string()),
            exchange: None,
            kind: SecurityKind::Other("Crypto".to_string()),
        }]));

        let response = execute(&service, "/search bitcoin");

        match response {
            TerminalCommandResponse::Text { content } => {
                assert_eq!(content, "No live results found for \"bitcoin\".");
            }
            other => panic!("expected text response, got {other:?}"),
        }
    }

    #[test]
    fn prefers_us_exchange_priority_for_exact_matches() {
        let service = build_service(Ok(vec![
            SecurityMatch {
                symbol: "ABC".to_string(),
                name: Some("ABC Ltd".to_string()),
                exchange: Some("NYSE".to_string()),
                kind: SecurityKind::Equity,
            },
            SecurityMatch {
                symbol: "ABC".to_string(),
                name: Some("ABC Ltd".to_string()),
                exchange: Some("NASDAQ".to_string()),
                kind: SecurityKind::Equity,
            },
        ]));

        let response = execute(&service, "/search ABC");

        match response {
            TerminalCommandResponse::Panel {
                panel: PanelPayload::Company { data },
            } => assert_eq!(data.name, "NASDAQ"),
            other => panic!("expected company panel, got {other:?}"),
        }
    }

    #[test]
    fn returns_live_search_error_when_provider_search_fails() {
        let service = build_service(Err(SecurityLookupError::SearchUnavailable));

        let response = execute(&service, "/search AAPL");

        match response {
            TerminalCommandResponse::Text { content } => {
                assert_eq!(content, "Live search failed for \"AAPL\".");
            }
            other => panic!("expected text response, got {other:?}"),
        }
    }

    #[test]
    fn returns_live_detail_error_when_exact_resolution_fails() {
        let service = TerminalCommandService::with_dependencies(
            load_mock_financial_data(),
            Arc::new(FakeSecurityLookup {
                search_result: Ok(vec![SecurityMatch {
                    symbol: "AAPL".to_string(),
                    name: Some("Apple Inc.".to_string()),
                    exchange: Some("NASDAQ".to_string()),
                    kind: SecurityKind::Equity,
                }]),
                fail_detail: true,
            }),
        );

        let response = execute(&service, "/search AAPL");

        match response {
            TerminalCommandResponse::Text { content } => {
                assert_eq!(content, "Live security data unavailable for \"AAPL\".");
            }
            other => panic!("expected text response, got {other:?}"),
        }
    }

    #[test]
    fn returns_usage_for_search_without_query() {
        let service = TerminalCommandService::with_dependencies(
            load_mock_financial_data(),
            Arc::new(FakeSecurityLookup::successful(vec![])),
        );

        let response = execute(&service, "/search");

        match response {
            TerminalCommandResponse::Text { content } => {
                assert_eq!(content, "Usage: /search [ticker or company name]");
            }
            other => panic!("expected text response, got {other:?}"),
        }
    }

    #[test]
    fn returns_text_for_unknown_command() {
        let service = TerminalCommandService::with_dependencies(
            load_mock_financial_data(),
            Arc::new(FakeSecurityLookup::successful(vec![])),
        );

        let response = execute(&service, "/wat");

        match response {
            TerminalCommandResponse::Text { content } => {
                assert!(content.contains("Unknown command"));
            }
            other => panic!("expected text response, got {other:?}"),
        }
    }

    #[test]
    fn returns_usage_for_analyze_without_ticker() {
        let service = TerminalCommandService::with_dependencies(
            load_mock_financial_data(),
            Arc::new(FakeSecurityLookup::successful(vec![])),
        );

        let response = execute(&service, "/analyze");

        match response {
            TerminalCommandResponse::Text { content } => {
                assert_eq!(content, "Usage: /analyze [ticker]");
            }
            other => panic!("expected text response, got {other:?}"),
        }
    }
}
