use std::path::PathBuf;
use std::str::FromStr;

use clap::*;
use clap_complete::Shell;

/// Main struct for command-line arguments, parsed via Clap.
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
pub struct Args {
    #[command(flatten)]
    pub mode: ModeArgs,
    #[command(flatten)]
    pub item: ItemArgs,
    #[command(flatten)]
    pub options: OptionsArgs,

    #[arg(help("A list of either item IDs or tags"))]
    #[arg(value_parser = clap::value_parser!(NumberOrString))]
    #[arg(required = false)]
    pub ids_or_tags: Vec<NumberOrString>,
}

/// Struct for mode-specific arguments, defining CLI flags for different operations.
#[derive(Parser, Debug, Clone)]
pub struct ModeArgs {
    #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["get", "diff", "list", "delete", "info", "update", "status", "export", "import"]))]
    #[arg(help("Save an item using any tags or metadata provided"))]
    pub save: bool,

    #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "diff", "list", "delete", "info", "update", "status", "export", "import"]))]
    #[arg(help(
        "Get an item either by it's ID or by a combination of matching tags and metatdata"
    ))]
    pub get: bool,

    #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "list", "delete", "info", "update", "status", "export", "import"]))]
    #[arg(help("Show a diff between two items by ID"))]
    pub diff: bool,

    #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "delete", "info", "update", "status", "export", "import"]))]
    #[arg(help("List items, filtering on tags or metadata if given"))]
    pub list: bool,

    #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "info", "update", "status", "export", "import"]))]
    #[arg(help("Delete items either by ID or by matching tags"))]
    #[arg(requires = "ids_or_tags")]
    pub delete: bool,

    #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "delete", "update", "status", "export", "import"]))]
    #[arg(help(
        "Get an item either by it's ID or by a combination of matching tags and metatdata"
    ))]
    pub info: bool,

    #[arg(group("mode"), help_heading("Mode Options"), short('u'), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "status", "export", "import"]))]
    #[arg(help("Update an item's tags and metadata by ID"))]
    pub update: bool,

    #[arg(group("mode"), help_heading("Mode Options"), short('S'), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "server", "status_plugins", "export", "import"]))]
    #[arg(help("Show status of directories and supported compression algorithms"))]
    pub status: bool,

    #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "server", "export", "import"]))]
    #[arg(help("Show available plugins and their configurations"))]
    pub status_plugins: bool,

    #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "import"]))]
    #[arg(help("Export an item to data and metadata files (default: latest item)"))]
    pub export: bool,

    #[arg(group("mode"), help_heading("Mode Options"), long, value_name("META_FILE"), conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "export"]))]
    #[arg(help("Import an item from a metadata file (data from --import-data-file or stdin)"))]
    pub import: Option<String>,

    #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status"]))]
    #[arg(help("Start REST HTTP server"))]
    pub server: bool,

    #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "server", "export", "import"]))]
    #[arg(help("Generate default configuration and output to stdout"))]
    pub generate_config: bool,

    #[arg(help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "server", "generate_config", "export", "import"]))]
    #[arg(help("Generate shell completion script"))]
    pub generate_completion: Option<Shell>,

    #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_ADDRESS"))]
    #[arg(help("Server address to bind to"))]
    pub server_address: Option<String>,

    #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_PORT"))]
    #[arg(help("Server port to bind to"))]
    pub server_port: Option<u16>,

    #[cfg(feature = "tls")]
    #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_CERT"))]
    #[arg(help("Path to TLS certificate file (PEM) for HTTPS"))]
    pub server_cert: Option<PathBuf>,

    #[cfg(feature = "tls")]
    #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_KEY"))]
    #[arg(help("Path to TLS private key file (PEM) for HTTPS"))]
    pub server_key: Option<PathBuf>,
}

