use std::path::Path;

use crate::cc::attachments_dto::OutboundAttachment;

#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct UsedSkillReceipt {
    pub package_name: String,
    pub message: String,
}

#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct LearningSignal {
    pub kind: String,
    pub package_name_hint: String,
    pub trigger: String,
    pub reason_not_written: String,
    pub event_refs: Vec<String>,
    pub summary: String,
}

#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct SkillIssueSignal {
    pub kind: String,
    pub skill_name: String,
    pub issue: String,
    pub reason_not_patched: String,
    pub observed_effect: String,
    pub event_refs: Vec<String>,
    pub patch_hint: String,
}

/// Parsed output from CC structured JSON response (`result` field per D-03).
#[derive(Debug, serde::Deserialize)]
pub struct ReplyOutput {
    pub content: Option<String>,
    pub reply_to_message_id: Option<i32>,
    pub attachments: Option<Vec<OutboundAttachment>>,
    pub used_skill_receipts: Option<Vec<UsedSkillReceipt>>,
    pub learning_signal: Option<LearningSignal>,
    pub skill_issue_signal: Option<SkillIssueSignal>,
    /// Bootstrap mode: `true` signals agent claims onboarding is complete.
    /// Server-side file check (`should_accept_bootstrap`) gates actual completion.
    pub bootstrap_complete: Option<bool>,
}

/// Host-mode bootstrap completion check.
///
/// Sandboxed bootstrap is verified in `telegram::worker` by reconciling the
/// authoritative `/sandbox` identity files into the required host mirror.
pub(crate) fn should_accept_bootstrap(agent_dir: &Path) -> bool {
    right_agent::identity_mirror::host_identity_mirror_complete(agent_dir)
}

/// Parse CC structured JSON output (D-03, D-04).
///
/// Returns `Ok((ReplyOutput, Option<session_id>))` on success.
/// Returns `Err(String)` if JSON is malformed or the `result` field is missing.
/// Returns `Ok((ReplyOutput { content: None, .. }, _))` if content=null (silent response per D-04).
pub fn parse_reply_output(raw_json: &str) -> Result<(ReplyOutput, Option<String>), String> {
    tracing::debug!("CC raw JSON output: {}", raw_json);

    let parsed: serde_json::Value =
        serde_json::from_str(raw_json).map_err(|e| format!("JSON parse error: {e}"))?;

    let session_id = parsed
        .get("session_id")
        .and_then(|v| v.as_str())
        .map(str::to_string);

    let result_val = parsed
        .get("structured_output")
        .filter(|v| !v.is_null())
        .or_else(|| parsed.get("result"))
        .ok_or_else(|| {
            "CC response missing both 'structured_output' and 'result' fields".to_string()
        })?;

    // CC sometimes returns result as a plain string (e.g. after multi-turn MCP tool use)
    // instead of complying with --json-schema. Wrap it as ReplyOutput so the message is delivered.
    let output: ReplyOutput = if let Some(text) = result_val.as_str() {
        ReplyOutput {
            content: if text.is_empty() {
                None
            } else {
                Some(text.to_string())
            },
            reply_to_message_id: None,
            attachments: None,
            used_skill_receipts: None,
            learning_signal: None,
            skill_issue_signal: None,
            bootstrap_complete: None,
        }
    } else {
        serde_json::from_value(result_val.clone())
            .map_err(|e| format!("failed to deserialize result: {e}"))?
    };

    Ok((output, session_id))
}

