use crate::product::agent::config::Config;
use crate::product::agent::config::display_model_provider_ref;
use crate::product::agent::protocol::NetworkAccess;
use crate::product::agent::protocol::SandboxPolicy;
use crate::product::agent::protocol::TokenUsage;
use crate::product::agent::protocol::TokenUsageInfo;
use crate::product::common::summarize_sandbox_policy;
use crate::product::protocol::ThreadId;
use crate::product::protocol::openai_models::ReasoningEffort;
use crate::product::tui_app::history_cell::CompositeHistoryCell;
use crate::product::tui_app::history_cell::HistoryCell;
use crate::product::tui_app::history_cell::PlainHistoryCell;
use crate::product::tui_app::history_cell::with_border_with_inner_width;
use crate::product::tui_app::version::CODEX_CLI_VERSION;
use ratatui::prelude::*;
use ratatui::style::Stylize;
use std::collections::BTreeSet;
use std::path::PathBuf;
use url::Url;

use super::format::FieldFormatter;
use super::format::line_display_width;
use super::format::push_label;
use super::format::truncate_line_to_width;
use super::helpers::cache_hit_percent;
use super::helpers::compose_agents_summary;
use super::helpers::compose_model_display;
use super::helpers::format_directory_display;
use super::helpers::format_tokens_compact;
use crate::product::agent::AuthManager;

#[derive(Debug, Clone)]
struct StatusContextWindowData {
    percent_remaining: i64,
    tokens_in_context: i64,
    window: i64,
}

#[derive(Debug, Clone)]
pub(crate) struct StatusTokenUsageData {
    total: i64,
    input: i64,
    cached_input: i64,
    output: i64,
    cache_hit_percent: Option<i64>,
    context_window: Option<StatusContextWindowData>,
}

#[derive(Debug)]
struct StatusHistoryCell {
    model_name: String,
    model_details: Vec<String>,
    directory: PathBuf,
    approval: String,
    sandbox: String,
    agents_summary: String,
    identity: Option<String>,
    model_provider: Option<String>,
    thread_name: Option<String>,
    session_id: Option<String>,
    forked_from: Option<String>,
    context_compact_count: usize,
    token_usage: StatusTokenUsageData,
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn new_status_output_with_context_compact_count(
    config: &Config,
    _auth_manager: &AuthManager,
    token_info: Option<&TokenUsageInfo>,
    total_usage: &TokenUsage,
    session_id: &Option<ThreadId>,
    thread_name: Option<String>,
    forked_from: Option<ThreadId>,
    model_name: &str,
    identity: Option<&str>,
    reasoning_effort_override: Option<Option<ReasoningEffort>>,
    context_compact_count: usize,
) -> CompositeHistoryCell {
    let command = PlainHistoryCell::new(vec!["/status".magenta().into()]);
    let card = StatusHistoryCell::new(
        config,
        _auth_manager,
        token_info,
        total_usage,
        session_id,
        thread_name,
        forked_from,
        model_name,
        identity,
        reasoning_effort_override,
        context_compact_count,
    );

    CompositeHistoryCell::new(vec![Box::new(command), Box::new(card)])
}

impl StatusHistoryCell {
    #[allow(clippy::too_many_arguments)]
    fn new(
        config: &Config,
        _auth_manager: &AuthManager,
        token_info: Option<&TokenUsageInfo>,
        total_usage: &TokenUsage,
        session_id: &Option<ThreadId>,
        thread_name: Option<String>,
        forked_from: Option<ThreadId>,
        model_name: &str,
        identity: Option<&str>,
        reasoning_effort_override: Option<Option<ReasoningEffort>>,
        context_compact_count: usize,
    ) -> Self {
        let mut config_entries = vec![
            ("workdir", config.cwd.display().to_string()),
            ("model", model_name.to_string()),
            (
                "provider",
                display_model_provider_ref(&config.model_provider_id),
            ),
            ("approval", config.approval_policy.value().to_string()),
            (
                "sandbox",
                summarize_sandbox_policy(config.sandbox_policy.get()),
            ),
        ];
        if config.model_provider.uses_responses_api() {
            let effort_value = reasoning_effort_override
                .unwrap_or(None)
                .map(|effort| effort.to_string())
                .unwrap_or_else(|| "none".to_string());
            config_entries.push(("reasoning effort", effort_value));
            config_entries.push((
                "reasoning summaries",
                config.model_reasoning_summary.to_string(),
            ));
        }
        let (model_name, model_details) = compose_model_display(model_name, &config_entries);
        let approval = config_entries
            .iter()
            .find(|(k, _)| *k == "approval")
            .map(|(_, v)| v.clone())
            .unwrap_or_else(|| "<unknown>".to_string());
        let sandbox = match config.sandbox_policy.get() {
            SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
            SandboxPolicy::ReadOnly => "read-only".to_string(),
            SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(),
            SandboxPolicy::ExternalSandbox { network_access } => {
                if matches!(network_access, NetworkAccess::Enabled) {
                    "external-sandbox (network access enabled)".to_string()
                } else {
                    "external-sandbox".to_string()
                }
            }
        };
        let agents_summary = compose_agents_summary(config);
        let model_provider = format_model_provider(config);
        let session_id = session_id.as_ref().map(std::string::ToString::to_string);
        let forked_from = forked_from.map(|id| id.to_string());
        let default_usage = TokenUsage::default();
        let (context_usage, context_window) = match token_info {
            Some(info) => (&info.last_token_usage, info.model_context_window),
            None => (&default_usage, config.model_context_window),
        };
        let context_window = context_window.map(|window| StatusContextWindowData {
            percent_remaining: context_usage.percent_of_context_window_remaining(window),
            tokens_in_context: context_usage.tokens_in_context_window(),
            window,
        });

        let token_usage = StatusTokenUsageData {
            total: total_usage.blended_total(),
            input: total_usage.non_cached_input(),
            cached_input: total_usage.cached_input(),
            output: total_usage.output_tokens,
            cache_hit_percent: cache_hit_percent(total_usage),
            context_window,
        };
        Self {
            model_name,
            model_details,
            directory: config.cwd.clone(),
            approval,
            sandbox,
            agents_summary,
            identity: identity.map(ToString::to_string),
            model_provider,
            thread_name,
            session_id,
            forked_from,
            context_compact_count,
            token_usage,
        }
    }

