//! Lossless, presentation-neutral documents for arbitrary pass entries.

use std::{error::Error, fmt, ops::Range};

use sha2::{Digest as _, Sha256};

use crate::{
    command::EditRequest,
    crypto::{KeyStore, SecretProvider},
    otp::{OtpAlgorithm, OtpError, OtpKind, OtpUri},
    recipient::SigningPolicy,
    repository::{EncryptedEntry, EntryPath, Repository, SecretBytes},
    write::{EditSession, EntryCommitter, VaultWriter, WriteError, WriteOutcome},
};

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EntryFieldId(u64);

impl EntryFieldId {
    pub fn from_value(value: u64) -> Self {
        Self(value)
    }

    pub fn value(self) -> u64 {
        self.0
    }
}

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

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EntrySensitivity {
    Sensitive,
    Ordinary,
    Empty,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EntryFieldDiagnostic {
    MalformedOtpUri,
    NonUtf8Value,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryFieldMetadata {
    kind: EntryFieldKind,
    sensitivity: EntrySensitivity,
    name: Option<String>,
    otp: Option<EntryOtpMetadata>,
    diagnostic: Option<EntryFieldDiagnostic>,
    value: Range<usize>,
}

impl EntryFieldMetadata {
    pub fn kind(&self) -> EntryFieldKind {
        self.kind
    }

    pub fn sensitivity(&self) -> EntrySensitivity {
        self.sensitivity
    }

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

    pub fn otp(&self) -> Option<&EntryOtpMetadata> {
        self.otp.as_ref()
    }

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

/// Non-secret presentation metadata parsed from a validated `otpauth` URI.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryOtpMetadata {
    kind: OtpKind,
    issuer: Option<String>,
    account: String,
    algorithm: OtpAlgorithm,
    digits: u32,
    period: Option<u64>,
    counter: Option<u64>,
}

impl EntryOtpMetadata {
    pub fn kind(&self) -> OtpKind {
        self.kind
    }

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

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

    pub fn algorithm(&self) -> OtpAlgorithm {
        self.algorithm
    }

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

    pub fn period(&self) -> Option<u64> {
        self.period
    }

    pub fn counter(&self) -> Option<u64> {
        self.counter
    }
}

pub struct EntryField {
    id: EntryFieldId,
    contents: SecretBytes,
    ending: Vec<u8>,
    metadata: EntryFieldMetadata,
}

impl EntryField {
    pub fn id(&self) -> EntryFieldId {
        self.id
    }

    pub fn contents(&self) -> &SecretBytes {
        &self.contents
    }

    pub fn value(&self) -> &[u8] {
        &self.contents.expose()[self.metadata.value.clone()]
    }

    pub fn metadata(&self) -> &EntryFieldMetadata {
        &self.metadata
    }
}

impl fmt::Debug for EntryField {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("EntryField")
            .field("id", &self.id)
            .field("metadata", &self.metadata)
            .field("contents", &"[REDACTED]")
            .finish()
    }
}

pub struct EntryFieldDraft {
    contents: SecretBytes,
}

impl EntryFieldDraft {
    pub fn line(contents: Vec<u8>) -> Result<Self, DocumentError> {
        validate_line(&contents)?;
        Ok(Self {
            contents: SecretBytes::new(contents),
        })
    }

    pub fn field(name: impl Into<String>, value: Vec<u8>) -> Result<Self, DocumentError> {
        let name = name.into();
        validate_name(&name)?;
        let value = SecretBytes::new(value);
        let mut contents = Vec::with_capacity(name.len() + value.expose().len() + 2);
        contents.extend_from_slice(name.as_bytes());
        contents.extend_from_slice(b": ");
        contents.extend_from_slice(value.expose());
        Ok(Self {
            contents: SecretBytes::new(contents),
        })
    }

    pub fn otp_uri(uri: Vec<u8>) -> Result<Self, DocumentError> {
        validate_line(&uri)?;
        let encoded = SecretBytes::new(uri);
        OtpUri::parse(SecretBytes::new(encoded.expose().to_vec()))
            .map_err(|_| DocumentError::InvalidOtpUri)?;
        Ok(Self { contents: encoded })
    }

    pub fn blank() -> Self {
        Self {
            contents: SecretBytes::new(Vec::new()),
        }
    }

    pub fn multiline(contents: Vec<u8>) -> Self {
        Self {
            contents: SecretBytes::new(contents),
        }
    }

    fn render(self) -> SecretBytes {
        self.contents
    }
}

impl fmt::Debug for EntryFieldDraft {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("EntryFieldDraft([REDACTED])")
    }
}

