use codex_features::Feature;
use codex_protocol::items::ImageViewItem;
use codex_protocol::items::TurnItem;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ImageDetail;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::InputModality;
use codex_utils_image::PromptImageMode;
use codex_utils_image::data_url_from_bytes;
use codex_utils_image::load_for_prompt_bytes;
use serde::Deserialize;

use crate::function_tool::FunctionCallError;
use crate::original_image_detail::can_request_original_image_detail;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::context::boxed_tool_output;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::resolve_tool_environment;
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::handlers::view_image_spec::create_view_image_tool;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_utils_path_uri::PathUri;

pub struct ViewImageHandler {
    options: ViewImageToolOptions,
}

impl Default for ViewImageHandler {
    fn default() -> Self {
        Self {
            options: ViewImageToolOptions {
                can_request_original_image_detail: false,
                include_environment_id: false,
            },
        }
    }
}

impl ViewImageHandler {
    pub(crate) fn new(options: ViewImageToolOptions) -> Self {
        Self { options }
    }
}

const VIEW_IMAGE_UNSUPPORTED_MESSAGE: &str =
    "view_image is not allowed because you do not support image inputs";

#[derive(Deserialize)]
struct ViewImageArgs {
    path: String,
    #[serde(default)]
    environment_id: Option<String>,
    detail: Option<String>,
}

#[derive(Clone, Copy, Eq, PartialEq)]
enum ViewImageDetail {
    High,
    Original,
}

impl ToolExecutor<ToolInvocation> for ViewImageHandler {
    fn tool_name(&self) -> ToolName {
        ToolName::plain("view_image")
    }

    fn spec(&self) -> ToolSpec {
        create_view_image_tool(self.options)
    }

    fn supports_parallel_tool_calls(&self) -> bool {
        true
    }

    fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {
        Box::pin(self.handle_call(invocation))
    }
}

