use async_trait::async_trait;
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use zeroclaw::tools::{Tool, ToolResult};
use zip::write::FileOptions;
use zip::{CompressionMethod, ZipWriter};

const OPENXML_OFFICE_TOOL_NAME: &str = "openxml_office";
const DEFAULT_SHEET_NAME: &str = "知乎热榜";
const MAX_COLUMNS: [&str; 3] = ["rank", "title", "heat"];

pub struct OpenXmlOfficeTool {
    workspace_root: PathBuf,
}

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

#[derive(Debug, Deserialize)]
struct OpenXmlOfficeArgs {
    sheet_name: String,
    columns: Vec<String>,
    rows: Vec<Vec<Value>>,
    #[serde(default)]
    output_path: Option<String>,
}

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

    fn description(&self) -> &str {
        "Export structured Zhihu hotlist rows into a local .xlsx file through the OpenXML office pipeline."
    }

    fn parameters_schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["sheet_name", "columns", "rows"],
            "properties": {
                "sheet_name": { "type": "string" },
                "columns": {
                    "type": "array",
                    "items": { "type": "string" }
                },
                "rows": {
                    "type": "array",
                    "items": {
                        "type": "array",
                        "items": {}
                    }
                },
                "output_path": { "type": "string" }
            }
        })
    }

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

        if parsed.sheet_name.trim() != DEFAULT_SHEET_NAME {
            return Ok(failed_tool_result(format!(
                "unsupported sheet_name: expected {DEFAULT_SHEET_NAME}"
            )));
        }

        let expected_columns = MAX_COLUMNS
            .iter()
            .map(|value| value.to_string())
            .collect::<Vec<_>>();
        let column_order = match resolve_column_order(&parsed.columns, &expected_columns) {
            Some(order) => order,
            None => {
                return Ok(failed_tool_result(
                    "unsupported columns: expected [rank, title, heat]".to_string(),
                ))
            }
        };

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

        if parsed
            .rows
            .iter()
            .any(|row| row.len() != parsed.columns.len())
        {
            return Ok(failed_tool_result(
                "each row must match the declared columns length".to_string(),
            ));
        }

        if parsed.rows.iter().any(|row| row.len() != 3) {
            return Ok(failed_tool_result(
                "each row must contain exactly 3 values".to_string(),
            ));
        }
        let normalized_rows = parsed
            .rows
            .iter()
            .map(|row| reorder_row(row, &column_order))
            .collect::<Vec<_>>();

        let job_root = create_job_root(&self.workspace_root)?;
        let template_path = job_root.join("zhihu_hotlist_template.xlsx");
        let payload_path = job_root.join("payload.json");
        let request_path = job_root.join("request.json");
        let output_path = parsed
            .output_path
            .as_deref()
            .map(PathBuf::from)
            .unwrap_or_else(|| default_output_path(&self.workspace_root));

        write_hotlist_template(&template_path, normalized_rows.len())?;
        write_payload_json(&payload_path, &normalized_rows)?;
        write_request_json(&request_path, &template_path, &payload_path, &output_path)?;

        let rendered = run_openxml_cli(&request_path)
            .or_else(|_| render_locally(&template_path, &payload_path, &output_path))?;
        let artifact_path = rendered["data"]["artifact"]["path"]
            .as_str()
            .map(str::to_string)
            .unwrap_or_else(|| output_path.to_string_lossy().to_string());

        Ok(ToolResult {
            success: true,
            output: json!({
                "sheet_name": DEFAULT_SHEET_NAME,
                "output_path": artifact_path,
                "row_count": normalized_rows.len(),
                "renderer": OPENXML_OFFICE_TOOL_NAME
            })
            .to_string(),
            error: None,
        })
    }
}

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

fn create_job_root(workspace_root: &Path) -> anyhow::Result<PathBuf> {
    let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
    let path = workspace_root.join(".sgclaw-openxml").join(format!("{nanos}"));
    fs::create_dir_all(&path)?;
    Ok(path)
}

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-{nanos}.xlsx"))
}

