use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::{Result, anyhow};
use serde_json::{Value, json};
use tokio::sync::Notify;
use vtcode_core::config::loader::VTCodeConfig;
use vtcode_core::core::interfaces::ui::UiSession;
use vtcode_core::notifications::{NotificationEvent, send_global_notification};
use vtcode_core::tools::edited_file_monitor::FILE_CONFLICT_OVERRIDE_ARG;
use vtcode_core::tools::registry::{ExecSettlementMode, ToolRegistry};
use vtcode_core::tools::result_cache::ToolResultCache;
use vtcode_tui::app::{
    DiffOverlayRequest, DiffPreviewMode, InlineHandle, InlineListItem, InlineListSelection,
    ListOverlayRequest, TransientRequest, TransientSubmission,
};

use crate::agent::runloop::unified::inline_events::harness::HarnessEventEmitter;
use crate::agent::runloop::unified::overlay_prompt::{OverlayWaitOutcome, show_overlay_and_wait};
use crate::agent::runloop::unified::state::CtrlCState;

use super::execution_runtime::execute_with_cache_and_streaming;
use super::file_conflict_runtime::{PendingFileConflictStatus, RuntimeToolExecution};
use super::status::ToolExecutionStatus;

#[derive(Clone)]
struct PendingFileConflict {
    output: Value,
    display_path: String,
    absolute_path: PathBuf,
    message: String,
    approved_snapshot: Option<Value>,
    disk_content: Option<String>,
    intended_content: Option<String>,
    emit_hitl_notification: bool,
}

enum ConflictResolution {
    Reload,
    Abort,
    Proceed,
}

pub(super) async fn resolve_file_conflict_status<S>(
    registry: &mut ToolRegistry,
    tool_result_cache: &Arc<tokio::sync::RwLock<ToolResultCache>>,
    session: &mut S,
    handle: &InlineHandle,
    name: &str,
    tool_item_id: &str,
    tool_call_id: &str,
    args_val: &Value,
    mut execution: RuntimeToolExecution,
    ctrl_c_state: &Arc<CtrlCState>,
    ctrl_c_notify: &Arc<Notify>,
    harness_emitter: Option<HarnessEventEmitter>,
    vt_cfg: Option<&VTCodeConfig>,
    max_tool_retries: usize,
    safety_prevalidated: bool,
) -> Result<ToolExecutionStatus>
where
    S: UiSession + ?Sized,
{
    loop {
        let conflict = match execution {
            RuntimeToolExecution::Completed(status) => return Ok(status),
            RuntimeToolExecution::PendingFileConflict(conflict) => {
                hydrate_pending_conflict(registry, conflict).await?
            }
        };

        match prompt_for_conflict_resolution(
            session,
            handle,
            ctrl_c_state,
            ctrl_c_notify,
            &conflict,
            vt_cfg
                .map(|cfg| cfg.security.hitl_notification_bell)
                .unwrap_or(true),
        )
        .await?
        {
            OverlayWaitOutcome::Submitted(ConflictResolution::Reload) => {
                registry
                    .edited_file_monitor_ref()
                    .accept_disk_version(&conflict.absolute_path)
                    .await?;
                return Ok(conflict_resolution_status(
                    conflict.finalized_output(
                        "reloaded",
                        "Reloaded the file from disk. Pending agent changes were discarded.",
                        read_disk_text(&conflict.absolute_path).await,
                    ),
                    true,
                ));
            }
            OverlayWaitOutcome::Submitted(ConflictResolution::Abort) => {
                return Ok(aborted_conflict_status(&conflict));
            }
            OverlayWaitOutcome::Submitted(ConflictResolution::Proceed) => {
                let override_args = build_override_args(args_val, &conflict)?;
                execution = execute_with_cache_and_streaming(
                    registry,
                    tool_result_cache,
                    name,
                    tool_item_id,
                    tool_call_id,
                    &override_args,
                    ctrl_c_state,
                    ctrl_c_notify,
                    handle,
                    harness_emitter.clone(),
                    vt_cfg,
                    max_tool_retries,
                    ExecSettlementMode::Manual,
                    safety_prevalidated,
                )
                .await;
            }
            OverlayWaitOutcome::Cancelled => {
                return Ok(aborted_conflict_status(&conflict));
            }
            OverlayWaitOutcome::Interrupted | OverlayWaitOutcome::Exit => {
                return Ok(ToolExecutionStatus::Cancelled);
            }
        }
    }
}

