//! Doc CRUD + the chat endpoint that hands off to claude.
//!
//! Layout on disk:
//!   ${DOCS_DIR}/<id>/doc.md         the editable markdown
//! Metadata (id/title/timestamps) in sqlite.

use std::path::PathBuf;
use std::process::Stdio;

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::{IntoResponse, Json as JsonResp},
    routing::{get, post, put},
    Router,
};
use rusqlite::{params, OptionalExtension};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tokio::process::Command;
use tracing::{error, info, warn};

use crate::state::AppState;

#[derive(Serialize)]
pub struct DocMeta {
    pub id: i64,
    pub title: String,
    pub created_at: String,
    pub updated_at: String,
}

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/docs", get(list_docs).post(create_doc))
        .route("/docs/:id", get(get_doc).delete(delete_doc))
        .route("/docs/:id/content", put(save_doc_content))
        .route("/docs/:id/chat", post(chat))
}

fn doc_dir(state: &AppState, id: i64) -> PathBuf {
    state.docs_dir.join(id.to_string())
}

fn doc_path(state: &AppState, id: i64) -> PathBuf {
    doc_dir(state, id).join("doc.md")
}

async fn list_docs(State(state): State<AppState>) -> impl IntoResponse {
    let db = state.db.lock().expect("db lock");
    let mut stmt = match db.prepare(
        "SELECT id, title, created_at, updated_at FROM docs ORDER BY updated_at DESC",
    ) {
        Ok(s) => s,
        Err(e) => return Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
    };
    let rows = stmt
        .query_map([], |row| {
            Ok(DocMeta {
                id: row.get(0)?,
                title: row.get(1)?,
                created_at: row.get(2)?,
                updated_at: row.get(3)?,
            })
        })
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    let out: Vec<DocMeta> = rows
        .filter_map(Result::ok)
        .collect();
    Ok(JsonResp(out))
}

#[derive(Deserialize)]
struct CreateDocReq {
    title: Option<String>,
}

async fn create_doc(
    State(state): State<AppState>,
    JsonResp(req): JsonResp<CreateDocReq>,
) -> impl IntoResponse {
    let title = req.title.unwrap_or_else(|| "未命名文档".to_string());
    let id: i64 = {
        let db = state.db.lock().expect("db lock");
        db.execute(
            "INSERT INTO docs (title) VALUES (?1)",
            params![title],
        )
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
        db.last_insert_rowid()
    };
    let dir = doc_dir(&state, id);
    if let Err(e) = std::fs::create_dir_all(&dir) {
        error!(error = %e, dir = %dir.display(), "create dir failed");
        return Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string()));
    }
    let path = doc_path(&state, id);
    let _ = std::fs::write(&path, "");
    let meta = read_meta(&state, id)
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
    Ok(JsonResp(meta))
}

fn read_meta(state: &AppState, id: i64) -> Result<DocMeta, String> {
    let db = state.db.lock().expect("db lock");
    db.query_row(
        "SELECT id, title, created_at, updated_at FROM docs WHERE id = ?1",
        params![id],
        |row| {
            Ok(DocMeta {
                id: row.get(0)?,
                title: row.get(1)?,
                created_at: row.get(2)?,
                updated_at: row.get(3)?,
            })
        },
    )
    .optional()
    .map_err(|e| e.to_string())?
    .ok_or_else(|| "doc not found".to_string())
}

#[derive(Serialize)]
struct DocResp {
    #[serde(flatten)]
    meta: DocMeta,
    content: String,
}

async fn get_doc(
    State(state): State<AppState>,
    Path(id): Path<i64>,
) -> impl IntoResponse {
    let meta = read_meta(&state, id)
        .map_err(|e| (StatusCode::NOT_FOUND, e))?;
    let content = std::fs::read_to_string(doc_path(&state, id))
        .unwrap_or_default();
    Ok::<_, (StatusCode, String)>(JsonResp(DocResp { meta, content }))
}

async fn delete_doc(
    State(state): State<AppState>,
    Path(id): Path<i64>,
) -> impl IntoResponse {
    {
        let db = state.db.lock().expect("db lock");
        let _ = db.execute("DELETE FROM docs WHERE id = ?1", params![id]);
    }
    let _ = std::fs::remove_dir_all(doc_dir(&state, id));
    StatusCode::NO_CONTENT
}

#[derive(Deserialize)]
struct SaveContentReq {
    content: String,
    title: Option<String>,
}

