use anyhow::Error;
use std::process::{Command, ExitStatus};

pub fn channel(exe: &std::path::Path, repo: &std::path::Path) -> Result<ChannelsList, Error> {
pub fn channel(exe: &std::path::Path, repo: &std::path::Path) -> Result<ChannelsList, Error> {
    let mut cmd = Command::new(exe);
    let cmd = cmd
        .arg("channel")
        .arg("--no-prompt")
        .arg("--repository")
        .arg(repo);
    let output = cmd.output()?;
    check_exit_status(cmd, output.status)?;
    parse_channel_output(output.stdout)
}

fn parse_channel_output(output: Vec<u8>) -> Result<ChannelsList, Error> {
    let channels_str = String::from_utf8(output)?;
    let channels_list = channels_str.split('\n').map(|substr| substr.trim()).fold(
        ChannelsList::new(),
        |mut channels_list, ch_str| {
            if ch_str.starts_with("* ") {
                channels_list.active = channels_list.entries.len();
                channels_list.entries.push(ch_str[2..].to_string());
            } else {
                channels_list.entries.push(ch_str.to_string());
            }
            channels_list
        },
    );
    Ok(channels_list)
    parse_channel_output(output.stdout)
}

fn parse_channel_output(output: Vec<u8>) -> Result<ChannelsList, Error> {
    let channels_str = String::from_utf8(output)?;
    let channels_list = channels_str.split('\n').map(|substr| substr.trim()).fold(
        ChannelsList::new(),
        |mut channels_list, ch_str| {
            if ch_str.starts_with("* ") {
                channels_list.active = channels_list.entries.len();
                channels_list.entries.push(ch_str[2..].to_string());
            } else {
                channels_list.entries.push(ch_str.to_string());
            }
            channels_list
        },
    );
    Ok(channels_list)
}

pub fn log(
    exe: &std::path::Path,
    repo: &std::path::Path,
    ch_name_maybe: Option<&str>,
) -> Result<Vec<ChangelogEntry>, Error> {
    let mut cmd = Command::new(exe);
    cmd.arg("log")
        .arg("--no-prompt")
        .arg("--repository")
        .arg(repo)
        .arg("--output-format")
        .arg("json");
    if let Some(ch_name) = ch_name_maybe {
        cmd.arg("--channel").arg(ch_name);
    }
    let output = cmd.output()?;
    check_exit_status(&cmd, output.status)?;
    Ok(if output.stdout.is_empty() {
        Vec::new()
    } else {
        serde_json::from_slice(&output.stdout)?
    })
}

pub fn change(exe: &std::path::Path, repo: &std::path::Path, hash: &str) -> Result<String, Error> {
    let mut cmd = Command::new(exe);
    let cmd = cmd
        .arg("change")
        .arg("--no-prompt")
        .arg("--repository")
        .arg(repo)
        .arg(hash);
    let output = cmd.output()?;
    check_exit_status(cmd, output.status)?;
    // TODO this will fail when diffing binary data
    Ok(String::from_utf8(output.stdout)?)
}

pub fn diff(
    exe: &std::path::Path,
    repo: &std::path::Path,
    ch_name_maybe: Option<&str>,
) -> Result<String, Error> {
    let mut cmd = Command::new(exe);
    let cmd = cmd
        .arg("diff")
        .arg("--no-prompt")
        .arg("--repository")
        .arg(repo);
    if let Some(ch_name) = ch_name_maybe {
        cmd.arg("--channel").arg(ch_name);
    }
    let output = cmd.output()?;
    check_exit_status(cmd, output.status)?;
    // TODO this will fail when diffing binary data
    Ok(String::from_utf8(output.stdout)?)
}

pub fn diff_short(
    exe: &std::path::Path,
    repo: &std::path::Path,
    ch_name_maybe: Option<&str>,
) -> Result<serde_json::Map<String, serde_json::Value>, Error> {
    let mut cmd = Command::new(exe);
    let cmd = cmd
        .arg("diff")
        .arg("--no-prompt")
        .arg("--repository")
        .arg(repo)
        .arg("--short")
        .arg("--json");
    if let Some(ch_name) = ch_name_maybe {
        cmd.arg("--channel").arg(ch_name);
    }
    let output = cmd.output()?;
    check_exit_status(cmd, output.status)?;
    // TODO this will fail when diffing binary data
    Ok(if output.stdout.is_empty() {
        serde_json::Map::new()
    } else {
        serde_json::from_slice(&output.stdout)?
    })
}

///////////////////////////////////////////////////////////////////////////////
// copied from https://users.rust-lang.org/t/best-error-handing-practices-when-using-std-command/42259/7
fn check_exit_status(cmd: &Command, exit_status: ExitStatus) -> Result<(), Error> {
    if exit_status.success() {
        Ok(())
    } else {
        Err(Error::from(ExecutionError {
            command: format!("{:?}", cmd),
            exit_code: exit_status.code(),
        }))
    }
}

/// Execution of a [`Command`] finished with an unsuccessful error code.
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[error("Received an exit code of {} from {}", exit_code.unwrap_or(1), command)]
pub struct ExecutionError {
    pub command: String,
    pub exit_code: Option<i32>,
}

#[derive(Debug)]
pub struct ChannelsList {
    pub(crate) entries: Vec<String>,
    pub(crate) active: usize,
}
// end copy
///////////////////////////////////////////////////////////////////////////////

#[derive(Debug)]
pub struct ChannelsList {
    pub(crate) entries: Vec<String>,
    pub(crate) active: usize,
}

impl ChannelsList {
    pub(crate) fn new() -> Self {
        Self {
            entries: Vec::new(),
            active: usize::MAX,
        }
    }
}

impl ChannelsList {
    pub(crate) fn new() -> Self {
        Self {
            entries: Vec::new(),
            active: usize::MAX,
        }
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct ChangelogEntry {
    pub(crate) hash: String,
    pub(crate) authors: Vec<String>,
    pub(crate) timestamp: String,
    pub(crate) message: String,
}