//! Storage-owned projection of lossless entry documents for native mobile viewers.

use std::{collections::BTreeSet, error::Error, fmt, num::NonZeroUsize};

use crate::document::{
    DocumentError, EntryDocument, EntryField, EntryFieldDiagnostic, EntryFieldDraft, EntryFieldId,
    EntryFieldKind, EntrySensitivity,
};
use crate::generate::{GenerateError, GeneratorConfig, MAX_PASSWORD_LENGTH};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MobileEntrySectionKind {
    Password,
    Details,
    OneTimePassword,
    Notes,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileEntryField {
    id: u64,
    label: String,
    system_image: String,
    value: Option<String>,
    masked_value: String,
    detail: Option<String>,
    diagnostic: Option<String>,
    sensitive: bool,
    multiline: bool,
    selectable: bool,
    editable: bool,
}

impl MobileEntryField {
    pub fn id(&self) -> u64 {
        self.id
    }

    pub fn label(&self) -> &str {
        &self.label
    }

    pub fn system_image(&self) -> &str {
        &self.system_image
    }

    pub fn value(&self) -> Option<&str> {
        self.value.as_deref()
    }

    pub fn masked_value(&self) -> &str {
        &self.masked_value
    }

    pub fn detail(&self) -> Option<&str> {
        self.detail.as_deref()
    }

    pub fn diagnostic(&self) -> Option<&str> {
        self.diagnostic.as_deref()
    }

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

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

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

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileEntrySection {
    kind: MobileEntrySectionKind,
    title: String,
    fields: Vec<MobileEntryField>,
}

impl MobileEntrySection {
    pub fn kind(&self) -> MobileEntrySectionKind {
        self.kind
    }

    pub fn title(&self) -> &str {
        &self.title
    }

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileEntryPage {
    id: String,
    title: String,
    sections: Vec<MobileEntrySection>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MobileEntryEditorFieldKind {
    Password,
    Field,
    Note,
    OtpUri,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileEntryEditorField {
    id: u64,
    kind: MobileEntryEditorFieldKind,
    name: Option<String>,
    label: String,
    system_image: String,
    value: Option<String>,
    masked_value: String,
    diagnostic: Option<String>,
    sensitive: bool,
    multiline: bool,
    name_editable: bool,
    removable: bool,
    reorderable: bool,
}

impl MobileEntryEditorField {
    pub fn id(&self) -> u64 {
        self.id
    }

    pub fn kind(&self) -> MobileEntryEditorFieldKind {
        self.kind
    }

    pub fn name(&self) -> Option<&str> {
        self.name.as_deref()
    }

    pub fn label(&self) -> &str {
        &self.label
    }

    pub fn system_image(&self) -> &str {
        &self.system_image
    }

    pub fn value(&self) -> Option<&str> {
        self.value.as_deref()
    }

    pub fn masked_value(&self) -> &str {
        &self.masked_value
    }

    pub fn diagnostic(&self) -> Option<&str> {
        self.diagnostic.as_deref()
    }

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

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

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

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

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileEntryEditorPage {
    path: String,
    title: String,
    fields: Vec<MobileEntryEditorField>,
    dirty: bool,
    creating: bool,
    default_password_length: u32,
    maximum_password_length: u32,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileEntryEditorSession {
    id: u64,
    page: MobileEntryEditorPage,
}

impl MobileEntryEditorSession {
    pub fn new(id: u64, page: MobileEntryEditorPage) -> Self {
        Self { id, page }
    }

    pub fn id(&self) -> u64 {
        self.id
    }

    pub fn page(&self) -> &MobileEntryEditorPage {
        &self.page
    }
}

impl MobileEntryEditorPage {
    pub fn path(&self) -> &str {
        &self.path
    }

    pub fn title(&self) -> &str {
        &self.title
    }

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

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

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

    pub fn default_password_length(&self) -> u32 {
        self.default_password_length
    }

    pub fn maximum_password_length(&self) -> u32 {
        self.maximum_password_length
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileEntryEditorInput {
    id: u64,
    name: Option<String>,
    value: Option<String>,
}

impl MobileEntryEditorInput {
    pub fn new(id: u64, name: Option<String>, value: Option<String>) -> Self {
        Self { id, name, value }
    }
}

pub struct MobileEntryDraft {
    document: EntryDocument,
    creating: bool,
}

impl MobileEntryDraft {
    pub fn new(document: EntryDocument, creating: bool) -> Self {
        Self { document, creating }
    }

    pub fn page(&self) -> MobileEntryEditorPage {
        let fields = self
            .document
            .fields()
            .iter()
            .enumerate()
            .map(|(index, field)| editor_field(index, field))
            .collect();
        MobileEntryEditorPage {
            path: self.document.path().to_string(),
            title: self
                .document
                .path()
                .as_path()
                .file_name()
                .and_then(|name| name.to_str())
                .unwrap_or("Password")
                .to_owned(),
            fields,
            dirty: self.document.is_modified(),
            creating: self.creating,
            default_password_length: GeneratorConfig::pass_defaults().default_length().get() as u32,
            maximum_password_length: MAX_PASSWORD_LENGTH as u32,
        }
    }

    pub fn apply(
        &mut self,
        inputs: Vec<MobileEntryEditorInput>,
    ) -> Result<(), MobileEntryEditorError> {
        if inputs.len() != self.document.fields().len() {
            return Err(MobileEntryEditorError::FieldSetChanged);
        }
        let mut seen = BTreeSet::new();
        let mut replacements = Vec::with_capacity(inputs.len());
        for input in inputs {
            if !seen.insert(input.id) {
                return Err(MobileEntryEditorError::FieldSetChanged);
            }
            let id = EntryFieldId::from_value(input.id);
            let field = self
                .document
                .field(id)
                .ok_or(MobileEntryEditorError::FieldSetChanged)?;
            if let Some(value) = input.value {
                replacements.push((id, editor_draft(field, input.name, value)?));
            } else if field.metadata().diagnostic() != Some(EntryFieldDiagnostic::NonUtf8Value) {
                return Err(MobileEntryEditorError::NonUtf8);
            }
        }
        for (id, replacement) in replacements {
            self.document.update(id, replacement)?;
        }
        Ok(())
    }

    pub fn add(
        &mut self,
        kind: MobileEntryEditorFieldKind,
        name: Option<String>,
        value: String,
    ) -> Result<(), MobileEntryEditorError> {
        let draft = match kind {
            MobileEntryEditorFieldKind::Password => {
                return Err(MobileEntryEditorError::ProtectedPasswordField);
            }
            MobileEntryEditorFieldKind::Field => EntryFieldDraft::field(
                name.ok_or(MobileEntryEditorError::MissingFieldName)?,
                value.into_bytes(),
            )?,
            MobileEntryEditorFieldKind::Note => {
                EntryFieldDraft::field("notes", value.into_bytes())?
            }
            MobileEntryEditorFieldKind::OtpUri => EntryFieldDraft::otp_uri(value.into_bytes())?,
        };
        self.document.add(self.document.fields().len(), draft)?;
        Ok(())
    }

    pub fn remove(&mut self, id: u64) -> Result<(), MobileEntryEditorError> {
        let id = EntryFieldId::from_value(id);
        if self
            .document
            .fields()
            .first()
            .is_some_and(|field| field.id() == id)
        {
            return Err(MobileEntryEditorError::ProtectedPasswordField);
        }
        self.document.remove(id)?;
        Ok(())
    }

    pub fn reorder(&mut self, id: u64, index: usize) -> Result<(), MobileEntryEditorError> {
        let id = EntryFieldId::from_value(id);
        if index == 0
            || self
                .document
                .fields()
                .first()
                .is_some_and(|field| field.id() == id)
        {
            return Err(MobileEntryEditorError::ProtectedPasswordField);
        }
        self.document.reorder(id, index)?;
        Ok(())
    }

    pub fn generate_password(
        &mut self,
        length: Option<u32>,
        no_symbols: bool,
    ) -> Result<(), MobileEntryEditorError> {
        let length = length
            .map(|length| {
                usize::try_from(length)
                    .ok()
                    .and_then(NonZeroUsize::new)
                    .ok_or(MobileEntryEditorError::Generate(
                        GenerateError::InvalidLength,
                    ))
            })
            .transpose()?;
        let generated = GeneratorConfig::pass_defaults().generate_secret(length, no_symbols)?;
        let value = generated.expose().to_vec();
        if let Some(password) = self
            .document
            .fields()
            .iter()
            .find(|field| field.metadata().kind() == EntryFieldKind::Password)
        {
            self.document
                .update(password.id(), EntryFieldDraft::line(value)?)?;
        } else {
            self.document.add(0, EntryFieldDraft::line(value)?)?;
        }
        Ok(())
    }

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

#[derive(Debug)]
pub enum MobileEntryEditorError {
    Document(DocumentError),
    Generate(GenerateError),
    FieldSetChanged,
    MissingFieldName,
    ProtectedPasswordField,
    NonUtf8,
}

impl fmt::Display for MobileEntryEditorError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Document(error) => error.fmt(formatter),
            Self::Generate(error) => error.fmt(formatter),
            Self::FieldSetChanged => formatter.write_str("the entry fields changed while editing"),
            Self::MissingFieldName => formatter.write_str("a named field requires a name"),
            Self::ProtectedPasswordField => {
                formatter.write_str("the first password field cannot be removed or moved")
            }
            Self::NonUtf8 => formatter.write_str("the entry field is not valid UTF-8"),
        }
    }
}

impl Error for MobileEntryEditorError {}

impl From<DocumentError> for MobileEntryEditorError {
    fn from(error: DocumentError) -> Self {
        Self::Document(error)
    }
}

impl From<GenerateError> for MobileEntryEditorError {
    fn from(error: GenerateError) -> Self {
        Self::Generate(error)
    }
}

impl MobileEntryPage {
    pub fn from_document(document: &EntryDocument) -> Self {
        let title = document
            .path()
            .as_path()
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("Password")
            .to_owned();
        let mut sections = Vec::new();
        for (kind, section_title) in [
            (MobileEntrySectionKind::Password, "Password"),
            (MobileEntrySectionKind::Details, "Details"),
            (MobileEntrySectionKind::OneTimePassword, "One-Time Password"),
            (MobileEntrySectionKind::Notes, "Notes"),
        ] {
            let fields = document
                .display_fields()
                .into_iter()
                .filter(|field| section_kind(field) == kind)
                .map(mobile_field)
                .collect::<Vec<_>>();
            if !fields.is_empty() {
                sections.push(MobileEntrySection {
                    kind,
                    title: section_title.to_owned(),
                    fields,
                });
            }
        }
        Self {
            id: format!("entry:{}", document.path()),
            title,
            sections,
        }
    }

    pub fn id(&self) -> &str {
        &self.id
    }

    pub fn title(&self) -> &str {
        &self.title
    }

    pub fn sections(&self) -> &[MobileEntrySection] {
        &self.sections
    }
}

#[derive(Debug)]
pub enum MobileEntryValueError {
    Document(DocumentError),
    NonUtf8,
}

impl fmt::Display for MobileEntryValueError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Document(error) => error.fmt(formatter),
            Self::NonUtf8 => formatter.write_str("the entry field is not valid UTF-8"),
        }
    }
}

impl Error for MobileEntryValueError {}

impl From<DocumentError> for MobileEntryValueError {
    fn from(error: DocumentError) -> Self {
        Self::Document(error)
    }
}

pub fn field_value(document: &EntryDocument, id: u64) -> Result<String, MobileEntryValueError> {
    let value = document.copy_field_value(EntryFieldId::from_value(id))?;
    String::from_utf8(value.expose().to_vec()).map_err(|_| MobileEntryValueError::NonUtf8)
}

fn mobile_field(field: &EntryField) -> MobileEntryField {
    let metadata = field.metadata();
    let sensitive = metadata.sensitivity() == EntrySensitivity::Sensitive;
    let value = if sensitive || metadata.sensitivity() == EntrySensitivity::Empty {
        None
    } else {
        String::from_utf8(field.value().to_vec()).ok()
    };
    MobileEntryField {
        id: field.id().value(),
        label: label(field),
        system_image: system_image(metadata.kind()).to_owned(),
        value,
        masked_value: if metadata.sensitivity() == EntrySensitivity::Empty {
            "Empty"
        } else if sensitive {
            "Hidden"
        } else {
            "Unavailable"
        }
        .to_owned(),
        detail: otp_detail(field),
        diagnostic: metadata.diagnostic().map(diagnostic),
        sensitive,
        multiline: matches!(metadata.kind(), EntryFieldKind::Note)
            || field.value().contains(&b'\n'),
        selectable: !sensitive && metadata.diagnostic() != Some(EntryFieldDiagnostic::NonUtf8Value),
        editable: true,
    }
}

fn editor_field(index: usize, field: &EntryField) -> MobileEntryEditorField {
    let metadata = field.metadata();
    let kind = if is_named_note(field) {
        MobileEntryEditorFieldKind::Note
    } else {
        match metadata.kind() {
            EntryFieldKind::Password => MobileEntryEditorFieldKind::Password,
            EntryFieldKind::OtpUri => MobileEntryEditorFieldKind::OtpUri,
            EntryFieldKind::Note => MobileEntryEditorFieldKind::Note,
            EntryFieldKind::Username
            | EntryFieldKind::Email
            | EntryFieldKind::Url
            | EntryFieldKind::Field => MobileEntryEditorFieldKind::Field,
        }
    };
    let sensitive = metadata.sensitivity() == EntrySensitivity::Sensitive;
    MobileEntryEditorField {
        id: field.id().value(),
        kind,
        name: metadata.name().map(str::to_owned),
        label: label(field),
        system_image: system_image(metadata.kind()).to_owned(),
        value: String::from_utf8(field.value().to_vec()).ok(),
        masked_value: if metadata.sensitivity() == EntrySensitivity::Empty {
            "Empty"
        } else if sensitive {
            "Hidden"
        } else {
            "Unavailable"
        }
        .to_owned(),
        diagnostic: metadata.diagnostic().map(diagnostic),
        sensitive,
        multiline: matches!(kind, MobileEntryEditorFieldKind::Note)
            || field.value().contains(&b'\n'),
        name_editable: matches!(kind, MobileEntryEditorFieldKind::Field),
        removable: index > 0,
        reorderable: index > 0,
    }
}

fn editor_draft(
    field: &EntryField,
    name: Option<String>,
    value: String,
) -> Result<EntryFieldDraft, MobileEntryEditorError> {
    Ok(match field.metadata().kind() {
        EntryFieldKind::Password => EntryFieldDraft::line(value.into_bytes())?,
        EntryFieldKind::OtpUri => EntryFieldDraft::otp_uri(value.into_bytes())?,
        EntryFieldKind::Note => EntryFieldDraft::multiline(value.into_bytes()),
        EntryFieldKind::Username
        | EntryFieldKind::Email
        | EntryFieldKind::Url
        | EntryFieldKind::Field => EntryFieldDraft::field(
            name.ok_or(MobileEntryEditorError::MissingFieldName)?,
            value.into_bytes(),
        )?,
    })
}

fn section_kind(field: &EntryField) -> MobileEntrySectionKind {
    if is_named_note(field) {
        return MobileEntrySectionKind::Notes;
    }
    match field.metadata().kind() {
        EntryFieldKind::Password => MobileEntrySectionKind::Password,
        EntryFieldKind::OtpUri => MobileEntrySectionKind::OneTimePassword,
        EntryFieldKind::Note => MobileEntrySectionKind::Notes,
        EntryFieldKind::Username
        | EntryFieldKind::Email
        | EntryFieldKind::Url
        | EntryFieldKind::Field => MobileEntrySectionKind::Details,
    }
}

fn is_named_note(field: &EntryField) -> bool {
    field.metadata().name().is_some_and(|name| {
        ["comment", "comments", "note", "notes"]
            .iter()
            .any(|candidate| name.eq_ignore_ascii_case(candidate))
    })
}

fn label(field: &EntryField) -> String {
    if field.metadata().kind() == EntryFieldKind::Password {
        return "Password".to_owned();
    }
    if field.metadata().kind() == EntryFieldKind::OtpUri {
        return "OTP URI".to_owned();
    }
    field
        .metadata()
        .name()
        .map(display_name)
        .unwrap_or_else(|| match field.metadata().kind() {
            EntryFieldKind::Username => "Username".to_owned(),
            EntryFieldKind::Email => "Email".to_owned(),
            EntryFieldKind::Url => "Website".to_owned(),
            EntryFieldKind::Field => "Field".to_owned(),
            EntryFieldKind::Note => "Notes".to_owned(),
            EntryFieldKind::Password | EntryFieldKind::OtpUri => unreachable!(),
        })
}

fn display_name(name: &str) -> String {
    let mut characters = name.chars();
    match characters.next() {
        Some(first) => first.to_uppercase().chain(characters).collect(),
        None => "Field".to_owned(),
    }
}

fn system_image(kind: EntryFieldKind) -> &'static str {
    match kind {
        EntryFieldKind::Password => "key.fill",
        EntryFieldKind::Username => "person.fill",
        EntryFieldKind::Email => "envelope.fill",
        EntryFieldKind::Url => "globe",
        EntryFieldKind::OtpUri => "timer",
        EntryFieldKind::Field => "text.alignleft",
        EntryFieldKind::Note => "note.text",
    }
}

fn diagnostic(diagnostic: EntryFieldDiagnostic) -> String {
    match diagnostic {
        EntryFieldDiagnostic::MalformedOtpUri => {
            "This OTP URI is malformed. Its original value is preserved.".to_owned()
        }
        EntryFieldDiagnostic::NonUtf8Value => {
            "This field is not UTF-8 text. Its original bytes are preserved.".to_owned()
        }
    }
}

fn otp_detail(field: &EntryField) -> Option<String> {
    let otp = field.metadata().otp()?;
    let mut parts = vec![format!("{:?}", otp.kind()).to_uppercase()];
    if let Some(issuer) = otp.issuer() {
        parts.push(issuer.to_owned());
    }
    if !otp.account().is_empty() {
        parts.push(otp.account().to_owned());
    }
    parts.push(format!("{:?}", otp.algorithm()).to_uppercase());
    parts.push(format!("{} digits", otp.digits()));
    if let Some(period) = otp.period() {
        parts.push(format!("{period} seconds"));
    }
    if let Some(counter) = otp.counter() {
        parts.push(format!("counter {counter}"));
    }
    Some(parts.join(" · "))
}