    fn token_usage_spans(&self) -> Vec<Span<'static>> {
        let total_fmt = format_tokens_compact(self.token_usage.total);
        let input_fmt = format_tokens_compact(self.token_usage.input);
        let output_fmt = format_tokens_compact(self.token_usage.output);

        vec![
            Span::from(total_fmt),
            Span::from(" total"),
            Span::from(" (").dim(),
            Span::from(input_fmt).dim(),
            Span::from(" input").dim(),
            Span::from(" + ").dim(),
            Span::from(output_fmt).dim(),
            Span::from(" output").dim(),
            Span::from(")").dim(),
        ]
    }

    fn cache_hit_spans(&self) -> Option<Vec<Span<'static>>> {
        let cache_hit_percent = self.token_usage.cache_hit_percent?;
        let cached_input_fmt = format_tokens_compact(self.token_usage.cached_input);

        Some(vec![
            Span::from(format!("{cache_hit_percent}% hit")),
            Span::from(" (").dim(),
            Span::from(cached_input_fmt).dim(),
            Span::from(" cached").dim(),
            Span::from(")").dim(),
        ])
    }

    fn context_window_spans(&self) -> Option<Vec<Span<'static>>> {
        let context = self.token_usage.context_window.as_ref()?;
        let percent = context.percent_remaining;
        let used_fmt = format_tokens_compact(context.tokens_in_context);
        let window_fmt = format_tokens_compact(context.window);

        Some(vec![
            Span::from(format!("{percent}% left")),
            Span::from(" (").dim(),
            Span::from(used_fmt).dim(),
            Span::from(" used / ").dim(),
            Span::from(window_fmt).dim(),
            Span::from(")").dim(),
        ])
    }
}

