use async_trait::async_trait;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use zeroclaw::tools::{Tool, ToolResult};

const SCREEN_HTML_EXPORT_TOOL_NAME: &str = "screen_html_export";
const DEFAULT_SCREEN_TITLE: &str = "知乎热榜主题分类分析大屏";
const TEMPLATE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/resources/zhihu-hotlist-echarts.html"
));
const PAYLOAD_START_MARKER: &str = "    const defaultPayload = ";
const PAYLOAD_END_MARKER: &str = "const themeMeta = {";

pub struct ScreenHtmlExportTool {
    workspace_root: PathBuf,
}

impl ScreenHtmlExportTool {
    pub fn new(workspace_root: PathBuf) -> Self {
        Self { workspace_root }
    }
}

#[derive(Debug, Deserialize)]
struct ScreenHtmlExportArgs {
    #[serde(default)]
    snapshot_id: Option<String>,
    #[serde(default)]
    generated_at_ms: Option<u64>,
    #[serde(default)]
    rows: Option<Vec<Vec<Value>>>,
    #[serde(default)]
    table: Option<Vec<ScreenTableRow>>,
    #[serde(default)]
    categories: Option<Vec<ScreenCategory>>,
    #[serde(default)]
    output_path: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
struct ScreenCategory {
    category_code: String,
    category_label: String,
    item_count: u64,
    total_heat: u64,
    avg_heat: u64,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
struct ScreenTableRow {
    rank: u64,
    title: String,
    url: String,
    category_code: String,
    category_label: String,
    heat_text: String,
    heat_value: u64,
    reply_count: u64,
    upvote_count: u64,
    favorite_count: u64,
    heart_count: u64,
}

#[derive(Debug, Serialize)]
struct ScreenPayload {
    snapshot_id: String,
    generated_at_ms: u64,
    categories: Vec<ScreenCategory>,
    table: Vec<ScreenTableRow>,
}

#[async_trait]
impl Tool for ScreenHtmlExportTool {
    fn name(&self) -> &str {
        SCREEN_HTML_EXPORT_TOOL_NAME
    }

    fn description(&self) -> &str {
        "Render a local Zhihu hotlist ECharts dashboard HTML for leadership demos and new-tab presentation."
    }

    fn parameters_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "snapshot_id": { "type": "string" },
                "generated_at_ms": { "type": "integer" },
                "rows": {
                    "type": "array",
                    "items": {
                        "type": "array",
                        "items": {}
                    }
                },
                "table": {
                    "type": "array",
                    "items": { "type": "object" }
                },
                "categories": {
                    "type": "array",
                    "items": { "type": "object" }
                },
                "output_path": { "type": "string" }
            }
        })
    }

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let parsed = match serde_json::from_value::<ScreenHtmlExportArgs>(args) {
            Ok(value) => value,
            Err(err) => return Ok(failed_tool_result(format!("invalid tool arguments: {err}"))),
        };

        let table = match parsed.table {
            Some(table) if !table.is_empty() => table,
            Some(_) => return Ok(failed_tool_result("table must not be empty".to_string())),
            None => match parsed.rows {
                Some(rows) => build_table_from_rows(&rows)?,
                None => {
                    return Ok(failed_tool_result(
                        "rows or table is required for screen_html_export".to_string(),
                    ))
                }
            },
        };

        if table.is_empty() {
            return Ok(failed_tool_result("table must not be empty".to_string()));
        }

        let categories = parsed
            .categories
            .filter(|items| !items.is_empty())
            .unwrap_or_else(|| derive_categories(&table));
        let payload = ScreenPayload {
            snapshot_id: parsed
                .snapshot_id
                .map(|value| value.trim().to_string())
                .filter(|value| !value.is_empty())
                .unwrap_or_else(default_snapshot_id),
            generated_at_ms: parsed.generated_at_ms.unwrap_or_else(now_ms),
            categories,
            table,
        };

        let rendered = render_template(&payload)?;
        let output_path = resolve_output_path(&self.workspace_root, parsed.output_path.as_deref());
        write_output_html(&output_path, &rendered)?;

        let presentation_url = file_url_for_path(&output_path);
        Ok(ToolResult {
            success: true,
            output: json!({
                "title": DEFAULT_SCREEN_TITLE,
                "output_path": output_path,
                "renderer": SCREEN_HTML_EXPORT_TOOL_NAME,
                "row_count": payload.table.len(),
                "snapshot_id": payload.snapshot_id,
                "presentation": {
                    "mode": "new_tab",
                    "title": DEFAULT_SCREEN_TITLE,
                    "url": presentation_url,
                    "open_in_new_tab": true
                }
            })
            .to_string(),
            error: None,
        })
    }
}

