use std::path::Path;
use std::process::Command;

use serde_json::{json, Value};

use crate::browser::BrowserBackend;
use crate::pipe::{Action, CommandOutput};

pub const LOCAL_DASHBOARD_EXPECTED_DOMAIN: &str = "__sgclaw_local_dashboard__";
pub const LOCAL_DASHBOARD_SOURCE: &str = "compat.workflow_executor";
pub const LOCAL_DASHBOARD_KIND_ZHIHU_HOTLIST_SCREEN: &str = "zhihu_hotlist_screen";
const DISABLE_POST_EXPORT_OPEN_ENV: &str = "SGCLAW_DISABLE_POST_EXPORT_OPEN";

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PostExportOpen {
    Opened,
    Failed(String),
}

pub fn open_exported_xlsx(output_path: &Path) -> PostExportOpen {
    open_exported_xlsx_with(output_path, launch_with_default_xlsx_app)
}

fn open_exported_xlsx_with<F>(output_path: &Path, opener: F) -> PostExportOpen
where
    F: FnOnce(&Path) -> Result<(), String>,
{
    if !output_path.exists() {
        return PostExportOpen::Failed(format!(
            "导出的 Excel 文件不存在：{}",
            output_path.display()
        ));
    }

    match opener(output_path) {
        Ok(()) => PostExportOpen::Opened,
        Err(reason) => PostExportOpen::Failed(reason),
    }
}

pub fn open_local_dashboard(
    browser_backend: &dyn BrowserBackend,
    output_path: &Path,
    presentation_url: &str,
) -> PostExportOpen {
    if !output_path.exists() {
        return PostExportOpen::Failed(format!(
            "生成的大屏文件不存在：{}",
            output_path.display()
        ));
    }
    if presentation_url.trim().is_empty() {
        return PostExportOpen::Failed("screen_html_export did not return presentation.url".to_string());
    }

    let params = json!({
        "url": presentation_url,
        "sgclaw_local_dashboard_open": {
            "source": LOCAL_DASHBOARD_SOURCE,
            "kind": LOCAL_DASHBOARD_KIND_ZHIHU_HOTLIST_SCREEN,
            "output_path": output_path.to_string_lossy(),
            "presentation_url": presentation_url,
        }
    });

    match browser_backend.invoke(Action::Navigate, params, LOCAL_DASHBOARD_EXPECTED_DOMAIN) {
        Ok(output) if output.success => PostExportOpen::Opened,
        Ok(output) => PostExportOpen::Failed(command_output_reason(&output)),
        Err(err) => PostExportOpen::Failed(err.to_string()),
    }
}

#[cfg(windows)]
fn launch_with_default_xlsx_app(output_path: &Path) -> Result<(), String> {
    if std::env::var_os(DISABLE_POST_EXPORT_OPEN_ENV).is_some() {
        return Ok(());
    }

    let output = Command::new("cmd")
        .args(["/C", "start", "", &output_path.display().to_string()])
        .output()
        .map_err(|err| format!("启动 Excel 默认程序失败：{err}"))?;
    if output.status.success() {
        Ok(())
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        if stderr.is_empty() {
            Err(format!(
                "启动 Excel 默认程序失败：exit status {}",
                output.status
            ))
        } else {
            Err(format!("启动 Excel 默认程序失败：{stderr}"))
        }
    }
}

#[cfg(target_os = "macos")]
fn launch_with_default_xlsx_app(output_path: &Path) -> Result<(), String> {
    if std::env::var_os(DISABLE_POST_EXPORT_OPEN_ENV).is_some() {
        return Ok(());
    }

    let status = Command::new("open")
        .arg(output_path)
        .status()
        .map_err(|err| format!("启动 Excel 默认程序失败：{err}"))?;
    if status.success() {
        Ok(())
    } else {
        Err(format!("启动 Excel 默认程序失败：exit status {status}"))
    }
}

#[cfg(all(unix, not(target_os = "macos")))]
fn launch_with_default_xlsx_app(output_path: &Path) -> Result<(), String> {
    if std::env::var_os(DISABLE_POST_EXPORT_OPEN_ENV).is_some() {
        return Ok(());
    }

    let status = Command::new("xdg-open")
        .arg(output_path)
        .status()
        .map_err(|err| format!("启动 Excel 默认程序失败：{err}"))?;
    if status.success() {
        Ok(())
    } else {
        Err(format!("启动 Excel 默认程序失败：exit status {status}"))
    }
}