fn resolve_column_order(
    provided_columns: &[String],
    expected_columns: &[String],
) -> Option<Vec<usize>> {
    if provided_columns.len() != expected_columns.len() {
        return None;
    }

    let canonicalized_columns = provided_columns
        .iter()
        .map(|value| canonicalize_column_name(value))
        .collect::<Option<Vec<_>>>()?;

    let provided_set = canonicalized_columns
        .iter()
        .map(|value| value.to_string())
        .collect::<BTreeSet<_>>();
    let expected_set = expected_columns.iter().cloned().collect::<BTreeSet<_>>();

    if provided_set != expected_set {
        return None;
    }

    expected_columns
        .iter()
        .map(|expected| {
            canonicalized_columns
                .iter()
                .position(|provided| *provided == expected)
        })
        .collect::<Option<Vec<_>>>()
}

fn canonicalize_column_name(value: &str) -> Option<&'static str> {
    match value.trim().to_ascii_lowercase().as_str() {
        "rank" | "排名" | "名次" | "序号" => Some("rank"),
        "title" | "标题" | "题目" | "问题" => Some("title"),
        "heat" | "热度" | "热值" => Some("heat"),
        _ => None,
    }
}

fn reorder_row(row: &[Value], column_order: &[usize]) -> Vec<Value> {
    column_order.iter().map(|index| row[*index].clone()).collect()
}

fn write_payload_json(path: &Path, rows: &[Vec<Value>]) -> anyhow::Result<()> {
    let mut variables = BTreeMap::new();
    for (idx, row) in rows.iter().enumerate() {
        let row_index = idx + 1;
        variables.insert(format!("RANK_{row_index}"), value_to_string(&row[0]));
        variables.insert(format!("TITLE_{row_index}"), value_to_string(&row[1]));
        variables.insert(format!("HEAT_{row_index}"), value_to_string(&row[2]));
    }

    let payload = json!({
        "variables": variables,
        "tables": {},
        "images": {}
    });
    fs::write(path, serde_json::to_vec_pretty(&payload)?)?;
    Ok(())
}

fn write_request_json(
    path: &Path,
    template_path: &Path,
    payload_path: &Path,
    output_path: &Path,
) -> anyhow::Result<()> {
    if let Some(parent) = output_path.parent() {
        fs::create_dir_all(parent)?;
    }

    let request = json!({
        "api_version": "2026-03-26",
        "job": "zhihu_hotlist_export",
        "template": {
            "kind": "xlsx",
            "path": template_path
        },
        "output": {
            "path": output_path
        },
        "data": {
            "json_path": payload_path
        },
        "options": {
            "strict": true,
            "allow_unresolved": false,
            "dry_run": false
        }
    });
    fs::write(path, serde_json::to_vec_pretty(&request)?)?;
    Ok(())
}

fn run_openxml_cli(request_path: &Path) -> anyhow::Result<Value> {
    let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .map(|path| path.join("openxml_cli").join("Cargo.toml"))
        .ok_or_else(|| anyhow::anyhow!("failed to resolve openxml_cli manifest path"))?;
    let output = if let Some(binary_path) = resolve_openxml_cli_binary(&manifest_path) {
        Command::new(binary_path)
            .args([
                "template",
                "render",
                "--request",
                request_path.to_string_lossy().as_ref(),
                "--json",
            ])
            .output()?
    } else {
        Command::new("cargo")
            .args([
                "run",
                "--quiet",
                "--manifest-path",
                manifest_path.to_string_lossy().as_ref(),
                "--",
                "template",
                "render",
                "--request",
                request_path.to_string_lossy().as_ref(),
                "--json",
            ])
            .output()?
    };

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        return Err(anyhow::anyhow!(if stderr.is_empty() {
            "openxml_cli render failed".to_string()
        } else {
            stderr
        }));
    }

    let stdout = String::from_utf8(output.stdout)?;
    Ok(serde_json::from_str(&stdout)?)
}