impl HistoryCell for StatusHistoryCell {
    fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
        let mut lines: Vec<Line<'static>> = Vec::new();
        lines.push(Line::from(vec![
            Span::from(format!("{}>_ ", FieldFormatter::INDENT)).dim(),
            Span::from("Long-Horizon Agent").bold(),
            Span::from(" ").dim(),
            Span::from(format!("(v{CODEX_CLI_VERSION})")).dim(),
        ]));
        lines.push(Line::from(Vec::<Span<'static>>::new()));

        let available_inner_width = usize::from(width.saturating_sub(4));
        if available_inner_width == 0 {
            return Vec::new();
        }

        let mut labels: Vec<String> =
            vec!["Model", "Directory", "Approval", "Sandbox", "Agents.md"]
                .into_iter()
                .map(str::to_string)
                .collect();
        let mut seen: BTreeSet<String> = labels.iter().cloned().collect();
        let thread_name = self.thread_name.as_deref().filter(|name| !name.is_empty());

        if self.model_provider.is_some() {
            push_label(&mut labels, &mut seen, "Model provider");
        }
        if thread_name.is_some() {
            push_label(&mut labels, &mut seen, "Thread name");
        }
        if self.session_id.is_some() {
            push_label(&mut labels, &mut seen, "Session");
        }
        if self.session_id.is_some() && self.forked_from.is_some() {
            push_label(&mut labels, &mut seen, "Forked from");
        }
        if self.identity.is_some() {
            push_label(&mut labels, &mut seen, "Identity");
        }
        push_label(&mut labels, &mut seen, "Token usage");
        if self.token_usage.cache_hit_percent.is_some() {
            push_label(&mut labels, &mut seen, "Cache hit");
        }
        if self.token_usage.context_window.is_some() {
            push_label(&mut labels, &mut seen, "Context window");
        }
        if self.context_compact_count > 0 {
            push_label(&mut labels, &mut seen, "Context compact");
        }

        let formatter = FieldFormatter::from_labels(labels.iter().map(String::as_str));
        let value_width = formatter.value_width(available_inner_width);

        let mut model_spans = vec![Span::from(self.model_name.clone())];
        if !self.model_details.is_empty() {
            model_spans.push(Span::from(" (").dim());
            model_spans.push(Span::from(self.model_details.join(", ")).dim());
            model_spans.push(Span::from(")").dim());
        }

        let directory_value = format_directory_display(&self.directory, Some(value_width));

        lines.push(formatter.line("Model", model_spans));
        if let Some(model_provider) = self.model_provider.as_ref() {
            lines.push(formatter.line("Model provider", vec![Span::from(model_provider.clone())]));
        }
        lines.push(formatter.line("Directory", vec![Span::from(directory_value)]));
        lines.push(formatter.line("Approval", vec![Span::from(self.approval.clone())]));
        lines.push(formatter.line("Sandbox", vec![Span::from(self.sandbox.clone())]));
        lines.push(formatter.line("Agents.md", vec![Span::from(self.agents_summary.clone())]));

        if let Some(thread_name) = thread_name {
            lines.push(formatter.line("Thread name", vec![Span::from(thread_name.to_string())]));
        }
        if let Some(identity) = self.identity.as_ref() {
            lines.push(formatter.line("Identity", vec![Span::from(identity.clone())]));
        }
        if let Some(session) = self.session_id.as_ref() {
            lines.push(formatter.line("Session", vec![Span::from(session.clone())]));
        }
        if self.session_id.is_some()
            && let Some(forked_from) = self.forked_from.as_ref()
        {
            lines.push(formatter.line("Forked from", vec![Span::from(forked_from.clone())]));
        }

        lines.push(Line::from(Vec::<Span<'static>>::new()));
        lines.push(formatter.line("Token usage", self.token_usage_spans()));
        if let Some(spans) = self.cache_hit_spans() {
            lines.push(formatter.line("Cache hit", spans));
        }

        if let Some(spans) = self.context_window_spans() {
            lines.push(formatter.line("Context window", spans));
        }
        if self.context_compact_count > 0 {
            lines.push(formatter.line(
                "Context compact",
                vec![Span::from(self.context_compact_count.to_string())],
            ));
        }

        let content_width = lines.iter().map(line_display_width).max().unwrap_or(0);
        let inner_width = content_width.min(available_inner_width);
        let truncated_lines: Vec<Line<'static>> = lines
            .into_iter()
            .map(|line| truncate_line_to_width(line, inner_width))
            .collect();

        with_border_with_inner_width(truncated_lines, inner_width)
    }
}

fn format_model_provider(config: &Config) -> Option<String> {
    let provider = &config.model_provider;
    let name = provider.name.trim();
    let provider_name = if name.is_empty() {
        config.model_provider_id.as_str()
    } else {
        name
    };
    let provider_name = if name.is_empty() {
        display_model_provider_ref(provider_name)
    } else {
        provider_name.to_string()
    };
    let base_url = provider.base_url.as_deref().and_then(sanitize_base_url);
    let is_default_openai = provider.is_openai() && base_url.is_none();
    if is_default_openai {
        return None;
    }

    Some(match base_url {
        Some(base_url) => format!("{provider_name} - {base_url}"),
        None => provider_name,
    })
}

fn sanitize_base_url(raw: &str) -> Option<String> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return None;
    }

    let Ok(mut url) = Url::parse(trimmed) else {
        return None;
    };
    let _ = url.set_username("");
    let _ = url.set_password(None);
    url.set_query(None);
    url.set_fragment(None);
    Some(url.to_string().trim_end_matches('/').to_string()).filter(|value| !value.is_empty())
}

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

