use serde::Serialize;

use crate::cli::robot::RobotMeta;
use crate::core::time::ms_to_iso;

use super::types::{
    ActivityEventType, AttentionState, MeActivityEvent, MeDashboard, MeIssue, MeMention, MeMr,
    MeSummary, SinceCheckEvent, SinceCheckGroup, SinceLastCheck,
};

// ─── Robot JSON Output (Task #18) ────────────────────────────────────────────

/// Print the full me dashboard as robot-mode JSON.
pub fn print_me_json(
    dashboard: &MeDashboard,
    elapsed_ms: u64,
    fields: Option<&[String]>,
    gitlab_base_url: &str,
) -> crate::core::error::Result<()> {
    let envelope = MeJsonEnvelope {
        ok: true,
        data: MeDataJson::from_dashboard(dashboard),
        meta: RobotMeta::with_base_url(elapsed_ms, gitlab_base_url),
    };

    let mut value = serde_json::to_value(&envelope)
        .map_err(|e| crate::core::error::LoreError::Other(format!("JSON serialization: {e}")))?;

    // Apply --fields filtering (Task #19)
    if let Some(f) = fields {
        let expanded = crate::cli::robot::expand_fields_preset(f, "me_items");
        // Filter issue/MR arrays with the items preset
        for key in &["open_issues", "open_mrs_authored", "reviewing_mrs"] {
            crate::cli::robot::filter_fields(&mut value, key, &expanded);
        }

        // Mentioned-in gets its own preset (needs entity_type + state to disambiguate)
        let mentions_expanded = crate::cli::robot::expand_fields_preset(f, "me_mentions");
        crate::cli::robot::filter_fields(&mut value, "mentioned_in", &mentions_expanded);

        // Activity gets its own minimal preset
        let activity_expanded = crate::cli::robot::expand_fields_preset(f, "me_activity");
        crate::cli::robot::filter_fields(&mut value, "activity", &activity_expanded);
    }

    let json = serde_json::to_string(&value)
        .map_err(|e| crate::core::error::LoreError::Other(format!("JSON serialization: {e}")))?;
    println!("{json}");
    Ok(())
}

/// Print `--reset-cursor` response using standard robot envelope.
pub fn print_cursor_reset_json(elapsed_ms: u64) -> crate::core::error::Result<()> {
    let value = cursor_reset_envelope_json(elapsed_ms);
    let json = serde_json::to_string(&value)
        .map_err(|e| crate::core::error::LoreError::Other(format!("JSON serialization: {e}")))?;
    println!("{json}");
    Ok(())
}

fn cursor_reset_envelope_json(elapsed_ms: u64) -> serde_json::Value {
    serde_json::json!({
        "ok": true,
        "data": {
            "cursor_reset": true
        },
        "meta": {
            "elapsed_ms": elapsed_ms
        }
    })
}

// ─── JSON Envelope ───────────────────────────────────────────────────────────

#[derive(Serialize)]
struct MeJsonEnvelope {
    ok: bool,
    data: MeDataJson,
    meta: RobotMeta,
}

#[derive(Serialize)]
struct MeDataJson {
    username: String,
    since_iso: Option<String>,
    summary: SummaryJson,
    #[serde(skip_serializing_if = "Option::is_none")]
    since_last_check: Option<SinceLastCheckJson>,
    open_issues: Vec<IssueJson>,
    open_mrs_authored: Vec<MrJson>,
    reviewing_mrs: Vec<MrJson>,
    mentioned_in: Vec<MentionJson>,
    activity: Vec<ActivityJson>,
}