impl PendingFileConflict {
    fn finalized_output(
        &self,
        resolution: &str,
        message: &str,
        disk_content: Option<String>,
    ) -> Value {
        let mut output = self.output.clone();
        if let Some(obj) = output.as_object_mut() {
            obj.insert("resolution".to_string(), json!(resolution));
            obj.insert("message".to_string(), json!(message));
            obj.insert("emit_hitl_notification".to_string(), Value::Bool(false));
            obj.insert(
                "disk_content".to_string(),
                disk_content.map(Value::String).unwrap_or(Value::Null),
            );
        }
        output
    }
}

async fn hydrate_pending_conflict(
    registry: &ToolRegistry,
    conflict: PendingFileConflictStatus,
) -> Result<PendingFileConflict> {
    let absolute_path = registry
        .file_ops_tool()
        .normalize_user_path(&conflict.display_path)
        .await?;

    Ok(PendingFileConflict {
        output: conflict.output,
        display_path: conflict.display_path,
        absolute_path,
        message: conflict.message,
        approved_snapshot: conflict.approved_snapshot,
        disk_content: conflict.disk_content,
        intended_content: conflict.intended_content,
        emit_hitl_notification: conflict.emit_hitl_notification,
    })
}

async fn prompt_for_conflict_resolution<S>(
    session: &mut S,
    handle: &InlineHandle,
    ctrl_c_state: &Arc<CtrlCState>,
    ctrl_c_notify: &Arc<Notify>,
    conflict: &PendingFileConflict,
    hitl_notification_bell: bool,
) -> Result<OverlayWaitOutcome<ConflictResolution>>
where
    S: UiSession + ?Sized,
{
    let can_show_diff = conflict.approved_snapshot.is_some()
        && conflict.disk_content.is_some()
        && conflict.intended_content.is_some();
    if hitl_notification_bell && conflict.emit_hitl_notification {
        let _ = send_global_notification(NotificationEvent::HumanInTheLoop {
            prompt: "File conflict requires review".to_string(),
            context: format!("File: {}", conflict.display_path),
        })
        .await;
    }

    let lines = vec![
        conflict.message.clone(),
        format!("File: {}", conflict.display_path),
        if can_show_diff {
            "Reload uses the external disk version. View diff shows disk content versus the agent's intended write.".to_string()
        } else {
            "Reload uses the external disk version. Diff preview is unavailable for this file content.".to_string()
        },
    ];
    let outcome = show_overlay_and_wait(
        handle,
        session,
        TransientRequest::List(ListOverlayRequest {
            title: "File Changed On Disk".to_string(),
            lines,
            footer_hint: None,
            items: conflict_resolution_items(can_show_diff),
            selected: Some(InlineListSelection::FileConflictReload),
            search: None,
            hotkeys: Vec::new(),
        }),
        ctrl_c_state,
        ctrl_c_notify,
        |submission| match submission {
            TransientSubmission::Selection(InlineListSelection::FileConflictReload) => {
                Some(InlineListSelection::FileConflictReload)
            }
            TransientSubmission::Selection(InlineListSelection::FileConflictAbort) => {
                Some(InlineListSelection::FileConflictAbort)
            }
            TransientSubmission::Selection(InlineListSelection::FileConflictViewDiff)
                if can_show_diff =>
            {
                Some(InlineListSelection::FileConflictViewDiff)
            }
            _ => None,
        },
    )
    .await?;

    match outcome {
        OverlayWaitOutcome::Submitted(InlineListSelection::FileConflictReload) => {
            Ok(OverlayWaitOutcome::Submitted(ConflictResolution::Reload))
        }
        OverlayWaitOutcome::Submitted(InlineListSelection::FileConflictAbort) => {
            Ok(OverlayWaitOutcome::Submitted(ConflictResolution::Abort))
        }
        OverlayWaitOutcome::Submitted(InlineListSelection::FileConflictViewDiff)
            if can_show_diff =>
        {
            show_overlay_and_wait(
                handle,
                session,
                TransientRequest::Diff(DiffOverlayRequest {
                    file_path: conflict.display_path.clone(),
                    before: conflict.disk_content.clone().unwrap_or_default(),
                    after: conflict.intended_content.clone().unwrap_or_default(),
                    hunks: Vec::new(),
                    current_hunk: 0,
                    mode: DiffPreviewMode::FileConflict,
                }),
                ctrl_c_state,
                ctrl_c_notify,
                |submission| match submission {
                    TransientSubmission::DiffProceed => Some(ConflictResolution::Proceed),
                    TransientSubmission::DiffReload => Some(ConflictResolution::Reload),
                    TransientSubmission::DiffAbort => Some(ConflictResolution::Abort),
                    _ => None,
                },
            )
            .await
        }
        OverlayWaitOutcome::Cancelled => Ok(OverlayWaitOutcome::Cancelled),
        OverlayWaitOutcome::Interrupted => Ok(OverlayWaitOutcome::Interrupted),
        OverlayWaitOutcome::Exit => Ok(OverlayWaitOutcome::Exit),
        OverlayWaitOutcome::Submitted(_) => unreachable!("handled list selections above"),
    }
}