#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct DocumentConflictToken([u8; 32]);

impl DocumentConflictToken {
    pub fn as_bytes(&self) -> &[u8; 32] {
        &self.0
    }
}

impl fmt::Debug for DocumentConflictToken {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("DocumentConflictToken([REDACTED])")
    }
}

pub struct EntryDocument {
    path: EntryPath,
    fields: Vec<EntryField>,
    next_id: u64,
    conflict_token: DocumentConflictToken,
    session: EditSession,
    modified: bool,
}

impl EntryDocument {
    fn from_session(session: EditSession) -> Self {
        let path = session.path().clone();
        let conflict_token = conflict_token(&session);
        let mut fields = parse_lines(session.plaintext().expose());
        classify_all(&mut fields);
        let next_id = fields.len() as u64;
        Self {
            path,
            fields,
            next_id,
            conflict_token,
            session,
            modified: false,
        }
    }

    pub fn path(&self) -> &EntryPath {
        &self.path
    }

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

    /// Fields in the shared presentation order without changing repository order.
    pub fn display_fields(&self) -> Vec<&EntryField> {
        let mut fields = self.fields.iter().collect::<Vec<_>>();
        fields.sort_by_cached_key(|field| display_key(field));
        fields
    }

    pub fn field(&self, id: EntryFieldId) -> Option<&EntryField> {
        self.fields.iter().find(|field| field.id == id)
    }

    pub fn password(&self) -> Option<&EntryField> {
        self.fields.first()
    }

    /// Copy one structured field value into an independently zeroizing buffer.
    ///
    /// Frontends use this for explicit presentation actions without reparsing
    /// the pass entry or copying label syntax alongside the selected value.
    pub fn copy_field_value(&self, id: EntryFieldId) -> Result<SecretBytes, DocumentError> {
        self.field(id)
            .map(|field| SecretBytes::new(field.value().to_vec()))
            .ok_or(DocumentError::UnknownField { id })
    }

    /// Replace only a structured field's value while preserving its storage-
    /// supplied name and syntax.
    pub fn replace_field_value(
        &mut self,
        id: EntryFieldId,
        value: Vec<u8>,
    ) -> Result<(), DocumentError> {
        let field = self.field(id).ok_or(DocumentError::UnknownField { id })?;
        let draft = match field.metadata().kind() {
            EntryFieldKind::OtpUri => EntryFieldDraft::otp_uri(value)?,
            EntryFieldKind::Password => EntryFieldDraft::line(value)?,
            EntryFieldKind::Note => EntryFieldDraft::multiline(value),
            EntryFieldKind::Username
            | EntryFieldKind::Email
            | EntryFieldKind::Url
            | EntryFieldKind::Field => EntryFieldDraft::field(
                field
                    .metadata()
                    .name()
                    .ok_or(DocumentError::InvalidFieldName)?,
                value,
            )?,
        };
        self.update(id, draft)
    }

    pub fn conflict_token(&self) -> DocumentConflictToken {
        self.conflict_token
    }

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

    pub fn add(
        &mut self,
        index: usize,
        draft: EntryFieldDraft,
    ) -> Result<EntryFieldId, DocumentError> {
        if index > self.fields.len() {
            return Err(DocumentError::InvalidIndex { index });
        }
        let final_newline = self.has_final_newline();
        let default_ending = self.default_ending();
        let id = EntryFieldId(self.next_id);
        self.next_id = self
            .next_id
            .checked_add(1)
            .ok_or(DocumentError::FieldIdExhausted)?;
        self.fields.insert(
            index,
            EntryField {
                id,
                contents: draft.render(),
                ending: default_ending.clone(),
                metadata: empty_note_metadata(),
            },
        );
        normalize_endings(&mut self.fields, &default_ending, final_newline);
        classify_all(&mut self.fields);
        self.modified = true;
        Ok(id)
    }

    pub fn update(
        &mut self,
        id: EntryFieldId,
        draft: EntryFieldDraft,
    ) -> Result<(), DocumentError> {
        let index = self
            .fields
            .iter()
            .position(|field| field.id == id)
            .ok_or(DocumentError::UnknownField { id })?;
        self.fields[index].contents = draft.render();
        classify_all(&mut self.fields);
        self.modified = true;
        Ok(())
    }

    pub fn remove(&mut self, id: EntryFieldId) -> Result<EntryField, DocumentError> {
        let index = self
            .fields
            .iter()
            .position(|field| field.id == id)
            .ok_or(DocumentError::UnknownField { id })?;
        let final_newline = self.has_final_newline();
        let default_ending = self.default_ending();
        let removed = self.fields.remove(index);
        normalize_endings(&mut self.fields, &default_ending, final_newline);
        classify_all(&mut self.fields);
        self.modified = true;
        Ok(removed)
    }

