//! Moonshot AI (Kimi) Device Code flow configuration.
//!
//! Requires additional `X-Msh-*` request headers for platform information.

use async_trait::async_trait;
use byokey_types::{ByokError, ProviderId, Result};
use rand::RngCore as _;

use crate::token::DeviceCodeResponse;

pub const SCOPES: &[&str] = &["openid", "offline_access"];
pub const PLATFORM: &str = "mac";
pub const VERSION: &str = "0.13.0";
pub const DEVICE_MODEL: &str = "MacBookPro";

#[must_use]
pub fn device_id() -> String {
    let mut bytes = [0u8; 16];
    rand::thread_rng().fill_bytes(&mut bytes);
    // Set UUID version 4 and variant bits
    bytes[6] = (bytes[6] & 0x0f) | 0x40;
    bytes[8] = (bytes[8] & 0x3f) | 0x80;
    format!(
        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
        bytes[0],
        bytes[1],
        bytes[2],
        bytes[3],
        bytes[4],
        bytes[5],
        bytes[6],
        bytes[7],
        bytes[8],
        bytes[9],
        bytes[10],
        bytes[11],
        bytes[12],
        bytes[13],
        bytes[14],
        bytes[15]
    )
}

#[must_use]
pub fn device_name() -> String {
    "byokey-client".to_string()
}

#[must_use]
pub fn x_msh_headers() -> Vec<(&'static str, String)> {
    vec![
        ("X-Msh-Platform", PLATFORM.to_string()),
        ("X-Msh-Version", VERSION.to_string()),
        ("X-Msh-Device-Name", device_name()),
        ("X-Msh-Device-Model", DEVICE_MODEL.to_string()),
        ("X-Msh-Device-Id", device_id()),
    ]
}

#[must_use]
pub fn build_device_code_params(client_id: &str, scope: &str) -> Vec<(String, String)> {
    vec![
        ("client_id".into(), client_id.into()),
        ("scope".into(), scope.into()),
    ]
}

#[must_use]
pub fn build_token_poll_params(client_id: &str, device_code: &str) -> Vec<(String, String)> {
    vec![
        ("client_id".into(), client_id.into()),
        ("device_code".into(), device_code.into()),
        (
            "grant_type".into(),
            "urn:ietf:params:oauth:grant-type:device_code".into(),
        ),
    ]
}

/// # Errors
///
/// Returns an error if the response is missing required fields (`device_code`, `user_code`, or `verification_uri`).
pub fn parse_device_code_response(json: &serde_json::Value) -> Result<DeviceCodeResponse> {
    crate::token::parse_device_code_json(
        json,
        &crate::token::DeviceCodeParseConfig {
            verification_uri_fallback: None,
            default_expires_in: 600,
        },
    )
}

// ── DeviceCodeFlow implementation ─────────────────────────────────────────────

use crate::credentials::OAuthCredentials;
use crate::flow::device_code::{self, DeviceCodeFlow, PollResult};
use crate::token::DeviceCodeResponse as DcResp;

/// Kimi device-code provider (with `X-Msh-*` headers).
pub struct Kimi;

