//! Main application implementation.

use std::borrow::Cow;

use crossterm::clipboard::{ClipboardSelection, ClipboardType, CopyToClipboard};
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::Frame;
use ratatui::layout::Alignment;
use ratatui::prelude::{Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, Cell, Paragraph, Row, Table};
use zeroize::Zeroizing;

use crate::{AppEvent, Error, Event, EventHandler, Result};

mod db_key;
mod entry;
mod input;
mod input_list;
mod nonce;
mod password;
mod popup;
mod salt;
mod session_key;
mod state;

pub use db_key::DbKey;
pub use entry::{
    Entry, EntryEncryption, EntryMode, EntrySelection, EntryState, EntryVisibility, ExtraField,
    ExtraFieldList, ExtraFieldListState,
};
pub use input::{InputField, InputMode, InputVisibility};
pub use input_list::{InputList, InputListState};
pub use nonce::Nonce;
pub use password::Password;
pub use popup::Popup;
pub use salt::Salt;
pub use session_key::SessionKey;
pub use state::AppState;

/// Convenience alias for app's terminal type.
pub type Terminal = ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>;

static CLEAR_HANDLE: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>> =
    tokio::sync::Mutex::const_new(None);

/// The main application which holds the state and logic of the application.
pub struct App {
    state: AppState,
    entries: Vec<Entry>,
    pos: usize,
    db: Option<sled::Db>,
    db_nonce: Nonce,
    password: Password,
    events: EventHandler,
}

impl App {
    /// Represents the cryptographic MAC length.
    pub const MAC_LEN: usize = 16;
    /// Represents the number of seconds to wait before auto-clearing the clipboard.
    pub const CLIPBOARD_CLEAR_DELAY: u64 = 10;
    /// Represents the number of required input fields.
    pub const REQUIRED_FIELDS: usize = 4;

    /// Creates a new [App].
    pub fn new() -> Self {
        Self {
            state: AppState::new(),
            entries: Vec::new(),
            pos: 0,
            db: None,
            db_nonce: Nonce::counter(),
            password: Password::new(),
            events: EventHandler::new(),
        }
    }

