use std::error::Error;
use std::convert::AsRef;
use std::ffi::OsString;
use std::sync::mpsc;
use std::string::FromUtf8Error;
use strum_macros::AsRefStr;

use thiserror::Error as ThisError;
use aws_sdk_sts::{
    types::SdkError as AwsSdkError, 
    error::GetSessionTokenError,
};
use sqlx::{
    error::Error as SqlxError,
    migrate::MigrateError,
};
use tauri::api::dialog::{
    MessageDialogBuilder, 
    MessageDialogKind,
};
use serde::{
    Serialize,
    Serializer,
    ser::SerializeMap,
    Deserialize,
};


pub trait ShowError {
    fn error_popup(self, title: &str);
    fn error_popup_nowait(self, title: &str);
    fn error_print(self);
    fn error_print_prefix(self, prefix: &str);
}

impl<E: std::fmt::Display> ShowError for Result<(), E> {
    fn error_popup(self, title: &str) {
        if let Err(e) = self {
            let (tx, rx) = mpsc::channel();
            MessageDialogBuilder::new(title, format!("{e}"))
                .kind(MessageDialogKind::Error)
                .show(move |_| tx.send(true).unwrap());

            rx.recv().unwrap();
        }
    }

    fn error_popup_nowait(self, title: &str) {
        if let Err(e) = self {
            MessageDialogBuilder::new(title, format!("{e}"))
                .kind(MessageDialogKind::Error)
                .show(|_| {})
        }
    }

    fn error_print(self) {
        if let Err(e) = self {
            eprintln!("{e}");
        }
    }

    fn error_print_prefix(self, prefix: &str) {
        if let Err(e) = self {
            eprintln!("{prefix}: {e}");
        }
    }
}


fn serialize_basic_err<E, S>(err: &E, serializer: S) -> Result<S::Ok, S::Error>
where
    E: std::error::Error + AsRef<str>,
    S: Serializer,
{
    let mut map = serializer.serialize_map(None)?;
    map.serialize_entry("code", err.as_ref())?;
    map.serialize_entry("msg", &format!("{err}"))?;
    if let Some(src) = err.source() {
        map.serialize_entry("source", &format!("{src}"))?;
    }
    map.end()
}


fn serialize_upstream_err<E, M>(err: &E, map: &mut M) -> Result<(), M::Error> 
where
    E: Error,
    M: serde::ser::SerializeMap,
{
    let msg = err.source().map(|s| format!("{s}"));
    map.serialize_entry("msg", &msg)?;
    map.serialize_entry("code", &None::<&str>)?;
    map.serialize_entry("source", &None::<&str>)?;

    Ok(())
}


macro_rules! impl_serialize_basic {
    ($err_type:ident) => {
        impl Serialize for $err_type {
            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
                serialize_basic_err(self, serializer)
            }
        }
    }
}


