use serde::{Deserialize, Serialize};
use crate::event::{EventRecord, MarkReadTarget};
use crate::ids::{GroupId, SurfaceId, WorkspaceId};
use crate::layout::LayoutNode;
use crate::status::SurfaceState;
use crate::workspace::{Group, WorkspaceView};

/// Wire envelope. `kind` is the serde tag.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Envelope {
    Req {
        id: u64,
        cmd: Cmd,
    },
    Res {
        id: u64,
        ok: bool,
        #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
        data: serde_json::Value,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        error: Option<ErrorBody>,
    },
    Evt(Evt),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ErrorBody {
    pub code: String,
    pub msg: String,
}

/// Direction a split grows the new neighbor.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SplitDir {
    Right,
    Down,
}

/// Edge of a target leaf to drop a moved panel against.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Edge {
    Left,
    Right,
    Top,
    Bottom,
}

/// One panel slot when applying a preset.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PresetSlot {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub args: Vec<String>,
}

/// Client → daemon commands. The active subset for M0+M1.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "cmd", content = "args", rename_all = "snake_case")]
pub enum Cmd {
    Open { path: String },
    NewSurface {
        workspace_id: WorkspaceId,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        command: Option<String>,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        args: Vec<String>,
        cols: u16,
        rows: u16,
    },
    Input {
        surface_id: SurfaceId,
        /// base64-encoded keyboard bytes.
        bytes: String,
    },
    Resize { surface_id: SurfaceId, cols: u16, rows: u16 },
    Attach { surface_id: SurfaceId },
    Detach { surface_id: SurfaceId },
    Focus { surface_id: SurfaceId },
    Close { surface_id: SurfaceId },
    SplitSurface {
        surface_id: SurfaceId,
        dir: SplitDir,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        command: Option<String>,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        args: Vec<String>,
    },
    SetRatios { workspace_id: WorkspaceId, node_path: Vec<u32>, ratios: Vec<f32> },
    MoveSurface { surface_id: SurfaceId, target_surface_id: SurfaceId, edge: Edge },
    ApplyPreset { workspace_id: WorkspaceId, preset_id: String, slots: Vec<PresetSlot> },
    RestartSurface {
        surface_id: SurfaceId,
        #[serde(default)]
        resume: bool,
    },
    CloseWorkspace { workspace_id: WorkspaceId },
    SetWorkspaceMeta {
        workspace_id: WorkspaceId,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        name: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        group_id: Option<Option<GroupId>>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        unread: Option<bool>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        order: Option<u32>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        pinned: Option<bool>,
    },
    CreateGroup { name: String, color: String },
    SetGroup {
        group_id: GroupId,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        name: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        color: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        order: Option<u32>,
    },
    DeleteGroup { group_id: GroupId },
    SetState { surface_id: SurfaceId, state: SurfaceState },
    EventLog {
        #[serde(default, skip_serializing_if = "Option::is_none")]
        limit: Option<u32>,
    },
    MarkRead { target: MarkReadTarget },
    ClearEvents,
    SetZoom {
        workspace_id: WorkspaceId,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        surface_id: Option<SurfaceId>,
    },
    Health,
    /// Which of the given CLI candidates are actually installed on the spawn PATH.
    WhichAgents { candidates: Vec<String> },
    Status,
    Shutdown,
    GetConfig,
    SetConfig {
        #[serde(default, skip_serializing_if = "Option::is_none")]
        default_shell: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        font_family: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        font_size: Option<u16>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        theme: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        accent: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        background: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        background_image: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        log_shell_commands: Option<bool>,
    },
}

/// Daemon → subscribers push events. The active subset for M0+M1.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "evt", content = "data", rename_all = "snake_case")]
pub enum Evt {
    Output { surface_id: SurfaceId, bytes: Vec<u8> },
    Exit { surface_id: SurfaceId, code: i32 },
    SurfaceCreated { surface_id: SurfaceId, workspace_id: WorkspaceId },
    SurfaceClosed { surface_id: SurfaceId },
    LayoutChanged { workspace_id: WorkspaceId, layout: Option<LayoutNode> },
    WorkspaceChanged { workspace: WorkspaceView },
    WorkspaceClosed { workspace_id: WorkspaceId },
    GroupsChanged { groups: Vec<Group> },
    SurfaceRestarted { surface_id: SurfaceId },
    State { surface_id: SurfaceId, state: SurfaceState },
    Event { record: EventRecord },
    EventsRead { ids: Vec<u64> },
    EventsCleared,
    ConfigChanged { config: crate::config_view::ConfigView },
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ids::{SurfaceId, WorkspaceId};

    #[test]
    fn req_round_trips_through_json() {
        let env = Envelope::Req {
            id: 42,
            cmd: Cmd::Focus { surface_id: SurfaceId("s_8f3".into()) },
        };
        let json = serde_json::to_string(&env).unwrap();
        let back: Envelope = serde_json::from_str(&json).unwrap();
        assert_eq!(env, back);
    }

