// Copyright (c) 2026 Blacknon. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.

// TODO(blacknon): outputやcommandの型をbyteに変更する
// TODO(blacknon): `command`は別のトコで保持するように変更する？(メモリの節約のため)
// TODO(blacknon): HWATCH_DATAのサイズが大きくなりすぎる場合、環境変数に出力できなくなるのでファイルへの出力をするオプションを追加する

// module
use chardetng::EncodingDetector;
use crossbeam_channel::Sender;
use flate2::{read::GzDecoder, write::GzEncoder};
#[cfg(unix)]
use nix::pty::{openpty, OpenptyResult, Winsize};
#[cfg(unix)]
use nix::sys::termios::{cfmakeraw, tcgetattr, tcsetattr, SetArg};
#[cfg(unix)]
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
#[cfg(unix)]
use std::os::fd::OwnedFd;
use std::process::{Command, Stdio};
use std::thread;
use tempfile::NamedTempFile;

// local module
use crate::common;
use crate::common::OutputMode;
use crate::event::AppEvent;

// struct for handling data during log/after exec
#[derive(Serialize, Deserialize)]
pub struct CommandResultData {
    pub timestamp: String,
    pub command: String,
    pub status: bool,
    pub output: String,
    pub stdout: String,
    pub stderr: String,
}

impl CommandResultData {
    pub fn generate_result(&self, is_compress: bool) -> CommandResult {
        let output = self.output.as_bytes().to_vec();
        let stdout = self.stdout.as_bytes().to_vec();
        let stderr = self.stderr.as_bytes().to_vec();

        CommandResult {
            timestamp: self.timestamp.clone(),
            command: self.command.clone(),
            status: self.status,
            is_compress,
            output: vec![],
            stdout: vec![],
            stderr: vec![],
        }
        .set_output(output)
        .set_stdout(stdout)
        .set_stderr(stderr)
    }
}

#[derive(Clone, Deserialize, Serialize)]
pub struct CommandResult {
    pub timestamp: String,
    pub command: String,
    pub status: bool,
    pub is_compress: bool,
    pub output: Vec<u8>,
    pub stdout: Vec<u8>,
    pub stderr: Vec<u8>,
}

impl Default for CommandResult {
    fn default() -> Self {
        CommandResult {
            timestamp: String::default(),
            command: String::default(),
            status: true,
            is_compress: false,
            output: vec![],
            stdout: vec![],
            stderr: vec![],
        }
    }
}

impl PartialEq for CommandResult {
    fn eq(&self, other: &Self) -> bool {
        self.command == other.command
            && self.status == other.status
            && self.output == other.output
            && self.stdout == other.stdout
            && self.stderr == other.stderr
    }
}

impl CommandResult {
    fn set_data(&self, data: Vec<u8>, data_type: OutputMode) -> Self {
        let u8_data = if self.is_compress {
            let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::default());
            encoder.write_all(&data).unwrap();
            encoder.finish().unwrap()
        } else {
            data
        };

        match data_type {
            OutputMode::Output => CommandResult {
                output: u8_data,
                ..self.clone()
            },
            OutputMode::Stdout => CommandResult {
                stdout: u8_data,
                ..self.clone()
            },
            OutputMode::Stderr => CommandResult {
                stderr: u8_data,
                ..self.clone()
            },
        }
    }

    pub fn set_output(&self, data: Vec<u8>) -> Self {
        self.set_data(data, OutputMode::Output)
    }

    pub fn set_stdout(&self, data: Vec<u8>) -> Self {
        self.set_data(data, OutputMode::Stdout)
    }

    pub fn set_stderr(&self, data: Vec<u8>) -> Self {
        self.set_data(data, OutputMode::Stderr)
    }

    fn get_data(&self, data_type: OutputMode) -> String {
        let data = match data_type {
            OutputMode::Output => &self.output,
            OutputMode::Stdout => &self.stdout,
            OutputMode::Stderr => &self.stderr,
        };

        if self.is_compress {
            let mut decoder = GzDecoder::new(&data[..]);
            let mut decoded = Vec::new();
            let _ = decoder.read_to_end(&mut decoded);
            decode_bytes(&decoded)
        } else {
            decode_bytes(data)
        }
    }

    pub fn get_output(&self) -> String {
        self.get_data(OutputMode::Output)
    }

    pub fn get_stdout(&self) -> String {
        self.get_data(OutputMode::Stdout)
    }

    pub fn get_stderr(&self) -> String {
        self.get_data(OutputMode::Stderr)
    }

    pub fn export_data(&self) -> CommandResultData {
        CommandResultData {
            timestamp: self.timestamp.clone(),
            command: self.command.clone(),
            status: self.status,
            output: self.get_output(),
            stdout: self.get_stdout(),
            stderr: self.get_stderr(),
        }
    }
}

