use std::process::{Command, Stdio};
use std::time::Duration;

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::app::{App, Focus, InputResult};
use crate::model::CiSystem;

pub fn handle_input(app: &mut App, key: KeyEvent) -> InputResult {
    // Clear transient messages on any keypress
    app.error = None;
    app.status_message = None;

    match key.code {
        // Quit
        KeyCode::Char('q') | KeyCode::Esc => return InputResult::Quit,
        KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            return InputResult::Quit;
        }

        // Navigation in DAG: arrow keys and hjkl
        KeyCode::Left | KeyCode::Char('h') => app.select_prev_stage(),
        KeyCode::Right | KeyCode::Char('l') => app.select_next_stage(),
        KeyCode::Up | KeyCode::Char('k') => app.select_prev_job(),
        KeyCode::Down | KeyCode::Char('j') => app.select_next_job(),

        // Tab switching in detail view
        KeyCode::Tab => app.next_tab(),
        KeyCode::BackTab => app.prev_tab(),

        // Scroll detail view
        KeyCode::PageDown => app.scroll_detail(10),
        KeyCode::PageUp => app.scroll_detail(-10),

        // Scroll DAG view
        KeyCode::Char('H') => app.scroll_dag(-4, 0),
        KeyCode::Char('L') => app.scroll_dag(4, 0),
        KeyCode::Char('K') => app.scroll_dag(0, -2),
        KeyCode::Char('J') => app.scroll_dag(0, 2),

        // Reload
        KeyCode::F(5) => return InputResult::Reload,

        // Run / launch container (r/R/c/C)
        KeyCode::Char('r') => {
            if let Some(r) = try_container_action(app, false, "R", InputResult::RunJob) {
                return r;
            }
        }
        KeyCode::Char('R') => {
            if let Some(r) = try_container_action(app, true, "", InputResult::RunJob) {
                return r;
            }
        }
        KeyCode::Char('c') => {
            if let Some(r) = try_container_action(app, false, "C", InputResult::LaunchContainer) {
                return r;
            }
        }
        KeyCode::Char('C') => {
            if let Some(r) = try_container_action(app, true, "", InputResult::LaunchContainer) {
                return r;
            }
        }

        // Edit variables via $EDITOR
        KeyCode::Char('v') => {
            if let Some(job) = app.selected_job_data() {
                return InputResult::EditVariables(job.name.clone());
            } else {
                app.error = Some("No job selected".to_string());
            }
        }

        // Enter: navigate trigger/parent links, or toggle focus
        KeyCode::Enter => {
            if app.focus == Focus::Dag {
                // Navigate to parent pipeline
                if let Some(parent_tab) = app.selected_is_parent_link() {
                    app.active_tab = parent_tab;
                    return InputResult::Continue;
                }
                // Open child pipeline (trigger file or step-pipeline)
                if let Some(path) = app.selected_trigger_path() {
                    return InputResult::OpenChildPipeline(path);
                }
                if app.selected_has_steps() {
                    return InputResult::OpenStepPipeline;
                }
            }
            app.toggle_focus();
        }

        // Pipeline tab switching
        KeyCode::Char(']') => app.next_pipeline_tab(),
        KeyCode::Char('[') => app.prev_pipeline_tab(),

        // Close current pipeline tab (Backspace)
        KeyCode::Backspace => app.close_current_tab(),

        // Copy detail tab content to clipboard
        KeyCode::Char('y') => {
            if let Some(content) = app.detail_tab_content() {
                if let Err(e) = copy_to_clipboard(&content) {
                    app.error = Some(format!("Clipboard: {}", e));
                } else {
                    app.status_message = Some("Copied to clipboard".to_string());
                }
            } else {
                app.error = Some("Nothing to copy".to_string());
            }
        }

        _ => {}
    }

    InputResult::Continue
}

