use std::error::Error;
use std::convert::AsRef;
use std::ffi::OsString;
use std::sync::mpsc;
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};


pub trait ErrorPopup {
    fn error_popup(self, title: &str);
}

impl<E: Error> ErrorPopup 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 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),
}


#[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("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,
    #[error("HTTP request malformed")]
    BadRequest(Vec<u8>),
    #[error("HTTP request too large")]
    RequestTooLarge,
    #[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,
}


#[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("Couldn't get client socket details: {0}")]
    NetstatError(#[from] netstat2::error::Error),
}


// Errors encountered while requesting credentials via CLI (creddy show, creddy exec)
#[derive(Debug, ThisError, AsRefStr)]
pub enum RequestError {
    #[error("Credentials request failed: HTTP {0}")]
    Failed(String),
    #[error("Credentials request was rejected")]
    Rejected,
    #[error("Couldn't interpret the server's response")]
    MalformedHttpResponse,
    #[error("The server did not respond with valid JSON")]
    InvalidJson,
    #[error("Error reading/writing stream: {0}")]
    StreamIOError(#[from] std::io::Error),
    #[error("Error loading configuration data: {0}")]
    Setup(#[from] SetupError),
}


// Errors encountered while running a subprocess via creddy exec
#[derive(Debug, ThisError, AsRefStr)]
pub enum ExecError {
    #[error("Please specify a command")]
    NoCommand,
    #[error("Failed to execute command: {0}")]
    ExecutionFailed(#[from] std::io::Error)
}


#[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 LaunchError {
    #[error("Executable not found: {0:?}")]
    ExeNotFound(OsString),
    #[error("Failed to execute command: {0}")]
    ExecutionFailed(#[from] std::io::Error),
    #[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 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}"))?;

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

        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)?,
            _ => serialize_upstream_err(self, &mut map)?,
        }
        map.end()
    }
}


impl Serialize for LaunchError {
    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 {
            LaunchError::GetCredentials(src) => map.serialize_entry("source", &src)?,
            _ => serialize_upstream_err(self, &mut map)?,
        }
        map.end()
    }
}