/// Represents a meta plugin argument with optional JSON config.
///
/// Parsed from `name` or `name:{"options":{...},"outputs":{...}}` syntax.
#[derive(Debug, Clone)]
pub struct MetaPluginArg {
    pub name: String,
    pub options: Option<serde_json::Value>,
}

impl FromStr for MetaPluginArg {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Some((name, json_str)) = s.split_once(':') {
            let value: serde_json::Value = serde_json::from_str(json_str)
                .map_err(|e| anyhow::anyhow!("Invalid JSON for meta plugin '{}': {}", name, e))?;
            Ok(MetaPluginArg {
                name: name.to_string(),
                options: Some(value),
            })
        } else {
            Ok(MetaPluginArg {
                name: s.to_string(),
                options: None,
            })
        }
    }
}

/// Represents a metadata key-value argument.
///
/// Parsed from `key=value` (set) or `key` (delete/filter by existence).
#[derive(Debug, Clone)]
pub enum MetaArg {
    /// Set metadata with a value.
    Set { key: String, value: String },
    /// Bare key without a value (delete in update mode, filter by existence otherwise).
    Key(String),
}

impl MetaArg {
    /// Returns the key.
    pub fn key(&self) -> &str {
        match self {
            MetaArg::Set { key, .. } | MetaArg::Key(key) => key,
        }
    }

    /// Returns the value if this is a Set variant.
    pub fn value(&self) -> Option<&str> {
        match self {
            MetaArg::Set { value, .. } => Some(value),
            MetaArg::Key(_) => None,
        }
    }
}

impl FromStr for MetaArg {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Some((key, value)) = s.split_once('=') {
            Ok(MetaArg::Set {
                key: key.to_string(),
                value: value.to_string(),
            })
        } else {
            Ok(MetaArg::Key(s.to_string()))
        }
    }
}

/// Struct for item-specific arguments, such as compression and plugins.
#[derive(Parser, Debug, Clone)]
pub struct ItemArgs {
    #[arg(help_heading("Item Options"), short, long, env("KEEP_COMPRESSION"))]
    #[arg(help("Compression algorithm to use when saving items"))]
    pub compression: Option<String>,

    #[arg(
        help_heading("Item Options"),
        short('M'),
        long = "meta-plugin",
        value_parser = clap::value_parser!(MetaPluginArg),
        env("KEEP_META_PLUGINS")
    )]
    #[arg(help("Meta plugin to use (repeatable): name or name:{json}"))]
    pub meta_plugins: Vec<MetaPluginArg>,

    #[arg(help_heading("Item Options"), long)]
    #[arg(help("Metadata key=value to set (or key to delete in --update)"))]
    pub meta: Vec<String>,

    #[arg(help_heading("Item Options"), long, env("KEEP_FILTERS"))]
    #[arg(help("Filter string to apply to content when getting items"))]
    pub filters: Option<String>,

    #[arg(
        help_heading("Export Options"),
        long,
        default_value = "{id}_{tags}_{ts}"
    )]
    #[arg(help("Template for export filename. Variables: {id} {tags} {ts} {compression}"))]
    pub export_filename_format: String,

    #[arg(help_heading("Import Options"), long, value_name("DATA_FILE"))]
    #[arg(help("Data file for import (reads from stdin if omitted)"))]
    pub import_data_file: Option<PathBuf>,
}

/// Struct for general options, including verbosity, paths, and output settings.
#[derive(Parser, Debug, Default, Clone)]
pub struct OptionsArgs {
    #[arg(long, env("KEEP_CONFIG"))]
    #[arg(help("Specify the configuration file to use"))]
    pub config: Option<PathBuf>,

    #[arg(long, env("KEEP_DIR"))]
    #[arg(help("Specify the directory to use for storage"))]
    pub dir: Option<PathBuf>,

