use crate::kernel::WorkspaceSourceKind;
use serde::de;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AiSession {
    pub session_id: String,
    pub workspace_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub document_id: Option<String>,
    pub owner: AiRuntimeOwner,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum AiRuntimeOwner {
    RustWebHermes,
    CompatReactIsland,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AiToolCall {
    pub call_id: String,
    pub tool_name: String,
    pub args: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AiEvent {
    pub event_id: String,
    pub session_id: String,
    pub kind: AiEventKind,
    #[serde(default)]
    pub payload: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum AiEventKind {
    SessionCreated,
    ToolCallPlanned,
    ToolCallApplied,
    StructuredWriteCompleted,
    Error,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AiStructuredWriteResult {
    pub ok: bool,
    pub write_kind: AiStructuredWriteKind,
    pub command_name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revision: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum AiStructuredWriteKind {
    SummaryNode,
    AiNoteNode,
    ReferenceEdge,
    PageBody,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AiAccessPermissionLevel {
    Admin,
    ReadOnly,
    ReadWrite,
    SharedRead,
    SharedWrite,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AiShareContext {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub share_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub mode: Option<String>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AiAccessScope {
    pub user_id: String,
    pub workspace_id: String,
    pub session_id: String,
    pub source_kind: WorkspaceSourceKind,
    pub permission_level: AiAccessPermissionLevel,
    #[serde(default)]
    pub allowed_roots: Vec<String>,
    #[serde(default)]
    pub allowed_file_paths: Vec<String>,
    #[serde(default)]
    pub allowed_resource_ids: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub share_context: Option<AiShareContext>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AiAccessScopeRaw {
    user_id: String,
    workspace_id: String,
    session_id: String,
    source_kind: WorkspaceSourceKind,
    permission_level: AiAccessPermissionLevel,
    #[serde(default)]
    allowed_roots: Vec<String>,
    #[serde(default)]
    allowed_file_paths: Vec<String>,
    #[serde(default)]
    allowed_resource_ids: Vec<String>,
    #[serde(default)]
    share_context: Option<AiShareContext>,
}

impl<'de> Deserialize<'de> for AiAccessScope {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let raw = AiAccessScopeRaw::deserialize(deserializer)?;
        let scope = Self {
            user_id: raw.user_id,
            workspace_id: raw.workspace_id,
            session_id: raw.session_id,
            source_kind: raw.source_kind,
            permission_level: raw.permission_level,
            allowed_roots: raw.allowed_roots,
            allowed_file_paths: raw.allowed_file_paths,
            allowed_resource_ids: raw.allowed_resource_ids,
            share_context: raw.share_context,
        };
        scope.validate().map_err(de::Error::custom)?;
        Ok(scope)
    }
}

impl AiAccessScope {
    pub fn validate(&self) -> Result<(), String> {
        reject_blank("userId", &self.user_id)?;
        reject_blank("workspaceId", &self.workspace_id)?;
        reject_blank("sessionId", &self.session_id)?;
        reject_blank_vec("allowedRoots", &self.allowed_roots)?;
        reject_blank_vec("allowedFilePaths", &self.allowed_file_paths)?;
        reject_blank_vec("allowedResourceIds", &self.allowed_resource_ids)?;
        if let Some(share_context) = &self.share_context {
            if let Some(share_id) = &share_context.share_id {
                reject_blank("shareContext.shareId", share_id)?;
            }
            if let Some(mode) = &share_context.mode {
                reject_blank("shareContext.mode", mode)?;
            }
        }
        Ok(())
    }
}

fn reject_blank(field: &str, value: &str) -> Result<(), String> {
    if value.trim().is_empty() {
        return Err(format!("{field} must not be blank"));
    }
    Ok(())
}

fn reject_blank_vec(field: &str, values: &[String]) -> Result<(), String> {
    for value in values {
        reject_blank(field, value)?;
    }
    Ok(())
}

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

    #[test]
    fn ai_access_scope_uses_camel_case_payload() {
        let scope: AiAccessScope = serde_json::from_value(json!({
            "userId": "user_1",
            "workspaceId": "workspace_1",
            "sessionId": "session_1",
            "sourceKind": "local_folder",
            "permissionLevel": "read_write",
            "allowedRoots": ["file:///mnt/Data1T/Mnote_data/users/user_1"],
            "allowedFilePaths": ["file:///mnt/Data1T/Mnote_data/users/user_1/我的空间/README.md"],
            "allowedResourceIds": ["local-md:README.md"],
            "shareContext": {
                "shareId": "share_1",
                "mode": "shared_write"
            }
        }))
        .expect("scope");

        assert_eq!(scope.user_id, "user_1");
        assert_eq!(scope.workspace_id, "workspace_1");
        assert_eq!(scope.session_id, "session_1");
        assert_eq!(scope.source_kind, WorkspaceSourceKind::LocalFolder);
        assert_eq!(scope.permission_level, AiAccessPermissionLevel::ReadWrite);
        assert_eq!(scope.allowed_roots.len(), 1);
        assert_eq!(scope.allowed_file_paths.len(), 1);
        assert_eq!(scope.allowed_resource_ids, vec!["local-md:README.md"]);
        assert_eq!(
            scope
                .share_context
                .as_ref()
                .and_then(|value| value.share_id.as_deref()),
            Some("share_1")
        );

        let serialized = serde_json::to_value(&scope).expect("serialized");
        assert_eq!(serialized["userId"], "user_1");
        assert_eq!(serialized["permissionLevel"], "read_write");
        assert_eq!(
            serialized["allowedRoots"][0],
            "file:///mnt/Data1T/Mnote_data/users/user_1"
        );
    }

    #[test]
    fn ai_access_scope_rejects_blank_required_fields_and_paths() {
        let error = serde_json::from_value::<AiAccessScope>(json!({
            "userId": " ",
            "workspaceId": "workspace_1",
            "sessionId": "session_1",
            "sourceKind": "local_folder",
            "permissionLevel": "read_only",
            "allowedRoots": ["file:///mnt/Data1T/Mnote_data/users/user_1"],
            "allowedFilePaths": [],
            "allowedResourceIds": []
        }))
        .expect_err("blank user id should be rejected");
        assert!(error.to_string().contains("userId"));

        let error = serde_json::from_value::<AiAccessScope>(json!({
            "userId": "user_1",
            "workspaceId": "workspace_1",
            "sessionId": "session_1",
            "sourceKind": "local_folder",
            "permissionLevel": "read_only",
            "allowedRoots": [" "],
            "allowedFilePaths": [],
            "allowedResourceIds": []
        }))
        .expect_err("blank allowed root should be rejected");
        assert!(error.to_string().contains("allowedRoots"));
    }
}