async fn save_doc_content(
    State(state): State<AppState>,
    Path(id): Path<i64>,
    JsonResp(req): JsonResp<SaveContentReq>,
) -> impl IntoResponse {
    let dir = doc_dir(&state, id);
    std::fs::create_dir_all(&dir).ok();
    std::fs::write(doc_path(&state, id), &req.content)
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    {
        let db = state.db.lock().expect("db lock");
        if let Some(t) = &req.title {
            let _ = db.execute(
                "UPDATE docs SET title = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
                params![t, id],
            );
        } else {
            let _ = db.execute(
                "UPDATE docs SET updated_at = CURRENT_TIMESTAMP WHERE id = ?1",
                params![id],
            );
        }
    }
    let meta = read_meta(&state, id)
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
    Ok::<_, (StatusCode, String)>(JsonResp(meta))
}

#[derive(Deserialize)]
struct ChatReq {
    text: String,
}

#[derive(Serialize)]
struct ChatResp {
    content: String,
    reply: String,
    cost_usd: Option<f64>,
    turns: Option<i64>,
    duration_ms: Option<i64>,
}

async fn chat(
    State(state): State<AppState>,
    Path(id): Path<i64>,
    JsonResp(req): JsonResp<ChatReq>,
) -> impl IntoResponse {
    // Verify doc exists
    let _meta = match read_meta(&state, id) {
        Ok(m) => m,
        Err(e) => return Err((StatusCode::NOT_FOUND, e)),
    };

    let dir = doc_dir(&state, id);
    std::fs::create_dir_all(&dir).ok();
    let path = doc_path(&state, id);
    if !path.exists() {
        let _ = std::fs::write(&path, "");
    }

    info!(id, "spawning claude in {}", dir.display());

    // Build the prompt for claude. claude runs in the doc's directory with full
    // read/write access via Read/Edit/Write tools — it edits `doc.md` directly.
    let prompt = format!(
        r#"你是 write.famzheng.me 的 AI 协作者。Fam 在跟你协作编辑一个 markdown 文档。

工作目录里只有一个文件: `doc.md`。这就是当前正在编辑的文档。

Fam 刚说:
---
{}
---

根据 Fam 说的话，决定动作：
- 如果他要求改文档（例如 "把第二段重写"、"加一个章节" 等），用 Read / Edit / Write 工具修改 `doc.md`。
- 如果他在问问题，**也直接把答案写进 `doc.md`**（追加，或合适地融入），因为这个 app 没有独立对话框——所有对话都体现在文档上。
- 如果他说的是闲聊或不相关，简短回应一下别动文档。

**重要**: 最终输出一句话简短反馈（不超过 30 字），告诉 Fam 你做了什么。比如 "已扩写第二段"、"已加 TODO 章节"、"没看懂你想改啥"。**不要在输出里贴文档全文**。

完成后退出。
"#,
        req.text
    );

    // Spawn claude
    let mut cmd = Command::new(&state.claude_bin);
    cmd.current_dir(&dir)
        .arg("-p")
        .arg(&prompt)
        .arg("--dangerously-skip-permissions")
        .arg("--model")
        .arg(&state.claude_model)
        .arg("--output-format")
        .arg("json")
        .arg("--max-budget-usd")
        .arg(state.claude_max_budget_usd.to_string())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    let child = match cmd.spawn() {
        Ok(c) => c,
        Err(e) => {
            error!(error = %e, "spawn claude failed");
            return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("spawn: {e}")));
        }
    };

    // Read stdout fully (JSON result format gives a single JSON object)
    let output = match child.wait_with_output().await {
        Ok(o) => o,
        Err(e) => {
            error!(error = %e, "claude wait failed");
            return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("wait: {e}")));
        }
    };

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        warn!(rc = ?output.status.code(), "claude exited non-zero");
        return Err((
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("claude exit={:?}: {}", output.status.code(), &stderr[..stderr.len().min(2000)]),
        ));
    }

    let stdout_text = String::from_utf8_lossy(&output.stdout);
    let parsed: Value = serde_json::from_str(&stdout_text)
        .unwrap_or_else(|_| json!({"result": stdout_text}));

    let reply = parsed.get("result")
        .and_then(Value::as_str)
        .unwrap_or("")
        .trim()
        .to_string();
    let cost = parsed.get("total_cost_usd").and_then(Value::as_f64);
    let turns = parsed.get("num_turns").and_then(Value::as_i64);
    let duration = parsed.get("duration_ms").and_then(Value::as_i64);

    // Re-read doc.md
    let content = std::fs::read_to_string(&path).unwrap_or_default();

    {
        let db = state.db.lock().expect("db lock");
        let _ = db.execute(
            "UPDATE docs SET updated_at = CURRENT_TIMESTAMP WHERE id = ?1",
            params![id],
        );
    }

    info!(id, ?cost, ?turns, ?duration, "claude chat done");

    Ok::<_, (StatusCode, String)>(JsonResp(ChatResp {
        content,
        reply,
        cost_usd: cost,
        turns,
        duration_ms: duration,
    }))
}
