use base64::Encoding;
use chacha20::{ChaCha20Poly1305, KeyInit, aead::Aead};
use ratatui::prelude::Stylize;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Cell, ListItem, Row};
use zeroize::Zeroizing;

use crate::{Error, Nonce, Password, Result, SessionKey, field_access};

mod state;

pub use state::{EntryEncryption, EntryMode, EntrySelection, EntryState, EntryVisibility};

/// Represents a user entry.
#[derive(Clone, Debug)]
pub struct Entry {
    title: Zeroizing<String>,
    username: Zeroizing<String>,
    password: Zeroizing<String>,
    totp: Option<totp::TOTP>,
    state: EntryState,
    session_key: SessionKey,
    session_nonce: Nonce,
}

impl Entry {
    /// Length of the encoded entry length field.
    ///
    /// Database entry fields are encoded as:
    ///
    /// ```text,no_run,no_build
    /// [LENGTH {32-bit}][ENTRY {LENGTH-bytes}]
    /// ```
    pub const LENGTH_LEN: usize = std::mem::size_of::<u32>();
    /// Represents hidden password string.
    pub const HIDDEN_CONTENT: &str = "********";
    /// Represents an emtpy TOTP token.
    pub const NO_TOTP: &str = "      ";
    /// Represents the number of columns when displayed in a table.
    pub const COLUMNS: usize = 4;

    /// Creates a new [Entry].
    pub fn new() -> Self {
        Self {
            title: Zeroizing::new(String::new()),
            username: Zeroizing::new(String::new()),
            password: Zeroizing::new(String::new()),
            totp: None,
            state: EntryState::new(),
            session_key: SessionKey::random(),
            session_nonce: Nonce::counter(),
        }
    }

    /// Gets whether the entry is empty.
    pub fn is_empty(&self) -> bool {
        self.title.is_empty()
    }

    /// Renders the password based on the [EntryState].
    pub fn render_password(&self) -> &str {
        match self.state.visibility() {
            EntryVisibility::Show => self.password.as_str(),
            EntryVisibility::Hide => Self::HIDDEN_CONTENT,
        }
    }

    /// Gets the number of columns when displayed in a table.
    #[inline]
    pub const fn columns() -> usize {
        Self::COLUMNS
    }

    #[inline]
    fn style(&self) -> Style {
        match self.state.selection() {
            EntrySelection::Selected => Style::default().fg(Color::Yellow),
            EntrySelection::Unselected => Style::default(),
        }
    }

    /// Gets a reference to the TOTP instance.
    pub fn totp(&self) -> Option<&totp::TOTP> {
        self.totp.as_ref()
    }

    /// Sets the TOTP instance.
    pub fn set_totp(&mut self, totp: &str) -> Result<()> {
        Self::create_totp(totp).map(|t| self.totp = Some(t))
    }

    /// Builder function that sets the TOTP instance.
    pub fn with_totp(self, totp: &str) -> Result<Self> {
        Self::create_totp(totp).map(|t| Self {
            totp: Some(t),
            ..self
        })
    }

    /// Creates a RFC6238 TOTP from the provided secret.
    fn create_totp(secret: &str) -> Result<totp::TOTP> {
        totp::Rfc6238::with_defaults(secret.as_bytes().to_vec())
            .map_err(Error::from)
            .and_then(|t| totp::TOTP::from_rfc6238(t).map_err(Error::from))
    }

    /// Gets the TOTP secret.
    pub fn totp_secret(&self) -> Option<&str> {
        self.totp
            .as_ref()
            .map(|t| t.secret.as_slice())
            .and_then(|t| str::from_utf8(t).ok())
    }

    /// Gets the current TOTP token.
    pub(crate) fn totp_token(&self) -> Result<String> {
        self.totp
            .as_ref()
            .ok_or(Error::io_other("missing TOTP"))
            .and_then(|t| t.generate_current().map_err(Error::from))
    }

    /// Renders the next TOTP token.
    ///
    /// Generates an empty string if no TOTP is configured.
    pub fn render_totp(&self) -> String {
        self.totp_token()
            .map(|t| format!(" | {t}"))
            .unwrap_or_default()
    }