    fn cell_for_usage(token_usage: StatusTokenUsageData) -> StatusHistoryCell {
        StatusHistoryCell {
            model_name: "gpt-5".to_string(),
            model_details: Vec::new(),
            directory: PathBuf::from("/tmp"),
            approval: "never".to_string(),
            sandbox: "read-only".to_string(),
            agents_summary: "<none>".to_string(),
            identity: None,
            model_provider: None,
            thread_name: None,
            session_id: None,
            forked_from: None,
            context_compact_count: 0,
            token_usage,
        }
    }

    #[test]
    fn token_usage_spans_omit_cached_tokens_and_hit_rate() {
        let cell = cell_for_usage(StatusTokenUsageData {
            total: 17_000,
            input: 15_000,
            cached_input: 5_000,
            output: 2_000,
            cache_hit_percent: Some(25),
            context_window: None,
        });

        let rendered = cell
            .token_usage_spans()
            .into_iter()
            .map(|span| span.content.to_string())
            .collect::<String>();

        assert_eq!(rendered, "17K total (15K input + 2K output)");
    }

    #[test]
    fn cache_hit_spans_include_cached_tokens_and_hit_rate() {
        let cell = cell_for_usage(StatusTokenUsageData {
            total: 17_000,
            input: 15_000,
            cached_input: 5_000,
            output: 2_000,
            cache_hit_percent: Some(25),
            context_window: None,
        });

        let rendered = cell
            .cache_hit_spans()
            .expect("cache hit should be known")
            .into_iter()
            .map(|span| span.content.to_string())
            .collect::<String>();

        assert_eq!(rendered, "25% hit (5K cached)");
    }

    #[test]
    fn token_usage_spans_omit_cached_when_hit_rate_unknown() {
        let cell = cell_for_usage(StatusTokenUsageData {
            total: 17_000,
            input: 15_000,
            cached_input: 0,
            output: 2_000,
            cache_hit_percent: None,
            context_window: None,
        });

        let rendered = cell
            .token_usage_spans()
            .into_iter()
            .map(|span| span.content.to_string())
            .collect::<String>();

        assert_eq!(rendered, "17K total (15K input + 2K output)");
    }

    #[test]
    fn display_lines_render_cache_hit_as_separate_line() {
        let cell = cell_for_usage(StatusTokenUsageData {
            total: 17_000,
            input: 15_000,
            cached_input: 5_000,
            output: 2_000,
            cache_hit_percent: Some(25),
            context_window: None,
        });

        let rendered_lines = cell
            .display_lines(120)
            .into_iter()
            .map(|line| {
                line.spans
                    .into_iter()
                    .map(|span| span.content.to_string())
                    .collect::<String>()
            })
            .collect::<Vec<_>>();
        let token_usage_line = rendered_lines
            .iter()
            .find(|line| line.contains("Token usage"))
            .unwrap_or_else(|| panic!("missing token usage line: {rendered_lines:?}"));
        let cache_hit_line = rendered_lines
            .iter()
            .find(|line| line.contains("Cache hit"))
            .unwrap_or_else(|| panic!("missing cache hit line: {rendered_lines:?}"));

        assert!(token_usage_line.contains("17K total (15K input + 2K output)"));
        assert!(!token_usage_line.contains("cached"));
        assert!(!token_usage_line.contains("hit"));
        assert!(cache_hit_line.contains("25% hit (5K cached)"));
    }

    #[test]
    fn display_lines_omit_cache_hit_when_unknown() {
        let cell = cell_for_usage(StatusTokenUsageData {
            total: 17_000,
            input: 15_000,
            cached_input: 0,
            output: 2_000,
            cache_hit_percent: None,
            context_window: None,
        });

        let rendered = cell
            .display_lines(120)
            .into_iter()
            .flat_map(|line| line.spans.into_iter())
            .map(|span| span.content.to_string())
            .collect::<String>();

        assert!(!rendered.contains("Cache hit"));
    }
}
