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

use liwe::markdown::MarkdownReader;
use liwe::model::config::Command;
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 = value.to_string();
    let mut i = 0;
    while i < result.len() {
        if result[i..].starts_with('$') {
            let rest = &result[i + 1..];
            let (var_name, end_offset) = if rest.starts_with('{') {
                if let Some(close) = rest.find('}') {
                    (&rest[1..close], close + 2)
                } else {
                    i += 1;
                    continue;
                }
            } else {
                let end = rest
                    .find(|c: char| !c.is_alphanumeric() && c != '_')
                    .unwrap_or(rest.len());
                if end == 0 {
                    i += 1;
                    continue;
                }
                (&rest[..end], end + 1)
            };
            let replacement = std::env::var(var_name).unwrap_or_default();
            result = format!("{}{}{}", &result[..i], replacement, &result[i + end_offset..]);
            i += replacement.len();
        } else {
            i += 1;
        }
    }
    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_markdown(&key.parent(), context.markdown_options());

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