/// Try to build a container config and return the appropriate action.
///
/// `kvm_hint`: when non-empty, check for kvm tag and show a hint to use the
/// uppercase key instead. Pass `None` to skip the check (privileged mode).
fn try_container_action(
    app: &mut App,
    privileged: bool,
    kvm_hint: &str,
    make_result: fn(Box<crate::container::ContainerConfig>) -> InputResult,
) -> Option<InputResult> {
    if let Some(config) = app.build_container_config(privileged) {
        if !kvm_hint.is_empty() && app.selected_job_has_kvm_tag() {
            app.error = Some(format!(
                "Job has 'kvm' tag — use {} for privileged mode",
                kvm_hint
            ));
            None
        } else {
            Some(make_result(Box::new(config)))
        }
    } else if app.selected_job_data().is_none() {
        app.error = Some("No job selected".to_string());
        None
    } else {
        app.error = Some(
            match app.ci_system {
                CiSystem::GitHub => "Selected job has no container defined",
                CiSystem::GitLab => "Selected job has no image defined",
            }
            .to_string(),
        );
        None
    }
}

/// Timeout for clipboard commands.
const CLIPBOARD_TIMEOUT: Duration = Duration::from_secs(3);

/// Try to copy text to the system clipboard using available tools.
fn copy_to_clipboard(text: &str) -> Result<(), String> {
    // Try clipboard commands in order of preference
    let commands: &[&[&str]] = &[
        &["wl-copy"],
        &["xclip", "-selection", "clipboard"],
        &["xsel", "--clipboard", "--input"],
    ];

    for args in commands {
        if let Some(result) = try_clipboard_command(args, text) {
            return result;
        }
    }

    Err("no clipboard tool found (install wl-copy, xclip, or xsel)".to_string())
}