fn conflict_resolution_items(can_show_diff: bool) -> Vec<InlineListItem> {
    let mut items = Vec::with_capacity(if can_show_diff { 3 } else { 2 });
    items.push(InlineListItem {
        title: "Reload from disk".to_string(),
        subtitle: Some(
            "Discard pending agent changes and continue from the external version.".to_string(),
        ),
        badge: None,
        indent: 0,
        selection: Some(InlineListSelection::FileConflictReload),
        search_value: Some("reload disk external version".to_string()),
    });
    if can_show_diff {
        items.push(InlineListItem {
            title: "View unified diff".to_string(),
            subtitle: Some(
                "Review external changes against the agent's intended write.".to_string(),
            ),
            badge: None,
            indent: 0,
            selection: Some(InlineListSelection::FileConflictViewDiff),
            search_value: Some("diff compare review changes".to_string()),
        });
    }
    items.push(InlineListItem {
        title: "Abort".to_string(),
        subtitle: Some("Cancel this write and leave disk unchanged.".to_string()),
        badge: None,
        indent: 0,
        selection: Some(InlineListSelection::FileConflictAbort),
        search_value: Some("abort cancel stop".to_string()),
    });
    items
}

fn build_override_args(args_val: &Value, conflict: &PendingFileConflict) -> Result<Value> {
    let mut args = args_val.clone();
    let Some(map) = args.as_object_mut() else {
        return Err(anyhow!(
            "Cannot override a file-conflict tool call without structured arguments"
        ));
    };
    let approved_snapshot = conflict.approved_snapshot.clone().ok_or_else(|| {
        anyhow!("Cannot proceed with a file-conflict write without an approved disk snapshot")
    })?;
    map.insert(FILE_CONFLICT_OVERRIDE_ARG.to_string(), approved_snapshot);
    Ok(args)
}

fn conflict_resolution_status(output: Value, command_success: bool) -> ToolExecutionStatus {
    ToolExecutionStatus::Success {
        output,
        stdout: None,
        modified_files: vec![],
        command_success,
    }
}

fn aborted_conflict_status(conflict: &PendingFileConflict) -> ToolExecutionStatus {
    conflict_resolution_status(
        conflict.finalized_output(
            "aborted",
            "Aborted the agent write because the file changed on disk.",
            None,
        ),
        false,
    )
}

async fn read_disk_text(path: &Path) -> Option<String> {
    let bytes = tokio::fs::read(path).await.ok()?;
    String::from_utf8(bytes).ok()
}

#[cfg(test)]
mod tests {
    use super::*;
    use async_trait::async_trait;
    use tempfile::TempDir;
    use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
    use vtcode_core::config::constants::tools;
    use vtcode_core::core::interfaces::ui::UiSession;
    use vtcode_core::tools::result_cache::ToolResultCache;
    use vtcode_tui::app::{InlineCommand, InlineEvent, TransientEvent};

    use crate::agent::runloop::unified::state::CtrlCState;

    struct TestUiSession {
        handle: InlineHandle,
        events: UnboundedReceiver<InlineEvent>,
    }