fn decode_bytes(data: &[u8]) -> String {
    if data.is_empty() {
        return String::new();
    }

    let mut detector = EncodingDetector::new();
    detector.feed(data, true);
    let encoding = detector.guess(None, true);
    let (cow, _, _) = encoding.decode(data);
    cow.into_owned()
}

// TODO(blacknon): commandは削除？
pub struct ExecuteCommand {
    pub shell_command: String,
    pub command: Vec<String>,
    pub is_exec: bool,
    pub is_compress: bool,
    pub is_pty: bool,
    pub tx: Sender<AppEvent>,
}

impl ExecuteCommand {
    // set default value
    pub fn new(tx: Sender<AppEvent>) -> Self {
        Self {
            shell_command: "".to_string(),
            command: vec![],
            is_exec: false,
            is_compress: false,
            is_pty: false,
            tx,
        }
    }

    // exec command
    // TODO(blacknon): Resultからcommandを削除して、実行時はこのfunctionの引数として受け付けるように改修する？
    pub fn exec_command(&mut self) {
        let command_str = self.command.clone().join(" ");

        // create exec_commands...
        let exec_commands = match create_exec_cmd_args(
            self.is_exec,
            self.shell_command.clone(),
            command_str.clone(),
        ) {
            Ok(exec_commands) => exec_commands,
            Err(err) => {
                let result = CommandResult {
                    timestamp: common::now_str(),
                    command: command_str,
                    status: false,
                    is_compress: self.is_compress,
                    output: vec![],
                    stdout: vec![],
                    stderr: vec![],
                }
                .set_output(format!("{err}\n").into_bytes())
                .set_stderr(format!("{err}\n").into_bytes());

                let _ = self.tx.send(AppEvent::OutputUpdate(result));
                return;
            }
        };

        let (status, vec_output, vec_stdout, vec_stderr) =
            exec_command(&exec_commands, self.is_pty);

        // Set result
        let result = CommandResult {
            timestamp: common::now_str(),
            command: command_str,
            status,
            is_compress: self.is_compress,
            output: vec![],
            stdout: vec![],
            stderr: vec![],
        }
        .set_output(vec_output)
        .set_stdout(vec_stdout)
        .set_stderr(vec_stderr);

        // Send result
        let _ = self.tx.send(AppEvent::OutputUpdate(result));
    }
}

// TODO: 変化が発生した時の後処理コマンドを実行するためのstruct
#[derive(Serialize)]
pub struct ExecuteAfterResultData {
    pub before_result: CommandResultData,
    pub after_result: CommandResultData,
}

