//! 微信 iLink Bot 协议客户端（HTTP/JSON，直连官方 ilinkai.weixin.qq.com）。
//!
//! 协议为社区逆向梳理、非官方公开文档，字段/版本可能随微信更新变动；本模块集中
//! 所有协议细节，便于失效时单点适配。全程用 ureq 同步请求（与 quota.rs 同栈，无 tokio）。

use base64::Engine;
use serde_json::{json, Value};

/// 默认基址；扫码登录可能返回不同的 IDC 基址（redirect），之后一律用返回值。
const DEFAULT_BASE: &str = "https://ilinkai.weixin.qq.com";
const APP_ID: &str = "bot";
/// iLink-App-ClientVersion：(major<<16)|(minor<<8)|patch，服务端不严格校验，给合理值即可。
const CLIENT_VERSION: &str = "67108867";
/// 请求体 base_info.channel_version，服务端不严格校验。
const CHANNEL_VERSION: &str = "1.0.2";

/// 构造 ureq 客户端：长轮询 getupdates 服务端最长 hold ~35s，读超时必须 >35s。
fn agent() -> ureq::Agent {
    ureq::AgentBuilder::new()
        .timeout_connect(std::time::Duration::from_secs(10))
        .timeout_read(std::time::Duration::from_secs(60))
        .timeout_write(std::time::Duration::from_secs(30))
        .build()
}

/// X-WECHAT-UIN：base64(随机 u32 的十进制字符串)，每次请求重新生成（防重放）。
/// 用 uuid v4 的随机字节派生 u32，避免引入额外的 rand 依赖。
fn random_uin() -> String {
    let id = uuid::Uuid::new_v4();
    let b = id.as_bytes();
    let n = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
    base64::engine::general_purpose::STANDARD.encode(n.to_string().as_bytes())
}

/// 给请求加公共头；token 为 None 时不带 Authorization（拿二维码/轮询状态阶段）。
fn with_headers(req: ureq::Request, token: Option<&str>) -> ureq::Request {
    let mut r = req
        .set("AuthorizationType", "ilink_bot_token")
        .set("iLink-App-Id", APP_ID)
        .set("iLink-App-ClientVersion", CLIENT_VERSION)
        .set("X-WECHAT-UIN", &random_uin());
    if let Some(t) = token {
        r = r.set("Authorization", &format!("Bearer {t}"));
    }
    r
}

fn base_or_default(base: &str) -> String {
    let b = base.trim().trim_end_matches('/');
    if b.is_empty() {
        DEFAULT_BASE.to_string()
    } else {
        b.to_string()
    }
}

fn fmt_err(e: ureq::Error) -> String {
    match e {
        ureq::Error::Status(code, r) => {
            let body = r.into_string().unwrap_or_default();
            format!("HTTP {code}: {body}")
        }
        ureq::Error::Transport(t) => format!("网络错误: {t}"),
    }
}

// ---------- 登录二维码 ----------

pub struct QrInfo {
    /// 轮询用的 qrcode token（get_qrcode_status 的 qrcode 参数）
    pub qrcode: String,
    /// 待展示的二维码内容：实测 qrcode_img_content 为微信深链 URL
    /// （https://liteapp.weixin.qq.com/q/...，需本地编码成二维码图）；
    /// 也兼容服务端直接给 data:/base64 PNG 的情况（按前缀区分，见 qr_to_data_url）
    pub img_content: String,
}

/// GET /ilink/bot/get_bot_qrcode?bot_type=3
pub fn get_bot_qrcode(base: &str) -> Result<QrInfo, String> {
    let url = format!("{}/ilink/bot/get_bot_qrcode", base_or_default(base));
    let resp = with_headers(agent().get(&url), None)
        .query("bot_type", "3")
        .call()
        .map_err(fmt_err)?;
    let v: Value = resp.into_json().map_err(|e| e.to_string())?;
    let qrcode = v.get("qrcode").and_then(Value::as_str).unwrap_or("").to_string();
    let img_content = v
        .get("qrcode_img_content")
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    if qrcode.is_empty() && img_content.is_empty() {
        return Err("二维码接口未返回有效内容".into());
    }
    Ok(QrInfo { qrcode, img_content })
}

