//! Secret-aware presentation state for one storage-owned entry document.

use std::{cell::Cell, fmt};

use ironstorage::{
    document::{DocumentError, EntryDocument, EntryField},
    repository::SecretBytes,
};

/// Focus and scrolling state for an authenticated document.
///
/// The document remains the source of field order, labels, kinds, sensitivity,
/// and values. This type only tracks transient presentation choices.
pub struct EntryViewer {
    document: EntryDocument,
    focused: usize,
    scroll: Cell<usize>,
}

impl EntryViewer {
    pub fn new(document: EntryDocument) -> Self {
        Self {
            document,
            focused: 0,
            scroll: Cell::new(0),
        }
    }

    pub fn document(&self) -> &EntryDocument {
        &self.document
    }

    pub fn into_document(self) -> EntryDocument {
        self.document
    }

    pub fn focused_index(&self) -> Option<usize> {
        (!self.document.fields().is_empty()).then_some(self.focused)
    }

    pub fn focused_field(&self) -> Option<&EntryField> {
        self.document.display_fields().get(self.focused).copied()
    }

    pub fn focus_next(&mut self) {
        if !self.document.fields().is_empty() {
            self.focused = (self.focused + 1) % self.document.fields().len();
        }
    }

    pub fn focus_previous(&mut self) {
        if !self.document.fields().is_empty() {
            self.focused = self
                .focused
                .checked_sub(1)
                .unwrap_or(self.document.fields().len() - 1);
        }
    }

    pub fn copy_focused(&self) -> Result<SecretBytes, DocumentError> {
        let field = self.focused_field().ok_or(DocumentError::InvalidIndex {
            index: self.focused,
        })?;
        self.document.copy_field_value(field.id())
    }

    pub fn scroll(&self) -> usize {
        self.scroll.get()
    }

    /// Keep the focused rendered row range inside the supplied viewport while
    /// retaining the current offset whenever it is already visible.
    pub fn ensure_focus_visible(
        &self,
        focused_start: usize,
        focused_height: usize,
        total_rows: usize,
        viewport_rows: usize,
    ) -> usize {
        if self.document.fields().is_empty() || total_rows == 0 {
            self.scroll.set(0);
            return 0;
        }
        if viewport_rows == 0 {
            let scroll = self.scroll.get().min(total_rows.saturating_sub(1));
            self.scroll.set(scroll);
            return scroll;
        }
        let maximum = total_rows.saturating_sub(viewport_rows);
        let mut scroll = self.scroll.get().min(maximum);
        let focused_height = focused_height.max(1);
        let focused_end = focused_start.saturating_add(focused_height);
        let viewport_end = scroll.saturating_add(viewport_rows);
        if focused_height > viewport_rows {
            if focused_end <= scroll {
                scroll = focused_end.saturating_sub(1);
            } else if focused_start >= viewport_end {
                scroll = focused_start;
            }
        } else if focused_start < scroll {
            scroll = focused_start;
        } else {
            if focused_end > viewport_end {
                scroll = focused_end.saturating_sub(viewport_rows);
            }
        }
        scroll = scroll.min(maximum);
        self.scroll.set(scroll);
        scroll
    }

    pub fn scroll_down(&mut self, rows: usize) {
        self.scroll.set(self.scroll.get().saturating_add(rows));
    }

    pub fn scroll_up(&mut self, rows: usize) {
        self.scroll.set(self.scroll.get().saturating_sub(rows));
    }
}

impl fmt::Debug for EntryViewer {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("EntryViewer")
            .field("document", &self.document)
            .field("focused", &self.focused)
            .field("scroll", &self.scroll.get())
            .finish()
    }
}

#[cfg(test)]
pub(crate) mod test_support {
    use std::path::{Path, PathBuf};

    use ironstorage::{
        command::GrepRequest,
        crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
        document::{EntryDocument, EntryDocumentService},
        read::{GrepResults, VaultReader},
        repository::{EntryPath, Repository, SecretBytes},
    };
    use tempfile::TempDir;

    struct FixtureSecrets;

    impl SecretProvider for FixtureSecrets {
        fn secret_for(&mut self, key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
            let passphrase = match key.fingerprint().as_str() {
                "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30" => b"fixture-alice-passphrase".to_vec(),
                "B37027B56FC406BD3F6A622B2AC03492B992D06F" => b"fixture-bob-passphrase".to_vec(),
                _ => return Err(SecretProviderError::Unavailable),
            };
            Ok(SecretBytes::new(passphrase))
        }
    }

    pub(crate) fn fixture_document(entry: &str) -> EntryDocument {
        let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        fixture_document_from(&root.join("stores/basic"), entry)
    }

    pub(crate) fn fixture_grep() -> GrepResults {
        let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let repository = Repository::open(root.join("stores/basic")).expect("fixture repository");
        let keys = KeyStore::load(root.join("keys")).expect("fixture keys");
        VaultReader::new(&repository, &keys)
            .grep(
                &GrepRequest {
                    pattern: "alice".to_owned(),
                    ignore_case: false,
                    invert_match: false,
                    line_number: true,
                    fixed_strings: true,
                },
                &mut FixtureSecrets,
            )
            .expect("fixture grep")
    }