fn render_locally(template_path: &Path, payload_path: &Path, output_path: &Path) -> anyhow::Result<Value> {
    let payload: Value = serde_json::from_slice(&fs::read(payload_path)?)?;
    let variables = payload["variables"]
        .as_object()
        .ok_or_else(|| anyhow::anyhow!("payload.variables must be an object"))?;

    let worksheet = render_template_xml(&worksheet_xml_from_xlsx(template_path)?, variables);
    write_rendered_xlsx(template_path, output_path, "xl/worksheets/sheet1.xml", &worksheet)?;

    Ok(json!({
        "data": {
            "artifact": {
                "path": output_path.to_string_lossy().to_string(),
            }
        }
    }))
}

fn worksheet_xml_from_xlsx(path: &Path) -> anyhow::Result<String> {
    let file = fs::File::open(path)?;
    let mut archive = zip::ZipArchive::new(file)?;
    let mut sheet = archive.by_name("xl/worksheets/sheet1.xml")?;
    let mut xml = String::new();
    sheet.read_to_string(&mut xml)?;
    Ok(xml)
}

fn render_template_xml(template: &str, variables: &serde_json::Map<String, Value>) -> String {
    let mut rendered = template.to_string();
    for (key, value) in variables {
        let placeholder = format!("{{{{{key}}}}}");
        let replacement = value.as_str().unwrap_or_default();
        rendered = rendered.replace(&placeholder, &xml_escape(replacement));
    }
    rendered
}

fn write_rendered_xlsx(
    template_path: &Path,
    output_path: &Path,
    replaced_entry: &str,
    replaced_body: &str,
) -> anyhow::Result<()> {
    if let Some(parent) = output_path.parent() {
        fs::create_dir_all(parent)?;
    }
    if output_path.exists() {
        fs::remove_file(output_path)?;
    }

    let input = fs::File::open(template_path)?;
    let mut archive = zip::ZipArchive::new(input)?;
    let output = fs::File::create(output_path)?;
    let mut writer = ZipWriter::new(output);
    let options = FileOptions::default().compression_method(CompressionMethod::Stored);

    for index in 0..archive.len() {
        let mut entry = archive.by_index(index)?;
        let name = entry.name().to_string();
        writer.start_file(name.as_str(), options)?;
        if name == replaced_entry {
            writer.write_all(replaced_body.as_bytes())?;
        } else {
            std::io::copy(&mut entry, &mut writer)?;
        }
    }

    writer.finish()?;
    Ok(())
}

fn xml_escape(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
}

fn resolve_openxml_cli_binary(manifest_path: &Path) -> Option<PathBuf> {
    let cli_dir = manifest_path.parent()?;
    openxml_cli_candidate_paths(cli_dir)
        .into_iter()
        .find(|path| path.exists())
}

fn openxml_cli_candidate_paths(cli_dir: &Path) -> Vec<PathBuf> {
    let mut paths = Vec::new();
    for profile in ["release", "debug"] {
        paths.push(
            cli_dir
                .join("target")
                .join(profile)
                .join(openxml_cli_binary_name()),
        );
    }
    paths
}

fn openxml_cli_binary_name() -> &'static str {
    if cfg!(windows) {
        "openxml-cli.exe"
    } else {
        "openxml-cli"
    }
}

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 write_hotlist_template(path: &Path, row_count: usize) -> anyhow::Result<()> {
    write_zip_file(
        &path,
        &[
            Content {
                path: "[Content_Types].xml",
                body: content_types_xml().to_string(),
            },
            Content {
                path: "_rels/.rels",
                body: root_rels_xml().to_string(),
            },
            Content {
                path: "docProps/app.xml",
                body: app_xml().to_string(),
            },
            Content {
                path: "docProps/core.xml",
                body: core_xml().to_string(),
            },
            Content {
                path: "xl/workbook.xml",
                body: workbook_xml().to_string(),
            },
            Content {
                path: "xl/_rels/workbook.xml.rels",
                body: workbook_rels_xml().to_string(),
            },
            Content {
                path: "xl/worksheets/sheet1.xml",
                body: worksheet_xml(row_count),
            },
        ],
    )?;
    Ok(())
}