pub fn exec_after_command(
    shell_command: String,
    after_command: String,
    before: CommandResult,
    after: CommandResult,
    after_command_result_write_file: bool,
) {
    let before_result: CommandResultData = before.export_data();
    let after_result = after.export_data();

    let result_data = ExecuteAfterResultData {
        before_result,
        after_result,
    };

    // create json_data
    let json_data: String = match serde_json::to_string(&result_data) {
        Ok(json_data) => json_data,
        Err(err) => {
            eprintln!("Failed to serialize after command payload: {err}");
            return;
        }
    };

    // execute command
    let exec_commands = match create_exec_cmd_args(false, shell_command, after_command) {
        Ok(exec_commands) => exec_commands,
        Err(err) => {
            eprintln!("Failed to prepare after command: {err}");
            return;
        }
    };
    let length = exec_commands.len();

    // let mut hwatch_result_data = String::new();
    if after_command_result_write_file {
        let mut file = match NamedTempFile::new() {
            Ok(file) => file,
            Err(err) => {
                eprintln!("Failed to create temporary file for after command result: {err}");
                return;
            }
        };

        // write json_data to file
        if let Err(err) = file.write_all(json_data.as_bytes()) {
            eprintln!("Failed to write after command payload to file: {err}");
            return;
        }
        if let Err(err) = file.flush() {
            eprintln!("Failed to flush after command payload file: {err}");
            return;
        }

        let hwatch_result_data = file.path().to_string_lossy().into_owned();

        let child = Command::new(&exec_commands[0])
            .args(&exec_commands[1..length])
            .env("HWATCH_DATA", hwatch_result_data)
            .spawn();

        match child {
            Ok(mut child) => {
                let _ = child.wait();
            }
            Err(err) => {
                eprintln!("Failed to execute after command: {err}");
            }
        }

        if let Err(err) = file.close() {
            eprintln!("Failed to close temporary file for after command result: {err}");
        }
    } else {
        let hwatch_result_data = json_data;

        let child = Command::new(&exec_commands[0])
            .args(&exec_commands[1..length])
            .env("HWATCH_DATA", hwatch_result_data)
            .spawn();

        match child {
            Ok(mut child) => {
                let _ = child.wait();
            }
            Err(err) => {
                eprintln!("Failed to execute after command: {err}");
            }
        }
    }
}

//
fn create_exec_cmd_args(
    is_exec: bool,
    shell_command: String,
    command: String,
) -> Result<Vec<String>, String> {
    // Declaration at child.
    let mut exec_commands = vec![];
    let mut is_shellcmd_template = false;

    if is_exec {
        // is -x option enable
        exec_commands = shell_words::split(&command)
            .map_err(|err| format!("shell command parse error: {err}"))?;
    } else {
        // if `-e` option disable. (default)
        // split self.shell_command
        let shell_commands = shell_words::split(&shell_command)
            .map_err(|err| format!("shell command parse error: {err}"))?;
        if shell_commands.is_empty() {
            return Err("shell command parse error: shell command is empty".to_string());
        }

        // set shell_command args, to exec_cmd_args.
        exec_commands.push(shell_commands[0].to_string());

        if shell_commands.len() >= 2 {
            // set string command.
            let length = shell_commands.len();
            let shell_command_args = shell_commands[1..length].to_vec();

            // shell_command_args to exec_cmd_args
            for shell_command_arg in shell_command_args {
                if shell_command_arg == crate::SHELL_COMMAND_EXECCMD
                    && should_append_command_to_previous_arg(&exec_commands)
                {
                    if let Some(previous_arg) = exec_commands.last_mut() {
                        previous_arg.push(' ');
                        previous_arg.push_str(&command);
                        is_shellcmd_template = true;
                        continue;
                    }
                }

                let exec_cmd_arg = if shell_command_arg.contains("{COMMAND}") {
                    is_shellcmd_template = true;
                    str::replace(&shell_command_arg, crate::SHELL_COMMAND_EXECCMD, &command)
                } else {
                    shell_command_arg
                };

                // push exec_cmd_arg to exec_cmd_args
                exec_commands.push(exec_cmd_arg);
            }
        }

        // add exec command..
        if !is_shellcmd_template {
            exec_commands.push(command);
        }
    }

    Ok(exec_commands)
}