/// Attempt a single clipboard command. Returns:
/// - `Some(Ok(()))` if the command succeeded
/// - `Some(Err(...))` if the command was found but timed out
/// - `None` if the command was not found (try next)
fn try_clipboard_command(args: &[&str], text: &str) -> Option<Result<(), String>> {
    use wait_timeout::ChildExt;

    let program = args[0];
    let cmd_args = &args[1..];
    let mut child = match Command::new(program)
        .args(cmd_args)
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
    {
        Ok(child) => child,
        Err(_) => return None,
    };

    {
        use std::io::Write;
        if let Some(ref mut stdin) = child.stdin {
            let _ = stdin.write_all(text.as_bytes());
        }
    }
    // Drop stdin so the child sees EOF
    child.stdin.take();

    match child.wait_timeout(CLIPBOARD_TIMEOUT) {
        Ok(Some(status)) if status.success() => Some(Ok(())),
        Ok(Some(_)) => None, // non-zero exit, try next command
        Ok(None) => {
            // Timed out
            let _ = child.kill();
            let _ = child.wait();
            Some(Err(format!("{} timed out", program)))
        }
        Err(_) => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{Job, JobDebugInfo, Pipeline, Stage};
    use crate::resolve::GitContext;
    use indexmap::IndexMap;
    use std::path::PathBuf;

    fn make_test_job(name: &str, stage: &str, image: Option<&str>, tags: Vec<&str>) -> Job {
        Job {
            name: name.to_string(),
            stage: stage.to_string(),
            image: image.map(|s| s.to_string()),
            services: vec![],
            variables: IndexMap::new(),
            before_script: vec![],
            script: vec!["echo hi".to_string()],
            after_script: vec![],
            needs: vec![],
            artifact_sources: Some(vec![]),
            tags: tags.into_iter().map(|s| s.to_string()).collect(),
            when_val: "on_success".to_string(),
            allow_failure: false,
            rules_never: false,
            unresolved_extends: vec![],
            rules_summary: vec![],
            artifacts_paths: vec![],
            artifacts_exclude: vec![],
            artifacts_untracked: false,
            artifacts_when: "on_success".to_string(),
            dotenv_report: None,
            timeout: None,
            resource_group: None,
            coverage: None,
            resolved_yaml: String::new(),
            trigger_file: None,
            build_context: None,
            docker_entrypoint: None,
            step_id: None,
            steps: vec![],
            debug_info: JobDebugInfo::default(),
        }
    }

    fn make_test_app() -> App {
        let mut jobs = IndexMap::new();
        jobs.insert(
            "build".to_string(),
            make_test_job("build", "build", Some("rust:latest"), vec![]),
        );
        jobs.insert(
            "test".to_string(),
            make_test_job("test", "test", None, vec![]),
        );
        jobs.insert(
            "lint".to_string(),
            make_test_job("lint", "test", None, vec!["kvm"]),
        );
        let pipeline = Pipeline {
            stages: vec![
                Stage {
                    name: "build".to_string(),
                    job_names: vec!["build".to_string()],
                },
                Stage {
                    name: "test".to_string(),
                    job_names: vec!["test".to_string(), "lint".to_string()],
                },
            ],
            jobs,
            execution: Default::default(),
            checkout_path: None,
        };
        App::new(
            PathBuf::from("test.yml"),
            CiSystem::GitLab,
            None,
            GitContext::default(),
            pipeline,
        )
    }

    #[test]
    fn test_run_job_returns_run_result() {
        let mut app = make_test_app();
        // "build" has an image, so r should return RunJob
        let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE);
        let result = handle_input(&mut app, key);
        assert!(matches!(result, InputResult::RunJob(_)));
    }

    #[test]
    fn test_launch_container_returns_launch_result() {
        let mut app = make_test_app();
        let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
        let result = handle_input(&mut app, key);
        assert!(matches!(result, InputResult::LaunchContainer(_)));
    }

    #[test]
    fn test_run_job_no_image_uses_default() {
        let mut app = make_test_app();
        app.select_next_stage(); // select "test" which has no image — gets default
        let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE);
        let result = handle_input(&mut app, key);
        assert!(matches!(result, InputResult::RunJob(_)));
    }

    #[test]
    fn test_kvm_tag_suggests_uppercase() {
        let mut app = make_test_app();
        // Test that r on "build" (has image, no kvm) succeeds.
        let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE);
        let result = handle_input(&mut app, key);
        assert!(matches!(result, InputResult::RunJob(_)));
    }

    #[test]
    fn test_clipboard_command_success() {
        // "cat" reads stdin and exits 0
        let result = try_clipboard_command(&["cat"], "hello");
        assert!(matches!(result, Some(Ok(()))));
    }

    #[test]
    fn test_clipboard_command_not_found() {
        let result = try_clipboard_command(&["nonexistent_clipboard_tool_xyz"], "hello");
        assert!(result.is_none());
    }

    #[test]
    fn test_clipboard_command_nonzero_exit() {
        // "false" exits with code 1
        let result = try_clipboard_command(&["false"], "hello");
        assert!(result.is_none());
    }

    #[test]
    fn test_clipboard_command_timeout() {
        use wait_timeout::ChildExt;

        // "sleep 60" hangs longer than the short timeout
        let mut child = Command::new("sleep")
            .arg("60")
            .stdin(Stdio::piped())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .unwrap();
        child.stdin.take();

        let result = child.wait_timeout(Duration::from_millis(50)).unwrap();
        assert!(result.is_none(), "expected timeout (None), got exit status");
        let _ = child.kill();
        let _ = child.wait();
    }

    #[test]
    fn test_copy_to_clipboard_no_tools() {
        // With no valid clipboard tool, should return an error
        // This test only verifies the function doesn't hang;
        // it may succeed if wl-copy/xclip/xsel is installed
        let result = copy_to_clipboard("test");
        // Either Ok (tool found) or Err (no tool) — both are valid, just must not hang
        assert!(result.is_ok() || result.is_err());
    }

    #[test]
    fn test_edit_variables_key_returns_edit_result() {
        let mut app = make_test_app();
        let key = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE);
        let result = handle_input(&mut app, key);
        match result {
            InputResult::EditVariables(name) => assert_eq!(name, "build"),
            _ => panic!("expected EditVariables, got something else"),
        }
    }

    #[test]
    fn test_edit_variables_no_job_sets_error() {
        let mut app = make_test_app();
        // Deselect by clearing selected_job
        app.current_tab_mut().selected_job = None;
        let key = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE);
        let result = handle_input(&mut app, key);
        assert!(matches!(result, InputResult::Continue));
        assert_eq!(app.error.as_ref().unwrap(), "No job selected");
    }
}
