use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::{
    runtime::RuntimeHandle,
    tool::{helpers::invalid_tool_input, spec::typed_spec, ToolError},
    types::{ToolCapabilityFamily, TrustLevel},
};

use super::{serialize_success, BuiltinToolDefinition};
use crate::tool::helpers::{parse_tool_args, validate_non_empty};

pub(crate) const NAME: &str = "MemoryGet";
const MAX_CHARS_MAX: usize = 50_000;
const ALLOWED_SOURCE_REF_PREFIXES: &[&str] = &[
    "agent_memory:",
    "workspace_profile:",
    "brief:",
    "episode:",
    "work_item:",
    "tool_execution:",
    "task:",
];

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub(crate) struct MemoryGetArgs {
    pub(crate) source_ref: String,
    #[schemars(range(min = 1, max = 50000))]
    pub(crate) max_chars: Option<usize>,
}

#[derive(Serialize)]
struct MemoryGetResponse {
    memory: crate::memory::MemoryGetResult,
}

pub(crate) fn definition() -> Result<BuiltinToolDefinition> {
    Ok(BuiltinToolDefinition {
        family: ToolCapabilityFamily::CoreAgent,
        spec: typed_spec::<MemoryGetArgs>(
            NAME,
            "Fetch exact bounded Holon memory content by a source_ref copied verbatim from MemorySearch.results[].source_ref. Do not invent source_ref values or use MemoryGet for skill paths, file paths, or URLs.",
        )?,
    })
}

pub(crate) async fn execute(
    runtime: &RuntimeHandle,
    _agent_id: &str,
    _trust: &TrustLevel,
    input: &Value,
) -> Result<crate::tool::ToolResult> {
    let args: MemoryGetArgs = parse_tool_args(NAME, input)?;
    let source_ref = validate_source_ref(args.source_ref)?;
    let max_chars = validate_max_chars(args.max_chars)?;
    let Some(memory) = runtime.get_memory(&source_ref, max_chars).await? else {
        return Err(ToolError::new(
            "memory_source_not_found",
            format!("memory source `{source_ref}` was not found"),
        )
        .with_details(json!({
            "source_ref": source_ref,
            "allowed_source_ref_prefixes": ALLOWED_SOURCE_REF_PREFIXES,
            "reason": "source_ref was syntactically valid but is not present in the current visible memory index",
        }))
        .with_recovery_hint(
            "call MemorySearch again and pass one of its returned source_ref values verbatim",
        )
        .into());
    };
    serialize_success(NAME, &MemoryGetResponse { memory })
}

fn validate_source_ref(source_ref: String) -> Result<String> {
    let source_ref = validate_non_empty(source_ref, NAME, "source_ref")?;
    if source_ref.chars().any(char::is_whitespace) {
        return Err(invalid_tool_input(
            NAME,
            "MemoryGet `source_ref` must be a single opaque handle without whitespace",
            json!({
                "field": "source_ref",
                "source_ref": source_ref,
                "validation_error": "must not contain whitespace",
            }),
            "copy a source_ref exactly from MemorySearch.results[].source_ref; do not paste snippets or paths",
        ));
    }

    let Some((prefix, suffix)) = source_ref.split_once(':') else {
        return Err(invalid_source_ref_error(
            &source_ref,
            "missing source_ref prefix",
        ));
    };
    let prefix = format!("{prefix}:");
    if prefix == "tool_execution:" {
        return validate_tool_execution_source_ref(&source_ref, suffix);
    }

    if suffix.is_empty() {
        return Err(invalid_source_ref_error(
            &source_ref,
            "missing source_ref identifier",
        ));
    }
    if !suffix
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
    {
        return Err(invalid_source_ref_error(
            &source_ref,
            "source_ref identifier must be an opaque id, not a path, URL, or query",
        ));
    }

    if !ALLOWED_SOURCE_REF_PREFIXES.contains(&prefix.as_str()) {
        return Err(invalid_source_ref_error(
            &source_ref,
            "unsupported source_ref prefix",
        ));
    }

    Ok(source_ref)
}

fn validate_tool_execution_source_ref(source_ref: &str, suffix: &str) -> Result<String> {
    let parts = suffix.split(':').collect::<Vec<_>>();
    let valid = match parts.as_slice() {
        [tool_execution_id, "cmd"] => valid_source_ref_segment(tool_execution_id),
        [tool_execution_id, "batch_item", index, "cmd"] => {
            valid_source_ref_segment(tool_execution_id)
                && !index.is_empty()
                && index.chars().all(|ch| ch.is_ascii_digit())
        }
        _ => false,
    };
    if !valid {
        return Err(invalid_source_ref_error(
            source_ref,
            "tool_execution source_ref must match tool_execution:<id>:cmd or tool_execution:<id>:batch_item:<index>:cmd",
        ));
    }
    Ok(source_ref.to_string())
}