    #[async_trait]
    impl UiSession for TestUiSession {
        fn inline_handle(&self) -> &InlineHandle {
            &self.handle
        }

        async fn next_event(&mut self) -> Option<InlineEvent> {
            self.events.recv().await
        }
    }

    fn test_session() -> (
        TestUiSession,
        UnboundedSender<InlineEvent>,
        UnboundedReceiver<InlineCommand>,
    ) {
        let (command_tx, command_rx) = unbounded_channel();
        let (event_tx, event_rx) = unbounded_channel();
        (
            TestUiSession {
                handle: InlineHandle::new_for_tests(command_tx),
                events: event_rx,
            },
            event_tx,
            command_rx,
        )
    }

    async fn create_registry(workspace: &TempDir) -> ToolRegistry {
        ToolRegistry::new(workspace.path().to_path_buf()).await
    }

    fn disable_hitl_notification(output: &mut Value) {
        output["emit_hitl_notification"] = Value::Bool(false);
    }

    fn pending_conflict(output: Value) -> RuntimeToolExecution {
        RuntimeToolExecution::PendingFileConflict(
            PendingFileConflictStatus::from_output(output).expect("valid file-conflict output"),
        )
    }

    #[tokio::test]
    async fn reload_resolution_discards_agent_write() -> Result<()> {
        let workspace = TempDir::new()?;
        let mut registry = create_registry(&workspace).await;
        let path = workspace.path().join("sample.txt");
        std::fs::write(&path, "before\n")?;
        registry.read_file(json!({ "path": "sample.txt" })).await?;
        std::fs::write(&path, "external\n")?;
        let mut conflict_output = registry
            .write_file(json!({"path": "sample.txt", "content": "agent\n", "mode": "overwrite"}))
            .await?;
        disable_hitl_notification(&mut conflict_output);

        let (mut session, event_tx, _commands) = test_session();
        let handle = session.inline_handle().clone();
        event_tx.send(InlineEvent::Transient(TransientEvent::Submitted(
            TransientSubmission::Selection(InlineListSelection::FileConflictReload),
        )))?;

        let status = resolve_file_conflict_status(
            &mut registry,
            &Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(8))),
            &mut session,
            &handle,
            tools::WRITE_FILE,
            "tool_1",
            "tool_call_0",
            &json!({"path": "sample.txt", "content": "agent\n", "mode": "overwrite"}),
            pending_conflict(conflict_output),
            &Arc::new(CtrlCState::new()),
            &Arc::new(Notify::new()),
            None,
            None,
            0,
            false,
        )
        .await?;

        match status {
            ToolExecutionStatus::Success { output, .. } => {
                assert_eq!(output["resolution"], json!("reloaded"));
                assert_eq!(output["disk_content"], json!("external\n"));
            }
            other => panic!("unexpected status: {other:?}"),
        }
        assert_eq!(std::fs::read_to_string(path)?, "external\n");
        Ok(())
    }

    #[tokio::test]
    async fn abort_resolution_leaves_disk_unchanged() -> Result<()> {
        let workspace = TempDir::new()?;
        let mut registry = create_registry(&workspace).await;
        let path = workspace.path().join("sample.txt");
        std::fs::write(&path, "before\n")?;
        registry.read_file(json!({ "path": "sample.txt" })).await?;
        std::fs::write(&path, "external\n")?;
        let mut conflict_output = registry
            .write_file(json!({"path": "sample.txt", "content": "agent\n", "mode": "overwrite"}))
            .await?;
        disable_hitl_notification(&mut conflict_output);

        let (mut session, event_tx, _commands) = test_session();
        let handle = session.inline_handle().clone();
        event_tx.send(InlineEvent::Transient(TransientEvent::Submitted(
            TransientSubmission::Selection(InlineListSelection::FileConflictAbort),
        )))?;

        let status = resolve_file_conflict_status(
            &mut registry,
            &Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(8))),
            &mut session,
            &handle,
            tools::WRITE_FILE,
            "tool_1",
            "tool_call_0",
            &json!({"path": "sample.txt", "content": "agent\n", "mode": "overwrite"}),
            pending_conflict(conflict_output),
            &Arc::new(CtrlCState::new()),
            &Arc::new(Notify::new()),
            None,
            None,
            0,
            false,
        )
        .await?;

        match status {
            ToolExecutionStatus::Success {
                output,
                command_success,
                ..
            } => {
                assert!(!command_success);
                assert_eq!(output["resolution"], json!("aborted"));
            }
            other => panic!("unexpected status: {other:?}"),
        }
        assert_eq!(std::fs::read_to_string(path)?, "external\n");
        Ok(())
    }

    #[tokio::test]
    async fn proceed_resolution_reexecutes_with_override() -> Result<()> {
        let workspace = TempDir::new()?;
        let mut registry = create_registry(&workspace).await;
        let path = workspace.path().join("sample.txt");
        std::fs::write(&path, "before\n")?;
        registry.read_file(json!({ "path": "sample.txt" })).await?;
        std::fs::write(&path, "external\n")?;
        let mut conflict_output = registry
            .write_file(json!({"path": "sample.txt", "content": "agent\n", "mode": "overwrite"}))
            .await?;
        disable_hitl_notification(&mut conflict_output);

        let (mut session, event_tx, _commands) = test_session();
        let handle = session.inline_handle().clone();
        event_tx.send(InlineEvent::Transient(TransientEvent::Submitted(
            TransientSubmission::Selection(InlineListSelection::FileConflictViewDiff),
        )))?;
        event_tx.send(InlineEvent::Transient(TransientEvent::Submitted(
            TransientSubmission::DiffProceed,
        )))?;

        let status = resolve_file_conflict_status(
            &mut registry,
            &Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(8))),
            &mut session,
            &handle,
            tools::WRITE_FILE,
            "tool_1",
            "tool_call_0",
            &json!({"path": "sample.txt", "content": "agent\n", "mode": "overwrite"}),
            pending_conflict(conflict_output),
            &Arc::new(CtrlCState::new()),
            &Arc::new(Notify::new()),
            None,
            None,
            0,
            false,
        )
        .await?;

        match status {
            ToolExecutionStatus::Success { output, .. } => {
                assert_ne!(output["resolution"], json!("pending"));
            }
            other => panic!("unexpected status: {other:?}"),
        }
        assert_eq!(std::fs::read_to_string(path)?, "agent\n");
        Ok(())
    }

    #[tokio::test]
    async fn proceed_resolution_reconflicts_when_disk_changes_again() -> Result<()> {
        let workspace = TempDir::new()?;
        let mut registry = create_registry(&workspace).await;
        let path = workspace.path().join("sample.txt");
        std::fs::write(&path, "before\n")?;
        registry.read_file(json!({ "path": "sample.txt" })).await?;
        std::fs::write(&path, "external one\n")?;
        let mut conflict_output = registry
            .write_file(json!({"path": "sample.txt", "content": "agent\n", "mode": "overwrite"}))
            .await?;
        disable_hitl_notification(&mut conflict_output);
        std::fs::write(&path, "external two\n")?;

        let (mut session, event_tx, _commands) = test_session();
        let handle = session.inline_handle().clone();
        event_tx.send(InlineEvent::Transient(TransientEvent::Submitted(
            TransientSubmission::Selection(InlineListSelection::FileConflictViewDiff),
        )))?;
        event_tx.send(InlineEvent::Transient(TransientEvent::Submitted(
            TransientSubmission::DiffProceed,
        )))?;
        event_tx.send(InlineEvent::Transient(TransientEvent::Submitted(
            TransientSubmission::Selection(InlineListSelection::FileConflictReload),
        )))?;

        let status = resolve_file_conflict_status(
            &mut registry,
            &Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(8))),
            &mut session,
            &handle,
            tools::WRITE_FILE,
            "tool_1",
            "tool_call_0",
            &json!({"path": "sample.txt", "content": "agent\n", "mode": "overwrite"}),
            pending_conflict(conflict_output),
            &Arc::new(CtrlCState::new()),
            &Arc::new(Notify::new()),
            None,
            None,
            0,
            false,
        )
        .await?;

        match status {
            ToolExecutionStatus::Success { output, .. } => {
                assert_eq!(output["resolution"], json!("reloaded"));
                assert_eq!(output["disk_content"], json!("external two\n"));
            }
            other => panic!("unexpected status: {other:?}"),
        }
        assert_eq!(std::fs::read_to_string(path)?, "external two\n");
        Ok(())
    }
}
