//! Structured desktop editing state over storage-owned entry documents.

use std::fmt;

use iced::widget::text_editor;
use ironstorage::{
    document::{DocumentError, EntryDocument, EntryField, EntryFieldDraft, EntryFieldId},
    repository::SecretBytes,
};

pub struct EntryEditor {
    document: EntryDocument,
    focused: Option<EntryFieldId>,
    multiline: Vec<(EntryFieldId, text_editor::Content)>,
    dirty: bool,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FieldNavigation {
    First,
    Last,
    Next,
    Previous,
}

impl EntryEditor {
    pub fn new(document: EntryDocument) -> Self {
        let focused = document.display_fields().first().map(|field| field.id());
        let multiline = document
            .fields()
            .iter()
            .filter_map(|field| {
                let value = std::str::from_utf8(field.value()).ok()?;
                value
                    .contains('\n')
                    .then(|| (field.id(), text_editor::Content::with_text(value)))
            })
            .collect();
        Self {
            document,
            focused,
            multiline,
            dirty: false,
        }
    }

    pub fn new_entry(
        document: EntryDocument,
        password: SecretBytes,
    ) -> Result<Self, DocumentError> {
        let mut editor = Self::new(document);
        editor.add_after(None)?;
        let id = editor
            .focused
            .expect("new entry receives its password field");
        if !password.expose().is_empty() {
            editor.replace_value(id, password)?;
        }
        Ok(editor)
    }

    pub fn entry(&self) -> String {
        self.document.path().to_string()
    }

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

    pub fn fields(&self) -> &[EntryField] {
        self.document.fields()
    }

    pub fn is_dirty(&self) -> bool {
        self.dirty
    }

    pub fn focused(&self) -> Option<EntryFieldId> {
        self.focused
    }

    pub fn multiline_content(&self, id: EntryFieldId) -> Option<&text_editor::Content> {
        self.multiline
            .iter()
            .find_map(|(field_id, content)| (*field_id == id).then_some(content))
    }

    pub fn focused_ratio(&self) -> f32 {
        let fields = self.document.display_fields();
        self.focused
            .and_then(|id| fields.iter().position(|field| field.id() == id))
            .map_or(0.0, |index| {
                index as f32 / fields.len().saturating_sub(1).max(1) as f32
            })
    }

    pub fn select(&mut self, id: EntryFieldId) {
        if self.document.field(id).is_some() {
            self.focused = Some(id);
        }
    }

    pub fn edit_multiline(
        &mut self,
        id: EntryFieldId,
        action: text_editor::Action,
    ) -> Result<(), DocumentError> {
        let previous = self
            .document
            .field(id)
            .ok_or(DocumentError::UnknownField { id })?
            .value();
        let value = {
            let content = self
                .multiline
                .iter_mut()
                .find_map(|(field_id, content)| (*field_id == id).then_some(content))
                .ok_or(DocumentError::UnknownField { id })?;
            content.perform(action);
            content.text().into_bytes()
        };
        self.focused = Some(id);
        if value != previous {
            self.document.replace_field_value(id, value)?;
            self.dirty = true;
        }
        Ok(())
    }

    pub fn navigate(&mut self, navigation: FieldNavigation) {
        let fields = self.document.display_fields();
        if fields.is_empty() {
            self.focused = None;
            return;
        }
        let current = self
            .focused
            .and_then(|id| fields.iter().position(|field| field.id() == id))
            .unwrap_or(0);
        let index = match navigation {
            FieldNavigation::First => 0,
            FieldNavigation::Last => fields.len() - 1,
            FieldNavigation::Next => (current + 1).min(fields.len() - 1),
            FieldNavigation::Previous => current.saturating_sub(1),
        };
        self.focused = Some(fields[index].id());
    }

    #[cfg(test)]
    pub fn update_raw(&mut self, id: EntryFieldId, value: &[u8]) -> Result<(), DocumentError> {
        let unchanged = self
            .document
            .field(id)
            .is_some_and(|field| field.contents().expose() == value);
        if unchanged {
            return Ok(());
        }
        self.document
            .update(id, EntryFieldDraft::multiline(value.to_vec()))?;
        self.sync_multiline(id);
        self.dirty = true;
        Ok(())
    }