    /// Gets the title block for the application.
    pub fn title_block() -> Block<'static> {
        Block::bordered()
            .title(" Keyper ")
            .title_alignment(Alignment::Center)
            .title_style(Style::default().bold().blue())
    }

    /// Run the application's main loop.
    pub async fn run(&mut self, terminal: &mut Terminal) -> Result<()> {
        self.state = AppState::Running;

        terminal.clear()?;

        while let Err(err) = self.load_db(terminal).await {
            let err = match err {
                e if e.kind().is_crypto() => format!("invalid password: {e}"),
                e => format!("invalid database path: {e}"),
            };

            self.show_retry_error(terminal, err).await?;

            if !self.state.is_running() {
                break;
            }
        }

        while self.state.is_running() {
            terminal.draw(|frame| self.render(frame))?;

            match self.events.next().await? {
                Event::Crossterm(event) => self.handle_crossterm_events(terminal, event).await?,
                Event::App(AppEvent::ClearClipboard) => Self::clear_clipboard().await?,
                _ => (),
            }
        }

        Ok(())
    }

    /// Copies content to the system clipboard.
    pub fn copy_to_clipboard<'c, C: Into<Cow<'c, str>>>(&mut self, content: C) -> Result<()> {
        let c = C::into(content);
        let destination =
            ClipboardSelection(vec![ClipboardType::Clipboard, ClipboardType::Primary]);

        let to_clipboard = CopyToClipboard {
            content: c.as_bytes(),
            destination,
        };

        crossterm::terminal::enable_raw_mode()?;
        crossterm::execute!(std::io::stdout(), to_clipboard)?;
        crossterm::terminal::disable_raw_mode()?;

        self.events.send(AppEvent::ClearClipboard);

        Ok(())
    }

    /// Clears the clipboard after a configured timeout.
    pub async fn clear_clipboard() -> Result<()> {
        let delay = Self::CLIPBOARD_CLEAR_DELAY;

        let handle = tokio::task::spawn(async move {
            tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await;

            let content = b"\0";
            let destination =
                ClipboardSelection(vec![ClipboardType::Clipboard, ClipboardType::Primary]);

            let to_clipboard = CopyToClipboard {
                content,
                destination,
            };

            crossterm::terminal::enable_raw_mode().ok();
            crossterm::execute!(std::io::stdout(), to_clipboard).ok();
            crossterm::terminal::disable_raw_mode().ok();
        });

        let mut global = CLEAR_HANDLE.lock().await;

        // if a previous clear event exists, cancel it to avoid  multiple clipboard clears
        if let Some(g) = global.as_mut() {
            g.abort();
        }

        global.replace(handle);

        Ok(())
    }

    /// Renders the user interface.
    fn render(&mut self, frame: &mut Frame) {
        let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(1)]);
        let [title_area, entries_area] = vertical.areas(frame.area());

        let text = "Press `Esc` or `q` to stop running | `Ctrl-N` or `a` to add an entry | `Ctrl-H` or `h` to show help";

        frame.render_widget(
            Paragraph::new(text).block(Self::title_block()).centered(),
            title_area,
        );

        self.render_entries(frame, entries_area);
    }

    /// Gets a reference to the database.
    pub fn db(&self) -> Result<&sled::Db> {
        self.db.as_ref().ok_or(Error::io_db("missing database"))
    }

    /// Loads the database.
    async fn load_db(&mut self, terminal: &mut Terminal) -> Result<()> {
        let res = InputList::new()
            .with_items(vec![
                InputField::create("Path"),
                InputField::create_hidden("Password"),
            ])
            .run(terminal, &mut self.events)
            .await?;

        let db_path = res
            .first()
            .and_then(|p| {
                if p.starts_with("~/") {
                    let p = p.strip_prefix("~/")?;
                    home::home_dir().and_then(|mut h| {
                        h.push(p);
                        h.into_os_string().into_string().ok()
                    })
                } else {
                    Some(p.clone())
                }
            })
            .ok_or(Error::io_other("missing database path"))?;

        let db = sled::Config::new().path(db_path).open()?;

        let salt_key = res
            .get(1)
            .cloned()
            .map(|p| Password::hash(p.as_bytes(), &Salt::new()))
            .ok_or(Error::io_other("missing password"))?;

        let salt_db_key = DbKey::from_slice(b"salt");

        let salt = match db.get(salt_db_key.as_ref()) {
            Ok(Some(s)) => {
                let salt_bytes = Zeroizing::new(s.to_vec());
                let salt_nonce = Nonce::from_slice(&salt_bytes[..Nonce::LEN])?;

                Salt::decrypt(&salt_key, &salt_nonce, &salt_bytes[Nonce::LEN..])?
            }
            _ => {
                let salt = Salt::random();
                let salt_nonce = Nonce::random();

                let mut salt_entry = Zeroizing::new(salt_nonce.as_ref().to_vec());

                // encrypt the salt, and append it to the nonce for the DB entry
                salt.encrypt(&salt_key, &salt_nonce)
                    .map(|c| salt_entry.extend_from_slice(c.as_ref()))?;

                db.insert(salt_db_key.as_ref(), salt_entry.as_slice())?;
                salt
            }
        };

        let password = Password::hash(salt_key.as_ref(), &salt);

        self.entries = self
            .decrypt_entries(&db, &password)
            .map_err(|err| Error::io_crypto(format!("error decrypting entries: {err}")))?;

        self.password = password;
        self.db.replace(db);

        Ok(())
    }

    /// Decrypts the database entries to store the in memory.
    fn decrypt_entries(&mut self, db: &sled::Db, password: &Password) -> Result<Vec<Entry>> {
        let mut entries = Vec::new();

        let salt_db_key = DbKey::from_slice(b"salt");

        for item in db.iter() {
            let (key, val) = item?;

            if key.as_ref() != salt_db_key.as_ref() {
                let nonce = Nonce::from_slice(&val[..Nonce::LEN])?;
                if nonce > self.db_nonce {
                    self.db_nonce = nonce;
                }

                let mut entry = Entry::decrypt(password, &nonce, &val[Nonce::LEN..])?;
                entry.session_encrypt()?;
                entry.set_db_key(DbKey::from_db(key.as_ref()));

                entries.push(entry);
            }
        }

        Ok(entries)
    }

    /// Renders user entries to the frame.
    fn render_entries(&self, frame: &mut Frame, entries_area: Rect) {
        let entries = self
            .entries
            .iter()
            .map(|e| e.as_row())
            .collect::<Vec<Row>>();

        let columns = Entry::columns() as u16;
        let col_width_max = entries_area
            .width
            .saturating_div(columns)
            .saturating_sub(columns.saturating_mul(2));

        let widths = (0..columns)
            .map(|_| Constraint::Max(col_width_max))
            .collect::<Vec<_>>();

        let entries = Table::new(entries, widths)
            .header(
                Row::new(vec![
                    Cell::new("Title"),
                    Cell::new("Username"),
                    Cell::new("Password"),
                    Cell::new("TOTP"),
                ])
                .style(Style::default().bold().blue()),
            )
            .block(Block::bordered().title("Entries"));
        frame.render_widget(entries, entries_area);
    }

    /// Reads the crossterm events and updates the state of [App].
    async fn handle_crossterm_events(
        &mut self,
        terminal: &mut Terminal,
        event: CrosstermEvent,
    ) -> Result<()> {
        match event {
            // it's important to check KeyEventKind::Press to avoid handling key release events
            CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => {
                self.on_key_event(key, terminal).await
            }
            CrosstermEvent::Mouse(_) => Ok(()),
            CrosstermEvent::Resize(_, _) => Ok(()),
            _ => Ok(()),
        }
    }

    /// Handles the key events and updates the state of [App].
    async fn on_key_event(&mut self, key: KeyEvent, terminal: &mut Terminal) -> Result<()> {
        match (key.modifiers, key.code) {
            (_, KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q')) => self.quit(),
            (_, KeyCode::Char('a') | KeyCode::Char('A'))
            | (KeyModifiers::CONTROL, KeyCode::Char('n') | KeyCode::Char('N')) => {
                if let Err(err) = self.add_entry(terminal).await {
                    self.show_error(terminal, "Error adding entry", err.to_string())
                        .await?;
                }
            }
            (_, KeyCode::Char('r') | KeyCode::Char('R')) => self.toggle_entry_visibility()?,
            (_, KeyCode::Char('h') | KeyCode::Char('H')) => self.show_help(terminal).await?,
            (_, KeyCode::Left | KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('p')) => {
                self.previous_entry()?
            }
            (
                _,
                KeyCode::Tab
                | KeyCode::Right
                | KeyCode::Down
                | KeyCode::Char('j')
                | KeyCode::Char('n'),
            ) => self.next_entry()?,
            (_, KeyCode::Enter) => {
                let entry = self
                    .entries
                    .get_mut(self.pos)
                    .ok_or(Error::io_db("missing entries"))?;

                if let Err(err) = entry
                    .run(
                        terminal,
                        &mut self.events,
                        self.db.as_ref().ok_or(Error::io_db("missing db"))?,
                        &self.password,
                        &mut self.db_nonce,
                    )
                    .await
                {
                    self.show_error(terminal, "Error editing entry", err.to_string())
                        .await?;
                }
            }
            (_, KeyCode::Char('b') | KeyCode::Char('B')) => {
                let Ok(username) = self.current_entry().map(|e| e.username().to_string()) else {
                    self.show_error(terminal, "Missing current entry", "")
                        .await?;
                    return Ok(());
                };
                if let Err(err) = self.copy_to_clipboard(username) {
                    self.show_error(terminal, "Error copying entry username", err.to_string())
                        .await?;
                }
            }
            (_, KeyCode::Char('c') | KeyCode::Char('C')) => {
                let Ok(password) = self.current_entry_mut().and_then(|e| e.reveal_password())
                else {
                    self.show_error(terminal, "Missing current entry", "")
                        .await?;
                    return Ok(());
                };
                if let Err(err) = self.copy_to_clipboard(password.as_str()) {
                    self.show_error(terminal, "Error copying entry password", err.to_string())
                        .await?;
                }
            }
            (_, KeyCode::Char('t') | KeyCode::Char('T')) => {
                let Ok(totp) = self.current_entry().and_then(|e| e.totp_token()) else {
                    self.show_error(terminal, "Missing TOTP token", "").await?;
                    return Ok(());
                };
                if let Err(err) = self.copy_to_clipboard(totp) {
                    self.show_error(terminal, "Error copying entry TOTP token", err.to_string())
                        .await?;
                }
            }
            // Add other key handlers here.
            _ => {}
        }

        Ok(())
    }

    /// Gets a reference to the current entry.
    fn current_entry(&self) -> Result<&Entry> {
        self.entries
            .get(self.pos)
            .ok_or(Error::io_other("empty entry list"))
    }

    /// Gets a mutable reference to the current entry.
    fn current_entry_mut(&mut self) -> Result<&mut Entry> {
        self.entries
            .get_mut(self.pos)
            .ok_or(Error::io_other("empty entry list"))
    }

    /// Selects the previous entry.
    fn previous_entry(&mut self) -> Result<()> {
        if !self.entries.is_empty() {
            self.set_entry_visibility(EntryVisibility::Hide)?;
            self.set_entry_selection(EntrySelection::Unselected);

            match self.pos {
                0 => self.pos = self.entries.len().saturating_sub(1),
                p => self.pos = p.saturating_sub(1),
            }

            self.set_entry_selection(EntrySelection::Selected);
        }

        Ok(())
    }

    /// Selects the next entry.
    fn next_entry(&mut self) -> Result<()> {
        if !self.entries.is_empty() {
            let next_pos = if self
                .entries
                .get_mut(self.pos)
                .map(|e| e.state().is_selected())
                .unwrap_or_default()
            {
                self.pos.saturating_add(1) % self.entries.len()
            } else {
                self.pos
            };

            self.set_entry_visibility(EntryVisibility::Hide)?;
            self.set_entry_selection(EntrySelection::Unselected);

            self.pos = next_pos;

            self.set_entry_selection(EntrySelection::Selected);
        }

        Ok(())
    }

    /// Toggles the current entry visibility.
    fn toggle_entry_visibility(&mut self) -> Result<()> {
        if let Some(e) = self.entries.get_mut(self.pos) {
            e.toggle_visibility()?;
        }

        Ok(())
    }

    /// Sets the visibility of the entry content for the current entry.
    fn set_entry_visibility(&mut self, vis: EntryVisibility) -> Result<()> {
        if let Some(e) = self.entries.get_mut(self.pos) {
            e.set_visibility(vis)?;
        }

        Ok(())
    }

    /// Sets the visibility of the entry content for the current entry.
    fn set_entry_selection(&mut self, sel: EntrySelection) {
        if let Some(e) = self.entries.get_mut(self.pos) {
            e.set_selection(sel);
        }
    }

    /// Adds an entry to the application.
    async fn add_entry(&mut self, terminal: &mut Terminal) -> Result<()> {
        let db_key = DbKey::from_u64(self.entries.len().saturating_add(1) as u64);

        if let Some(db) = self.db.as_ref()
            && let Some(entry) = Entry::add_to_db(
                terminal,
                &mut self.events,
                db,
                &self.password,
                &mut self.db_nonce,
                db_key,
            )
            .await?
        {
            self.entries.push(entry);
        }

        Ok(())
    }

    /// Shows the help menu.
    async fn show_help(&mut self, terminal: &mut Terminal) -> Result<()> {
        terminal.draw(|frame| {
            let area = frame.area();

            let popup_area = Rect {
                x: area.width / 4,
                y: area.height / 3,
                width: area.width / 2,
                height: area.height / 3,
            };

            let bold = Style::new().bold();

            let popup = Popup::default()
                .title("Help")
                .title_style(Style::new().white().bold())
                .content(Text::from(vec![
                    Line::from(vec![
                        Span::styled("n | Right-Arrow | Down-Arrow | Tab :", bold),
                        Span::raw(" select next entry"),
                    ]),
                    Line::from(vec![
                        Span::styled("p | Left-Arrow | Up-Arrow :", bold),
                        Span::raw(" select previous entry"),
                    ]),
                    Line::from(vec![
                        Span::styled("a | Ctrl-A | Ctrl-N :", bold),
                        Span::raw(" add new entry"),
                    ]),
                    Line::from(vec![
                        Span::styled("r | Ctrl-R :", bold),
                        Span::raw(" show/hide entry password"),
                    ]),
                    Line::from(vec![
                        Span::styled("b | Ctrl-B :", bold),
                        Span::raw(" copy entry username to clipboard"),
                    ]),
                    Line::from(vec![
                        Span::styled("c | Ctrl-C :", bold),
                        Span::raw(" copy entry password to clipboard"),
                    ]),
                    Line::from(vec![
                        Span::styled("t | Ctrl-T :", bold),
                        Span::raw(" copy entry TOTP token to clipboard"),
                    ]),
                    Line::from(vec![
                        Span::styled("h | Ctrl-H :", bold),
                        Span::raw(" show help menu"),
                    ]),
                    Line::from(vec![Span::styled("q | Esc :", bold), Span::raw(" quit")]),
                    Line::from(vec![
                        Span::styled("Enter :", bold),
                        Span::raw(" edit selected entry"),
                    ]),
                ]))
                .style(Style::new().yellow())
                .border_style(Style::new().green());

            frame.render_widget(popup, popup_area);
        })?;

        while let Ok(event) = self.events.next().await {
            match event {
                Event::Crossterm(CrosstermEvent::Key(key)) if key.kind == KeyEventKind::Press => {
                    break;
                }
                Event::App(AppEvent::ClearClipboard) => App::clear_clipboard().await?,
                _ => (),
            }
        }

        Ok(())
    }

    /// Shows an error message for a retry-or-quit condition.
    pub async fn show_retry_error<'e, E: Into<Cow<'e, str>>>(
        &mut self,
        terminal: &mut Terminal,
        err: E,
    ) -> Result<()> {
        terminal.draw(|frame| {
            let area = frame.area();

            let popup_area = Rect {
                x: area.width / 4,
                y: area.height / 3,
                width: area.width / 2,
                height: area.height / 3,
            };

            let bold = Style::new().bold();

            let popup = Popup::default()
                .title("Error")
                .title_style(Style::new().white().bold())
                .content(Text::from(vec![
                    Line::from(vec![Span::styled(E::into(err).to_string(), bold)]),
                    Line::from(vec![Span::raw("Press Escape or 'q' to quit")]),
                    Line::from(vec![Span::raw("Press any other key to retry")]),
                ]))
                .style(Style::new().yellow())
                .border_style(Style::new().green());

            frame.render_widget(popup, popup_area);
        })?;

        while let Ok(event) = self.events.next().await {
            match event {
                Event::Crossterm(CrosstermEvent::Key(key)) if key.kind == KeyEventKind::Press => {
                    match key.code {
                        KeyCode::Esc | KeyCode::Char('q') => {
                            self.quit();
                            break;
                        }
                        _ => break,
                    }
                }
                Event::App(AppEvent::ClearClipboard) => App::clear_clipboard().await?,
                _ => (),
            }
        }

        Ok(())
    }

    /// Shows an error message.
    pub async fn show_error<'e, 'm, E: Into<Cow<'e, str>>, M: Into<Cow<'m, str>>>(
        &mut self,
        terminal: &mut Terminal,
        err: E,
        msg: M,
    ) -> Result<()> {
        terminal.draw(|frame| {
            let area = frame.area();

            let popup_area = Rect {
                x: area.width / 4,
                y: area.height / 3,
                width: area.width / 2,
                height: area.height / 3,
            };

            let bold = Style::new().bold();

            let popup = Popup::default()
                .title("Error")
                .title_style(Style::new().white().bold())
                .content(Text::from(vec![
                    Line::from(vec![Span::styled(E::into(err), bold)]),
                    Line::from(vec![Span::raw(M::into(msg))]),
                ]))
                .style(Style::new().yellow())
                .border_style(Style::new().green());

            frame.render_widget(popup, popup_area);
        })?;

        while let Ok(event) = self.events.next().await {
            match event {
                Event::Crossterm(CrosstermEvent::Key(key)) if key.kind == KeyEventKind::Press => {
                    break;
                }
                Event::App(AppEvent::ClearClipboard) => App::clear_clipboard().await?,
                _ => (),
            }
        }

        Ok(())
    }

    /// Set running to false to quit the application.
    fn quit(&mut self) {
        self.state = AppState::Halted;
    }
}

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