pub struct QrStatus {
    /// wait / scaned / confirmed / expired / scaned_but_redirect / ...
    pub status: String,
    /// confirmed 时返回的登录令牌
    pub bot_token: String,
    /// 登录返回的基址（可能因重定向不同于默认域名）
    pub base_url: String,
    pub account_id: String,
    /// 需要切 IDC 基址时的主机名
    pub redirect_host: String,
    pub nick: String,
}

/// GET /ilink/bot/get_qrcode_status?qrcode=<...>
pub fn get_qrcode_status(base: &str, qrcode: &str) -> Result<QrStatus, String> {
    let url = format!("{}/ilink/bot/get_qrcode_status", base_or_default(base));
    let resp = with_headers(agent().get(&url), None)
        .query("qrcode", qrcode)
        .call()
        .map_err(fmt_err)?;
    let v: Value = resp.into_json().map_err(|e| e.to_string())?;
    let s = |k: &str| v.get(k).and_then(Value::as_str).unwrap_or("").to_string();
    let base_url = v
        .get("baseurl")
        .or_else(|| v.get("base_url"))
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    let account_id = v
        .get("ilink_user_id")
        .or_else(|| v.get("ilink_bot_id"))
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    let nick = v
        .get("nickname")
        .or_else(|| v.get("nick_name"))
        .or_else(|| v.get("nick"))
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    Ok(QrStatus {
        status: s("status"),
        bot_token: s("bot_token"),
        base_url,
        account_id,
        redirect_host: s("redirect_host"),
        nick,
    })
}

// ---------- 收/发消息 ----------

pub struct InboundMsg {
    /// 发消息的微信用户（xxx@im.wechat），回复时即出站 to_user_id
    pub from_user_id: String,
    /// 回复必带的会话令牌
    pub context_token: String,
    /// 文本消息内容（图片消息时为空）
    pub text: String,
    /// 图片消息时为 Some（CDN 地址 + AES 密钥）
    pub image: Option<ImageRef>,
}

/// 图片消息的下载引用：CDN 完整地址 + 十六进制 AES-128 密钥
pub struct ImageRef {
    pub url: String,
    pub aeskey: String,
}

pub struct UpdatesResp {
    /// 新游标，必须覆盖并持久化续传，否则重复收消息
    pub cursor: String,
    pub msgs: Vec<InboundMsg>,
}

/// 调试：把一条含非文本 item 的入站消息原样追加到 %TEMP%/ccgui/wechat-debug.jsonl，
/// 用于逆向真实结构、排查未支持的媒体变体（图片已支持，其它媒体留作样本）。
fn debug_dump_message(m: &Value) {
    use std::io::Write;
    let dir = std::env::temp_dir().join("ccgui");
    let _ = std::fs::create_dir_all(&dir);
    let path = dir.join("wechat-debug.jsonl");
    if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) {
        let line = json!({ "ts": crate::store::now_ms(), "msg": m });
        let _ = writeln!(f, "{line}");
    }
}

/// POST /ilink/bot/getupdates —— 长轮询，服务端最长 hold ~35s。
pub fn get_updates(base: &str, token: &str, cursor: &str) -> Result<UpdatesResp, String> {
    let url = format!("{}/ilink/bot/getupdates", base_or_default(base));
    let body = json!({
        "get_updates_buf": cursor,
        "base_info": { "channel_version": CHANNEL_VERSION }
    });
    let resp = with_headers(agent().post(&url), Some(token))
        .send_json(body)
        .map_err(fmt_err)?;
    let v: Value = resp.into_json().map_err(|e| e.to_string())?;
    let new_cursor = v
        .get("get_updates_buf")
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    let mut msgs = Vec::new();
    if let Some(arr) = v.get("msgs").and_then(Value::as_array) {
        for m in arr {
            // 只处理 message_type==1（用户发来的），2 是 bot 自己发的
            if m.get("message_type").and_then(Value::as_i64).unwrap_or(0) != 1 {
                continue;
            }
            let from = m
                .get("from_user_id")
                .and_then(Value::as_str)
                .unwrap_or("")
                .to_string();
            if from.is_empty() {
                continue;
            }
            let ctx = m
                .get("context_token")
                .and_then(Value::as_str)
                .unwrap_or("")
                .to_string();
            let text = extract_text(m);
            let image = extract_image(m);
            // 含非文本 item（图片/其它媒体）的消息仍抓取原始结构留样
            let has_nontext = m
                .get("item_list")
                .and_then(Value::as_array)
                .map(|items| {
                    items
                        .iter()
                        .any(|it| it.get("type").and_then(Value::as_i64).unwrap_or(1) != 1)
                })
                .unwrap_or(false);
            if has_nontext {
                debug_dump_message(m);
            }
            // 既无文本也无图片（未支持的媒体）→ 跳过
            if text.is_empty() && image.is_none() {
                continue;
            }
            msgs.push(InboundMsg {
                from_user_id: from,
                context_token: ctx,
                text,
                image,
            });
        }
    }
    Ok(UpdatesResp {
        cursor: new_cursor,
        msgs,
    })
}

