// SPDX-FileCopyrightText: 2023 CELESTIFYX Team
// SPDX-License-Identifier: GPL-3.0-or-later

use std::{
    io::Error,
    borrow::Cow,
    iter::Peekable,
    str::Chars,

    process::{
        Command,
        ExitStatus,
        Output,
        Stdio
    }
};

use super::ports::Port;

pub(super) const COMMAND:        &str = env!("PKG_COMMAND");
pub(super) const REPO_LIST:      &str = env!("PKG_REPO_LIST");
pub(super) const INSTALLED_DB:   &str = env!("PKG_INSTALLED_DB");
pub(super) const MASK_FILE:      &str = env!("PKG_MASK_FILE");
pub(super) const BUILDFILE_NAME: &str = env!("PKG_BUILDFILE_NAME");

#[derive(Debug, thiserror::Error)]
pub(super) enum PkgError {
    #[error("failed to spawn `{}`: {}", cmd, source)]
    Spawn {
        cmd: String,
        #[source]
        source: Error
    },

    #[error("`{}` exited with status {}", cmd, status)]
    Status {
        cmd: String,
        status: i32
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Action {
    Install,
    Remove,
    Update,
    Rebuild
}

impl Action {
    pub(super) fn verb(self) -> &'static str {
        match self {
            Action::Install => "install",
            Action::Remove  => "remove",
            Action::Update  => "upgrade",
            Action::Rebuild => "install"
        }
    }

    pub(super) fn label(self) -> &'static str {
        match self {
            Action::Install => "Install",
            Action::Remove  => "Remove",
            Action::Update  => "Update",
            Action::Rebuild => "Rebuild"
        }
    }
}

fn strip_ansi(s: &str) -> String {
    let mut out: String = String::with_capacity(s.len());
    let mut chars: Peekable<Chars> = s.chars().peekable();

    while let Some(c) = (&mut chars).next() {
        if c != '\x1b' {
            (&mut out).push(c);
            continue;
        }

        if (&mut chars).next_if_eq(&'[').is_some() {
            for c in (&mut chars).by_ref() {
                if matches!(c, '@'..='~') {
                    break;
                }
            }
        }
    }

    out
}

pub(super) fn info(name: &str) -> String {
    let output: Output = match Command::new(COMMAND).args([
        "info", name
    ]).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).output() {
        Ok(o) => o,
        Err(e) => return format!("failed to run `{} info {}`: {}", COMMAND, name, e)
    };

    let raw: String = if output.status.success() {
        String::from_utf8_lossy(&output.stdout).into_owned()
    } else {
        let stderr: Cow<str> = String::from_utf8_lossy(&output.stderr);
        format!("`{} info {}` exited with {}\n{}", COMMAND, name, output.status, stderr.trim())
    };

    strip_ansi(&raw)
}

pub(super) fn sync() -> Result<(), PkgError> {
    let cmd_str: String = format!("{} sync", COMMAND);

    let status: ExitStatus = Command::new(COMMAND).args(["sync"]).stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status().map_err(|source: Error| PkgError::Spawn {
        cmd: cmd_str.clone(),
        source
    })?;

    if !status.success() {
        return Err(PkgError::Status {
            cmd: cmd_str,
            status: status.code().unwrap_or(-1)
        });
    }

    Ok(())
}

pub(super) fn toggle(port: &Port, action: Action) -> Result<(), PkgError> {
    let cmd_str: String = match action {
        Action::Rebuild => format!("{} install -fr {}", COMMAND, port.name),
        _               => format!("{} {} {}", COMMAND, action.verb(), port.name)
    };

    let status: ExitStatus = match action {
        Action::Rebuild => Command::new(COMMAND).args([
            "install", "-fr", &port.name
        ]).stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status().map_err(|source: Error| PkgError::Spawn {
            cmd: cmd_str.clone(),
            source
        })?,

        _ => Command::new(COMMAND).args([
            action.verb(), &port.name
        ]).stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status().map_err(|source: Error| PkgError::Spawn {
            cmd: cmd_str.clone(),
            source
        })?
    };

    if !status.success() {
        return Err(PkgError::Status {
            cmd: cmd_str,
            status: status.code().unwrap_or(-1)
        });
    }

    Ok(())
}
