//! 设备消息处理逻辑。
//!
//! 每个 `act` 对应一个处理函数，返回 `Option<ServerResponse>`。
//! 返回 `None` 表示无需回复（或已通过连接通道异步处理）。

use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use std::time::{Duration, Instant};

use rand::Rng;
use sha2::{Digest, Sha256};
use sqlx::MySqlPool;

use super::connection::ConnectionPool;
use super::protocol::{DeviceMessage, ServerResponse};

// Redis key 前缀
const REDIS_KEY_AUTH_STR: &str = "auth_str:";
// ponytail: 签名截取范围固定，抽为常量
const SIGN_START: usize = 56;
const SIGN_END: usize = 63; // 7 字符

/// 签名校验窗口：设备时间戳与服务器时间差在 ±60 秒内有效。
const TIMESTAMP_TOLERANCE_SECS: i64 = 60;

/// auth_str 频率限制：每个设备在窗口内最多请求次数
const AUTH_STR_MAX_ATTEMPTS: usize = 5;
/// auth_str 频率限制窗口（秒）
const AUTH_STR_WINDOW_SECS: u64 = 300;

// ---------------------------------------------------------------------------
// 频率限制器
// ---------------------------------------------------------------------------

/// 简单的滑动窗口频率限制器。
///
/// 以 `dev_id` 为键，记录每次请求的时间戳，
/// 清理窗口外的旧记录后判断是否超限。
struct RateLimiter {
    /// 每个 key 的请求时间戳列表
    attempts: Mutex<HashMap<String, Vec<Instant>>>,
    /// 窗口内允许的最大请求数
    max_attempts: usize,
    /// 滑动窗口时长
    window: Duration,
}

impl RateLimiter {
    /// 创建新的频率限制器。
    fn new(max_attempts: usize, window: Duration) -> Self {
        Self {
            attempts: Mutex::new(HashMap::new()),
            max_attempts,
            window,
        }
    }

    /// 检查是否允许请求，同时记录本次请求。
    /// 返回 `true` 表示允许，`false` 表示超限。
    fn check(&self, key: &str) -> bool {
        let mut map = self.attempts.lock().unwrap_or_else(|e| e.into_inner());
        let now = Instant::now();
        let window_start = now - self.window;

        let entry = map.entry(key.to_string()).or_default();
        // 清理窗口外的过期记录
        entry.retain(|t| *t > window_start);

        if entry.len() >= self.max_attempts {
            return false;
        }

        entry.push(now);
        true
    }
}

/// 全局 auth_str 频率限制器（5次/5分钟）
static AUTH_STR_LIMITER: LazyLock<RateLimiter> = LazyLock::new(|| {
    RateLimiter::new(AUTH_STR_MAX_ATTEMPTS, Duration::from_secs(AUTH_STR_WINDOW_SECS))
});

/// 分发消息到对应处理函数。
pub async fn handle_message(
    msg: DeviceMessage,
    pool: &ConnectionPool,
    redis: &redis::aio::ConnectionManager,
    mysql: &MySqlPool,
) -> Option<ServerResponse> {
    match msg.act.as_str() {
        "auth_str" => Some(handle_auth_str(&msg, redis).await),
        "login" => Some(handle_login(&msg, pool, redis).await),
        "status_post" => handle_status_post(&msg, pool, mysql, redis).await,
        "pow_fail" => handle_pow_fail(&msg, mysql).await,
        // ponytail: 未知指令统一返回失败，不阻塞连接
        _ => Some(ServerResponse::error(msg.msg_id, format!("未知指令: {}", msg.act))),
    }
}

/// 处理 `auth_str`：生成 8 位随机安全码并存入 Redis。
///
/// 受频率限制保护：每个设备 5 分钟内最多请求 5 次。
async fn handle_auth_str(msg: &DeviceMessage, redis: &redis::aio::ConnectionManager) -> ServerResponse {
    let dev_id = match &msg.dev_id {
        Some(id) => id,
        None => return ServerResponse::error(msg.msg_id, "缺少 dev_id"),
    };

    // 频率限制检查
    if !AUTH_STR_LIMITER.check(dev_id) {
        tracing::warn!("[auth_str] 频率超限 dev_id={}", dev_id);
        return ServerResponse::error(msg.msg_id, "请求过于频繁，请稍后再试");
    }

    let auth_str: String = rand::thread_rng()
        .sample_iter(&rand::distributions::Alphanumeric)
        .take(8)
        .map(char::from)
        .collect();

    let key = format!("{REDIS_KEY_AUTH_STR}{dev_id}");
    // ponytail: auth_str 永久有效（设备仅首次获取后本地存储）
    let mut conn = redis.clone();
    let _: Result<(), _> = redis::cmd("SET")
        .arg(&key)
        .arg(&auth_str)
        .query_async(&mut conn)
        .await;

    tracing::info!("[auth_str] 已生成安全码 dev_id={}", dev_id);

    let mut resp = ServerResponse::success(msg.msg_id);
    resp.auth_str = Some(auth_str);
    resp
}