/// 取一条消息的纯文本（拼接所有文本 item）
fn extract_text(m: &Value) -> String {
    m.get("item_list")
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(|it| {
                    if it.get("type").and_then(Value::as_i64).unwrap_or(0) == 1 {
                        it.get("text_item")
                            .and_then(|ti| ti.get("text"))
                            .and_then(Value::as_str)
                            .map(str::to_string)
                    } else {
                        None
                    }
                })
                .collect::<Vec<_>>()
                .join("")
        })
        .unwrap_or_default()
        .trim()
        .to_string()
}

/// 提取消息中的第一张图片（type==2 的 image_item）：CDN 地址 + 十六进制 AES 密钥
fn extract_image(m: &Value) -> Option<ImageRef> {
    let items = m.get("item_list")?.as_array()?;
    for it in items {
        if it.get("type").and_then(Value::as_i64) != Some(2) {
            continue;
        }
        let img = it.get("image_item")?;
        let url = img
            .pointer("/media/full_url")
            .and_then(Value::as_str)
            .unwrap_or("")
            .to_string();
        let aeskey = img
            .get("aeskey")
            .and_then(Value::as_str)
            .unwrap_or("")
            .to_string();
        if !url.is_empty() && !aeskey.is_empty() {
            return Some(ImageRef { url, aeskey });
        }
    }
    None
}

/// 下载并解密一张图片，返回明文字节（原图 PNG/JPEG 等）。
/// CDN 密文为 AES-128-ECB + PKCS7，密钥为 aeskey 十六进制解出的 16 字节（实测验证）。
pub fn download_image(img: &ImageRef) -> Result<Vec<u8>, String> {
    use std::io::Read;
    let key = hex16(&img.aeskey).ok_or("aeskey 非法（应为 32 位十六进制）")?;
    let resp = agent().get(&img.url).call().map_err(fmt_err)?;
    let mut enc = Vec::new();
    resp.into_reader()
        .read_to_end(&mut enc)
        .map_err(|e| e.to_string())?;
    aes128_ecb_pkcs7_decrypt(&key, &enc)
}

/// 32 位十六进制字符串 → 16 字节
fn hex16(s: &str) -> Option<[u8; 16]> {
    if s.len() != 32 {
        return None;
    }
    let mut out = [0u8; 16];
    for i in 0..16 {
        out[i] = u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok()?;
    }
    Some(out)
}

/// AES-128-ECB + PKCS7 解密
fn aes128_ecb_pkcs7_decrypt(key: &[u8; 16], data: &[u8]) -> Result<Vec<u8>, String> {
    use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyInit};
    type Dec = ecb::Decryptor<aes::Aes128>;
    Dec::new(key.into())
        .decrypt_padded_vec_mut::<Pkcs7>(data)
        .map_err(|e| format!("AES 解密失败: {e}"))
}

/// POST /ilink/bot/sendmessage —— 主动发/回复。长文按字符分片多次发。
pub fn send_message(
    base: &str,
    token: &str,
    to_user_id: &str,
    context_token: &str,
    text: &str,
) -> Result<(), String> {
    for chunk in chunk_text(text, 3500) {
        send_one(base, token, to_user_id, context_token, &chunk)?;
    }
    Ok(())
}