impl MeDataJson {
    fn from_dashboard(d: &MeDashboard) -> Self {
        Self {
            username: d.username.clone(),
            since_iso: d.since_ms.map(ms_to_iso),
            summary: SummaryJson::from(&d.summary),
            since_last_check: d.since_last_check.as_ref().map(SinceLastCheckJson::from),
            open_issues: d.open_issues.iter().map(IssueJson::from).collect(),
            open_mrs_authored: d.open_mrs_authored.iter().map(MrJson::from).collect(),
            reviewing_mrs: d.reviewing_mrs.iter().map(MrJson::from).collect(),
            mentioned_in: d.mentioned_in.iter().map(MentionJson::from).collect(),
            activity: d.activity.iter().map(ActivityJson::from).collect(),
        }
    }
}

// ─── Summary ─────────────────────────────────────────────────────────────────

#[derive(Serialize)]
struct SummaryJson {
    project_count: usize,
    open_issue_count: usize,
    authored_mr_count: usize,
    reviewing_mr_count: usize,
    mentioned_in_count: usize,
    needs_attention_count: usize,
}

impl From<&MeSummary> for SummaryJson {
    fn from(s: &MeSummary) -> Self {
        Self {
            project_count: s.project_count,
            open_issue_count: s.open_issue_count,
            authored_mr_count: s.authored_mr_count,
            reviewing_mr_count: s.reviewing_mr_count,
            mentioned_in_count: s.mentioned_in_count,
            needs_attention_count: s.needs_attention_count,
        }
    }
}

// ─── Issue ───────────────────────────────────────────────────────────────────

#[derive(Serialize)]
struct IssueJson {
    project: String,
    iid: i64,
    title: String,
    state: String,
    attention_state: String,
    attention_reason: String,
    status_name: Option<String>,
    labels: Vec<String>,
    updated_at_iso: String,
    web_url: Option<String>,
}

impl From<&MeIssue> for IssueJson {
    fn from(i: &MeIssue) -> Self {
        Self {
            project: i.project_path.clone(),
            iid: i.iid,
            title: i.title.clone(),
            state: "opened".to_string(),
            attention_state: attention_state_str(&i.attention_state),
            attention_reason: i.attention_reason.clone(),
            status_name: i.status_name.clone(),
            labels: i.labels.clone(),
            updated_at_iso: ms_to_iso(i.updated_at),
            web_url: i.web_url.clone(),
        }
    }
}

// ─── MR ──────────────────────────────────────────────────────────────────────

#[derive(Serialize)]
struct MrJson {
    project: String,
    iid: i64,
    title: String,
    state: String,
    attention_state: String,
    attention_reason: String,
    draft: bool,
    detailed_merge_status: Option<String>,
    author_username: Option<String>,
    labels: Vec<String>,
    updated_at_iso: String,
    web_url: Option<String>,
}

impl From<&MeMr> for MrJson {
    fn from(m: &MeMr) -> Self {
        Self {
            project: m.project_path.clone(),
            iid: m.iid,
            title: m.title.clone(),
            state: "opened".to_string(),
            attention_state: attention_state_str(&m.attention_state),
            attention_reason: m.attention_reason.clone(),
            draft: m.draft,
            detailed_merge_status: m.detailed_merge_status.clone(),
            author_username: m.author_username.clone(),
            labels: m.labels.clone(),
            updated_at_iso: ms_to_iso(m.updated_at),
            web_url: m.web_url.clone(),
        }
    }
}

// ─── Mention ─────────────────────────────────────────────────────────────

#[derive(Serialize)]
struct MentionJson {
    entity_type: String,
    project: String,
    iid: i64,
    title: String,
    state: String,
    attention_state: String,
    attention_reason: String,
    updated_at_iso: String,
    web_url: Option<String>,
}

impl From<&MeMention> for MentionJson {
    fn from(m: &MeMention) -> Self {
        Self {
            entity_type: m.entity_type.clone(),
            project: m.project_path.clone(),
            iid: m.iid,
            title: m.title.clone(),
            state: m.state.clone(),
            attention_state: attention_state_str(&m.attention_state),
            attention_reason: m.attention_reason.clone(),
            updated_at_iso: ms_to_iso(m.updated_at),
            web_url: m.web_url.clone(),
        }
    }
}

