use serde::Serialize;
use std::collections::BTreeMap;
use thiserror::Error;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
    UsageError,
    ConfigError,
    ProfileNotFound,
    ConfigInsecurePermissions,
    UnsupportedAction,
    HelpTopicNotFound,
    SchemaUnavailable,
    RemoteSchemaUnavailable,
    CapabilityProbeFailed,
    RemoteSchemaStale,
    AuthFailed,
    NetworkError,
    RosApiFailure,
    SecretBackendUnavailable,
    SecretNotFound,
    SecretDecryptFailed,
    SshHostKeyRequired,
    SshHostKeyMismatch,
    SshWhitelistRequired,
    SshWhitelistUnsafe,
    SshRestoreFailed,
    FileTooLarge,
    FileTransferFailed,
    InternalError,
}

#[derive(Debug, Clone, Serialize)]
pub struct ErrorContext {
    pub command: String,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub path: Vec<String>,
    #[serde(skip_serializing_if = "String::is_empty")]
    pub action: String,
    pub requested_protocol: String,
    pub selected_protocol: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub transfer_backend: Option<String>,
    pub routeros_version: String,
    pub host: String,
    pub resolved_args: BTreeMap<String, String>,
}

impl Default for ErrorContext {
    fn default() -> Self {
        Self {
            command: String::new(),
            path: Vec::new(),
            action: String::new(),
            requested_protocol: "unknown".to_owned(),
            selected_protocol: "unknown".to_owned(),
            transfer_backend: None,
            routeros_version: "unknown".to_owned(),
            host: String::new(),
            resolved_args: BTreeMap::new(),
        }
    }
}

#[derive(Debug, Clone, Error, Serialize)]
#[error("{message}")]
pub struct RosWireError {
    pub error_code: ErrorCode,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hint: Option<String>,
    pub context: ErrorContext,
    #[serde(skip)]
    pub exit_code: u8,
}

pub type RosWireResult<T> = Result<T, Box<RosWireError>>;

impl RosWireError {
    pub fn usage(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::UsageError,
            message: message.into(),
            hint: None,
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn config(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::ConfigError,
            message: message.into(),
            hint: None,
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn auth_failed(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::AuthFailed,
            message: message.into(),
            hint: None,
            context: ErrorContext::default(),
            exit_code: 3,
        }
    }

    pub fn network(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::NetworkError,
            message: message.into(),
            hint: None,
            context: ErrorContext::default(),
            exit_code: 4,
        }
    }