    #[test]
    fn res_ok_and_err_serialize_distinctly() {
        let ok = Envelope::Res { id: 1, ok: true, data: serde_json::json!({"workspace_id":"w_1"}), error: None };
        let err = Envelope::Res { id: 2, ok: false, data: serde_json::Value::Null,
            error: Some(ErrorBody { code: "NOT_FOUND".into(), msg: "no surface".into() }) };
        assert!(serde_json::to_string(&ok).unwrap().contains("\"ok\":true"));
        assert!(serde_json::to_string(&err).unwrap().contains("NOT_FOUND"));
    }

    #[test]
    fn evt_output_carries_workspace_scoped_surface() {
        let evt = Envelope::Evt(Evt::Output {
            surface_id: SurfaceId("s_1".into()),
            bytes: vec![104, 105],
        });
        let json = serde_json::to_string(&evt).unwrap();
        let back: Envelope = serde_json::from_str(&json).unwrap();
        assert_eq!(evt, back);
    }

    #[test]
    fn new_surface_defaults_cmd_to_none() {
        let json = r#"{"kind":"req","id":7,"cmd":{"cmd":"new_surface","args":{"workspace_id":"w_1","cols":80,"rows":24}}}"#;
        let env: Envelope = serde_json::from_str(json).unwrap();
        match env {
            Envelope::Req { cmd: Cmd::NewSurface { command, args, .. }, .. } => {
                assert!(command.is_none());
                assert!(args.is_empty());
            }
            _ => panic!("wrong variant"),
        }
    }