// ─── Activity ────────────────────────────────────────────────────────────────

#[derive(Serialize)]
struct ActivityJson {
    timestamp_iso: String,
    event_type: String,
    entity_type: String,
    entity_iid: i64,
    project: String,
    actor: Option<String>,
    is_own: bool,
    summary: String,
    body_preview: Option<String>,
}

impl From<&MeActivityEvent> for ActivityJson {
    fn from(e: &MeActivityEvent) -> Self {
        Self {
            timestamp_iso: ms_to_iso(e.timestamp),
            event_type: event_type_str(&e.event_type),
            entity_type: e.entity_type.clone(),
            entity_iid: e.entity_iid,
            project: e.project_path.clone(),
            actor: e.actor.clone(),
            is_own: e.is_own,
            summary: e.summary.clone(),
            body_preview: e.body_preview.clone(),
        }
    }
}

// ─── Since Last Check ────────────────────────────────────────────────────────

#[derive(Serialize)]
struct SinceLastCheckJson {
    cursor_iso: String,
    total_event_count: usize,
    groups: Vec<SinceCheckGroupJson>,
}

impl From<&SinceLastCheck> for SinceLastCheckJson {
    fn from(s: &SinceLastCheck) -> Self {
        Self {
            cursor_iso: ms_to_iso(s.cursor_ms),
            total_event_count: s.total_event_count,
            groups: s.groups.iter().map(SinceCheckGroupJson::from).collect(),
        }
    }
}

#[derive(Serialize)]
struct SinceCheckGroupJson {
    entity_type: String,
    entity_iid: i64,
    entity_title: String,
    project: String,
    events: Vec<SinceCheckEventJson>,
}

impl From<&SinceCheckGroup> for SinceCheckGroupJson {
    fn from(g: &SinceCheckGroup) -> Self {
        Self {
            entity_type: g.entity_type.clone(),
            entity_iid: g.entity_iid,
            entity_title: g.entity_title.clone(),
            project: g.project_path.clone(),
            events: g.events.iter().map(SinceCheckEventJson::from).collect(),
        }
    }
}

#[derive(Serialize)]
struct SinceCheckEventJson {
    timestamp_iso: String,
    event_type: String,
    actor: Option<String>,
    summary: String,
    body_preview: Option<String>,
}

impl From<&SinceCheckEvent> for SinceCheckEventJson {
    fn from(e: &SinceCheckEvent) -> Self {
        Self {
            timestamp_iso: ms_to_iso(e.timestamp),
            event_type: event_type_str(&e.event_type),
            actor: e.actor.clone(),
            summary: e.summary.clone(),
            body_preview: e.body_preview.clone(),
        }
    }
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

/// Convert `AttentionState` to its programmatic string representation.
fn attention_state_str(state: &AttentionState) -> String {
    match state {
        AttentionState::NeedsAttention => "needs_attention",
        AttentionState::NotStarted => "not_started",
        AttentionState::AwaitingResponse => "awaiting_response",
        AttentionState::Stale => "stale",
        AttentionState::NotReady => "not_ready",
    }
    .to_string()
}

/// Convert `ActivityEventType` to its programmatic string representation.
fn event_type_str(event_type: &ActivityEventType) -> String {
    match event_type {
        ActivityEventType::Note => "note",
        ActivityEventType::StatusChange => "status_change",
        ActivityEventType::LabelChange => "label_change",
        ActivityEventType::Assign => "assign",
        ActivityEventType::Unassign => "unassign",
        ActivityEventType::ReviewRequest => "review_request",
        ActivityEventType::MilestoneChange => "milestone_change",
    }
    .to_string()
}

// ─── Tests ───────────────────────────────────────────────────────────────────

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