    pub fn reorder(&mut self, id: EntryFieldId, index: usize) -> Result<(), DocumentError> {
        if index >= self.fields.len() {
            return Err(DocumentError::InvalidIndex { index });
        }
        let current = self
            .fields
            .iter()
            .position(|field| field.id == id)
            .ok_or(DocumentError::UnknownField { id })?;
        if current == index {
            return Ok(());
        }
        let final_newline = self.has_final_newline();
        let default_ending = self.default_ending();
        let field = self.fields.remove(current);
        self.fields.insert(index, field);
        normalize_endings(&mut self.fields, &default_ending, final_newline);
        classify_all(&mut self.fields);
        self.modified = true;
        Ok(())
    }

    pub fn serialize(&self) -> SecretBytes {
        if !self.modified {
            return SecretBytes::new(self.session.plaintext().expose().to_vec());
        }
        let capacity = self
            .fields
            .iter()
            .map(|field| field.contents.expose().len() + field.ending.len())
            .sum();
        let mut output = Vec::with_capacity(capacity);
        for field in &self.fields {
            append_stable_contents(&mut output, field);
            output.extend_from_slice(&field.ending);
        }
        SecretBytes::new(output)
    }

    pub(crate) fn original_ciphertext(&self) -> Option<&EncryptedEntry> {
        self.session.original_ciphertext()
    }

    pub(crate) fn otp_uri(&self) -> Result<Option<OtpUri>, OtpError> {
        let plaintext = self.serialize();
        crate::otp::find_uri(&plaintext, &self.path).map(|found| found.map(|(_, uri)| uri))
    }

    fn has_final_newline(&self) -> bool {
        self.fields
            .last()
            .is_some_and(|field| !field.ending.is_empty())
    }

    fn default_ending(&self) -> Vec<u8> {
        self.fields
            .iter()
            .find(|field| !field.ending.is_empty())
            .map(|field| field.ending.clone())
            .unwrap_or_else(|| b"\n".to_vec())
    }
}

impl fmt::Debug for EntryDocument {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("EntryDocument")
            .field("path", &self.path)
            .field("field_count", &self.fields.len())
            .field("conflict_token", &self.conflict_token)
            .finish_non_exhaustive()
    }
}

pub struct EntryDocumentService<'a> {
    repository: &'a Repository,
    keys: &'a KeyStore,
}

impl<'a> EntryDocumentService<'a> {
    pub fn new(repository: &'a Repository, keys: &'a KeyStore) -> Self {
        Self { repository, keys }
    }

    pub fn open(
        &self,
        entry: &str,
        secrets: &mut impl SecretProvider,
    ) -> Result<EntryDocument, DocumentError> {
        let session = VaultWriter::new(self.repository, self.keys).begin_edit(
            &EditRequest {
                entry: entry.to_owned(),
            },
            secrets,
        )?;
        Ok(EntryDocument::from_session(session))
    }

    pub fn save(
        &self,
        document: EntryDocument,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<WriteOutcome, DocumentError> {
        let replacement = document.serialize();
        Ok(VaultWriter::new(self.repository, self.keys).finish_edit(
            document.session,
            replacement,
            "IronStorage structured editor",
            signing,
            committer,
        )?)
    }

    /// Save without consuming the document, allowing an in-process editor to
    /// present a conflict or encryption failure and retain the user's draft.
    pub fn save_recoverable(
        &self,
        document: &EntryDocument,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<WriteOutcome, DocumentError> {
        let replacement = document.serialize();
        Ok(
            VaultWriter::new(self.repository, self.keys).finish_edit_recoverable(
                &document.session,
                replacement,
                "IronStorage structured editor",
                signing,
                committer,
            )?,
        )
    }
}

#[derive(Debug)]
pub enum DocumentError {
    Write(WriteError),
    InvalidIndex { index: usize },
    UnknownField { id: EntryFieldId },
    LineBreak,
    InvalidFieldName,
    InvalidOtpUri,
    FieldIdExhausted,
}

impl fmt::Display for DocumentError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Write(error) => error.fmt(formatter),
            Self::InvalidIndex { index } => write!(formatter, "invalid entry field index: {index}"),
            Self::UnknownField { id } => write!(formatter, "unknown entry field: {}", id.value()),
            Self::LineBreak => formatter.write_str("an entry field may not contain a line break"),
            Self::InvalidFieldName => formatter.write_str("entry field name is invalid"),
            Self::InvalidOtpUri => formatter.write_str("entry OTP URI is invalid"),
            Self::FieldIdExhausted => formatter.write_str("entry field identifiers are exhausted"),
        }
    }
}

