use base64::Encoding;
use chacha20::{ChaCha20Poly1305, KeyInit, aead::Aead};
use ratatui::widgets::{Cell, Row};
use zeroize::{Zeroize, Zeroizing};

use crate::{
    EntryEncryption, EntryState, EntryVisibility, Error, Nonce, Result, SessionKey, field_access,
};

/// Represents an extra entry field with a customizable title.
#[derive(Clone, Debug, Eq, PartialEq, Zeroize)]
pub struct ExtraField {
    title: String,
    content: String,
    state: EntryState,
    session_key: SessionKey,
    session_nonce: Nonce,
}

impl ExtraField {
    /// Represents the number of columns when displayed as a table row.
    pub const COLUMNS: usize = 2;
    /// Represents a hidden string.
    pub const HIDDEN: &str = "********";

    /// Creates a new [ExtraField].
    pub fn new() -> Self {
        Self {
            title: String::new(),
            content: String::new(),
            state: EntryState::new(),
            session_key: SessionKey::random(),
            session_nonce: Nonce::counter(),
        }
    }

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

    /// Converts the [ExtraField] to a TUI widget.
    pub fn as_widget(&self) -> Row<'_> {
        Row::new(vec![
            Cell::new(self.title()),
            Cell::new(self.render_content()),
        ])
    }

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

    /// Encrypts protected entry with a session key.
    pub fn session_encrypt(&mut self) -> Result<()> {
        if self.state.is_encrypted() {
            return Ok(());
        }

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

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

        self.content = encrypted;
        self.state.set_visibility(EntryVisibility::Hide);
        self.state.set_encryption(EntryEncryption::Encrypted);

        Ok(())
    }

    /// Decrypts protected entry with a session key.
    pub fn session_decrypt(&mut self) -> Result<()> {
        if !self.state.is_encrypted() {
            return Ok(());
        }

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

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

        let decrypted = 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))?;

        self.content = decrypted;

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

        Ok(())
    }
}

field_access! {
    ExtraField {
        title: cow { str },
        content: cow { str },
    }
}

field_access! {
    ExtraField {
        state: as_ref { EntryState },
    }
}

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