// error during initial setup (primarily loading state from db)
#[derive(Debug, ThisError, AsRefStr)]
pub enum SetupError {
    #[error("Invalid database record")]
    InvalidRecord, // e.g. wrong size blob for nonce or salt
    #[error("Error from database: {0}")]
    DbError(#[from] SqlxError),
    #[error("Error running migrations: {0}")]
    MigrationError(#[from] MigrateError),
    #[error("Error parsing configuration from database")]
    ConfigParseError(#[from] serde_json::Error),
    #[error("Failed to set up start-on-login: {0}")]
    AutoLaunchError(#[from] auto_launch::Error),
    #[error("Failed to start listener: {0}")]
    ServerSetupError(#[from] std::io::Error),
    #[error("Failed to resolve data directory: {0}")]
    DataDir(#[from] DataDirError),
    #[error("Failed to register hotkeys: {0}")]
    RegisterHotkeys(#[from] tauri::Error),
}


#[derive(Debug, ThisError, AsRefStr)]
pub enum DataDirError {
    #[error("Could not determine data directory")]
    NotFound,
    #[error("Failed to create data directory: {0}")]
    Io(#[from] std::io::Error),
}


// error when attempting to tell a request handler whether to release or deny credentials
#[derive(Debug, ThisError, AsRefStr)]
pub enum SendResponseError {
    #[error("The specified credentials request was not found")]
    NotFound,
    #[error("The specified request was already closed by the client")]
    Abandoned,
    #[error("A response has already been received for the specified request")]
    Fulfilled,
    #[error("Could not renew AWS sesssion: {0}")]
    SessionRenew(#[from] GetSessionError),
}


// errors encountered while handling an HTTP request
#[derive(Debug, ThisError, AsRefStr)]
pub enum HandlerError {
    #[error("Error writing to stream: {0}")]
    StreamIOError(#[from] std::io::Error),
    #[error("Received invalid UTF-8 in request")]
    InvalidUtf8(#[from] FromUtf8Error),
    #[error("HTTP request malformed")]
    BadRequest(#[from] serde_json::Error),
    #[error("HTTP request too large")]
    RequestTooLarge,
    #[error("Internal server error")]
    Internal,
    #[error("Error accessing credentials: {0}")]
    NoCredentials(#[from] GetCredentialsError),
    #[error("Error getting client details: {0}")]
    ClientInfo(#[from] ClientInfoError),
    #[error("Error from Tauri: {0}")]
    Tauri(#[from] tauri::Error),
    #[error("No main application window found")]
    NoMainWindow,
    #[error("Request was denied")]
    Denied,
}


#[derive(Debug, ThisError, AsRefStr)]
pub enum WindowError {
    #[error("Failed to find main application window")]
    NoMainWindow,
    #[error(transparent)]
    ManageFailure(#[from] tauri::Error),
}


#[derive(Debug, ThisError, AsRefStr)]
pub enum GetCredentialsError {
    #[error("Credentials are currently locked")]
    Locked,
    #[error("No credentials are known")]
    Empty,
}


#[derive(Debug, ThisError, AsRefStr)]
pub enum GetSessionError {
    #[error("Request completed successfully but no credentials were returned")]
    EmptyResponse, // SDK returned successfully but credentials are None
    #[error("Error response from AWS SDK: {0}")]
    SdkError(#[from] AwsSdkError<GetSessionTokenError>),
    #[error("Could not construt session: credentials are locked")]
    CredentialsLocked,
    #[error("Could not construct session: no credentials are known")]
    CredentialsEmpty,
}


#[derive(Debug, ThisError, AsRefStr)]
pub enum UnlockError {
    #[error("App is not locked")]
    NotLocked,
    #[error("No saved credentials were found")]
    NoCredentials,
    #[error(transparent)]
    Crypto(#[from] CryptoError),
    #[error("Data was found to be corrupt after decryption")]
    InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded
    #[error("Database error: {0}")]
    DbError(#[from] SqlxError),
    #[error("Failed to create AWS session: {0}")]
    GetSession(#[from] GetSessionError),
}


#[derive(Debug, ThisError, AsRefStr)]
pub enum CryptoError {
    #[error(transparent)]
    Argon2(#[from] argon2::Error),
    #[error("Invalid passphrase")] // I think this is the only way decryption fails
    Aead(#[from] chacha20poly1305::aead::Error),
}


// Errors encountered while trying to figure out who's on the other end of a request
#[derive(Debug, ThisError, AsRefStr)]
pub enum ClientInfoError {
    #[error("Found PID for client socket, but no corresponding process")]
    ProcessNotFound,
    #[error("Could not determine parent PID of connected client")]
    ParentPidNotFound,
    #[error("Found PID for parent process of client, but no corresponding process")]
    ParentProcessNotFound,
    #[error("Could not determine PID of connected client")]
    WindowsError(#[from] windows::core::Error),
    #[error(transparent)]
    Io(#[from] std::io::Error),
}


// Technically also an error, but formatted as a struct for easy deserialization
#[derive(Debug, Serialize, Deserialize)]
pub struct ServerError {
    code: String,
    msg: String,
}

impl std::fmt::Display for ServerError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        write!(f, "{} ({})", self.msg, self.code)?;
        Ok(())
    }
}


// Errors encountered while requesting credentials via CLI (creddy show, creddy exec)
#[derive(Debug, ThisError, AsRefStr)]
pub enum RequestError {
    #[error("Error response from server: {0}")]
    Server(ServerError),
    #[error("Unexpected response from server")]
    Unexpected(crate::server::Response),
    #[error("The server did not respond with valid JSON")]
    InvalidJson(#[from] serde_json::Error),
    #[error("Error reading/writing stream: {0}")]
    StreamIOError(#[from] std::io::Error),
}

impl From<ServerError> for RequestError {
    fn from(s: ServerError) -> Self {
        Self::Server(s)
    }
}


#[derive(Debug, ThisError, AsRefStr)]
pub enum CliError {
    #[error(transparent)]
    Request(#[from] RequestError),
    #[error(transparent)]
    Exec(#[from] ExecError),
    #[error(transparent)]
    Io(#[from] std::io::Error),
}


// Errors encountered while trying to launch a child process
#[derive(Debug, ThisError, AsRefStr)]
pub enum ExecError {
    #[error("Please specify a command")]
    NoCommand,
    #[error("Executable not found: {0:?}")]
    NotFound(OsString),
    #[error("Failed to execute command: {0}")]
    ExecutionFailed(#[from] std::io::Error),
    #[error(transparent)]
    GetCredentials(#[from] GetCredentialsError),
}


#[derive(Debug, ThisError, AsRefStr)]
pub enum LaunchTerminalError {
    #[error("Could not discover main window")]
    NoMainWindow,
    #[error("Failed to communicate with main Creddy window")]
    IpcFailed(#[from] tauri::Error),
    #[error("Failed to launch terminal: {0}")]
    Exec(#[from] ExecError),
    #[error(transparent)]
    GetCredentials(#[from] GetCredentialsError),
}


// =========================
// Serialize implementations
// =========================


struct SerializeWrapper<E>(pub E);

impl Serialize for SerializeWrapper<&GetSessionTokenError> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let err = self.0;
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("code", &err.code())?;
        map.serialize_entry("msg", &err.message())?;
        map.serialize_entry("source", &None::<&str>)?;
        map.end()
    }
}



impl_serialize_basic!(SetupError);
impl_serialize_basic!(GetCredentialsError);
impl_serialize_basic!(ClientInfoError);
impl_serialize_basic!(WindowError);


impl Serialize for HandlerError {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("code", self.as_ref())?;
        map.serialize_entry("msg", &format!("{self}"))?;
        map.end()
    }
}


impl Serialize for SendResponseError {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("code", self.as_ref())?;
        map.serialize_entry("msg", &format!("{self}"))?;

        match self {
            SendResponseError::SessionRenew(src) => map.serialize_entry("source", &src)?,
            _ => serialize_upstream_err(self, &mut map)?,
        }

        map.end()
    }
}


impl Serialize for GetSessionError {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("code", self.as_ref())?;
        map.serialize_entry("msg", &format!("{self}"))?;

        match self {
            GetSessionError::SdkError(AwsSdkError::ServiceError(se_wrapper)) => {
                let err = se_wrapper.err();
                map.serialize_entry("source", &SerializeWrapper(err))?
            }
            _ => serialize_upstream_err(self, &mut map)?,
        }

        map.end()
    }
}


impl Serialize for UnlockError {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("code", self.as_ref())?;
        map.serialize_entry("msg", &format!("{self}"))?;

        match self {
            UnlockError::GetSession(src) => map.serialize_entry("source", &src)?,
            // The string representation of the AEAD error is not very helpful, so skip it
            UnlockError::Crypto(_src) => map.serialize_entry("source", &None::<&str>)?,
            _ => serialize_upstream_err(self, &mut map)?,
        }
        map.end()
    }
}


impl Serialize for ExecError {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("code", self.as_ref())?;
        map.serialize_entry("msg", &format!("{self}"))?;

        match self {
            ExecError::GetCredentials(src) => map.serialize_entry("source", &src)?,
            _ => serialize_upstream_err(self, &mut map)?,
        }
        map.end()
    }
}


impl Serialize for LaunchTerminalError {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("code", self.as_ref())?;
        map.serialize_entry("msg", &format!("{self}"))?;

        match self {
            LaunchTerminalError::Exec(src) => map.serialize_entry("source", &src)?,
            _ => serialize_upstream_err(self, &mut map)?,
        }
        map.end()
    }
}