    #[test]
    fn split_surface_serializes() {
        let env = Envelope::Req {
            id: 1,
            cmd: Cmd::SplitSurface {
                surface_id: SurfaceId("s_1".into()),
                dir: SplitDir::Right,
                command: None,
                args: vec![],
            },
        };
        let j = serde_json::to_string(&env).unwrap();
        assert!(j.contains("split_surface"));
        assert!(j.contains(r#""dir":"right""#));
        let back: Envelope = serde_json::from_str(&j).unwrap();
        assert_eq!(back, env);
    }

    #[test]
    fn apply_preset_round_trips() {
        let env = Envelope::Req {
            id: 2,
            cmd: Cmd::ApplyPreset {
                workspace_id: WorkspaceId("w_1".into()),
                preset_id: "2x2".into(),
                slots: vec![
                    PresetSlot { command: Some("claude".into()), args: vec![] },
                    PresetSlot { command: None, args: vec![] },
                ],
            },
        };
        let back: Envelope = serde_json::from_str(&serde_json::to_string(&env).unwrap()).unwrap();
        assert_eq!(back, env);
    }

    #[test]
    fn set_ratios_round_trips() {
        let env = Envelope::Req {
            id: 3,
            cmd: Cmd::SetRatios {
                workspace_id: WorkspaceId("w_1".into()),
                node_path: vec![0, 1],
                ratios: vec![0.3, 0.7],
            },
        };
        let back: Envelope = serde_json::from_str(&serde_json::to_string(&env).unwrap()).unwrap();
        assert_eq!(back, env);
    }

    #[test]
    fn layout_changed_event_round_trips() {
        let evt = Envelope::Evt(Evt::LayoutChanged {
            workspace_id: WorkspaceId("w_1".into()),
            layout: Some(crate::layout::LayoutNode::leaf(SurfaceId("s_1".into()))),
        });
        let back: Envelope = serde_json::from_str(&serde_json::to_string(&evt).unwrap()).unwrap();
        assert_eq!(back, evt);
    }

    #[test]
    fn set_state_round_trips() {
        let env = Envelope::Req {
            id: 1,
            cmd: Cmd::SetState { surface_id: SurfaceId("s_1".into()), state: crate::status::SurfaceState::Done },
        };
        let back: Envelope = serde_json::from_str(&serde_json::to_string(&env).unwrap()).unwrap();
        assert_eq!(back, env);
    }

    #[test]
    fn state_event_round_trips() {
        let evt = Envelope::Evt(Evt::State { surface_id: SurfaceId("s_1".into()), state: crate::status::SurfaceState::Wait });
        let j = serde_json::to_string(&evt).unwrap();
        assert!(j.contains(r#""evt":"state""#));
        let back: Envelope = serde_json::from_str(&j).unwrap();
        assert_eq!(back, evt);
    }

    #[test]
    fn event_log_cmd_round_trips() {
        let env = Envelope::Req { id: 1, cmd: Cmd::EventLog { limit: Some(50) } };
        let j = serde_json::to_string(&env).unwrap();
        assert!(j.contains(r#""cmd":"event_log""#));
        let back: Envelope = serde_json::from_str(&j).unwrap();
        assert_eq!(back, env);
    }

    #[test]
    fn mark_read_cmd_round_trips() {
        let env = Envelope::Req {
            id: 2,
            cmd: Cmd::MarkRead { target: crate::event::MarkReadTarget::All },
        };
        let j = serde_json::to_string(&env).unwrap();
        assert!(j.contains(r#""cmd":"mark_read""#));
        let back: Envelope = serde_json::from_str(&j).unwrap();
        assert_eq!(back, env);
    }

    #[test]
    fn event_evt_round_trips() {
        let evt = Envelope::Evt(Evt::Event {
            record: crate::event::EventRecord {
                id: 3,
                surface_id: SurfaceId("s_1".into()),
                workspace_id: WorkspaceId("w_1".into()),
                workspace_name: "p".into(),
                agent_label: None,
                kind: crate::event::EventKind::Done,
                ts: 1,
                read: false,
            },
        });
        let j = serde_json::to_string(&evt).unwrap();
        assert!(j.contains(r#""evt":"event""#));
        let back: Envelope = serde_json::from_str(&j).unwrap();
        assert_eq!(back, evt);
    }

    #[test]
    fn events_read_evt_round_trips() {
        let evt = Envelope::Evt(Evt::EventsRead { ids: vec![1, 2, 3] });
        let j = serde_json::to_string(&evt).unwrap();
        assert!(j.contains(r#""evt":"events_read""#));
        let back: Envelope = serde_json::from_str(&j).unwrap();
        assert_eq!(back, evt);
    }

    #[test]
    fn set_zoom_cmd_round_trips() {
        let z = Envelope::Req { id: 1, cmd: Cmd::SetZoom {
            workspace_id: WorkspaceId("w_1".into()), surface_id: Some(SurfaceId("s_1".into())) } };
        let j = serde_json::to_string(&z).unwrap();
        assert!(j.contains(r#""cmd":"set_zoom""#));
        assert_eq!(serde_json::from_str::<Envelope>(&j).unwrap(), z);
        let unz = Envelope::Req { id: 2, cmd: Cmd::SetZoom {
            workspace_id: WorkspaceId("w_1".into()), surface_id: None } };
        assert_eq!(serde_json::from_str::<Envelope>(&serde_json::to_string(&unz).unwrap()).unwrap(), unz);
    }

    #[test]
    fn health_cmd_round_trips() {
        let env = Envelope::Req { id: 1, cmd: Cmd::Health };
        let j = serde_json::to_string(&env).unwrap();
        assert!(j.contains(r#""cmd":"health""#));
        let back: Envelope = serde_json::from_str(&j).unwrap();
        assert_eq!(back, env);
    }

    #[test]
    fn restart_surface_resume_defaults_false_and_round_trips() {
        // Legacy frame without `resume` decodes to false.
        let legacy = r#"{"kind":"req","id":5,"cmd":{"cmd":"restart_surface","args":{"surface_id":"s_1"}}}"#;
        let env: Envelope = serde_json::from_str(legacy).unwrap();
        match env {
            Envelope::Req { cmd: Cmd::RestartSurface { resume, .. }, .. } => assert!(!resume),
            _ => panic!("wrong variant"),
        }
        // resume=true round-trips.
        let e = Envelope::Req { id: 6, cmd: Cmd::RestartSurface { surface_id: SurfaceId("s_1".into()), resume: true } };
        let back: Envelope = serde_json::from_str(&serde_json::to_string(&e).unwrap()).unwrap();
        assert_eq!(back, e);
    }

    #[test]
    fn event_log_cmd_no_limit_round_trips() {
        let env = Envelope::Req { id: 9, cmd: Cmd::EventLog { limit: None } };
        let j = serde_json::to_string(&env).unwrap();
        assert!(j.contains(r#""cmd":"event_log""#));
        assert!(j.contains(r#""args":{}"#), "no-limit serializes to empty args, got: {j}");
        let back: Envelope = serde_json::from_str(&j).unwrap();
        assert_eq!(back, env);
    }

    #[test]
    fn mark_read_cmd_ids_and_surface_round_trip() {
        let ids = Envelope::Req { id: 10, cmd: Cmd::MarkRead { target: crate::event::MarkReadTarget::Ids(vec![1, 2]) } };
        let j = serde_json::to_string(&ids).unwrap();
        assert!(j.contains(r#""target":"ids""#));
        assert_eq!(serde_json::from_str::<Envelope>(&j).unwrap(), ids);

        let surf = Envelope::Req { id: 11, cmd: Cmd::MarkRead { target: crate::event::MarkReadTarget::Surface(SurfaceId("s_3".into())) } };
        let j = serde_json::to_string(&surf).unwrap();
        assert!(j.contains(r#""target":"surface""#));
        assert_eq!(serde_json::from_str::<Envelope>(&j).unwrap(), surf);
    }
}