#[async_trait]
impl DeviceCodeFlow for Kimi {
    fn provider_id(&self) -> ProviderId {
        ProviderId::Kimi
    }
    fn provider_name(&self) -> &'static str {
        "kimi"
    }

    async fn request_device_code(
        &self,
        http: &rquest::Client,
        creds: &OAuthCredentials,
    ) -> Result<DcResp> {
        let device_code_url = creds
            .device_code_url
            .as_deref()
            .ok_or_else(|| ByokError::Auth("kimi credentials missing device_code_url".into()))?;
        let scope_str = SCOPES.join(" ");
        let params = build_device_code_params(&creds.client_id, &scope_str);
        let headers = x_msh_headers();
        let mut req = http
            .post(device_code_url)
            .header("Accept", "application/json")
            .form(&params);
        for (name, value) in &headers {
            req = req.header(*name, value.as_str());
        }
        let resp = req.send().await?;
        let json: serde_json::Value = resp
            .json()
            .await
            .map_err(|e| ByokError::Auth(format!("failed to parse device code response: {e}")))?;
        parse_device_code_response(&json)
    }

    async fn poll_token(
        &self,
        http: &rquest::Client,
        creds: &OAuthCredentials,
        device_code: &str,
    ) -> Result<PollResult> {
        let token_url = creds
            .token_url
            .as_deref()
            .ok_or_else(|| ByokError::Auth("kimi credentials missing token_url".into()))?;
        let params = build_token_poll_params(&creds.client_id, device_code);
        let headers = x_msh_headers();
        let mut req = http
            .post(token_url)
            .header("Accept", "application/json")
            .form(&params);
        for (name, value) in &headers {
            req = req.header(*name, value.as_str());
        }
        let resp = req.send().await?;
        let json: serde_json::Value = resp
            .json()
            .await
            .map_err(|e| ByokError::Auth(format!("failed to parse token response: {e}")))?;
        device_code::parse_poll_response(&json)
    }
}

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

    #[test]
    fn test_device_id_format() {
        let id = device_id();
        let parts: Vec<&str> = id.split('-').collect();
        assert_eq!(parts.len(), 5);
        assert_eq!(parts[0].len(), 8);
        assert_eq!(parts[1].len(), 4);
        assert_eq!(parts[2].len(), 4);
        assert_eq!(parts[3].len(), 4);
        assert_eq!(parts[4].len(), 12);
        assert!(parts[2].starts_with('4'));
        let variant_char = parts[3].chars().next().unwrap();
        assert!(
            "89ab".contains(variant_char),
            "variant char should be 8/9/a/b, got {variant_char}"
        );
    }

    #[test]
    fn test_device_id_unique() {
        let id1 = device_id();
        let id2 = device_id();
        assert_ne!(id1, id2);
    }

    #[test]
    fn test_device_name() {
        assert_eq!(device_name(), "byokey-client");
    }

    #[test]
    fn test_x_msh_headers() {
        let headers = x_msh_headers();
        assert_eq!(headers.len(), 5);
        assert_eq!(headers[0].0, "X-Msh-Platform");
        assert_eq!(headers[0].1, "mac");
        assert_eq!(headers[1].0, "X-Msh-Version");
        assert_eq!(headers[1].1, "0.13.0");
        assert_eq!(headers[2].0, "X-Msh-Device-Name");
        assert_eq!(headers[2].1, "byokey-client");
        assert_eq!(headers[3].0, "X-Msh-Device-Model");
        assert_eq!(headers[3].1, "MacBookPro");
        assert_eq!(headers[4].0, "X-Msh-Device-Id");
        assert_eq!(headers[4].1.len(), 36);
    }

    const TEST_CLIENT_ID: &str = "test-kimi-client-id";

    #[test]
    fn test_build_device_code_params() {
        let params = build_device_code_params(TEST_CLIENT_ID, "openid offline_access");
        assert!(
            params
                .iter()
                .any(|(k, v)| k == "client_id" && v == TEST_CLIENT_ID)
        );
        assert!(
            params
                .iter()
                .any(|(k, v)| k == "scope" && v == "openid offline_access")
        );
    }

    #[test]
    fn test_build_token_poll_params() {
        let params = build_token_poll_params(TEST_CLIENT_ID, "kimi-dc-abc");
        assert!(
            params
                .iter()
                .any(|(k, v)| k == "client_id" && v == TEST_CLIENT_ID)
        );
        assert!(
            params
                .iter()
                .any(|(k, v)| k == "device_code" && v == "kimi-dc-abc")
        );
        assert!(
            params
                .iter()
                .any(|(k, v)| k == "grant_type"
                    && v == "urn:ietf:params:oauth:grant-type:device_code")
        );
    }

    #[test]
    fn test_parse_device_code_success() {
        let resp = json!({
            "device_code": "kimi-dc-123",
            "user_code": "KIMI-5678",
            "verification_uri": "https://kimi.moonshot.cn/device",
            "expires_in": 1800,
            "interval": 5
        });
        let dc = parse_device_code_response(&resp).unwrap();
        assert_eq!(dc.device_code, "kimi-dc-123");
        assert_eq!(dc.user_code, "KIMI-5678");
        assert_eq!(dc.verification_uri, "https://kimi.moonshot.cn/device");
        assert_eq!(dc.expires_in, 1800);
        assert_eq!(dc.interval, 5);
    }

    #[test]
    fn test_parse_device_code_defaults() {
        let resp = json!({
            "device_code": "dc",
            "user_code": "UC",
            "verification_uri": "https://example.com"
        });
        let dc = parse_device_code_response(&resp).unwrap();
        assert_eq!(dc.expires_in, 600);
        assert_eq!(dc.interval, 5);
    }

    #[test]
    fn test_parse_device_code_missing_device_code() {
        let resp = json!({
            "user_code": "UC",
            "verification_uri": "https://example.com"
        });
        assert!(parse_device_code_response(&resp).is_err());
    }

    #[test]
    fn test_parse_device_code_missing_user_code() {
        let resp = json!({
            "device_code": "dc",
            "verification_uri": "https://example.com"
        });
        assert!(parse_device_code_response(&resp).is_err());
    }

    #[test]
    fn test_parse_device_code_missing_verification_uri() {
        let resp = json!({
            "device_code": "dc",
            "user_code": "UC"
        });
        assert!(parse_device_code_response(&resp).is_err());
    }
}
