use std::io;

use clap::{Parser, Subcommand, ValueEnum};
use serde::Serialize;

use crate::AppResult;

const DEFAULT_PROMPT_TIMEOUT_SECS: u64 = 172800;
const MAX_BUTTONS: usize = 8;
const MAX_BUTTON_LENGTH: usize = 50;

#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ParseMode {
    Markdown,
    MarkdownV2,
    Html,
}

impl ParseMode {
    pub(crate) fn wire_value(self) -> &'static str {
        match self {
            Self::Markdown | Self::MarkdownV2 => "MarkdownV2",
            Self::Html => "HTML",
        }
    }
}

impl Serialize for ParseMode {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(self.wire_value())
    }
}

#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Level {
    Info,
    Warn,
    Error,
}

#[derive(Subcommand, Debug)]
pub(crate) enum Command {
    /// Save your TGN token to the config file
    Login,
}

/// Send notifications to TGN (Telegram Notification Service)
#[derive(Parser, Debug)]
#[command(version, about)]
pub(crate) struct Args {
    #[command(subcommand)]
    pub(crate) command: Option<Command>,

    /// Message/file path to send (reads from stdin if not provided)
    #[arg()]
    pub(crate) message: Option<String>,

    /// Title prefix shown in brackets
    #[arg(short, long)]
    pub(crate) title: Option<String>,

    /// Severity level of the message
    #[arg(short, long)]
    pub(crate) level: Option<Level>,

    /// Telegram parse mode: HTML or MarkdownV2
    #[arg(short, long)]
    pub(crate) parse_mode: Option<ParseMode>,

    /// Run command and send its output
    #[arg(short = 'x', long = "exec")]
    pub(crate) exec: Option<String>,

    /// With --exec, repeat command every N seconds
    #[arg(short, long)]
    pub(crate) interval: Option<u64>,

    /// Comma-separated button labels for an interactive prompt
    #[arg(short, long)]
    pub(crate) buttons: Option<String>,

    /// Create a zip file from paths and send it
    #[arg(
        short = 'z',
        long,
        num_args = 1..,
        value_name = "PATHS",
        allow_hyphen_values = true
    )]
    pub(crate) zip: Vec<String>,

    /// Local timeout for prompt response polling, in seconds
    #[arg(long, default_value_t = DEFAULT_PROMPT_TIMEOUT_SECS)]
    pub(crate) timeout: u64,

    /// Enable debug output
    #[arg(short, long)]
    pub(crate) debug: bool,
}

pub(crate) fn parse_buttons(raw: &str) -> AppResult<Vec<String>> {
    let buttons = raw
        .split(',')
        .map(str::trim)
        .filter(|button| !button.is_empty())
        .map(str::to_string)
        .collect::<Vec<_>>();

    validate_buttons(&buttons)?;
    Ok(buttons)
}

fn validate_buttons(buttons: &[String]) -> AppResult<()> {
    if buttons.is_empty() {
        return Err(io::Error::other("At least one button is required").into());
    }

    if buttons.len() > MAX_BUTTONS {
        return Err(io::Error::other("Maximum 8 buttons allowed").into());
    }

    if buttons
        .iter()
        .any(|button| button.len() > MAX_BUTTON_LENGTH)
    {
        return Err(io::Error::other("Button text must be 50 characters or less").into());
    }

    Ok(())
}

pub(crate) fn long_message_caption(title: Option<String>, level: Option<Level>) -> Option<String> {
    let mut caption = title.unwrap_or_else(|| "Output".to_string());
    if let Some(level) = level {
        caption = format!("[{}] {}", level_name(level), caption);
    }
    Some(caption)
}

pub(crate) fn file_caption(title: Option<String>, level: Option<Level>) -> Option<String> {
    match (title, level) {
        (Some(title), Some(level)) => Some(format!("[{}] {}", level_name(level), title)),
        (Some(title), None) => Some(title),
        (None, Some(level)) => Some(format!("[{}]", level_name(level))),
        (None, None) => None,
    }
}

fn level_name(level: Level) -> &'static str {
    match level {
        Level::Info => "info",
        Level::Warn => "warn",
        Level::Error => "error",
    }
}

pub(crate) fn validate_mode_combinations(args: &Args) -> AppResult<()> {
    if args.interval.is_some() && args.exec.is_none() {
        return Err(io::Error::other("-i/--interval requires -x/--exec").into());
    }

    if args.buttons.is_some() && args.exec.is_some() {
        return Err(io::Error::other("--buttons cannot be combined with --exec").into());
    }

    if args.buttons.is_some() && !args.zip.is_empty() {
        return Err(io::Error::other("--buttons cannot be combined with --zip").into());
    }

    if args.exec.is_some() && !args.zip.is_empty() {
        return Err(io::Error::other("--exec cannot be combined with --zip").into());
    }

    Ok(())
}

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

    #[test]
    fn parse_mode_uses_server_wire_values() {
        assert_eq!(ParseMode::Html.wire_value(), "HTML");
        assert_eq!(ParseMode::Markdown.wire_value(), "MarkdownV2");
        assert_eq!(ParseMode::MarkdownV2.wire_value(), "MarkdownV2");
    }

    #[test]
    fn level_serializes_lowercase() {
        assert_eq!(serde_json::to_string(&Level::Info).unwrap(), "\"info\"");
        assert_eq!(serde_json::to_string(&Level::Warn).unwrap(), "\"warn\"");
        assert_eq!(serde_json::to_string(&Level::Error).unwrap(), "\"error\"");
    }

    #[test]
    fn parses_and_trims_buttons() {
        let buttons = parse_buttons(" yes, no ,later ").unwrap();
        assert_eq!(buttons, vec!["yes", "no", "later"]);
    }

    #[test]
    fn rejects_empty_buttons() {
        let error = parse_buttons(" , ").unwrap_err().to_string();
        assert!(error.contains("At least one button"));
    }

    #[test]
    fn rejects_too_many_buttons() {
        let raw = "a,b,c,d,e,f,g,h,i";
        let error = parse_buttons(raw).unwrap_err().to_string();
        assert!(error.contains("Maximum 8"));
    }

    #[test]
    fn rejects_too_long_button() {
        let raw = "a,b,abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
        let error = parse_buttons(raw).unwrap_err().to_string();
        assert!(error.contains("50 characters"));
    }

    #[test]
    fn formats_file_caption_like_bash() {
        assert_eq!(
            file_caption(Some("Deploy".to_string()), Some(Level::Warn)).unwrap(),
            "[warn] Deploy"
        );
        assert_eq!(file_caption(None, Some(Level::Error)).unwrap(), "[error]");
        assert_eq!(
            file_caption(Some("Report".to_string()), None).unwrap(),
            "Report"
        );
        assert!(file_caption(None, None).is_none());
    }
}