fn failed_tool_result(error: String) -> ToolResult {
    ToolResult {
        success: false,
        output: String::new(),
        error: Some(error),
    }
}

fn build_table_from_rows(rows: &[Vec<Value>]) -> anyhow::Result<Vec<ScreenTableRow>> {
    if rows.is_empty() {
        return Err(anyhow::anyhow!("rows must not be empty"));
    }

    rows.iter()
        .enumerate()
        .map(|(index, row)| {
            if row.len() != 3 {
                return Err(anyhow::anyhow!(
                    "each row must contain exactly 3 values: rank, title, heat"
                ));
            }

            let rank = value_to_rank(&row[0]).unwrap_or((index + 1) as u64);
            let title = value_to_string(&row[1]);
            if title.trim().is_empty() {
                return Err(anyhow::anyhow!("title must not be empty"));
            }
            let heat_text = value_to_string(&row[2]);
            let heat_value = parse_heat_value(&heat_text);
            let (category_code, category_label) = classify_title(&title);

            Ok(ScreenTableRow {
                rank,
                title,
                url: format!("https://www.zhihu.com/question/hotlist-{rank}"),
                category_code: category_code.to_string(),
                category_label: category_label.to_string(),
                heat_text,
                heat_value,
                reply_count: 0,
                upvote_count: 0,
                favorite_count: 0,
                heart_count: 0,
            })
        })
        .collect()
}

fn derive_categories(table: &[ScreenTableRow]) -> Vec<ScreenCategory> {
    let mut grouped: BTreeMap<(String, String), (u64, u64)> = BTreeMap::new();
    for row in table {
        let key = (row.category_code.clone(), row.category_label.clone());
        let entry = grouped.entry(key).or_insert((0, 0));
        entry.0 += 1;
        entry.1 += row.heat_value;
    }

    grouped
        .into_iter()
        .map(
            |((category_code, category_label), (item_count, total_heat))| ScreenCategory {
                category_code,
                category_label,
                item_count,
                total_heat,
                avg_heat: if item_count == 0 {
                    0
                } else {
                    total_heat / item_count
                },
            },
        )
        .collect()
}

fn classify_title(title: &str) -> (&'static str, &'static str) {
    let normalized = title.to_ascii_lowercase();
    if contains_any(
        &normalized,
        &["ai", "芯片", "科技", "算法", "机器人", "无人机"],
    ) {
        return ("technology", "科技");
    }
    if contains_any(
        &normalized,
        &["电影", "综艺", "明星", "周杰伦", "短剧", "娱乐"],
    ) {
        return ("entertainment", "娱乐");
    }
    if contains_any(
        &normalized,
        &["足球", "比赛", "联赛", "国足", "体育", "冠军"],
    ) {
        return ("sports", "体育");
    }
    if contains_any(&normalized, &["航母", "作战", "军", "军事", "演训"]) {
        return ("military", "军事");
    }
    if contains_any(&normalized, &["出口", "经济", "市场", "财经", "消费", "股"]) {
        return ("finance", "财经");
    }
    ("society", "社会")
}

fn contains_any(haystack: &str, needles: &[&str]) -> bool {
    needles.iter().any(|needle| haystack.contains(needle))
}

fn parse_heat_value(heat_text: &str) -> u64 {
    let compact = heat_text.trim().replace(',', "");
    if compact.is_empty() {
        return 0;
    }

    let number_part = compact
        .chars()
        .filter(|ch| ch.is_ascii_digit() || *ch == '.')
        .collect::<String>();
    let base = number_part.parse::<f64>().unwrap_or(0.0);

    let multiplier = if compact.contains('亿') {
        100_000_000.0
    } else if compact.contains('万') {
        10_000.0
    } else {
        1.0
    };

    (base * multiplier).round() as u64
}

fn value_to_string(value: &Value) -> String {
    match value {
        Value::String(text) => text.clone(),
        Value::Number(number) => number.to_string(),
        Value::Bool(flag) => flag.to_string(),
        Value::Null => String::new(),
        other => other.to_string(),
    }
}