    pub fn add_value_line_after(
        &mut self,
        id: EntryFieldId,
        line: usize,
    ) -> Result<(), DocumentError> {
        let field = self
            .document
            .field(id)
            .ok_or(DocumentError::UnknownField { id })?;
        let mut value = field.value().to_vec();
        let insertion =
            value_line_end(&value, line).ok_or(DocumentError::InvalidIndex { index: line })?;
        value.splice(insertion..insertion, *b"\n");
        self.document.replace_field_value(id, value)?;
        self.sync_multiline(id);
        self.dirty = true;
        Ok(())
    }

    pub fn add_after(&mut self, id: Option<EntryFieldId>) -> Result<(), DocumentError> {
        let index = id
            .and_then(|id| {
                self.document
                    .fields()
                    .iter()
                    .position(|field| field.id() == id)
            })
            .map_or(self.document.fields().len(), |index| index + 1);
        self.focused = Some(self.document.add(index, EntryFieldDraft::blank())?);
        self.dirty = true;
        Ok(())
    }

    pub fn remove(&mut self, id: EntryFieldId) -> Result<(), DocumentError> {
        let index = self
            .document
            .fields()
            .iter()
            .position(|field| field.id() == id)
            .ok_or(DocumentError::UnknownField { id })?;
        self.document.remove(id)?;
        self.multiline.retain(|(field_id, _)| *field_id != id);
        if self.focused == Some(id) {
            self.focused = self
                .document
                .fields()
                .get(index.min(self.document.fields().len().saturating_sub(1)))
                .map(EntryField::id);
        }
        self.dirty = true;
        Ok(())
    }

    pub fn move_up(&mut self, id: EntryFieldId) -> Result<(), DocumentError> {
        let Some(index) = self
            .document
            .fields()
            .iter()
            .position(|field| field.id() == id)
        else {
            return Err(DocumentError::UnknownField { id });
        };
        if index > 0 {
            self.document.reorder(id, index - 1)?;
            self.dirty = true;
        }
        Ok(())
    }

    pub fn move_down(&mut self, id: EntryFieldId) -> Result<(), DocumentError> {
        let Some(index) = self
            .document
            .fields()
            .iter()
            .position(|field| field.id() == id)
        else {
            return Err(DocumentError::UnknownField { id });
        };
        if index + 1 < self.document.fields().len() {
            self.document.reorder(id, index + 1)?;
            self.dirty = true;
        }
        Ok(())
    }

    pub fn replace_value(
        &mut self,
        id: EntryFieldId,
        value: SecretBytes,
    ) -> Result<(), DocumentError> {
        self.document
            .replace_field_value(id, value.expose().to_vec())?;
        self.sync_multiline(id);
        self.dirty = true;
        Ok(())
    }

    pub fn copy_value(&self, id: EntryFieldId) -> Result<SecretBytes, DocumentError> {
        self.document.copy_field_value(id)
    }

    fn sync_multiline(&mut self, id: EntryFieldId) {
        let value = self
            .document
            .field(id)
            .and_then(|field| std::str::from_utf8(field.value()).ok())
            .filter(|value| value.contains('\n'));
        let existing = self
            .multiline
            .iter()
            .position(|(field_id, _)| *field_id == id);
        match (existing, value) {
            (Some(index), Some(value)) => {
                self.multiline[index].1 = text_editor::Content::with_text(value);
            }
            (None, Some(value)) => self
                .multiline
                .push((id, text_editor::Content::with_text(value))),
            (Some(index), None) => {
                self.multiline.remove(index);
            }
            (None, None) => {}
        }
    }
}

fn value_line_end(value: &[u8], target: usize) -> Option<usize> {
    let mut end = 0;
    let mut lines = 0;
    for (line, segment) in value.split_inclusive(|byte| *byte == b'\n').enumerate() {
        lines = line + 1;
        end += segment.len();
        if line == target {
            return Some(end);
        }
    }
    (target == lines && (value.is_empty() || value.ends_with(b"\n"))).then_some(value.len())
}

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