    #[arg(
        long,
        env("KEEP_LIST_FORMAT"),
        default_value("id,time,size,tags,meta:hostname")
    )]
    #[arg(help("A comma separated list of columns to display with --list"))]
    pub list_format: String,

    #[arg(short('H'), long)]
    #[arg(help("Display file sizes with units"))]
    pub human_readable: bool,

    #[arg(short, long, action = clap::ArgAction::Count, conflicts_with("quiet"))]
    #[arg(help("Increase message verbosity, can be given more than once"))]
    pub verbose: u8,

    #[arg(short, long)]
    #[arg(help("Do not show any messages"))]
    pub quiet: bool,

    #[arg(long, value_enum, default_value("table"))]
    #[arg(help("Output format (only works with --info, --status, --list)"))]
    pub output_format: Option<String>,

    #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_PASSWORD"))]
    #[arg(help("Password for server authentication (requires --server)"))]
    pub server_password: Option<String>,

    #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_PASSWORD_HASH"))]
    #[arg(help("Password hash for server authentication (requires --server)"))]
    pub server_password_hash: Option<String>,

    #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_USERNAME"))]
    #[arg(help(
        "Username for server Basic authentication (requires --server, defaults to 'keep')"
    ))]
    pub server_username: Option<String>,

    #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_JWT_SECRET"))]
    #[arg(help("JWT secret for token-based authentication (requires --server)"))]
    pub server_jwt_secret: Option<String>,

    #[arg(
        help_heading("Server Options"),
        long,
        env("KEEP_SERVER_JWT_SECRET_FILE")
    )]
    #[arg(help("Path to file containing JWT secret (requires --server)"))]
    pub server_jwt_secret_file: Option<PathBuf>,

    #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_MAX_BODY_SIZE"))]
    #[arg(help("Maximum request body size in bytes (requires --server, default: unlimited)"))]
    pub server_max_body_size: Option<u64>,

    #[cfg(feature = "client")]
    #[arg(long, env("KEEP_CLIENT_URL"), help_heading("Client Options"))]
    #[arg(help("Remote keep server URL for client mode"))]
    pub client_url: Option<String>,

    #[cfg(feature = "client")]
    #[arg(long, env("KEEP_CLIENT_PASSWORD"), help_heading("Client Options"))]
    #[arg(help("Password for remote keep server authentication"))]
    pub client_password: Option<String>,

    #[cfg(feature = "client")]
    #[arg(long, env("KEEP_CLIENT_USERNAME"), help_heading("Client Options"))]
    #[arg(help("Username for remote keep server authentication (defaults to 'keep')"))]
    pub client_username: Option<String>,

    #[cfg(feature = "client")]
    #[arg(long, env("KEEP_CLIENT_JWT"), help_heading("Client Options"))]
    #[arg(help("JWT token for remote keep server authentication"))]
    pub client_jwt: Option<String>,

    #[arg(
        long,
        help("Force output even when binary data would be sent to a TTY")
    )]
    pub force: bool,
}

/// Enum for representing either a number (item ID) or a string (tag).
#[derive(Debug, Clone)]
pub enum NumberOrString {
    Number(i64),
    Str(String),
}

impl FromStr for NumberOrString {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(s.parse::<i64>()
            .map(NumberOrString::Number)
            .unwrap_or_else(|_| NumberOrString::Str(s.to_string())))
    }
}

/// Validates the parsed arguments based on mode constraints.
///
/// # Returns
///
/// `Result<(), String>` - Ok if valid, or an error message string.
impl Args {
    /// Validate the arguments based on the selected mode
    pub fn validate(&self) -> Result<(), String> {
        // Check if --delete is used and ids_or_tags is empty
        if self.mode.delete && self.ids_or_tags.is_empty() {
            return Err("At least one ID is required when using --delete".to_string());
        }

        // Check if --delete is used and any of the ids_or_tags are tags (strings)
        if self.mode.delete {
            for item in &self.ids_or_tags {
                if let NumberOrString::Str(_) = item {
                    return Err("Tags are not supported for --delete, only IDs".to_string());
                }
            }
        }

        Ok(())
    }
}
