use crate::applications::authentication_app::AuthenticationAppError::NotAuthenticatedError;
use crate::connections::utils::public_key_to_hostname;
use crate::connections::{Capability, Connection, ConnectionInterface, InboundConnection, OutboundConnection};
use crate::primitives::identity::Identity;
use crate::primitives::transcript::Transcript;
use ed25519_dalek::PublicKey;
use integer_encoding::VarInt;
use serde::Deserialize;
use serde::Serialize;
use sha3::Digest;
use std::sync::Arc;
use subtle::ConstantTimeEq;

pub struct AuthenicationApp {
    identity: Arc<Identity>,
}

#[derive(Debug)]
pub enum AuthenticationAppError {
    NotAuthenticatedError,
}

pub const AUTHENTICATION_CAPABILITY: Capability = Capability("AuthenticationCapability");

struct AuthenticationSession<Direction> {
    long_term_identity: Arc<Identity>,
    ephemeral_identity: Identity,
    local_auth_message_json: String,
    remote_auth_message_json: String,

    remote_long_term_identity: ed25519_dalek::PublicKey,
    remote_ephemeral_identity: ed25519_dalek::PublicKey,

    challenge: [u8; 64],

    conn: Connection<Direction>,
}

impl<Direction> AuthenticationSession<Direction>
where
    Direction: Clone,
{
    pub fn new_outbound(conn: Connection<OutboundConnection>, long_term_identity: Arc<Identity>) -> AuthenticationSession<OutboundConnection> {
        let ephemeral_identity = Identity::initialize_ephemeral_identity();
        let mut auth_session = AuthenticationSession {
            long_term_identity,
            ephemeral_identity,
            local_auth_message_json: "".to_string(),
            remote_auth_message_json: "".to_string(),
            remote_long_term_identity: Default::default(),
            remote_ephemeral_identity: Default::default(),
            conn,
            challenge: [0; 64],
        };
        auth_session.identity_exchange();
        auth_session
    }

    pub fn new_inbound(conn: Connection<InboundConnection>, long_term_identity: Arc<Identity>) -> AuthenticationSession<InboundConnection> {
        let ephemeral_identity = Identity::initialize_ephemeral_identity();
        let mut auth_session = AuthenticationSession {
            long_term_identity,
            ephemeral_identity,
            local_auth_message_json: "".to_string(),
            remote_auth_message_json: "".to_string(),
            remote_long_term_identity: Default::default(),
            remote_ephemeral_identity: Default::default(),
            conn,
            challenge: [0; 64],
        };
        auth_session.identity_exchange();
        auth_session
    }

    /// exchange identities with the remote parties
    /// Not that if network connections fail during this empty buffers will be returned which will
    /// cause subsequent steps to fail
    fn identity_exchange(&mut self) {
        let auth_message = AuthMessage {
            longterm_public_key: base64::encode(self.long_term_identity.public_key()),
            ephemeral_public_key: base64::encode(self.ephemeral_identity.public_key().to_bytes()),
        };
        self.local_auth_message_json = serde_json::to_string(&auth_message).unwrap();
        match self.conn.send(&self.local_auth_message_json) {
            Ok(()) => {
                self.remote_auth_message_json = String::from_utf8(self.conn.expect().unwrap()).unwrap();
                let remote_auth_message: AuthMessage = serde_json::from_str(self.remote_auth_message_json.as_str()).unwrap();
                self.remote_long_term_identity = PublicKey::from_bytes(base64::decode(&remote_auth_message.longterm_public_key).unwrap().as_slice()).unwrap();
                self.remote_ephemeral_identity = PublicKey::from_bytes(base64::decode(&remote_auth_message.ephemeral_public_key).unwrap().as_slice()).unwrap();
            }
            _ => {
                // we fall out of the exchange..if the network fails then the protocol will also fail, as nothing will be instantiated...
            }
        };
    }

    /// generate the message for the local challenge based a new transcript hash.
    fn generate_challenge_message(&self) -> Vec<u8> {
        let mut msg = vec![];
        let mut len = [0u8; 2];
        ((self.challenge.len() + self.long_term_identity.hostname().as_bytes().len()) as u16).encode_var(&mut len);
        msg.extend_from_slice(len.as_slice());
        msg.extend_from_slice(self.challenge.as_slice());
        msg.extend_from_slice(self.long_term_identity.hostname().as_bytes());
        msg
    }

    /// check the challenge from the remote
    fn check_remote_challenge(&mut self) -> Result<Connection<Direction>, AuthenticationAppError> {
        match self.conn.send_encrypted(self.generate_challenge_message()) {
            Ok(()) => {
                let remote_challenge = self.conn.expect_encrypted();
                let mut cmp_challenge = vec![];
                cmp_challenge.extend_from_slice(self.challenge.as_slice());
                cmp_challenge.extend_from_slice(public_key_to_hostname(&self.remote_long_term_identity).as_bytes());
                if remote_challenge.ct_eq(cmp_challenge.as_slice()).unwrap_u8() == 1 {
                    self.conn.set_hostname(&public_key_to_hostname(&self.remote_long_term_identity));
                    self.conn.set_capability(&AUTHENTICATION_CAPABILITY);
                    return Ok(self.conn.try_clone());
                }
                self.conn.shutdown();
                Err(NotAuthenticatedError)
            }
            Err(_err) => Err(NotAuthenticatedError),
        }
    }
}

