Documentation
use std::io::Write;
use std::process::{Command as ProcessCommand, Stdio};
use std::time::Duration;

use diwe::config::Command;
use liwe::markdown::MarkdownReader;
use liwe::model::node::{NodeIter, NodePointer};
use liwe::operations::Changes;

use super::templates;
use super::{Action, ActionContext, ActionProvider};

pub struct TransformBlockAction {
    pub title: String,
    pub identifier: String,
    pub command: String,
    pub input_template: String,
}

fn expand_env_var(value: &str) -> String {
    let mut result = String::with_capacity(value.len());
    let mut chars = value.chars().peekable();
    while let Some(c) = chars.next() {
        if c != '$' {
            result.push(c);
            continue;
        }
        match chars.peek() {
            Some('{') => {
                chars.next();
                let mut name = String::new();
                let mut closed = false;
                for nc in chars.by_ref() {
                    if nc == '}' {
                        closed = true;
                        break;
                    }
                    name.push(nc);
                }
                if closed {
                    result.push_str(&std::env::var(&name).unwrap_or_default());
                } else {
                    result.push('$');
                    result.push('{');
                    result.push_str(&name);
                }
            }
            Some(nc) if nc.is_alphanumeric() || *nc == '_' => {
                let mut name = String::new();
                while let Some(nc) = chars.peek() {
                    if nc.is_alphanumeric() || *nc == '_' {
                        name.push(*nc);
                        chars.next();
                    } else {
                        break;
                    }
                }
                result.push_str(&std::env::var(&name).unwrap_or_default());
            }
            _ => result.push('$'),
        }
    }
    result
}

fn execute_command(cmd: &Command, input: &str) -> Option<String> {
    let timeout = cmd.timeout_seconds.unwrap_or(120);
    let use_shell = cmd.shell.unwrap_or(true);

    let mut process = if use_shell {
        let mut p = ProcessCommand::new("sh");
        p.arg("-c").arg(&cmd.run);
        p
    } else {
        let mut p = ProcessCommand::new(&cmd.run);
        if let Some(args) = &cmd.args {
            p.args(args);
        }
        p
    };

    if let Some(cwd) = &cmd.cwd {
        process.current_dir(cwd);
    }

    if let Some(env) = &cmd.env {
        for (key, value) in env {
            let expanded = expand_env_var(value);
            process.env(key, expanded);
        }
    }

    process
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    let mut child = process.spawn().ok()?;

    if let Some(mut stdin) = child.stdin.take() {
        let _ = stdin.write_all(input.as_bytes());
    }

    let output = wait_with_timeout(&mut child, Duration::from_secs(timeout))?;

    if output.status.success() {
        Some(String::from_utf8_lossy(&output.stdout).to_string())
    } else {
        None
    }
}

fn wait_with_timeout(
    child: &mut std::process::Child,
    timeout: Duration,
) -> Option<std::process::Output> {
    use std::thread;
    use std::time::Instant;

    let start = Instant::now();
    let poll_interval = Duration::from_millis(100);

    loop {
        match child.try_wait() {
            Ok(Some(status)) => {
                let stdout = child
                    .stdout
                    .take()
                    .map(|mut s| {
                        let mut buf = Vec::new();
                        std::io::Read::read_to_end(&mut s, &mut buf).ok();
                        buf
                    })
                    .unwrap_or_default();
                let stderr = child
                    .stderr
                    .take()
                    .map(|mut s| {
                        let mut buf = Vec::new();
                        std::io::Read::read_to_end(&mut s, &mut buf).ok();
                        buf
                    })
                    .unwrap_or_default();
                return Some(std::process::Output {
                    status,
                    stdout,
                    stderr,
                });
            }
            Ok(None) => {
                if start.elapsed() >= timeout {
                    let _ = child.kill();
                    return None;
                }
                thread::sleep(poll_interval);
            }
            Err(_) => return None,
        }
    }
}

impl ActionProvider for TransformBlockAction {
    fn identifier(&self) -> String {
        format!("custom.{}", self.identifier)
    }

    fn action(
        &self,
        key: super::Key,
        selection: super::TextRange,
        context: impl ActionContext,
    ) -> Option<Action> {
        let _target_id = context.get_node_id_at(&key, selection.start.line as usize)?;
        Some(Action {
            title: self.title.clone(),
            identifier: self.identifier(),
            key: key.clone(),
            range: selection.clone(),
        })
    }

    fn changes(
        &self,
        key: super::Key,
        selection: super::TextRange,
        context: impl ActionContext,
    ) -> Option<Changes> {
        let target_id = context.get_node_id_at(&key, selection.start.line as usize)?;

        let tree = &context.collect(&key);

        let target_id = tree
            .get_surrounding_top_level_block(target_id)
            .unwrap_or(target_id);

        let input = templates::render_input_template(&self.input_template, target_id, tree);

        let command = context.get_command(&self.command)?;

        if command.run.is_empty() {
            return None;
        }

        let generated = execute_command(command, &input)?;

        let mut patch = context.patch();

        patch.from_markdown("new".into(), &generated, MarkdownReader::new());
        let tree = patch.maybe_key(&"new".into()).unwrap().collect_tree();

        let markdown = context
            .collect(&key)
            .replace(target_id, &tree)
            .iter()
            .to_text(&key.parent(), &context.format_options());

        Some(Changes::new().update(key, markdown))
    }
}

#[cfg(test)]
mod tests {
    use super::expand_env_var;

    #[test]
    fn multibyte_value_with_env_var_does_not_panic() {
        std::env::set_var("IWE_TEST_EXPAND_VALUE", "replaced");
        assert_eq!(
            expand_env_var("héllo $IWE_TEST_EXPAND_VALUE"),
            "héllo replaced"
        );
        assert_eq!(
            expand_env_var("${IWE_TEST_EXPAND_VALUE} café"),
            "replaced café"
        );
        assert_eq!(expand_env_var("café $missing_var €"), "café  €");
    }

    #[test]
    fn lone_dollar_and_unclosed_brace_are_kept() {
        assert_eq!(expand_env_var("price $ 5"), "price $ 5");
        assert_eq!(expand_env_var("trailing $"), "trailing $");
        assert_eq!(expand_env_var("open ${brace"), "open ${brace");
    }
}