fn command_output_reason(output: &CommandOutput) -> String {
    output
        .data
        .get("error")
        .and_then(Value::as_str)
        .or_else(|| output.data.get("message").and_then(Value::as_str))
        .map(str::to_string)
        .unwrap_or_else(|| output.data.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::VecDeque;
    use std::path::PathBuf;
    use std::sync::Mutex;

    use serde_json::json;

    use crate::pipe::{ExecutionSurfaceMetadata, PipeError, Timing};

    fn temp_file_path(name: &str) -> PathBuf {
        let root = std::env::temp_dir().join(format!(
            "sgclaw-artifact-open-{}-{}",
            std::process::id(),
            uuid::Uuid::new_v4()
        ));
        std::fs::create_dir_all(&root).expect("temp root should exist");
        root.join(name)
    }

    #[test]
    fn open_exported_xlsx_with_passes_generated_path_to_launcher() {
        let output_path = temp_file_path("zhihu-hotlist.xlsx");
        std::fs::write(&output_path, b"xlsx").expect("xlsx fixture should be writable");
        let seen = Mutex::new(None::<PathBuf>);

        let result = open_exported_xlsx_with(&output_path, |path| {
            *seen.lock().unwrap() = Some(path.to_path_buf());
            Ok(())
        });

        assert!(matches!(result, PostExportOpen::Opened));
        assert_eq!(seen.lock().unwrap().clone().unwrap(), output_path);
    }

    #[test]
    fn open_exported_xlsx_with_reports_launcher_failure() {
        let output_path = temp_file_path("zhihu-hotlist.xlsx");
        std::fs::write(&output_path, b"xlsx").expect("xlsx fixture should be writable");

        let result = open_exported_xlsx_with(&output_path, |_path| Err("launcher failed".to_string()));

        assert!(matches!(result, PostExportOpen::Failed(reason) if reason.contains("launcher failed")));
    }

    #[derive(Default)]
    struct FakeBrowserBackend {
        responses: Mutex<VecDeque<Result<CommandOutput, PipeError>>>,
        invocations: Mutex<Vec<(Action, Value, String)>>,
    }

    impl FakeBrowserBackend {
        fn new(responses: Vec<Result<CommandOutput, PipeError>>) -> Self {
            Self {
                responses: Mutex::new(VecDeque::from(responses)),
                invocations: Mutex::new(Vec::new()),
            }
        }
    }

    impl BrowserBackend for FakeBrowserBackend {
        fn invoke(
            &self,
            action: Action,
            params: Value,
            expected_domain: &str,
        ) -> Result<CommandOutput, PipeError> {
            self.invocations
                .lock()
                .unwrap()
                .push((action, params, expected_domain.to_string()));
            self.responses
                .lock()
                .unwrap()
                .pop_front()
                .unwrap_or_else(|| Err(PipeError::Timeout))
        }

        fn surface_metadata(&self) -> ExecutionSurfaceMetadata {
            ExecutionSurfaceMetadata::privileged_browser_pipe("fake_backend")
        }
    }

    #[test]
    fn open_local_dashboard_uses_exact_approved_marker_payload() {
        let output_path = temp_file_path("zhihu-hotlist-screen.html");
        std::fs::write(&output_path, "<html></html>").expect("dashboard fixture should be writable");
        let presentation_url = format!("file:///{}", output_path.display().to_string().replace('\\', "/"));
        let backend = FakeBrowserBackend::new(vec![Ok(CommandOutput {
            seq: 1,
            success: true,
            data: json!({ "navigated": true }),
            aom_snapshot: vec![],
            timing: Timing {
                queue_ms: 1,
                exec_ms: 1,
            },
        })]);

        let result = open_local_dashboard(&backend, &output_path, &presentation_url);
        let invocations = backend.invocations.lock().unwrap().clone();

        assert!(matches!(result, PostExportOpen::Opened));
        assert_eq!(invocations.len(), 1);
        assert_eq!(invocations[0].0, Action::Navigate);
        assert_eq!(invocations[0].2, LOCAL_DASHBOARD_EXPECTED_DOMAIN.to_string());
        assert_eq!(invocations[0].1["url"], json!(presentation_url));
        assert_eq!(
            invocations[0].1["sgclaw_local_dashboard_open"]["source"],
            json!(LOCAL_DASHBOARD_SOURCE)
        );
        assert_eq!(
            invocations[0].1["sgclaw_local_dashboard_open"]["kind"],
            json!(LOCAL_DASHBOARD_KIND_ZHIHU_HOTLIST_SCREEN)
        );
        assert_eq!(
            invocations[0].1["sgclaw_local_dashboard_open"]["output_path"],
            json!(output_path.to_string_lossy().to_string())
        );
        assert_eq!(
            invocations[0].1["sgclaw_local_dashboard_open"]["presentation_url"],
            invocations[0].1["url"]
        );
    }
}