impl Error for DocumentError {}

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

fn parse_lines(contents: &[u8]) -> Vec<EntryField> {
    let mut fields = Vec::new();
    let mut start = 0;
    while start < contents.len() {
        let newline = contents[start..]
            .iter()
            .position(|byte| *byte == b'\n')
            .map(|offset| start + offset);
        let (content_end, end, ending) = match newline {
            Some(newline) if newline > start && contents[newline - 1] == b'\r' => {
                (newline - 1, newline + 1, b"\r\n".to_vec())
            }
            Some(newline) => (newline, newline + 1, b"\n".to_vec()),
            None => (contents.len(), contents.len(), Vec::new()),
        };
        fields.push(EntryField {
            id: EntryFieldId(fields.len() as u64),
            contents: SecretBytes::new(contents[start..content_end].to_vec()),
            ending,
            metadata: empty_note_metadata(),
        });
        start = end;
    }
    let mut logical = Vec::<EntryField>::new();
    for field in fields {
        let continuation = classify(logical.len(), field.contents.expose()).kind
            == EntryFieldKind::Note
            && logical.last().is_some_and(|previous| {
                let metadata =
                    classify(logical.len().saturating_sub(1), previous.contents.expose());
                metadata.kind == EntryFieldKind::Note
                    || (metadata.name.is_some()
                        && !matches!(
                            metadata.kind,
                            EntryFieldKind::Password | EntryFieldKind::OtpUri
                        ))
            });
        if continuation {
            let previous = logical
                .last_mut()
                .expect("continuation has a previous field");
            let mut contents = previous.contents.expose().to_vec();
            contents.extend_from_slice(&previous.ending);
            contents.extend_from_slice(strip_continuation_marker(field.contents.expose()));
            previous.contents = SecretBytes::new(contents);
            previous.ending = field.ending;
        } else {
            let mut field = field;
            if classify(logical.len(), field.contents.expose()).kind == EntryFieldKind::Note
                && let Some(contents) = field.contents.expose().strip_prefix(b" ")
            {
                field.contents = SecretBytes::new(contents.to_vec());
            }
            logical.push(field);
        }
    }
    for (id, field) in logical.iter_mut().enumerate() {
        field.id = EntryFieldId(id as u64);
    }
    logical
}

fn classify_all(fields: &mut [EntryField]) {
    for (index, field) in fields.iter_mut().enumerate() {
        field.metadata = classify(index, field.contents.expose());
    }
}

fn classify(index: usize, line: &[u8]) -> EntryFieldMetadata {
    if line.starts_with(b"otpauth://")
        && let Ok(uri) = OtpUri::parse(SecretBytes::new(line.to_vec()))
    {
        let otp = EntryOtpMetadata {
            kind: uri.kind(),
            issuer: uri.issuer().map(str::to_owned),
            account: uri.account().to_owned(),
            algorithm: uri.algorithm(),
            digits: uri.digits(),
            period: uri.period(),
            counter: uri.counter(),
        };
        return EntryFieldMetadata {
            kind: EntryFieldKind::OtpUri,
            sensitivity: EntrySensitivity::Sensitive,
            name: Some("otp".to_owned()),
            otp: Some(otp),
            diagnostic: None,
            value: 0..line.len(),
        };
    }
    if index == 0 {
        return EntryFieldMetadata {
            kind: EntryFieldKind::Password,
            sensitivity: if line.is_empty() {
                EntrySensitivity::Empty
            } else {
                EntrySensitivity::Sensitive
            },
            name: Some("password".to_owned()),
            otp: None,
            diagnostic: (!line.is_ascii() && std::str::from_utf8(line).is_err())
                .then_some(EntryFieldDiagnostic::NonUtf8Value),
            value: 0..line.len(),
        };
    }
    if line.is_empty() {
        return empty_note_metadata();
    }
    if line.starts_with(b"otpauth://") {
        return EntryFieldMetadata {
            kind: EntryFieldKind::OtpUri,
            sensitivity: EntrySensitivity::Sensitive,
            name: Some("otp".to_owned()),
            otp: None,
            diagnostic: Some(EntryFieldDiagnostic::MalformedOtpUri),
            value: 0..line.len(),
        };
    }
    let first_line_end = line
        .iter()
        .position(|byte| *byte == b'\n')
        .unwrap_or(line.len());
    let first_line = &line[..first_line_end];
    if let Some(colon) = first_line.iter().position(|byte| *byte == b':')
        && let Ok(name) = std::str::from_utf8(&first_line[..colon])
        && is_field_name(name)
    {
        let value_start = colon + 1 + usize::from(line.get(colon + 1) == Some(&b' '));
        let kind = semantic_field_kind(name);
        return EntryFieldMetadata {
            kind,
            sensitivity: if name.eq_ignore_ascii_case("pin") {
                EntrySensitivity::Sensitive
            } else {
                EntrySensitivity::Ordinary
            },
            name: Some(name.to_owned()),
            otp: None,
            diagnostic: std::str::from_utf8(&line[value_start..])
                .is_err()
                .then_some(EntryFieldDiagnostic::NonUtf8Value),
            value: value_start..line.len(),
        };
    }
    EntryFieldMetadata {
        kind: EntryFieldKind::Note,
        sensitivity: EntrySensitivity::Ordinary,
        name: None,
        otp: None,
        diagnostic: std::str::from_utf8(line)
            .is_err()
            .then_some(EntryFieldDiagnostic::NonUtf8Value),
        value: 0..line.len(),
    }
}