struct Content<'a> {
    path: &'a str,
    body: String,
}

fn write_zip_file(path: &Path, entries: &[Content<'_>]) -> anyhow::Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    if path.exists() {
        fs::remove_file(path)?;
    }

    let file = fs::File::create(path)?;
    let mut zip = ZipWriter::new(file);
    let options = FileOptions::default().compression_method(CompressionMethod::Stored);
    for entry in entries {
        zip.start_file(entry.path, options)?;
        zip.write_all(entry.body.as_bytes())?;
    }
    zip.finish()?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{openxml_cli_binary_name, openxml_cli_candidate_paths, zip_entry_name};
    use std::path::Path;

    #[test]
    fn openxml_cli_candidates_prefer_release_before_debug() {
        let paths = openxml_cli_candidate_paths(Path::new("E:\\coding\\codex\\openxml_cli"));
        assert_eq!(paths.len(), 2);
        assert_eq!(
            paths[0],
            Path::new("E:\\coding\\codex\\openxml_cli")
                .join("target")
                .join("release")
                .join(openxml_cli_binary_name())
        );
        assert_eq!(
            paths[1],
            Path::new("E:\\coding\\codex\\openxml_cli")
                .join("target")
                .join("debug")
                .join(openxml_cli_binary_name())
        );
    }

    #[test]
    fn zip_entry_name_normalizes_windows_separators() {
        let rel = Path::new("xl\\worksheets\\sheet1.xml");
        assert_eq!(zip_entry_name(rel), "xl/worksheets/sheet1.xml");
    }
}

fn zip_entry_name(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}

fn worksheet_xml(row_count: usize) -> String {
    let mut rows = Vec::new();
    rows.push(
        "<row r=\"1\"><c r=\"A1\" t=\"inlineStr\"><is><t>rank</t></is></c><c r=\"B1\" t=\"inlineStr\"><is><t>title</t></is></c><c r=\"C1\" t=\"inlineStr\"><is><t>heat</t></is></c></row>"
            .to_string(),
    );

    for idx in 1..=row_count {
        let excel_row = idx + 1;
        rows.push(format!(
            "<row r=\"{excel_row}\"><c r=\"A{excel_row}\" t=\"inlineStr\"><is><t>{{{{RANK_{idx}}}}}</t></is></c><c r=\"B{excel_row}\" t=\"inlineStr\"><is><t>{{{{TITLE_{idx}}}}}</t></is></c><c r=\"C{excel_row}\" t=\"inlineStr\"><is><t>{{{{HEAT_{idx}}}}}</t></is></c></row>"
        ));
    }

    format!(
        "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\
<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\
  <sheetData>{}</sheetData>\
</worksheet>",
        rows.join("")
    )
}

fn content_types_xml() -> &'static str {
    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
  <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
  <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
  <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
</Types>"#
}

fn root_rels_xml() -> &'static str {
    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>"#
}

fn app_xml() -> &'static str {
    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
            xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
  <Application>sgClaw</Application>
</Properties>"#
}

fn core_xml() -> &'static str {
    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
                   xmlns:dc="http://purl.org/dc/elements/1.1/"
                   xmlns:dcterms="http://purl.org/dc/terms/"
                   xmlns:dcmitype="http://purl.org/dc/dcmitype/"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <dc:title>Zhihu Hotlist Export</dc:title>
</cp:coreProperties>"#
}

fn workbook_xml() -> &'static str {
    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
          xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <sheets>
    <sheet name="知乎热榜" sheetId="1" r:id="rId1"/>
  </sheets>
</workbook>"#
}

fn workbook_rels_xml() -> &'static str {
    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
</Relationships>"#
}