fn valid_source_ref_segment(segment: &str) -> bool {
    !segment.is_empty()
        && segment
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
}

fn validate_max_chars(max_chars: Option<usize>) -> Result<Option<usize>> {
    let Some(max_chars) = max_chars else {
        return Ok(None);
    };
    if !(1..=MAX_CHARS_MAX).contains(&max_chars) {
        return Err(invalid_tool_input(
            NAME,
            "MemoryGet `max_chars` must be between 1 and 50000 when provided",
            json!({
                "field": "max_chars",
                "max_chars": max_chars,
                "validation_error": "out of range",
                "minimum": 1,
                "maximum": MAX_CHARS_MAX,
            }),
            "omit `max_chars` for the default bound, or provide an integer from 1 through 50000",
        ));
    }
    Ok(Some(max_chars))
}

fn invalid_source_ref_error(source_ref: &str, validation_error: &'static str) -> anyhow::Error {
    invalid_tool_input(
        NAME,
        "MemoryGet `source_ref` must be copied from MemorySearch results",
        json!({
            "field": "source_ref",
            "source_ref": source_ref,
            "validation_error": validation_error,
            "allowed_source_ref_prefixes": ALLOWED_SOURCE_REF_PREFIXES,
        }),
        "call MemorySearch first and copy one returned source_ref verbatim; use ExecCommand for workspace files or skill docs instead of MemoryGet",
    )
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use chrono::Utc;
    use serde_json::json;
    use tempfile::tempdir;

    use super::*;
    use crate::{
        context::ContextConfig,
        provider::StubProvider,
        runtime::RuntimeHandle,
        types::{
            TaskKind, TaskRecord, TaskStatus, ToolExecutionRecord, ToolExecutionStatus, TrustLevel,
        },
    };

    fn tool_error(error: anyhow::Error) -> crate::tool::ToolError {
        error
            .downcast_ref::<crate::tool::ToolError>()
            .expect("tool error")
            .clone()
    }

    #[test]
    fn source_ref_accepts_known_memory_prefixes() {
        for source_ref in [
            "agent_memory:self",
            "workspace_profile:ws-123",
            "brief:abc",
            "episode:ep_123",
            "work_item:work_123",
            "task:task_123",
            "tool_execution:tool-123:cmd",
            "tool_execution:tool-123:batch_item:2:cmd",
        ] {
            assert_eq!(
                validate_source_ref(source_ref.to_string()).unwrap(),
                source_ref
            );
        }
    }

    #[test]
    fn source_ref_rejects_paths_and_unknown_prefixes() {
        for source_ref in [
            "/Users/jolestar/.agents/skills/agentinbox/SKILL.md",
            "skill:/Users/jolestar/.agents/skills/agentinbox/SKILL.md",
            "skill.md:/Users/jolestar/.agents/skills/agentinbox/SKILL.md",
            "agentinbox:///SKILL.md",
            "memory:invalid-ref-123",
            "brief:/Users/jolestar/project/README.md",
            "brief:https://example.com/memory",
            "episode:../ledger/episode-1",
            "work_item:work_123?raw=true",
            "tool_execution:tool-123",
            "tool_execution:tool-123:batch_item:abc:cmd",
            "tool_execution:tool-123:batch_item:2",
        ] {
            let error = tool_error(validate_source_ref(source_ref.to_string()).unwrap_err());
            assert_eq!(error.kind, "invalid_tool_input");
            assert_eq!(
                error.recovery_hint.as_deref(),
                Some("call MemorySearch first and copy one returned source_ref verbatim; use ExecCommand for workspace files or skill docs instead of MemoryGet")
            );
        }
    }

    #[test]
    fn source_ref_rejects_empty_suffix_and_whitespace() {
        let empty_suffix = tool_error(validate_source_ref("brief:".to_string()).unwrap_err());
        assert_eq!(empty_suffix.kind, "invalid_tool_input");
        assert!(empty_suffix
            .details
            .as_ref()
            .and_then(|details| details.get("validation_error"))
            .and_then(Value::as_str)
            .is_some_and(|message| message.contains("missing")));

        let whitespace = tool_error(validate_source_ref("brief:abc def".to_string()).unwrap_err());
        assert_eq!(whitespace.kind, "invalid_tool_input");
        assert!(whitespace
            .details
            .as_ref()
            .and_then(|details| details.get("validation_error"))
            .and_then(Value::as_str)
            .is_some_and(|message| message.contains("whitespace")));
    }

    #[test]
    fn max_chars_accepts_omitted_and_bounded_values() {
        assert_eq!(validate_max_chars(None).unwrap(), None);
        assert_eq!(validate_max_chars(Some(1)).unwrap(), Some(1));
        assert_eq!(
            validate_max_chars(Some(MAX_CHARS_MAX)).unwrap(),
            Some(MAX_CHARS_MAX)
        );
    }

    #[test]
    fn max_chars_rejects_zero_and_oversized_values() {
        for max_chars in [0, MAX_CHARS_MAX + 1] {
            let error = tool_error(validate_max_chars(Some(max_chars)).unwrap_err());
            assert_eq!(error.kind, "invalid_tool_input");
            assert!(error
                .details
                .as_ref()
                .and_then(|details| details.get("maximum"))
                .is_some());
        }
    }

    #[tokio::test]
    async fn memory_get_tool_accepts_command_receipt_source_refs() {
        let dir = tempdir().unwrap();
        let workspace = tempdir().unwrap();
        let runtime = RuntimeHandle::new(
            "default",
            dir.path().to_path_buf(),
            workspace.path().to_path_buf(),
            "http://127.0.0.1:7878".into(),
            Arc::new(StubProvider::new("done")),
            "default".into(),
            ContextConfig::default(),
        )
        .unwrap();
        let command = "python - <<'PY'\nprint('memory_get_tool_receipt_1246')\nPY";
        runtime
            .storage()
            .append_tool_execution(&ToolExecutionRecord {
                id: "tool-get-1246".into(),
                agent_id: "default".into(),
                work_item_id: None,
                turn_index: 1,
                tool_name: "ExecCommand".into(),
                created_at: Utc::now(),
                completed_at: Some(Utc::now()),
                duration_ms: 10,
                trust: TrustLevel::TrustedOperator,
                status: ToolExecutionStatus::Success,
                input: json!({
                    "cmd": command,
                    "workdir": "src",
                    "yield_time_ms": 1000,
                    "max_output_tokens": 1200
                }),
                output: json!({"exit_code": 0}),
                summary: "command exited with status 0".into(),
                invocation_surface: None,
            })
            .unwrap();

        let result = execute(
            &runtime,
            "default",
            &TrustLevel::TrustedOperator,
            &json!({
                "source_ref": "tool_execution:tool-get-1246:cmd"
            }),
        )
        .await
        .unwrap();
        let content = result.envelope.result.unwrap()["memory"]["content"]
            .as_str()
            .unwrap()
            .to_string();

        assert!(content.contains("memory_get_tool_receipt_1246"));
        assert!(content.contains("\"workdir\": \"src\""));
        assert!(content.contains("\"yield_time_ms\": 1000"));
        assert!(content.contains("\"max_output_tokens\": 1200"));
    }

    #[tokio::test]
    async fn memory_get_tool_accepts_task_source_refs() {
        let dir = tempdir().unwrap();
        let workspace = tempdir().unwrap();
        let runtime = RuntimeHandle::new(
            "default",
            dir.path().to_path_buf(),
            workspace.path().to_path_buf(),
            "http://127.0.0.1:7878".into(),
            Arc::new(StubProvider::new("done")),
            "default".into(),
            ContextConfig::default(),
        )
        .unwrap();

        runtime
            .storage()
            .append_task(&TaskRecord {
                id: "task-get-1246".into(),
                agent_id: "default".into(),
                kind: TaskKind::CommandTask,
                status: TaskStatus::Completed,
                created_at: Utc::now(),
                updated_at: Utc::now(),
                parent_message_id: None,
                work_item_id: Some("wi-get-1246".into()),
                summary: Some("task get memory source ref".into()),
                detail: Some(serde_json::json!({"cmd": "echo task-get-1246"})),
                recovery: None,
            })
            .unwrap();

        let result = execute(
            &runtime,
            "default",
            &TrustLevel::TrustedOperator,
            &json!({
                "source_ref": "task:task-get-1246"
            }),
        )
        .await
        .unwrap();
        let content = result.envelope.result.unwrap()["memory"]["content"]
            .as_str()
            .unwrap()
            .to_string();

        assert!(content.contains("task_id: task-get-1246"));
        assert!(content.contains("summary: task get memory source ref"));
        assert!(content.contains("cmd: echo task-get-1246"));
        assert!(content.contains("work_item_id: wi-get-1246"));
    }
}