    #[test]
    fn attention_state_str_all_variants() {
        assert_eq!(
            attention_state_str(&AttentionState::NeedsAttention),
            "needs_attention"
        );
        assert_eq!(
            attention_state_str(&AttentionState::NotStarted),
            "not_started"
        );
        assert_eq!(
            attention_state_str(&AttentionState::AwaitingResponse),
            "awaiting_response"
        );
        assert_eq!(attention_state_str(&AttentionState::Stale), "stale");
        assert_eq!(attention_state_str(&AttentionState::NotReady), "not_ready");
    }

    #[test]
    fn event_type_str_all_variants() {
        assert_eq!(event_type_str(&ActivityEventType::Note), "note");
        assert_eq!(
            event_type_str(&ActivityEventType::StatusChange),
            "status_change"
        );
        assert_eq!(
            event_type_str(&ActivityEventType::LabelChange),
            "label_change"
        );
        assert_eq!(event_type_str(&ActivityEventType::Assign), "assign");
        assert_eq!(event_type_str(&ActivityEventType::Unassign), "unassign");
        assert_eq!(
            event_type_str(&ActivityEventType::ReviewRequest),
            "review_request"
        );
        assert_eq!(
            event_type_str(&ActivityEventType::MilestoneChange),
            "milestone_change"
        );
    }

    #[test]
    fn issue_json_from_me_issue() {
        let issue = MeIssue {
            iid: 42,
            title: "Fix auth bug".to_string(),
            project_path: "group/repo".to_string(),
            attention_state: AttentionState::NeedsAttention,
            attention_reason: "Others commented recently; you haven't replied".to_string(),
            status_name: Some("In progress".to_string()),
            labels: vec!["bug".to_string()],
            updated_at: 1_700_000_000_000,
            web_url: Some("https://gitlab.com/group/repo/-/issues/42".to_string()),
        };
        let json = IssueJson::from(&issue);
        assert_eq!(json.iid, 42);
        assert_eq!(json.attention_state, "needs_attention");
        assert_eq!(
            json.attention_reason,
            "Others commented recently; you haven't replied"
        );
        assert_eq!(json.state, "opened");
        assert_eq!(json.status_name, Some("In progress".to_string()));
    }

    #[test]
    fn mr_json_from_me_mr() {
        let mr = MeMr {
            iid: 99,
            title: "Add feature".to_string(),
            project_path: "group/repo".to_string(),
            attention_state: AttentionState::AwaitingResponse,
            attention_reason: "You replied moments ago; awaiting others".to_string(),
            draft: true,
            detailed_merge_status: Some("mergeable".to_string()),
            author_username: Some("alice".to_string()),
            labels: vec![],
            updated_at: 1_700_000_000_000,
            web_url: None,
        };
        let json = MrJson::from(&mr);
        assert_eq!(json.iid, 99);
        assert_eq!(json.attention_state, "awaiting_response");
        assert_eq!(
            json.attention_reason,
            "You replied moments ago; awaiting others"
        );
        assert!(json.draft);
        assert_eq!(json.author_username, Some("alice".to_string()));
    }

    #[test]
    fn activity_json_from_event() {
        let event = MeActivityEvent {
            timestamp: 1_700_000_000_000,
            event_type: ActivityEventType::Note,
            entity_type: "issue".to_string(),
            entity_iid: 42,
            project_path: "group/repo".to_string(),
            actor: Some("bob".to_string()),
            is_own: false,
            summary: "Added a comment".to_string(),
            body_preview: Some("This looks good".to_string()),
        };
        let json = ActivityJson::from(&event);
        assert_eq!(json.event_type, "note");
        assert_eq!(json.entity_iid, 42);
        assert!(!json.is_own);
        assert_eq!(json.body_preview, Some("This looks good".to_string()));
    }

    #[test]
    fn cursor_reset_envelope_includes_meta_elapsed_ms() {
        let value = cursor_reset_envelope_json(17);
        assert_eq!(value["ok"], serde_json::json!(true));
        assert_eq!(value["data"]["cursor_reset"], serde_json::json!(true));
        assert_eq!(value["meta"]["elapsed_ms"], serde_json::json!(17));
    }