/// 处理 `login`：验证签名。
async fn handle_login(
    msg: &DeviceMessage,
    pool: &ConnectionPool,
    redis: &redis::aio::ConnectionManager,
) -> ServerResponse {
    let msg_id = msg.msg_id;

    let dev_id = match &msg.dev_id {
        Some(id) => id,
        None => return ServerResponse::error(msg_id, "缺少 dev_id"),
    };
    let timestamp = match msg.timestamp {
        Some(ts) => ts,
        None => return ServerResponse::error(msg_id, "缺少 timestamp"),
    };
    let sign = match &msg.sign {
        Some(s) => s,
        None => return ServerResponse::error(msg_id, "缺少 sign"),
    };

    // 校验时间窗口 ±60 秒
    let now = chrono::Utc::now().timestamp();
    let ts = timestamp as i64;
    if (now - ts).abs() > TIMESTAMP_TOLERANCE_SECS {
        tracing::warn!("[login] 时间戳超限 dev_id={} timestamp={}", dev_id, timestamp);
        return ServerResponse::error(msg_id, "时间戳无效");
    }

    // 从 Redis 获取安全码
    let key = format!("{REDIS_KEY_AUTH_STR}{dev_id}");
    let mut conn = redis.clone();
    let auth_str: Option<String> = redis::cmd("GET")
        .arg(&key)
        .query_async(&mut conn)
        .await
        .unwrap_or(None);

    let auth_str = match auth_str {
        Some(s) => s,
        None => return ServerResponse::error(msg_id, "未获取安全码，请先调用 auth_str"),
    };

    // 计算期望签名: SHA256(dev_id + auth_str + timestamp)[56..63]
    let input = format!("{}{}{}", dev_id, auth_str, timestamp);
    let hash = Sha256::digest(input.as_bytes());
    let hex_hash = hex::encode(hash);
    let expected_sign = &hex_hash[SIGN_START..SIGN_END];

    if sign == expected_sign {
        // 标记已验证
        pool.set_authenticated(dev_id).await;
        tracing::info!("[login] 成功 dev_id={}", dev_id);
        ServerResponse::success(msg_id)
    } else {
        tracing::warn!(
            "[login] 签名不匹配 dev_id={} expected={} got={}",
            dev_id,
            expected_sign,
            sign
        );
        ServerResponse::error(msg_id, "签名错误")
    }
}

/// 处理 `status_post`：解析 status HEX 并持久化。
async fn handle_status_post(
    msg: &DeviceMessage,
    pool: &ConnectionPool,
    mysql: &MySqlPool,
    redis: &redis::aio::ConnectionManager,
) -> Option<ServerResponse> {
    let dev_id = match &msg.dev_id {
        Some(id) => id,
        None => return Some(ServerResponse::error(msg.msg_id, "缺少 dev_id")),
    };

    // 状态上报来自已验证设备才处理
    if !pool.is_online(dev_id).await {
        return Some(ServerResponse::error(msg.msg_id, "设备未登录"));
    }

    pool.touch(dev_id).await;

    // 存入 Redis 实时数据 (key: device:status:{dev_id})
    let redis_key = format!("device:status:{}", dev_id);
    let status_json = serde_json::to_string(msg).unwrap_or_default();
    let mut conn = redis.clone();
    let _: Result<(), _> = redis::cmd("SET")
        .arg(&redis_key)
        .arg(&status_json)
        .query_async(&mut conn)
        .await;

    // 解析 status HEX 字段（协议定义 status 为 HEX 字符串）
    let status_hex = msg.status.as_deref().unwrap_or("");
    let status_bytes = hex::decode(status_hex).unwrap_or_default();

    // 持久化到 MySQL device_logs 表
    let content = serde_json::json!({
        "dev_id": dev_id,
        "sub_device_id": msg.sub_device_id,
        "status_hex": status_hex,
        "status_raw": if status_bytes.is_empty() { None } else { {
            Some(status_bytes.iter().map(|b| format!("{b:02x}")).collect::<Vec<_>>().join(" "))
        } },
        "rssi": msg.rssi,
        "timestamp": msg.timestamp,
    });

    let result = sqlx::query(
        "INSERT INTO device_logs (cabinet_id, log_type, content) VALUES (
            (SELECT id FROM cabinets WHERE imei = ?), 1, ?
        )",
    )
    .bind(dev_id)
    .bind(content.to_string())
    .execute(mysql)
    .await;

    if let Err(e) = result {
        tracing::error!("[status_post] 写入失败 dev_id={} err={}", dev_id, e);
    }

    tracing::debug!("[status_post] 已处理 dev_id={}", dev_id);
    Some(ServerResponse::success(msg.msg_id))
}

/// 处理 `pow_fail`：记录停电日志。
async fn handle_pow_fail(
    msg: &DeviceMessage,
    mysql: &MySqlPool,
) -> Option<ServerResponse> {
    let dev_id = match &msg.dev_id {
        Some(id) => id,
        None => return Some(ServerResponse::error(msg.msg_id, "缺少 dev_id")),
    };

    let content = serde_json::json!({
        "dev_id": dev_id,
        "timestamp": msg.timestamp,
        "sub_device_id": msg.sub_device_id,
    });

    let result = sqlx::query(
        "INSERT INTO device_logs (cabinet_id, log_type, content) VALUES (
            (SELECT id FROM cabinets WHERE imei = ?), 4, ?
        )",
    )
    .bind(dev_id)
    .bind(content.to_string())
    .execute(mysql)
    .await;

    if let Err(e) = result {
        tracing::error!("[pow_fail] 写入失败 dev_id={} err={}", dev_id, e);
    }

    tracing::info!("[pow_fail] 停电上报 dev_id={}", dev_id);
    Some(ServerResponse::success(msg.msg_id))
}