fn semantic_field_kind(name: &str) -> EntryFieldKind {
    match name.to_ascii_lowercase().as_str() {
        "user" | "username" | "login" => EntryFieldKind::Username,
        "email" | "e-mail" => EntryFieldKind::Email,
        "url" | "uri" | "website" => EntryFieldKind::Url,
        _ => EntryFieldKind::Field,
    }
}

fn display_key(field: &EntryField) -> (u8, String) {
    let metadata = field.metadata();
    let name = metadata.name().unwrap_or_default().to_lowercase();
    let priority = match metadata.kind() {
        EntryFieldKind::Username => 0,
        EntryFieldKind::Password => 1,
        EntryFieldKind::Url => 2,
        EntryFieldKind::OtpUri => 3,
        EntryFieldKind::Note => 4,
        _ if matches!(name.as_str(), "note" | "notes") => 4,
        _ if name == "comments" => 5,
        EntryFieldKind::Email | EntryFieldKind::Field => 6,
    };
    (priority, if priority == 6 { name } else { String::new() })
}

fn empty_note_metadata() -> EntryFieldMetadata {
    EntryFieldMetadata {
        kind: EntryFieldKind::Note,
        sensitivity: EntrySensitivity::Empty,
        name: None,
        otp: None,
        diagnostic: None,
        value: 0..0,
    }
}

fn validate_line(value: &[u8]) -> Result<(), DocumentError> {
    if value.iter().any(|byte| matches!(byte, b'\r' | b'\n')) {
        Err(DocumentError::LineBreak)
    } else {
        Ok(())
    }
}

fn validate_name(name: &str) -> Result<(), DocumentError> {
    if !is_field_name(name) {
        Err(DocumentError::InvalidFieldName)
    } else {
        Ok(())
    }
}

fn is_field_name(name: &str) -> bool {
    !name.is_empty() && !name.contains([':', '\r', '\n']) && !name.chars().any(char::is_whitespace)
}

fn strip_continuation_marker(contents: &[u8]) -> &[u8] {
    contents.strip_prefix(b" ").unwrap_or(contents)
}

fn append_stable_contents(output: &mut Vec<u8>, field: &EntryField) {
    if field.metadata().kind() == EntryFieldKind::Note {
        output.push(b' ');
    }
    for byte in field.contents.expose() {
        output.push(*byte);
        if *byte == b'\n' {
            output.push(b' ');
        }
    }
}

fn normalize_endings(fields: &mut [EntryField], default: &[u8], final_newline: bool) {
    let last = fields.len().saturating_sub(1);
    for (index, field) in fields.iter_mut().enumerate() {
        if index < last && field.ending.is_empty() {
            field.ending = default.to_vec();
        } else if index == last {
            if final_newline && field.ending.is_empty() {
                field.ending = default.to_vec();
            } else if !final_newline {
                field.ending.clear();
            }
        }
    }
}

fn conflict_token(session: &EditSession) -> DocumentConflictToken {
    let mut digest = Sha256::new();
    digest.update(b"ironstorage-entry-document-v1\0");
    digest.update(session.path().to_string().as_bytes());
    digest.update(b"\0");
    match session.original_ciphertext() {
        Some(ciphertext) => {
            digest.update(b"present\0");
            digest.update(ciphertext.as_bytes());
        }
        None => digest.update(b"missing\0"),
    }
    DocumentConflictToken(digest.finalize().into())
}