    pub(crate) fn fixture_document_from(store: &Path, entry: &str) -> EntryDocument {
        let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let repository = Repository::open(store).expect("fixture repository");
        let keys = KeyStore::load(root.join("keys")).expect("fixture keys");
        EntryDocumentService::new(&repository, &keys)
            .open(entry, &mut FixtureSecrets)
            .expect("fixture document")
    }

    pub(crate) fn fixture_document_from_plaintext(
        entry: &str,
        plaintext: &[u8],
    ) -> (TempDir, EntryDocument) {
        let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let store = tempfile::tempdir().expect("temporary fixture store");
        let repository = Repository::open(store.path()).expect("fixture repository");
        let keys = KeyStore::load(root.join("keys")).expect("fixture keys");
        let recipients = keys
            .resolve_recipients(b"7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30\n")
            .expect("fixture recipient");
        let ciphertext = keys
            .encrypt(SecretBytes::new(plaintext.to_vec()), &recipients)
            .expect("fixture encryption");
        let path = EntryPath::parse(entry).expect("fixture entry path");
        repository
            .write_entry(&path, &ciphertext)
            .expect("fixture entry write");
        let document = EntryDocumentService::new(&repository, &keys)
            .open(entry, &mut FixtureSecrets)
            .expect("fixture document");
        (store, document)
    }
}

#[cfg(test)]
mod tests {
    use super::{test_support::fixture_document, *};

    #[test]
    fn focus_wraps_without_changing_the_authenticated_document() {
        let mut viewer = EntryViewer::new(fixture_document("email/personal"));
        viewer.focus_next();
        assert_eq!(viewer.focused_index(), Some(1));
        viewer.focus_previous();
        assert_eq!(viewer.focused_index(), Some(0));
    }

    #[test]
    fn copy_uses_only_the_focused_structured_value() {
        let viewer = EntryViewer::new(fixture_document("email/personal"));
        let copied = viewer.copy_focused().expect("copy value");
        assert_eq!(copied.expose(), b"alice@example.test");
        assert!(!format!("{viewer:?}").contains("correct horse fixture"));
    }

    #[test]
    fn scrolling_saturates() {
        let mut viewer = EntryViewer::new(fixture_document("unicode/咖啡"));
        viewer.focus_next();
        viewer.scroll_down(usize::MAX);
        let end = viewer.scroll();
        viewer.scroll_down(1);
        assert_eq!(viewer.scroll(), end);
        viewer.scroll_up(usize::MAX);
        assert_eq!(viewer.scroll(), 0);
    }

    #[test]
    fn focus_only_scrolls_when_it_crosses_a_viewport_edge() {
        let mut viewer = EntryViewer::new(fixture_document("email/personal"));
        assert_eq!(viewer.document().fields().len(), 3);

        assert_eq!(viewer.ensure_focus_visible(0, 1, 3, 3), 0);
        viewer.focus_next();
        assert_eq!(viewer.ensure_focus_visible(1, 1, 3, 3), 0);
        viewer.focus_next();
        assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 3), 0);
        viewer.focus_next();
        assert_eq!(viewer.ensure_focus_visible(0, 1, 3, 3), 0);

        viewer.focus_previous();
        assert_eq!(viewer.focused_index(), Some(2));
        assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 2), 1);
        viewer.focus_previous();
        assert_eq!(viewer.focused_index(), Some(1));
        assert_eq!(viewer.ensure_focus_visible(1, 1, 3, 2), 1);
        viewer.focus_previous();
        assert_eq!(viewer.focused_index(), Some(0));
        assert_eq!(viewer.ensure_focus_visible(0, 1, 3, 2), 0);
    }

    #[test]
    fn viewport_resize_clamps_then_minimally_reveals_the_focus() {
        let mut viewer = EntryViewer::new(fixture_document("email/personal"));
        viewer.focus_next();
        viewer.focus_next();
        assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 1), 2);
        assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 2), 1);
        assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 3), 0);
        assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 1), 2);

        viewer.focus_previous();
        assert_eq!(viewer.ensure_focus_visible(1, 1, 3, 1), 1);
        viewer.focus_previous();
        assert_eq!(viewer.ensure_focus_visible(0, 1, 3, 1), 0);
    }

    #[test]
    fn wrapped_fields_use_rendered_row_ranges_for_minimal_scrolling() {
        let mut viewer = EntryViewer::new(fixture_document("email/personal"));
        viewer.focus_next();
        viewer.focus_next();

        assert_eq!(viewer.ensure_focus_visible(4, 2, 6, 4), 2);
        viewer.focus_previous();
        assert_eq!(viewer.ensure_focus_visible(2, 2, 6, 4), 2);
        viewer.focus_previous();
        assert_eq!(viewer.ensure_focus_visible(0, 2, 6, 4), 0);
    }

    #[test]
    fn a_field_taller_than_the_viewport_allows_needed_manual_scrolling() {
        let mut viewer = EntryViewer::new(fixture_document("email/personal"));
        viewer.focus_next();

        assert_eq!(viewer.ensure_focus_visible(1, 5, 7, 3), 0);
        viewer.scroll_down(2);
        assert_eq!(viewer.ensure_focus_visible(1, 5, 7, 3), 2);
        viewer.scroll_down(usize::MAX);
        assert_eq!(viewer.ensure_focus_visible(1, 5, 7, 3), 4);
    }
}