    pub fn ros_api_failure(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::RosApiFailure,
            message: message.into(),
            hint: None,
            context: ErrorContext::default(),
            exit_code: 1,
        }
    }

    pub fn secret_backend_unavailable(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::SecretBackendUnavailable,
            message: message.into(),
            hint: Some(
                "check keychain access or encrypted secret master key configuration".to_owned(),
            ),
            context: ErrorContext::default(),
            exit_code: 4,
        }
    }

    pub fn secret_not_found(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::SecretNotFound,
            message: message.into(),
            hint: Some(
                "run `roswire secret set ...` or update the referenced secret backend".to_owned(),
            ),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn secret_decrypt_failed(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::SecretDecryptFailed,
            message: message.into(),
            hint: Some("verify the encrypted secret master key and stored ciphertext".to_owned()),
            context: ErrorContext::default(),
            exit_code: 3,
        }
    }

    pub fn ssh_host_key_required(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::SshHostKeyRequired,
            message: message.into(),
            hint: Some(
                "set --ssh-host-key or profile ssh_host_key before using SSH transfer".to_owned(),
            ),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn ssh_host_key_mismatch(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::SshHostKeyMismatch,
            message: message.into(),
            hint: Some("verify the RouterOS SSH host key fingerprint out-of-band".to_owned()),
            context: ErrorContext::default(),
            exit_code: 3,
        }
    }

    pub fn ssh_whitelist_required(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::SshWhitelistRequired,
            message: message.into(),
            hint: Some("set --allow-from or profile allow_from to a narrow client CIDR".to_owned()),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn ssh_whitelist_unsafe(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::SshWhitelistUnsafe,
            message: message.into(),
            hint: Some(
                "use a narrow /32 IPv4 or /128 IPv6 client CIDR for SSH transfer".to_owned(),
            ),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn ssh_restore_failed(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::SshRestoreFailed,
            message: message.into(),
            hint: Some(
                "inspect `/ip service ssh` manually and restore the pre-transfer address/disabled state".to_owned(),
            ),
            context: ErrorContext::default(),
            exit_code: 6,
        }
    }

    pub fn file_too_large(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::FileTooLarge,
            message: message.into(),
            hint: Some("reduce file size or split the transfer into smaller artifacts".to_owned()),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn file_transfer_failed(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::FileTransferFailed,
            message: message.into(),
            hint: Some("check SSH credentials, permissions, paths, and available space".to_owned()),
            context: ErrorContext::default(),
            exit_code: 6,
        }
    }

    pub fn internal(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::InternalError,
            message: message.into(),
            hint: None,
            context: ErrorContext::default(),
            exit_code: 5,
        }
    }

    pub fn profile_not_found(profile: impl Into<String>) -> Self {
        let profile = profile.into();
        Self {
            error_code: ErrorCode::ProfileNotFound,
            message: format!("profile not found: {profile}"),
            hint: Some("set --profile or default_profile".to_owned()),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn config_insecure_permissions(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::ConfigInsecurePermissions,
            message: message.into(),
            hint: Some("fix permissions to 0700 for directories and 0600 for files".to_owned()),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn unsupported_action(message: impl Into<String>) -> Self {
        Self {
            error_code: ErrorCode::UnsupportedAction,
            message: message.into(),
            hint: Some("run `roswire commands --json` to discover supported commands".to_owned()),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn help_topic_not_found(topic: impl Into<String>) -> Self {
        let topic = topic.into();
        Self {
            error_code: ErrorCode::HelpTopicNotFound,
            message: format!("help topic not found: {topic}"),
            hint: Some("run `roswire commands --json` to discover available commands".to_owned()),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn schema_unavailable(topic: impl Into<String>) -> Self {
        let topic = topic.into();
        Self {
            error_code: ErrorCode::SchemaUnavailable,
            message: format!("schema unavailable: {topic}"),
            hint: Some("check command availability with `roswire commands --json`".to_owned()),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn remote_schema_unavailable() -> Self {
        Self {
            error_code: ErrorCode::RemoteSchemaUnavailable,
            message: "remote schema overlay is unavailable in current execution context".to_owned(),
            hint: Some("run local static schema commands without --remote or configure remote probe support".to_owned()),
            context: ErrorContext::default(),
            exit_code: 2,
        }
    }

    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
        self.hint = Some(hint.into());
        self
    }

    pub fn with_context(mut self, context: ErrorContext) -> Self {
        self.context = context;
        self
    }

    pub fn exit_code(&self) -> u8 {
        self.exit_code
    }

    pub fn to_json_payload(&self) -> String {
        serde_json::to_string_pretty(self).unwrap_or_else(|_| {
            "{\"error_code\":\"SERIALIZATION_ERROR\",\"message\":\"failed to serialize error\"}"
                .to_owned()
        })
    }

    pub fn print_to_stderr(&self) {
        let payload = self.to_json_payload();
        eprintln!("{payload}");
    }
}

pub fn redact_value(value: &str) -> String {
    if value.is_empty() {
        String::new()
    } else {
        "***REDACTED***".to_owned()
    }
}

pub fn is_sensitive_key(key: &str) -> bool {
    let lowercase = key.to_ascii_lowercase();
    if lowercase == "source" {
        return true;
    }

    [
        "password",
        "token",
        "secret",
        "passphrase",
        "private",
        "preshared",
        "ssh-key",
        "ssh_key",
        "ssh_password",
    ]
    .iter()
    .any(|needle| lowercase.contains(needle))
}

pub fn redact_resolved_args(args: &BTreeMap<String, String>) -> BTreeMap<String, String> {
    let mut sanitized = BTreeMap::new();
    for (key, value) in args {
        if is_sensitive_key(key) {
            sanitized.insert(key.clone(), redact_value(value));
        } else if looks_like_absolute_path(value) {
            sanitized.insert(key.clone(), redact_local_path(value));
        } else {
            sanitized.insert(key.clone(), value.clone());
        }
    }
    sanitized
}

fn looks_like_absolute_path(value: &str) -> bool {
    value.starts_with('/') || value.starts_with("~/")
}

fn redact_local_path(value: &str) -> String {
    let name = value
        .rsplit(['/', '\\'])
        .find(|part| !part.is_empty())
        .unwrap_or("path");
    format!("***REDACTED***/{name}")
}

#[cfg(test)]
mod tests {
    use super::{
        is_sensitive_key, redact_resolved_args, redact_value, ErrorCode, ErrorContext, RosWireError,
    };
    use std::collections::BTreeMap;

    #[test]
    fn usage_error_has_expected_code_and_exit_code() {
        let error = RosWireError::usage("missing arguments");
        assert_eq!(error.error_code, ErrorCode::UsageError);
        assert_eq!(error.message, "missing arguments");
        assert_eq!(error.exit_code(), 2);
    }

    #[test]
    fn internal_error_serializes_to_stable_json_shape() {
        let error = RosWireError::internal("unexpected");
        let payload = error.to_json_payload();
        let json: serde_json::Value =
            serde_json::from_str(&payload).expect("error payload should be valid JSON");

        assert_eq!(json["error_code"], "INTERNAL_ERROR");
        assert_eq!(json["message"], "unexpected");
        assert!(json.get("hint").is_none());
        assert!(json.get("context").is_some());
        assert!(payload.find("\"error_code\"") < payload.find("\"message\""));
        assert!(!payload.contains("timestamp"));
        assert!(!payload.contains("trace_id"));
    }

    #[test]
    fn print_to_stderr_does_not_panic() {
        RosWireError::usage("oops").print_to_stderr();
    }

    #[test]
    fn redaction_masks_sensitive_arguments() {
        let mut args = BTreeMap::new();
        args.insert("address".to_owned(), "192.168.88.2/24".to_owned());
        args.insert("password".to_owned(), "super-secret".to_owned());
        args.insert("key_passphrase".to_owned(), "phrase-secret".to_owned());
        args.insert("api_token".to_owned(), "abc123".to_owned());
        args.insert("src-path".to_owned(), "/Users/example/setup.rsc".to_owned());

        let sanitized = redact_resolved_args(&args);

        assert_eq!(
            sanitized.get("address").map(String::as_str),
            Some("192.168.88.2/24")
        );
        assert_eq!(
            sanitized.get("password").map(String::as_str),
            Some("***REDACTED***")
        );
        assert_eq!(
            sanitized.get("api_token").map(String::as_str),
            Some("***REDACTED***")
        );
        assert_eq!(
            sanitized.get("key_passphrase").map(String::as_str),
            Some("***REDACTED***")
        );
        assert_eq!(
            sanitized.get("src-path").map(String::as_str),
            Some("***REDACTED***/setup.rsc")
        );
    }

    #[test]
    fn sensitive_key_detection_is_case_insensitive() {
        assert!(is_sensitive_key("Password"));
        assert!(is_sensitive_key("SSH_KEY_PATH"));
        assert!(is_sensitive_key("ssh_key_passphrase"));
        assert!(is_sensitive_key("passphrase"));
        assert!(is_sensitive_key("privateKey"));
        assert!(is_sensitive_key("preshared-key"));
        assert!(is_sensitive_key("source"));
        assert!(!is_sensitive_key("interface"));
        assert!(!is_sensitive_key("src-address"));
    }

    #[test]
    fn constructor_exit_codes_match_contract() {
        let config = RosWireError::config("bad config");
        assert_eq!(config.error_code, ErrorCode::ConfigError);
        assert_eq!(config.exit_code(), 2);

        let profile_missing = RosWireError::profile_not_found("home");
        assert_eq!(profile_missing.error_code, ErrorCode::ProfileNotFound);
        assert_eq!(profile_missing.exit_code(), 2);

        let insecure = RosWireError::config_insecure_permissions("too wide");
        assert_eq!(insecure.error_code, ErrorCode::ConfigInsecurePermissions);
        assert_eq!(insecure.exit_code(), 2);

        let unsupported = RosWireError::unsupported_action("not implemented");
        assert_eq!(unsupported.error_code, ErrorCode::UnsupportedAction);
        assert_eq!(unsupported.exit_code(), 2);

        let help_missing = RosWireError::help_topic_not_found("foo bar");
        assert_eq!(help_missing.error_code, ErrorCode::HelpTopicNotFound);
        assert_eq!(help_missing.exit_code(), 2);

        let schema_missing = RosWireError::schema_unavailable("foo bar");
        assert_eq!(schema_missing.error_code, ErrorCode::SchemaUnavailable);
        assert_eq!(schema_missing.exit_code(), 2);

        let remote_unavailable = RosWireError::remote_schema_unavailable();
        assert_eq!(
            remote_unavailable.error_code,
            ErrorCode::RemoteSchemaUnavailable
        );
        assert_eq!(remote_unavailable.exit_code(), 2);

        let auth = RosWireError::auth_failed("invalid credentials");
        assert_eq!(auth.error_code, ErrorCode::AuthFailed);
        assert_eq!(auth.exit_code(), 3);

        let network = RosWireError::network("unreachable");
        assert_eq!(network.error_code, ErrorCode::NetworkError);
        assert_eq!(network.exit_code(), 4);

        let api = RosWireError::ros_api_failure("trap");
        assert_eq!(api.error_code, ErrorCode::RosApiFailure);
        assert_eq!(api.exit_code(), 1);

        let backend = RosWireError::secret_backend_unavailable("keychain unavailable");
        assert_eq!(backend.error_code, ErrorCode::SecretBackendUnavailable);
        assert_eq!(backend.exit_code(), 4);

        let missing = RosWireError::secret_not_found("secret missing");
        assert_eq!(missing.error_code, ErrorCode::SecretNotFound);
        assert_eq!(missing.exit_code(), 2);

        let decrypt = RosWireError::secret_decrypt_failed("decrypt failed");
        assert_eq!(decrypt.error_code, ErrorCode::SecretDecryptFailed);
        assert_eq!(decrypt.exit_code(), 3);

        let host_key = RosWireError::ssh_host_key_required("host key required");
        assert_eq!(host_key.error_code, ErrorCode::SshHostKeyRequired);
        assert_eq!(host_key.exit_code(), 2);

        let host_key_mismatch = RosWireError::ssh_host_key_mismatch("host key mismatch");
        assert_eq!(host_key_mismatch.error_code, ErrorCode::SshHostKeyMismatch);
        assert_eq!(host_key_mismatch.exit_code(), 3);

        let whitelist = RosWireError::ssh_whitelist_required("allow-from required");
        assert_eq!(whitelist.error_code, ErrorCode::SshWhitelistRequired);
        assert_eq!(whitelist.exit_code(), 2);

        let unsafe_whitelist = RosWireError::ssh_whitelist_unsafe("allow-from too wide");
        assert_eq!(unsafe_whitelist.error_code, ErrorCode::SshWhitelistUnsafe);
        assert_eq!(unsafe_whitelist.exit_code(), 2);

        let restore_failed = RosWireError::ssh_restore_failed("restore failed");
        assert_eq!(restore_failed.error_code, ErrorCode::SshRestoreFailed);
        assert_eq!(restore_failed.exit_code(), 6);

        let too_large = RosWireError::file_too_large("too large");
        assert_eq!(too_large.error_code, ErrorCode::FileTooLarge);
        assert_eq!(too_large.exit_code(), 2);

        let transfer_failed = RosWireError::file_transfer_failed("copy failed");
        assert_eq!(transfer_failed.error_code, ErrorCode::FileTransferFailed);
        assert_eq!(transfer_failed.exit_code(), 6);
    }

    #[test]
    fn hint_and_context_are_attached() {
        let mut args = BTreeMap::new();
        args.insert("interface".to_owned(), "bridge".to_owned());

        let context = ErrorContext {
            command: "ip/address/add".to_owned(),
            path: vec!["ip".to_owned(), "address".to_owned()],
            action: "add".to_owned(),
            requested_protocol: "auto".to_owned(),
            selected_protocol: "rest".to_owned(),
            transfer_backend: Some("ssh".to_owned()),
            routeros_version: "v7".to_owned(),
            host: "router.local".to_owned(),
            resolved_args: args,
        };

        let payload = RosWireError::usage("invalid interface")
            .with_hint("run interface print first")
            .with_context(context)
            .to_json_payload();

        let json: serde_json::Value =
            serde_json::from_str(&payload).expect("error payload should be valid JSON");

        assert_eq!(json["hint"], "run interface print first");
        assert_eq!(json["context"]["command"], "ip/address/add");
        assert_eq!(json["context"]["selected_protocol"], "rest");
        assert_eq!(json["context"]["transfer_backend"], "ssh");
    }

    #[test]
    fn redact_value_handles_empty_and_non_empty() {
        assert_eq!(redact_value(""), "");
        assert_eq!(redact_value("secret"), "***REDACTED***");
    }
}