fn exec_command(exec_commands: &[String], is_pty: bool) -> (bool, Vec<u8>, Vec<u8>, Vec<u8>) {
    let length = exec_commands.len();
    let mut command = Command::new(&exec_commands[0]);
    command.args(&exec_commands[1..length]);

    // Switch depending on OS type
    #[cfg(unix)]
    let mut stdin_master: Option<OwnedFd> = None;
    #[cfg(not(unix))]
    let stdin_master: Option<()> = None;

    let stdout_reader;
    let stderr_reader;

    #[cfg(unix)]
    {
        if is_pty {
            let stdin_pty = create_raw_pty();
            let stdout_pty = create_raw_pty();
            let stderr_pty = create_raw_pty();

            let (stdin_pty, stdout_pty, stderr_pty) = match (stdin_pty, stdout_pty, stderr_pty) {
                (Ok(stdin_pty), Ok(stdout_pty), Ok(stderr_pty)) => {
                    (stdin_pty, stdout_pty, stderr_pty)
                }
                (Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => {
                    let error_msg = err.to_string().into_bytes();
                    return (false, Vec::new(), Vec::new(), error_msg);
                }
            };

            stdin_master = Some(stdin_pty.master);
            stdout_reader = ReaderHandle::Fd(stdout_pty.master);
            stderr_reader = ReaderHandle::Fd(stderr_pty.master);

            command
                .stdin(Stdio::from(stdin_pty.slave))
                .stdout(Stdio::from(stdout_pty.slave))
                .stderr(Stdio::from(stderr_pty.slave));
        } else {
            command.stdout(Stdio::piped()).stderr(Stdio::piped());
            stdout_reader = ReaderHandle::Pipe;
            stderr_reader = ReaderHandle::Pipe;
        }
    }

    #[cfg(not(unix))]
    {
        // On non-unix targets PTY support isn't available; always use pipes.
        command.stdout(Stdio::piped()).stderr(Stdio::piped());
        stdout_reader = ReaderHandle::Pipe;
        stderr_reader = ReaderHandle::Pipe;
    }

    let child_result = command.spawn();
    drop(command);
    let _ = stdin_master;
    let mut vec_output = Vec::new();
    let mut vec_stdout = Vec::new();
    let mut vec_stderr = Vec::new();

    let status = match child_result {
        Ok(mut child) => {
            let stdout_thread = match stdout_reader {
                #[cfg(unix)]
                ReaderHandle::Fd(fd) => thread::spawn(move || read_from_fd(fd, "stdout")),
                ReaderHandle::Pipe => match child.stdout.take() {
                    Some(child_stdout) => {
                        thread::spawn(move || read_from_pipe(child_stdout, "stdout"))
                    }
                    None => thread::spawn(|| {
                        Err("stdout pipe was not available on spawned process".to_string())
                    }),
                },
            };
            let stderr_thread = match stderr_reader {
                #[cfg(unix)]
                ReaderHandle::Fd(fd) => thread::spawn(move || read_from_fd(fd, "stderr")),
                ReaderHandle::Pipe => match child.stderr.take() {
                    Some(child_stderr) => {
                        thread::spawn(move || read_from_pipe(child_stderr, "stderr"))
                    }
                    None => thread::spawn(|| {
                        Err("stderr pipe was not available on spawned process".to_string())
                    }),
                },
            };

            let status = if is_pty {
                child.wait().map(|status| status.success()).unwrap_or(false)
            } else {
                false
            };
            vec_stdout = stdout_thread
                .join()
                .unwrap_or_else(|_| Err("Failed to join stdout thread".to_string()))
                .unwrap_or_else(|err| format!("{err}\n").into_bytes());
            vec_stderr = stderr_thread
                .join()
                .unwrap_or_else(|_| Err("Failed to join stderr thread".to_string()))
                .unwrap_or_else(|err| format!("{err}\n").into_bytes());
            vec_output = vec_stdout.clone();
            vec_output.extend_from_slice(&vec_stderr);

            if is_pty {
                status
            } else {
                child.wait().map(|status| status.success()).unwrap_or(false)
            }
        }
        Err(err) => {
            let error_msg = err.to_string();

            let mut stdout_text: Vec<u8> = error_msg.as_bytes().to_vec();
            let mut stderr_text: Vec<u8> = error_msg.as_bytes().to_vec();
            vec_output.append(&mut stdout_text);
            vec_stderr.append(&mut stderr_text);

            false
        }
    };

    (status, vec_output, vec_stdout, vec_stderr)
}

#[cfg(unix)]
enum ReaderHandle {
    Pipe,
    Fd(OwnedFd),
}

#[cfg(not(unix))]
enum ReaderHandle {
    Pipe,
}

#[cfg(unix)]
fn create_raw_pty() -> Result<OpenptyResult, nix::Error> {
    let winsize = Winsize {
        ws_row: 24,
        ws_col: 80,
        ws_xpixel: 0,
        ws_ypixel: 0,
    };
    let result = openpty(Some(&winsize), None)?;

    let mut termios = tcgetattr(&result.slave)?;
    cfmakeraw(&mut termios);
    tcsetattr(&result.slave, SetArg::TCSANOW, &termios)?;

    Ok(result)
}

#[cfg(unix)]
fn read_from_fd(fd: OwnedFd, label: &str) -> Result<Vec<u8>, String> {
    use std::io::ErrorKind;

    let mut file = File::from(fd);
    let mut buf = Vec::new();
    let mut chunk = [0_u8; 8192];

    loop {
        match file.read(&mut chunk) {
            Ok(0) => break,
            Ok(size) => buf.extend_from_slice(&chunk[..size]),
            Err(err) if err.kind() == ErrorKind::Interrupted => continue,
            // PTY masters can report EIO when the slave side closes; treat it as EOF.
            Err(err) if err.kind() == ErrorKind::UnexpectedEof || err.raw_os_error() == Some(5) => {
                break;
            }
            Err(err) => return Err(format!("Failed to read {label}: {err}")),
        }
    }

    Ok(buf)
}

fn read_from_pipe<R: Read>(reader: R, label: &str) -> Result<Vec<u8>, String> {
    let mut reader = BufReader::new(reader);
    let mut buf = Vec::new();
    reader
        .read_to_end(&mut buf)
        .map_err(|err| format!("Failed to read {label}: {err}"))?;
    Ok(buf)
}

fn should_append_command_to_previous_arg(exec_commands: &[String]) -> bool {
    exec_commands.len() >= 3 && exec_commands[1] == "-c"
}

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

    #[test]
    fn test_command_result_comparison() {
        let command_result = CommandResult::default();
        let command_result_2 = CommandResult {
            timestamp: "SomeOtherTime".to_string(),
            ..Default::default()
        };
        //Timestamp is not part of the comparison. Let's ensure it's different to prove.
        assert!(command_result == command_result_2);
    }

    #[test]
    fn test_command_result_default() {
        let command_result = CommandResult::default();
        assert_eq!(command_result.command, "".to_string());
    }

    #[test]
    fn test_command_result_clone() {
        let command_result = CommandResult::default();
        let command_result2 = command_result.clone();
        assert!(command_result == command_result2);
    }

    #[test]
    fn test_command_result_equality() {
        let command_result1 = CommandResult::default();
        let command_result2 = CommandResult::default();
        assert!(command_result1 == command_result2);
    }

    #[test]
    fn test_command_result_command_diff() {
        let command_result1 = CommandResult::default();
        let command_result2 = CommandResult {
            command: "different".to_string(),
            ..Default::default()
        };
        assert!(command_result1 != command_result2);
    }

    #[test]
    fn test_command_result_status_diff() {
        let command_result1 = CommandResult::default();
        let command_result2 = CommandResult {
            status: false,
            ..Default::default()
        };
        assert!(command_result1 != command_result2);
    }

    #[test]
    fn test_command_result_output_diff() {
        let command_result1 = CommandResult::default();
        let command_result2 = CommandResult::default().set_output("different".as_bytes().to_vec());
        assert!(command_result1 != command_result2);
    }

    #[test]
    fn test_command_result_stdout_diff() {
        let command_result1 = CommandResult::default();
        let command_result2 = CommandResult::default().set_stdout("different".as_bytes().to_vec());
        assert!(command_result1 != command_result2);
    }

    #[test]
    fn test_command_result_stderr_diff() {
        let command_result1 = CommandResult::default();
        let command_result2 = CommandResult::default().set_stderr("different".as_bytes().to_vec());
        assert!(command_result1 != command_result2);
    }

    #[test]
    fn test_decode_shift_jis_bytes() {
        let (encoded, _, _) = SHIFT_JIS.encode("日本語テスト");
        let command_result = CommandResult::default().set_output(encoded.into_owned());
        assert_eq!(command_result.get_output(), "日本語テスト");
    }

    #[test]
    fn test_command_result_data_generate_result_round_trips_compressed_fields() {
        let data = CommandResultData {
            timestamp: "2026-04-08 12:00:00.000".to_string(),
            command: "echo hi".to_string(),
            status: true,
            output: "joined".to_string(),
            stdout: "stdout".to_string(),
            stderr: "stderr".to_string(),
        };

        let result = data.generate_result(true);

        assert!(result.is_compress);
        assert_eq!(result.get_output(), "joined");
        assert_eq!(result.get_stdout(), "stdout");
        assert_eq!(result.get_stderr(), "stderr");
    }

    #[test]
    fn test_command_result_export_data_decodes_compressed_buffers() {
        let result = CommandResult {
            timestamp: "2026-04-08 12:00:00.000".to_string(),
            command: "echo hi".to_string(),
            status: false,
            is_compress: true,
            output: vec![],
            stdout: vec![],
            stderr: vec![],
        }
        .set_output(b"joined".to_vec())
        .set_stdout(b"out".to_vec())
        .set_stderr(b"err".to_vec());

        let exported = result.export_data();

        assert_eq!(exported.output, "joined");
        assert_eq!(exported.stdout, "out");
        assert_eq!(exported.stderr, "err");
        assert!(!exported.status);
    }

    #[test]
    fn test_exec_command_without_force_color_stdout_is_not_tty() {
        let exec_commands = vec![
            "sh".to_string(),
            "-c".to_string(),
            "if [ -t 1 ]; then printf tty; else printf notty; fi".to_string(),
        ];

        let (_, _, stdout, _) = exec_command(&exec_commands, false);
        assert_eq!(String::from_utf8(stdout).unwrap(), "notty");
    }

    #[test]
    fn test_exec_command_with_force_color_stdout_is_tty() {
        let exec_commands = vec![
            "sh".to_string(),
            "-c".to_string(),
            "if [ -t 1 ]; then printf tty; else printf notty; fi".to_string(),
        ];

        let (_, _, stdout, _) = exec_command(&exec_commands, true);
        assert_eq!(String::from_utf8(stdout).unwrap(), "tty");
    }

    #[test]
    fn test_exec_command_with_force_color_stdin_is_tty() {
        let exec_commands = vec![
            "sh".to_string(),
            "-c".to_string(),
            "if [ -t 0 ]; then printf tty; else printf notty; fi".to_string(),
        ];

        let (_, _, stdout, _) = exec_command(&exec_commands, true);
        assert_eq!(String::from_utf8(stdout).unwrap(), "tty");
    }

    #[test]
    fn test_create_exec_cmd_args_replaces_template_in_argument() {
        let exec_commands =
            create_exec_cmd_args(false, "bash -c {COMMAND}".to_string(), "ls -la".to_string())
                .unwrap();

        assert_eq!(
            exec_commands,
            vec!["bash".to_string(), "-c".to_string(), "ls -la".to_string(),]
        );
    }

    #[test]
    fn test_create_exec_cmd_args_keeps_shell_command_string() {
        let exec_commands =
            create_exec_cmd_args(false, "sh -c".to_string(), "ls -la;pwd".to_string()).unwrap();

        assert_eq!(
            exec_commands,
            vec!["sh".to_string(), "-c".to_string(), "ls -la;pwd".to_string(),]
        );
    }

    #[test]
    fn test_create_exec_cmd_args_appends_command_after_dash_c_script() {
        let exec_commands = create_exec_cmd_args(
            false,
            "bash -c \"source ~/.bashrc\"; {COMMAND}".to_string(),
            "ls -la".to_string(),
        )
        .unwrap();

        assert_eq!(
            exec_commands,
            vec![
                "bash".to_string(),
                "-c".to_string(),
                "source ~/.bashrc; ls -la".to_string(),
            ]
        );
    }

    #[test]
    fn test_create_exec_cmd_args_splits_exec_mode_command() {
        let exec_commands =
            create_exec_cmd_args(true, "ignored".to_string(), "echo hello".to_string()).unwrap();

        assert_eq!(exec_commands, vec!["echo".to_string(), "hello".to_string()]);
    }

    #[test]
    fn test_create_exec_cmd_args_rejects_invalid_shell_command() {
        let err =
            create_exec_cmd_args(false, "\"".to_string(), "echo hello".to_string()).unwrap_err();

        assert!(err.contains("shell command parse error"));
    }

    #[test]
    fn test_create_exec_cmd_args_rejects_invalid_exec_command() {
        let err = create_exec_cmd_args(true, "ignored".to_string(), "\"".to_string()).unwrap_err();

        assert!(err.contains("shell command parse error"));
    }
}