    /// Converts the [Entry] to a TUI list item.
    pub fn as_list_item(&self) -> ListItem<'_> {
        ListItem::new(
            Line::from(vec![
                Span::styled(self.title(), Style::default().add_modifier(Modifier::BOLD)),
                Span::raw(" | "),
                Span::raw(self.username()),
                Span::raw(" | "),
                Span::raw(self.render_password()),
                Span::raw(self.render_totp()),
            ])
            .style(self.style()),
        )
    }

    /// Converts the [Entry] to a TUI row.
    pub fn as_row(&self) -> Row<'_> {
        Row::new(vec![
            Cell::new(self.title()).style(Style::default().bold()),
            Cell::new(self.username()),
            Cell::new(self.render_password()),
            Cell::new(self.totp_token().unwrap_or_default()),
        ])
        .style(self.style())
    }

    /// Gets whether the entry is encrypted.
    pub fn is_encrypted(&self) -> bool {
        self.state.is_encrypted()
    }

    /// Sets the entry selection state.
    pub fn set_selection(&mut self, selection: EntrySelection) {
        self.state.set_selection(selection);
    }

    /// Sets the entry visibility.
    pub fn set_visibility(&mut self, visibility: EntryVisibility) -> Result<()> {
        match (self.state.visibility(), visibility) {
            (EntryVisibility::Hide, EntryVisibility::Show) => self.session_decrypt(),
            (EntryVisibility::Show, EntryVisibility::Hide) => self.session_encrypt(),
            (EntryVisibility::Show, EntryVisibility::Show)
            | (EntryVisibility::Hide, EntryVisibility::Hide) => Ok(()),
        }
    }

    /// Toggles the entry visibility.
    pub fn toggle_visibility(&mut self) -> Result<()> {
        match self.state.visibility() {
            EntryVisibility::Show => self.session_encrypt(),
            EntryVisibility::Hide => self.session_decrypt(),
        }
    }

    /// Encrypts the entry for storage in the database.
    ///
    /// Encodes the [Entry] using length-prefix encoding:
    ///
    /// ```text,no_build,no_run
    /// [FIELD_LEN {32-bits}][FIELD {FIELD_LEN-bytes}]
    /// ```
    pub fn encrypt(&self, key: &Password, nonce: &Nonce) -> Result<Zeroizing<Vec<u8>>> {
        let cipher = ChaCha20Poly1305::new_from_slice(key.as_ref())?;

        let t = Self::serialize_field(self.title.as_str());
        let u = Self::serialize_field(self.username.as_str());
        let p = Self::serialize_field(self.password.as_str());

        let val = if let Some(totp) = self.totp_secret() {
            Zeroizing::new(
                t.chain(u)
                    .chain(p)
                    .chain(Self::serialize_field(totp))
                    .collect::<Vec<u8>>(),
            )
        } else {
            Zeroizing::new(t.chain(u).chain(p).collect::<Vec<u8>>())
        };

        cipher
            .encrypt(nonce.as_nonce(), val.as_slice())
            .map(Zeroizing::new)
            .map_err(Error::from)
    }

    /// Attempts to decrypt an encrypted [Entry].
    ///
    /// `cipher_text` should be the encrypted, length-prefix encoded [Entry] with the Poly1305 MAC.
    pub fn decrypt(key: &Password, nonce: &Nonce, cipher_text: &[u8]) -> Result<Self> {
        let cipher = ChaCha20Poly1305::new_from_slice(key.as_ref())?;

        let entry_bytes = cipher
            .decrypt(nonce.as_nonce(), cipher_text)
            .map(Zeroizing::new)
            .map_err(|err| Error::io_crypto(format!("error decrypting entry: {err}")))?;

        let (t, title_end) = Self::deserialize_field(entry_bytes.as_slice(), 0)
            .map_err(|err| Error::io_db(format!("invalid title field: {err}")))?;

        let (u, username_end) = entry_bytes
            .get(title_end..)
            .ok_or(Error::io_db("missing username"))
            .and_then(|remaining| Self::deserialize_field(remaining, title_end))
            .map_err(|err| Error::io_db(format!("invalid username field: {err}")))?;

        let (p, password_end) = entry_bytes
            .get(username_end..)
            .ok_or(Error::io_db("missing password"))
            .and_then(|remaining| Self::deserialize_field(remaining, username_end))
            .map_err(|err| Error::io_db(format!("invalid password field: {err}")))?;

        if let Some(remaining) = entry_bytes.get(password_end..)
            && !remaining.is_empty()
        {
            Self::deserialize_field(remaining, password_end)
                .and_then(|(totp, _)| {
                    Self::new()
                        .with_title(t)
                        .with_username(u)
                        .with_password(p)
                        .with_totp(totp)
                })
                .map_err(|err| Error::io_db(format!("invalid totp field: {err}")))
        } else {
            Ok(Self::new().with_title(t).with_username(u).with_password(p))
        }
    }

    /// Serializes a field into a length-prefix encoding.
    fn serialize_field(field: &str) -> impl Iterator<Item = u8> {
        (field.len() as u32)
            .to_le_bytes()
            .into_iter()
            .chain(field.as_bytes().iter().copied())
    }

    /// Deserializes a length-prefix encoded field.
    ///
    /// Returns the field passwords, and the field's ending position.
    fn deserialize_field(buf: &[u8], start_pos: usize) -> Result<(&str, usize)> {
        let field_start = Self::LENGTH_LEN;

        let buf_len = buf.len();

        let len = buf
            .get(..field_start)
            .ok_or(Error::io_db(format!(
                "invalid field length, start: {start_pos}, length: {buf_len}"
            )))
            .and_then(|b| b.try_into().map_err(Error::from))
            .map(u32::from_le_bytes)?;

        let field_end = field_start.saturating_add(len as usize);

        buf.get(field_start..field_end)
            .ok_or(Error::io_db("field length exceeds buffer length"))
            .and_then(|b| str::from_utf8(b).map_err(Error::from))
            .map_err(|err| Error::io_db(format!("invalid field: {err}")))
            .map(|s| (s, start_pos.saturating_add(field_end)))
    }

    /// Encrypts protected entry password with a session key.
    pub fn session_encrypt(&mut self) -> Result<()> {
        if self.state.is_encrypted() {
            return Err(Error::io_crypto("entry is already encrypted"));
        }

        let cipher = ChaCha20Poly1305::new_from_slice(self.session_key.as_ref())?;
        self.session_nonce.increment()?;

        let encrypted_password = cipher
            .encrypt(self.session_nonce.as_nonce(), self.password.as_bytes())
            .map(Zeroizing::new)
            .map(|d| base64::Base64::encode_string(d.as_slice()))
            .map(Zeroizing::new)?;

        self.password = encrypted_password;
        self.state.set_visibility(EntryVisibility::Hide);
        self.state.set_encryption(EntryEncryption::Encrypted);

        Ok(())
    }

    /// Decrypts protected entry password with a session key.
    pub fn session_decrypt(&mut self) -> Result<()> {
        if !self.state.is_encrypted() {
            return Err(Error::io_crypto("entry is already decrypted"));
        }

        let cipher = ChaCha20Poly1305::new_from_slice(self.session_key.as_ref())?;

        let decoded = base64::Base64::decode_vec(self.password.as_str()).map(Zeroizing::new)?;

        let decrypted_password = cipher
            .decrypt(self.session_nonce.as_nonce(), decoded.as_slice())
            .map_err(Error::from)
            .and_then(|d| String::from_utf8(d).map_err(Error::from))
            .map(Zeroizing::new)?;

        self.password = decrypted_password;
        self.state.set_visibility(EntryVisibility::Show);
        self.state.set_encryption(EntryEncryption::Decrypted);

        Ok(())
    }
}