    /// Integration test: full envelope serialization includes gitlab_base_url in meta.
    /// Guards against drift where the wiring from run_me -> print_me_json -> JSON
    /// could silently lose the base URL field.
    #[test]
    fn me_envelope_includes_gitlab_base_url_in_meta() {
        let dashboard = MeDashboard {
            username: "testuser".to_string(),
            since_ms: Some(1_700_000_000_000),
            summary: MeSummary {
                project_count: 1,
                open_issue_count: 0,
                authored_mr_count: 0,
                reviewing_mr_count: 0,
                mentioned_in_count: 0,
                needs_attention_count: 0,
            },
            open_issues: vec![],
            open_mrs_authored: vec![],
            reviewing_mrs: vec![],
            mentioned_in: vec![],
            activity: vec![],
            since_last_check: None,
        };

        let envelope = MeJsonEnvelope {
            ok: true,
            data: MeDataJson::from_dashboard(&dashboard),
            meta: RobotMeta::with_base_url(42, "https://gitlab.example.com"),
        };

        let value = serde_json::to_value(&envelope).unwrap();
        assert_eq!(value["ok"], serde_json::json!(true));
        assert_eq!(value["meta"]["elapsed_ms"], serde_json::json!(42));
        assert_eq!(
            value["meta"]["gitlab_base_url"],
            serde_json::json!("https://gitlab.example.com")
        );
    }

    /// Verify activity events carry the fields needed for URL construction
    /// (entity_type, entity_iid, project) so consumers can combine with
    /// meta.gitlab_base_url to build links.
    #[test]
    fn activity_event_carries_url_construction_fields() {
        let dashboard = MeDashboard {
            username: "testuser".to_string(),
            since_ms: Some(1_700_000_000_000),
            summary: MeSummary {
                project_count: 1,
                open_issue_count: 0,
                authored_mr_count: 0,
                reviewing_mr_count: 0,
                mentioned_in_count: 0,
                needs_attention_count: 0,
            },
            open_issues: vec![],
            open_mrs_authored: vec![],
            reviewing_mrs: vec![],
            mentioned_in: vec![],
            activity: vec![MeActivityEvent {
                timestamp: 1_700_000_000_000,
                event_type: ActivityEventType::Note,
                entity_type: "mr".to_string(),
                entity_iid: 99,
                project_path: "group/repo".to_string(),
                actor: Some("alice".to_string()),
                is_own: false,
                summary: "Commented on MR".to_string(),
                body_preview: None,
            }],
            since_last_check: None,
        };

        let envelope = MeJsonEnvelope {
            ok: true,
            data: MeDataJson::from_dashboard(&dashboard),
            meta: RobotMeta::with_base_url(0, "https://gitlab.example.com"),
        };

        let value = serde_json::to_value(&envelope).unwrap();
        let event = &value["data"]["activity"][0];

        // These three fields + meta.gitlab_base_url = complete URL
        assert_eq!(event["entity_type"], "mr");
        assert_eq!(event["entity_iid"], 99);
        assert_eq!(event["project"], "group/repo");

        // Consumer constructs: https://gitlab.example.com/group/repo/-/merge_requests/99
        let base = value["meta"]["gitlab_base_url"].as_str().unwrap();
        let project = event["project"].as_str().unwrap();
        let entity_path = match event["entity_type"].as_str().unwrap() {
            "issue" => "issues",
            "mr" => "merge_requests",
            other => panic!("unexpected entity_type: {other}"),
        };
        let iid = event["entity_iid"].as_i64().unwrap();
        let url = format!("{base}/{project}/-/{entity_path}/{iid}");
        assert_eq!(
            url,
            "https://gitlab.example.com/group/repo/-/merge_requests/99"
        );
    }
}