fn value_to_rank(value: &Value) -> Option<u64> {
    match value {
        Value::Number(number) => number.as_u64(),
        Value::String(text) => text.trim().parse::<u64>().ok(),
        _ => None,
    }
}

fn render_template(payload: &ScreenPayload) -> anyhow::Result<String> {
    let payload_json = serde_json::to_string_pretty(payload)?;
    let payload_start = TEMPLATE
        .find(PAYLOAD_START_MARKER)
        .ok_or_else(|| anyhow::anyhow!("default payload start marker missing"))?;
    let payload_end = TEMPLATE
        .find(PAYLOAD_END_MARKER)
        .ok_or_else(|| anyhow::anyhow!("default payload end marker missing"))?;
    let replacement = format!(
        "{PAYLOAD_START_MARKER}{}\n",
        indent_block(&payload_json, "      ")
    );

    Ok(format!(
        "{}{}{}",
        &TEMPLATE[..payload_start],
        replacement,
        &TEMPLATE[payload_end..],
    ))
}

fn indent_block(value: &str, indent: &str) -> String {
    value
        .lines()
        .map(|line| format!("{indent}{line}"))
        .collect::<Vec<_>>()
        .join("\n")
}

fn write_output_html(path: &Path, rendered: &str) -> anyhow::Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(path, rendered)?;
    Ok(())
}

fn default_output_path(workspace_root: &Path) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|value| value.as_nanos())
        .unwrap_or_default();
    workspace_root
        .join("out")
        .join(format!("zhihu-hotlist-screen-{nanos}.html"))
}

fn resolve_output_path(workspace_root: &Path, output_path: Option<&str>) -> PathBuf {
    output_path
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
        .map(|path| {
            if path.is_absolute() {
                path
            } else {
                workspace_root.join(path)
            }
        })
        .unwrap_or_else(|| default_output_path(workspace_root))
}

fn default_snapshot_id() -> String {
    format!("zhihu-hotlist-screen-{}", now_ms())
}

fn now_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|value| value.as_millis() as u64)
        .unwrap_or_default()
}

fn file_url_for_path(path: &Path) -> String {
    Url::from_file_path(path)
        .map(|url| url.to_string())
        .unwrap_or_else(|_| format!("file://{}", path.display()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::{json, Value};

    use crate::pipe::Action;
    use crate::security::MacPolicy;

    fn temp_workspace_root() -> PathBuf {
        let root = std::env::temp_dir().join(format!("sgclaw-screen-html-{}", now_ms()));
        std::fs::create_dir_all(&root).unwrap();
        root
    }

    #[tokio::test]
    async fn screen_html_export_resolves_relative_output_path_to_absolute_file_url() {
        let workspace_root = temp_workspace_root();
        let tool = ScreenHtmlExportTool::new(workspace_root.clone());

        let result = tool
            .execute(json!({
                "snapshot_id": "snapshot-relative-path",
                "generated_at_ms": 1774713600000u64,
                "rows": [
                    [1, "问题一", "344万"],
                    [2, "问题二", "266万"]
                ],
                "output_path": "../out/zhihu-hotlist-screen-relative.html"
            }))
            .await
            .unwrap();

        assert!(result.success, "{result:?}");

        let payload: Value = serde_json::from_str(&result.output).unwrap();
        let output_path = PathBuf::from(payload["output_path"].as_str().unwrap());
        let presentation_url = payload["presentation"]["url"].as_str().unwrap();
        let expected_output_path = workspace_root.join("../out/zhihu-hotlist-screen-relative.html");
        let expected_presentation_url = Url::from_file_path(&expected_output_path)
            .unwrap()
            .to_string();
        let policy = MacPolicy::load_from_path(
            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("resources")
                .join("rules.json"),
        )
        .unwrap();

        assert!(output_path.is_absolute());
        assert_eq!(output_path, expected_output_path);
        assert!(output_path.exists());
        assert_eq!(presentation_url, expected_presentation_url);
        assert!(presentation_url.starts_with("file:///"));
        policy
            .validate_local_dashboard_presentation(
                &Action::Navigate,
                "__sgclaw_local_dashboard__",
                presentation_url,
                output_path.to_string_lossy().as_ref(),
            )
            .unwrap();
    }
}