fn send_one(
    base: &str,
    token: &str,
    to_user_id: &str,
    context_token: &str,
    text: &str,
) -> Result<(), String> {
    let url = format!("{}/ilink/bot/sendmessage", base_or_default(base));
    let body = json!({
        "msg": {
            "from_user_id": "",
            "to_user_id": to_user_id,
            "client_id": format!("ccgui-{}", uuid::Uuid::new_v4()),
            "message_type": 2,
            "message_state": 2,
            "context_token": context_token,
            "item_list": [{ "type": 1, "text_item": { "text": text } }]
        },
        "base_info": { "channel_version": CHANNEL_VERSION }
    });
    let resp = with_headers(agent().post(&url), Some(token))
        .send_json(body)
        .map_err(fmt_err)?;
    let v: Value = resp.into_json().map_err(|e| e.to_string())?;
    let ret = v.get("ret").and_then(Value::as_i64).unwrap_or(0);
    let errcode = v.get("errcode").and_then(Value::as_i64).unwrap_or(0);
    if ret != 0 || errcode != 0 {
        let msg = v
            .get("errmsg")
            .or_else(|| v.get("err_msg"))
            .and_then(Value::as_str)
            .unwrap_or("未知错误");
        return Err(format!("发送失败 ret={ret} errcode={errcode}: {msg}"));
    }
    Ok(())
}

fn chunk_text(text: &str, max: usize) -> Vec<String> {
    if text.chars().count() <= max {
        return vec![text.to_string()];
    }
    let mut out = Vec::new();
    let mut cur = String::new();
    let mut count = 0usize;
    for ch in text.chars() {
        if count >= max {
            out.push(std::mem::take(&mut cur));
            count = 0;
        }
        cur.push(ch);
        count += 1;
    }
    if !cur.is_empty() {
        out.push(cur);
    }
    out
}

// ---------- 二维码图像 ----------

/// 把二维码内容串本地编码成 PNG 的 data URL（服务端未返回图片时兜底）。
pub fn qr_png_data_url(content: &str) -> Result<String, String> {
    use qrcode::types::Color;
    use qrcode::QrCode;

    let code = QrCode::new(content.as_bytes()).map_err(|e| e.to_string())?;
    let width = code.width();
    let colors = code.to_colors();
    const SCALE: usize = 6; // 每个模块放大为 6×6 像素
    const QUIET: usize = 4; // 静默区 4 模块
    let modules = width + QUIET * 2;
    let img = modules * SCALE;
    // 8 位灰度，初始全白
    let mut pixels = vec![255u8; img * img];
    for y in 0..width {
        for x in 0..width {
            if colors[y * width + x] == Color::Dark {
                let px0 = (x + QUIET) * SCALE;
                let py0 = (y + QUIET) * SCALE;
                for dy in 0..SCALE {
                    let row = (py0 + dy) * img;
                    for dx in 0..SCALE {
                        pixels[row + px0 + dx] = 0;
                    }
                }
            }
        }
    }
    let mut png_bytes: Vec<u8> = Vec::new();
    {
        let mut encoder = png::Encoder::new(&mut png_bytes, img as u32, img as u32);
        encoder.set_color(png::ColorType::Grayscale);
        encoder.set_depth(png::BitDepth::Eight);
        let mut writer = encoder.write_header().map_err(|e| e.to_string())?;
        writer
            .write_image_data(&pixels)
            .map_err(|e| e.to_string())?;
    }
    let b64 = base64::engine::general_purpose::STANDARD.encode(&png_bytes);
    Ok(format!("data:image/png;base64,{b64}"))
}

/// 由 get_bot_qrcode 的结果得到可直接 <img src> 的 data URL。
/// 实测 img_content 是微信深链 URL（需本地编码成二维码图）；同时兼容服务端
/// 直接返回 data:/base64 PNG 的情况，按前缀区分：
/// - "data:" 开头 → 已是图片 data URL，直接用
/// - "iVBOR" 开头 → 是 base64 PNG（PNG 魔数的 base64 前缀），补 data 前缀
/// - 其余非空（URL 或任意串）→ 作为二维码内容本地编码成 PNG
/// - 空 → 退回用 qrcode token 编码
pub fn qr_to_data_url(info: &QrInfo) -> Result<String, String> {
    let content = info.img_content.trim();
    if content.starts_with("data:") {
        return Ok(content.to_string());
    }
    if content.starts_with("iVBOR") {
        return Ok(format!("data:image/png;base64,{content}"));
    }
    if !content.is_empty() {
        return qr_png_data_url(content);
    }
    qr_png_data_url(&info.qrcode)
}