impl AuthenticationSession<OutboundConnection> {
    pub fn generate_challenge(&mut self, transcript: &mut Transcript) -> Result<Connection<OutboundConnection>, AuthenticationAppError> {
        let l2e = self.long_term_identity.edh(self.remote_ephemeral_identity);
        let e2l = self.ephemeral_identity.edh(self.remote_long_term_identity);
        let e2e = self.ephemeral_identity.edh(self.remote_ephemeral_identity);

        let mut shared_secret = vec![];
        // outbound calculates shared secret as Sha256(l2e:e2l:e2e)
        shared_secret.extend_from_slice(l2e.as_bytes());
        shared_secret.extend_from_slice(e2l.as_bytes());
        shared_secret.extend_from_slice(e2e.as_bytes());
        let key = sha3::Sha3_256::digest(shared_secret.as_slice());

        // transcript is updated using appropriate values for outbound and inbound hostname and initial messages
        transcript.new_protocol("auth-app");
        transcript.add_to_transcript("outbound-hostname", self.long_term_identity.hostname().as_bytes());
        transcript.add_to_transcript("inbound-hostname", public_key_to_hostname(&self.remote_long_term_identity).as_bytes());
        transcript.add_to_transcript("outbound-challenge", self.local_auth_message_json.as_bytes());
        transcript.add_to_transcript("inbound-challenge", self.remote_auth_message_json.as_bytes());
        self.challenge = transcript.commit_to_transcript("3dh-auth-challenge");
        self.conn.enable_encryption(key.to_vec());
        self.check_remote_challenge()
    }
}

impl AuthenticationSession<InboundConnection> {
    pub fn generate_challenge(&mut self, transcript: &mut Transcript) -> Result<Connection<InboundConnection>, AuthenticationAppError> {
        let l2e = self.long_term_identity.edh(self.remote_ephemeral_identity);
        let e2l = self.ephemeral_identity.edh(self.remote_long_term_identity);
        let e2e = self.ephemeral_identity.edh(self.remote_ephemeral_identity);

        // inbound calculates shared secret as Sha256(e2l:l2e:e2e)
        let mut shared_secret = vec![];
        shared_secret.extend_from_slice(e2l.as_bytes());
        shared_secret.extend_from_slice(l2e.as_bytes());
        shared_secret.extend_from_slice(e2e.as_bytes());
        let key = sha3::Sha3_256::digest(shared_secret.as_slice());

        // transcript is updated using appropriate values for outbound and inbound hostname and initial messages
        transcript.new_protocol("auth-app");
        transcript.add_to_transcript("outbound-hostname", public_key_to_hostname(&self.remote_long_term_identity).as_bytes());
        transcript.add_to_transcript("inbound-hostname", self.long_term_identity.hostname().as_bytes());
        transcript.add_to_transcript("outbound-challenge", self.remote_auth_message_json.as_bytes());
        transcript.add_to_transcript("inbound-challenge", self.local_auth_message_json.as_bytes());
        self.challenge = transcript.commit_to_transcript("3dh-auth-challenge");
        self.conn.enable_encryption(key.to_vec());
        self.check_remote_challenge()
    }
}

#[derive(Serialize, Deserialize, Debug)]
struct AuthMessage {
    #[serde(rename(serialize = "LongTermPublicKey", deserialize = "LongTermPublicKey"))]
    longterm_public_key: String,
    #[serde(rename(serialize = "EphemeralPublicKey", deserialize = "EphemeralPublicKey"))]
    ephemeral_public_key: String,
}

impl AuthenicationApp {
    pub fn new(identity: Arc<Identity>) -> AuthenicationApp {
        AuthenicationApp { identity }
    }

    pub fn run_outbound(&mut self, conn: Connection<OutboundConnection>, transcript: &mut Transcript) -> Result<Connection<OutboundConnection>, AuthenticationAppError> {
        let mut auth_session = AuthenticationSession::<OutboundConnection>::new_outbound(conn, self.identity.clone());
        auth_session.generate_challenge(transcript)
    }

    pub fn run_inbound(&mut self, conn: Connection<InboundConnection>, transcript: &mut Transcript) -> Result<Connection<InboundConnection>, AuthenticationAppError> {
        let mut auth_session = AuthenticationSession::<InboundConnection>::new_inbound(conn, self.identity.clone());
        auth_session.generate_challenge(transcript)
    }
}