pub(crate) fn append_used_skill_receipts(
    content: Option<String>,
    receipts: Option<&[UsedSkillReceipt]>,
) -> Option<String> {
    let Some(receipts) = receipts else {
        return content;
    };
    if receipts.is_empty() {
        return content;
    }

    let messages = receipts
        .iter()
        .map(|receipt| receipt.message.trim())
        .filter(|message| !message.is_empty())
        .collect::<Vec<_>>()
        .join("\n");
    if messages.is_empty() {
        return content.filter(|content| !content.is_empty());
    }

    match content {
        Some(content) if !content.is_empty() => Some(format!("{content}\n\n{messages}")),
        _ => Some(messages),
    }
}

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

    // parse_reply_output tests (new structured output format per D-03)
    #[test]
    fn parse_reply_output_content_string() {
        let json = r#"{"session_id":"abc","result":{"content":"hello","reply_to_message_id":null,"attachments":null}}"#;
        let (output, session_id) = parse_reply_output(json).unwrap();
        assert_eq!(output.content.as_deref(), Some("hello"));
        assert_eq!(session_id.as_deref(), Some("abc"));
    }

    #[test]
    fn parse_reply_output_content_null() {
        let json = r#"{"result":{"content":null}}"#;
        let (output, _) = parse_reply_output(json).unwrap();
        assert!(output.content.is_none());
    }

    #[test]
    fn parse_reply_output_missing_result_returns_error() {
        let json = r#"{"session_id":"x"}"#;
        let result = parse_reply_output(json);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("missing both"));
    }

    #[test]
    fn parse_reply_output_reply_to_message_id() {
        let json = r#"{"result":{"content":"hi","reply_to_message_id":42,"attachments":null}}"#;
        let (output, _) = parse_reply_output(json).unwrap();
        assert_eq!(output.reply_to_message_id, Some(42));
    }

    #[test]
    fn parse_reply_output_plain_string_result_wrapped_as_content() {
        // CC sometimes returns "result": "plain text" after MCP tool use instead of complying
        // with --json-schema. Must deliver the message rather than show an error.
        let json = r#"{"session_id":"abc","result":"hello from plain result"}"#;
        let (output, session_id) = parse_reply_output(json).unwrap();
        assert_eq!(output.content.as_deref(), Some("hello from plain result"));
        assert_eq!(session_id.as_deref(), Some("abc"));
    }

    #[test]
    fn parse_reply_output_empty_string_result_is_silent() {
        let json = r#"{"result":""}"#;
        let (output, _) = parse_reply_output(json).unwrap();
        assert!(output.content.is_none());
    }

    #[test]
    fn parse_reply_output_array_result_returns_error() {
        // Array instead of object should fail deserialization
        let json = r#"{"result":[{"type":"text"}]}"#;
        let result = parse_reply_output(json);
        assert!(result.is_err());
    }

    #[test]
    fn parse_reply_output_structured_output_field() {
        // When structured_output is present, it should be used instead of result
        let json = r#"{"session_id":"abc","result":"","structured_output":{"content":"Hello from structured!","reply_to_message_id":null,"attachments":null}}"#;
        let (output, session_id) = parse_reply_output(json).unwrap();
        assert_eq!(output.content.as_deref(), Some("Hello from structured!"));
        assert_eq!(session_id.as_deref(), Some("abc"));
    }

    #[test]
    fn parse_reply_output_falls_back_to_result_when_no_structured_output() {
        // When structured_output is absent, fall back to result field
        let json = r#"{"session_id":"xyz","result":{"content":"Fallback result","reply_to_message_id":null,"attachments":null}}"#;
        let (output, session_id) = parse_reply_output(json).unwrap();
        assert_eq!(output.content.as_deref(), Some("Fallback result"));
        assert_eq!(session_id.as_deref(), Some("xyz"));
    }

    #[test]
    fn parse_reply_output_missing_result_and_structured_output_returns_error() {
        let json = r#"{"session_id":"x"}"#;
        let result = parse_reply_output(json);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(
            err.contains("missing both"),
            "error should mention both fields: {err}"
        );
    }

    #[test]
    fn parse_reply_output_with_attachments() {
        let json = r#"{"session_id":"abc","result":{"content":"Here you go","attachments":[{"type":"document","path":"/sandbox/outbox/data.csv","filename":"results.csv","caption":"Exported data"}]}}"#;
        let (output, session_id) = parse_reply_output(json).unwrap();
        assert_eq!(output.content.as_deref(), Some("Here you go"));
        assert_eq!(session_id.as_deref(), Some("abc"));
        let atts = output.attachments.unwrap();
        assert_eq!(atts.len(), 1);
        assert_eq!(atts[0].path, "/sandbox/outbox/data.csv");
        assert_eq!(atts[0].filename.as_deref(), Some("results.csv"));
    }

    #[test]
    fn parse_reply_output_text_only() {
        let json =
            r#"{"result":{"content":"hello","reply_to_message_id":null,"attachments":null}}"#;
        let (output, _) = parse_reply_output(json).unwrap();
        assert_eq!(output.content.as_deref(), Some("hello"));
        assert!(output.attachments.is_none());
    }

    #[test]
    fn parse_reply_output_plain_string_fallback() {
        let json = r#"{"result":"plain text fallback"}"#;
        let (output, _) = parse_reply_output(json).unwrap();
        assert_eq!(output.content.as_deref(), Some("plain text fallback"));
        assert!(output.attachments.is_none());
    }

    #[test]
    fn parse_reply_output_accepts_used_skill_receipts() {
        let json = r#"{"result":{"content":"done","used_skill_receipts":[{"package_name":"right-mcp","message":"Used skill: right-mcp"}]}}"#;
        let (output, _) = parse_reply_output(json).unwrap();
        let receipts = output.used_skill_receipts.unwrap();
        assert_eq!(receipts.len(), 1);
        assert_eq!(receipts[0].package_name, "right-mcp");
        assert_eq!(receipts[0].message, "Used skill: right-mcp");
    }

    #[test]
    fn parse_reply_output_accepts_learning_signal() {
        let json = r#"{"result":{"content":"done","learning_signal":{"kind":"create_candidate","package_name_hint":"right-demo","trigger":"explicit_user_request","reason_not_written":"needs_full_context_review","event_refs":["event-1"],"summary":"Capture this workflow."}}}"#;
        let (output, _) = parse_reply_output(json).unwrap();
        let signal = output.learning_signal.unwrap();
        assert_eq!(signal.kind, "create_candidate");
        assert_eq!(signal.package_name_hint, "right-demo");
        assert_eq!(signal.trigger, "explicit_user_request");
        assert_eq!(signal.reason_not_written, "needs_full_context_review");
        assert_eq!(signal.event_refs, vec!["event-1"]);
        assert_eq!(signal.summary, "Capture this workflow.");
    }

    #[test]
    fn parse_reply_output_keeps_skill_fields_optional() {
        let json = r#"{"result":{"content":"hello"}}"#;
        let (output, _) = parse_reply_output(json).unwrap();
        assert!(output.used_skill_receipts.is_none());
        assert!(output.learning_signal.is_none());
        assert!(output.skill_issue_signal.is_none());
    }

    #[test]
    fn append_used_skill_receipts_adds_messages_after_content() {
        let receipts = vec![
            UsedSkillReceipt {
                package_name: "right-mcp".to_owned(),
                message: "Used skill: right-mcp".to_owned(),
            },
            UsedSkillReceipt {
                package_name: "right-memory".to_owned(),
                message: "Used skill: right-memory".to_owned(),
            },
        ];

        let content =
            append_used_skill_receipts(Some("Done".to_owned()), Some(receipts.as_slice()));

        assert_eq!(
            content.as_deref(),
            Some("Done\n\nUsed skill: right-mcp\nUsed skill: right-memory")
        );
    }

    #[test]
    fn append_used_skill_receipts_none_content_blank_receipts_returns_none() {
        let receipts = vec![
            UsedSkillReceipt {
                package_name: "right-mcp".to_owned(),
                message: "   ".to_owned(),
            },
            UsedSkillReceipt {
                package_name: "right-memory".to_owned(),
                message: "\n\t".to_owned(),
            },
        ];

        let content = append_used_skill_receipts(None, Some(receipts.as_slice()));

        assert_eq!(content, None);
    }

    #[test]
    fn append_used_skill_receipts_appends_only_nonblank_trimmed_messages() {
        let receipts = vec![
            UsedSkillReceipt {
                package_name: "right-mcp".to_owned(),
                message: "  Used skill: right-mcp  ".to_owned(),
            },
            UsedSkillReceipt {
                package_name: "right-memory".to_owned(),
                message: "\n".to_owned(),
            },
            UsedSkillReceipt {
                package_name: "right-codegen".to_owned(),
                message: "\tUsed skill: right-codegen\n".to_owned(),
            },
        ];

        let content =
            append_used_skill_receipts(Some("Done".to_owned()), Some(receipts.as_slice()));

        assert_eq!(
            content.as_deref(),
            Some("Done\n\nUsed skill: right-mcp\nUsed skill: right-codegen")
        );
    }

    #[test]
    fn append_used_skill_receipts_empty_receipts_leaves_content_unchanged() {
        let content = append_used_skill_receipts(Some("Done".to_owned()), Some(&[]));

        assert_eq!(content.as_deref(), Some("Done"));
    }

    // bootstrap mode tests
    #[test]
    fn parse_reply_output_bootstrap_complete_true() {
        let json = r#"{"type":"result","result":{"content":"Done!","bootstrap_complete":true},"session_id":"abc-123"}"#;
        let (output, _sid) = parse_reply_output(json).unwrap();
        assert_eq!(output.content.as_deref(), Some("Done!"));
        assert_eq!(output.bootstrap_complete, Some(true));
    }

    #[test]
    fn parse_reply_output_bootstrap_complete_false() {
        let json = r#"{"type":"result","result":{"content":"What's your name?","bootstrap_complete":false},"session_id":"abc-123"}"#;
        let (output, _sid) = parse_reply_output(json).unwrap();
        assert_eq!(output.bootstrap_complete, Some(false));
    }

    #[test]
    fn parse_reply_output_no_bootstrap_field() {
        let json = r#"{"type":"result","result":{"content":"Hello!"},"session_id":"abc-123"}"#;
        let (output, _sid) = parse_reply_output(json).unwrap();
        assert_eq!(output.bootstrap_complete, None);
    }

    #[test]
    fn should_accept_bootstrap_all_files_present() {
        let dir = tempfile::tempdir().unwrap();
        for f in right_agent::identity_mirror::IDENTITY_MIRROR_FILES {
            std::fs::write(dir.path().join(f), "# test").unwrap();
        }
        assert!(should_accept_bootstrap(dir.path()));
    }

    #[test]
    fn should_accept_bootstrap_missing_files() {
        let dir = tempfile::tempdir().unwrap();
        // No identity files created
        assert!(!should_accept_bootstrap(dir.path()));
    }

    #[test]
    fn should_accept_bootstrap_partial_files() {
        let dir = tempfile::tempdir().unwrap();
        // Only IDENTITY.md exists
        std::fs::write(dir.path().join("IDENTITY.md"), "# test").unwrap();
        assert!(!should_accept_bootstrap(dir.path()));
    }
}