impl ViewImageHandler {
    async fn handle_call(
        &self,
        invocation: ToolInvocation,
    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {
        if !invocation
            .turn
            .model_info
            .input_modalities
            .contains(&InputModality::Image)
        {
            return Err(FunctionCallError::RespondToModel(
                VIEW_IMAGE_UNSUPPORTED_MESSAGE.to_string(),
            ));
        }

        let ToolInvocation {
            session,
            turn,
            payload,
            call_id,
            ..
        } = invocation;

        let arguments = match payload {
            ToolPayload::Function { arguments } => arguments,
            _ => {
                return Err(FunctionCallError::RespondToModel(
                    "view_image handler received unsupported payload".to_string(),
                ));
            }
        };

        let ViewImageArgs {
            path,
            environment_id,
            detail,
        } = parse_arguments(&arguments)?;
        // `high` is the explicit spelling of the default resized path.
        // Other string values remain invalid rather than being silently reinterpreted.
        let detail = match detail.as_deref() {
            None => None,
            Some("high") => Some(ViewImageDetail::High),
            Some("original") => Some(ViewImageDetail::Original),
            Some(detail) => {
                return Err(FunctionCallError::RespondToModel(format!(
                    "view_image.detail only supports `high` or `original`; omit `detail` for default high resized behavior, got `{detail}`"
                )));
            }
        };

        let Some(turn_environment) =
            resolve_tool_environment(turn.as_ref(), environment_id.as_deref())?
        else {
            return Err(FunctionCallError::RespondToModel(
                "view_image is unavailable in this session".to_string(),
            ));
        };
        let cwd = turn_environment.cwd().clone();
        let abs_path = cwd.join(path);
        let sandbox = turn.file_system_sandbox_context(
            /*additional_permissions*/ None,
            turn_environment.cwd_uri(),
        );
        let fs = turn_environment.environment.get_filesystem();
        let path_uri = PathUri::from_abs_path(&abs_path);

        let metadata = fs
            .get_metadata(&path_uri, Some(&sandbox))
            .await
            .map_err(|error| {
                FunctionCallError::RespondToModel(format!(
                    "unable to locate image at `{}`: {error}",
                    abs_path.display()
                ))
            })?;

        if !metadata.is_file {
            return Err(FunctionCallError::RespondToModel(format!(
                "image path `{}` is not a file",
                abs_path.display()
            )));
        }
        let file_bytes = fs
            .read_file(&path_uri, Some(&sandbox))
            .await
            .map_err(|error| {
                FunctionCallError::RespondToModel(format!(
                    "unable to read image at `{}`: {error}",
                    abs_path.display()
                ))
            })?;
        let event_path = abs_path.clone();

        let can_request_original_detail = can_request_original_image_detail(&turn.model_info);
        let use_original_detail =
            can_request_original_detail && matches!(detail, Some(ViewImageDetail::Original));
        let image_detail = if use_original_detail {
            ImageDetail::Original
        } else {
            DEFAULT_IMAGE_DETAIL
        };

        let image_url = if turn.features.enabled(Feature::ResizeAllImages) {
            // The history insertion path owns image decoding and resizing when this is enabled.
            data_url_from_bytes("application/octet-stream", &file_bytes)
        } else {
            let image_mode = if use_original_detail {
                PromptImageMode::Original
            } else {
                PromptImageMode::ResizeToFit
            };
            load_for_prompt_bytes(abs_path.as_path(), file_bytes, image_mode)
                .map_err(|error| {
                    FunctionCallError::RespondToModel(format!(
                        "unable to process image at `{}`: {error}",
                        abs_path.display()
                    ))
                })?
                .into_data_url()
        };

        let item = TurnItem::ImageView(ImageViewItem {
            id: call_id,
            path: event_path,
        });
        session.emit_turn_item_started(turn.as_ref(), &item).await;
        session.emit_turn_item_completed(turn.as_ref(), item).await;

        Ok(boxed_tool_output(ViewImageOutput {
            image_url,
            image_detail,
        }))
    }
}

impl CoreToolRuntime for ViewImageHandler {}

pub struct ViewImageOutput {
    image_url: String,
    image_detail: ImageDetail,
}

impl ToolOutput for ViewImageOutput {
    fn log_preview(&self) -> String {
        format!("<image data URL omitted: {} bytes>", self.image_url.len())
    }

    fn success_for_logging(&self) -> bool {
        true
    }

    fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
        let body =
            FunctionCallOutputBody::ContentItems(vec![FunctionCallOutputContentItem::InputImage {
                image_url: self.image_url.clone(),
                detail: Some(self.image_detail),
            }]);
        let output = FunctionCallOutputPayload {
            body,
            success: Some(true),
        };

        ResponseInputItem::FunctionCallOutput {
            call_id: call_id.to_string(),
            output,
        }
    }