field_access! {
    Entry {
        /// The entry title.
        title: zeroize_cow { str },
        /// The entry username.
        username: zeroize_cow { str },
        /// The entry password.
        password: zeroize_cow { str },
    }
}

field_access! {
    Entry {
        /// Indicates whether to show/hide the entry password.
        state: EntryState,
    }
}

impl Default for Entry {
    fn default() -> Self {
        Self::new()
    }
}

impl PartialEq for Entry {
    fn eq(&self, oth: &Self) -> bool {
        self.title == oth.title
            && self.username == oth.username
            && self.password == oth.password
            && self.totp == oth.totp
            && self.state == oth.state
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{App, Salt};

    const RAND_ENTRIES: usize = 1024;
    const RAND_ENTRY_LEN: usize = 128;

    #[test]
    fn test_entry() {
        let title = "test title";
        let password = "test password";

        let mut entry = Entry::new().with_title(title).with_password(password);
        entry.session_encrypt().unwrap();

        assert_eq!(entry.title(), title);
        assert!(entry.password() != password);
        assert_eq!(
            base64::Base64::decode_vec(entry.password()).unwrap().len(),
            password.len() + App::MAC_LEN
        );

        assert_eq!(entry.state().visibility(), EntryVisibility::Hide);
        assert_eq!(entry.render_password(), Entry::HIDDEN_CONTENT);

        entry.toggle_visibility().unwrap();

        assert_eq!(entry.state().visibility(), EntryVisibility::Show);
        assert_eq!(entry.render_password(), password);

        let mut entries = Vec::with_capacity(RAND_ENTRIES);
        let mut nonces = Vec::with_capacity(RAND_ENTRIES);

        let password = Password::hash(b"test password", &Salt::random());

        (0..RAND_ENTRIES).for_each(|_| {
            let mut title = String::from("test title");
            (0..RAND_ENTRY_LEN).for_each(|_| title.push(rand::random::<char>()));

            let mut username = String::from("test username");
            (0..RAND_ENTRY_LEN).for_each(|_| username.push(rand::random::<char>()));

            let mut pass = String::from("test password");
            (0..RAND_ENTRY_LEN).for_each(|_| pass.push(rand::random::<char>()));

            let nonce = Nonce::random();
            let mut entry = Entry::new()
                .with_title(title)
                .with_username(username)
                .with_password(&pass);

            // ensure we have a unique Nonce and Entry
            assert!(!nonces.contains(&nonce));
            assert!(!entries.contains(&entry));

            let cipher_text = entry.encrypt(&password, &nonce).unwrap();
            assert_eq!(
                Entry::decrypt(&password, &nonce, cipher_text.as_slice()).unwrap(),
                entry
            );

            entry.session_encrypt().unwrap();
            assert_eq!(
                base64::Base64::decode_vec(entry.password()).unwrap().len(),
                pass.len() + App::MAC_LEN
            );
            assert!(entry.password() != pass);

            entry.session_decrypt().unwrap();
            assert_eq!(entry.password(), pass);

            nonces.push(nonce);
            entries.push(entry);
        });
    }
}