    fn code_mode_result(&self, _payload: &ToolPayload) -> serde_json::Value {
        serde_json::json!({
            "image_url": self.image_url,
            "detail": self.image_detail
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::session::tests::make_session_and_context;
    use crate::session::turn_context::TurnEnvironment;
    use crate::tools::context::ToolCallSource;
    use crate::tools::context::ToolInvocation;
    use crate::turn_diff_tracker::TurnDiffTracker;
    use codex_protocol::models::PermissionProfile;
    use codex_utils_absolute_path::AbsolutePathBuf;
    use core_test_support::TempDirExt;
    use pretty_assertions::assert_eq;
    use serde_json::json;
    use std::sync::Arc;
    use tokio::sync::Mutex;

    fn replace_primary_environment_cwd(turn: &mut crate::TurnContext, cwd: AbsolutePathBuf) {
        let current = turn
            .environments
            .turn_environments
            .first()
            .cloned()
            .expect("default local turn environment");
        turn.environments.turn_environments[0] = TurnEnvironment::new(
            current.environment_id,
            current.environment,
            cwd,
            current.shell,
        );
    }

    #[test]
    fn log_preview_omits_image_data() {
        let output = ViewImageOutput {
            image_url: "data:image/png;base64,AAA".to_string(),
            image_detail: DEFAULT_IMAGE_DETAIL,
        };

        assert_eq!(output.log_preview(), "<image data URL omitted: 25 bytes>");
    }

    #[test]
    fn code_mode_result_returns_image_url_object() {
        let output = ViewImageOutput {
            image_url: "data:image/png;base64,AAA".to_string(),
            image_detail: DEFAULT_IMAGE_DETAIL,
        };

        let result = output.code_mode_result(&ToolPayload::Function {
            arguments: "{}".to_string(),
        });

        assert_eq!(
            result,
            json!({
                "image_url": "data:image/png;base64,AAA",
                "detail": "high",
            })
        );
    }

    #[tokio::test]
    async fn handle_passes_sandbox_context_for_local_filesystem_reads() {
        let (session, mut turn) = make_session_and_context().await;
        let image_dir = tempfile::tempdir().expect("create image temp dir");
        let image_cwd = image_dir.abs();

        replace_primary_environment_cwd(&mut turn, image_cwd.clone());
        let image_path = image_cwd.join("image.png");
        std::fs::write(image_path.as_path(), b"not a real image").expect("write test image");
        turn.permission_profile = PermissionProfile::read_only();

        let result = ViewImageHandler::default()
            .handle(ToolInvocation {
                session: Arc::new(session),
                turn: Arc::new(turn),
                cancellation_token: tokio_util::sync::CancellationToken::new(),
                tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
                call_id: "call-view-image".to_string(),
                tool_name: codex_tools::ToolName::plain("view_image"),
                source: ToolCallSource::Direct,
                payload: ToolPayload::Function {
                    arguments: json!({ "path": "image.png" }).to_string(),
                },
            })
            .await;

        let Err(FunctionCallError::RespondToModel(message)) = result else {
            panic!("expected sandboxed filesystem error");
        };
        assert!(
            message.contains("sandboxed filesystem operations require configured runtime paths"),
            "{message}"
        );
    }

    #[tokio::test]
    async fn handle_rejects_unsupported_detail() {
        let (session, turn) = make_session_and_context().await;

        let result = ViewImageHandler::default()
            .handle(ToolInvocation {
                session: Arc::new(session),
                turn: Arc::new(turn),
                cancellation_token: tokio_util::sync::CancellationToken::new(),
                tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
                call_id: "call-view-image".to_string(),
                tool_name: codex_tools::ToolName::plain("view_image"),
                source: ToolCallSource::Direct,
                payload: ToolPayload::Function {
                    arguments: json!({ "path": "image.png", "detail": "low" }).to_string(),
                },
            })
            .await;

        let Err(FunctionCallError::RespondToModel(message)) = result else {
            panic!("expected unsupported detail error");
        };
        assert_eq!(
            message,
            "view_image.detail only supports `high` or `original`; omit `detail` for default high resized behavior, got `low`"
        );
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn handle_accepts_explicit_high_detail() {
        let (session, mut turn) = make_session_and_context().await;
        let image_dir = tempfile::tempdir().expect("create image temp dir");
        let image_cwd = image_dir.abs();

        replace_primary_environment_cwd(&mut turn, image_cwd.clone());
        let image_path = image_cwd.join("image.png");
        std::fs::write(image_path.as_path(), b"not a real image").expect("write test image");
        turn.permission_profile = PermissionProfile::Disabled;

        let result = ViewImageHandler::default()
            .handle(ToolInvocation {
                session: Arc::new(session),
                turn: Arc::new(turn),
                cancellation_token: tokio_util::sync::CancellationToken::new(),
                tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
                call_id: "call-view-image".to_string(),
                tool_name: codex_tools::ToolName::plain("view_image"),
                source: ToolCallSource::Direct,
                payload: ToolPayload::Function {
                    arguments: json!({ "path": "image.png", "detail": "high" }).to_string(),
                },
            })
            .await;

        let Err(FunctionCallError::RespondToModel(message)) = result else {
            panic!("expected image processing error");
        };
        assert!(message.contains("unable to process image"), "{message}");
    }
}
