//! Types for working with Paperless documents.
//!
//! Document mutations are applied locally first.
//! Methods such as [`set_title`](Document::set_title),
//! [`set_content`](Document::set_content),
//! [`add_tag`](Document::add_tag), etc..
//! only update the in-memory [`Document`] value and mark it as changed.
//! The changes are only sent to the Paperless server when
//! [`patch`](Document::patch) is called.

use std::{fmt::Display, sync::Arc, time::Duration};

use chrono::{DateTime, NaiveDate, Utc};
use derive_more::Display;
use enumflags2::{BitFlags, bitflags};
use futures_util::TryStreamExt;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;

use paperless_api_macros::UpdateDto;

use crate::{
    Error, Result,
    client::PaperlessClient,
    id::{
        CorrespondentId, CustomFieldId, DocumentId, DocumentTypeId, StoragePathId, TagId, UserId,
    },
    metadata::{custom_field::DocumentCustomField, permission::ItemPermissions},
    note::Note,
    share_link::{CreateShareLink, ShareLink, ShareLinkFileVersion},
};

/// Represents a document.
///
/// Changes made through mutating methods such as
/// [`set_title`](Document::set_title),
/// [`set_content`](Document::set_content),
/// [`add_tag`](Document::add_tag), and
/// [`set_custom_field`](Document::set_custom_field)
/// are only tracked locally at first.
///
/// They are not sent to the Paperless server until
/// [`patch`](Document::patch) is called.
#[derive(Debug, Clone)]
pub struct Document {
    data: DocumentData,

    client: Arc<PaperlessClient>,
    content_is_truncated: bool,
    changed_values: BitFlags<ChangedAttributes>,
}

#[derive(Debug, Clone, Deserialize, UpdateDto)]
pub(crate) struct DocumentData {
    #[dto(skip)]
    id: DocumentId,

    archive_serial_number: Option<ArchiveSerialNumber>,

    #[dto(skip)]
    original_file_name: String,

    #[dto(skip)]
    added: DateTime<Utc>,

    created: Option<NaiveDate>,

    #[dto(skip)]
    modified: DateTime<Utc>,

    #[dto(skip)]
    page_count: Option<u32>,

    title: String,
    content: String,
    tags: Vec<TagId>,
    owner: Option<UserId>,
    correspondent: Option<CorrespondentId>,
    custom_fields: Vec<DocumentCustomField>,
    document_type: Option<DocumentTypeId>,
    storage_path: Option<StoragePathId>,

    #[dto(skip)]
    notes: Vec<Note>,

    #[serde(flatten)]
    #[dto(skip)]
    permissions: ItemPermissions,

    #[dto(skip)]
    mime_type: Option<String>,
}

#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[repr(transparent)]
pub struct ArchiveSerialNumber(pub u32);

#[bitflags]
#[repr(u16)]
#[derive(Copy, Clone, Debug, PartialEq)]
enum ChangedAttributes {
    ArchiveSerialNumber,
    Title,
    Content,
    Tags,
    CustomFields,
    Correspondent,
    DocumentType,
    Created,
    Owner,
    StoragePath,

    Deleted,
}

/// The content (OCR) of a document, either full or truncated.
#[derive(Debug, Clone)]
pub enum Content<'a> {
    /// Full content of the document.
    Full(&'a str),

    /// Truncated content of the document.
    Truncated(&'a str),
}

impl Document {
    pub(crate) fn new(
        data: DocumentData,
        client: Arc<PaperlessClient>,
        content_is_truncated: bool,
    ) -> Self {
        Self {
            data,
            client,
            content_is_truncated,
            changed_values: BitFlags::default(),
        }
    }

    /// Get the unique identifier of the document.
    #[inline]
    #[must_use]
    pub fn id(&self) -> DocumentId {
        self.data.id
    }

    /// Get the archive serial number of the document.
    #[inline]
    #[must_use]
    pub fn archive_serial_number(&self) -> Option<ArchiveSerialNumber> {
        self.data.archive_serial_number
    }

    /// Get the timestamp when the document was added.
    #[inline]
    #[must_use]
    pub fn added(&self) -> &DateTime<Utc> {
        &self.data.added
    }

    /// Get the created timestamp of the document.
    #[inline]
    #[must_use]
    pub fn created(&self) -> Option<&NaiveDate> {
        self.data.created.as_ref()
    }

    /// Get the modified timestamp of the document.
    #[inline]
    #[must_use]
    pub fn modified(&self) -> &DateTime<Utc> {
        &self.data.modified
    }

    /// Get the title of the document.
    #[inline]
    #[must_use]
    pub fn title(&self) -> &str {
        &self.data.title
    }

    /// Get the original file name of the document.
    #[inline]
    #[must_use]
    pub fn original_file_name(&self) -> &str {
        &self.data.original_file_name
    }

    /// Get the MIME type
    #[inline]
    #[must_use]
    pub fn mime_type(&self) -> Option<&str> {
        self.data.mime_type.as_deref()
    }

    /// Get the correspondent id of the document.
    #[inline]
    #[must_use]
    pub fn correspondent(&self) -> Option<CorrespondentId> {
        self.data.correspondent
    }

    /// Get the owner id of the document.
    #[inline]
    #[must_use]
    pub fn owner(&self) -> Option<UserId> {
        self.data.owner
    }

    /// Get the document type id of the document.
    #[inline]
    #[must_use]
    pub fn document_type(&self) -> Option<DocumentTypeId> {
        self.data.document_type
    }

    /// Get the number of pages in the document.
    #[inline]
    #[must_use]
    pub fn page_count(&self) -> Option<u32> {
        self.data.page_count
    }

    /// Get all tag-ids for the document.
    #[inline]
    #[must_use]
    pub fn tags(&self) -> &[TagId] {
        &self.data.tags
    }

    /// Get all custom fields for the document.
    #[inline]
    #[must_use]
    pub fn custom_fields(&self) -> &[DocumentCustomField] {
        &self.data.custom_fields
    }

    /// Get the content of the document.
    #[inline]
    #[must_use]
    pub fn content(&self) -> Content<'_> {
        if self.content_is_truncated {
            Content::Truncated(&self.data.content)
        } else {
            Content::Full(&self.data.content)
        }
    }

    /// Get the storage path of the document.
    #[inline]
    #[must_use]
    pub fn storage_path(&self) -> Option<StoragePathId> {
        self.data.storage_path
    }

    /// Get the notes for the document.
    #[inline]
    #[must_use]
    pub fn notes(&self) -> &[Note] {
        &self.data.notes
    }

    /// Get the permissions for the document.
    #[inline]
    #[must_use]
    pub fn permissions(&self) -> &ItemPermissions {
        &self.data.permissions
    }

    /// Set the archive serial number of the document.
    #[inline]
    pub fn set_archive_serial_number(
        &mut self,
        archive_serial_number: Option<ArchiveSerialNumber>,
    ) {
        self.data.archive_serial_number = archive_serial_number;
        self.changed_values |= ChangedAttributes::ArchiveSerialNumber;
    }

    /// Add a tag to the document.
    pub fn add_tag(&mut self, tag_id: TagId) {
        if !self.data.tags.contains(&tag_id) {
            self.data.tags.push(tag_id);
            self.changed_values |= ChangedAttributes::Tags;
        }
    }

    /// Remove a tag from the document.
    pub fn remove_tag(&mut self, tag_id: TagId) {
        if let Some(index) = self.data.tags.iter().position(|id| *id == tag_id) {
            self.data.tags.remove(index);
            self.changed_values |= ChangedAttributes::Tags;
        }
    }

    /// Set the title of the document.
    pub fn set_title(&mut self, title: impl Into<String>) {
        self.data.title = title.into();
        self.changed_values |= ChangedAttributes::Title;
    }

    /// Set the content of the document.
    pub fn set_content(&mut self, content: impl Into<String>) {
        self.data.content = content.into();
        self.content_is_truncated = false;
        self.changed_values |= ChangedAttributes::Content;
    }

    /// Set a custom field for the document.
    pub fn set_custom_field(&mut self, field: CustomFieldId, value: impl Into<String>) {
        for custom_field in &mut self.data.custom_fields {
            if custom_field.field == field {
                custom_field.value = value.into();
                self.changed_values |= ChangedAttributes::CustomFields;
                return;
            }
        }

        self.data.custom_fields.push(DocumentCustomField {
            field,
            value: value.into(),
        });
        self.changed_values |= ChangedAttributes::CustomFields;
    }

    /// Remove a custom field from the document.
    pub fn remove_custom_field(&mut self, field: CustomFieldId) {
        if let Some(index) = self
            .data
            .custom_fields
            .iter()
            .position(|custom_field| custom_field.field == field)
        {
            self.data.custom_fields.remove(index);
            self.changed_values |= ChangedAttributes::CustomFields;
        }
    }

    /// Set the created date of the document.
    pub fn set_created(&mut self, created: NaiveDate) {
        self.data.created = Some(created);
        self.changed_values |= ChangedAttributes::Created;
    }

    /// Set the owner of the document.
    pub fn set_owner(&mut self, owner: UserId) {
        self.data.owner = Some(owner);
        self.changed_values |= ChangedAttributes::Owner;
    }

    /// Set the correspondent of the document.
    pub fn set_correspondent(&mut self, correspondent: CorrespondentId) {
        self.data.correspondent = Some(correspondent);
        self.changed_values |= ChangedAttributes::Correspondent;
    }

    /// Set the document type of the document.
    pub fn set_document_type(&mut self, document_type: DocumentTypeId) {
        self.data.document_type = Some(document_type);
        self.changed_values |= ChangedAttributes::DocumentType;
    }

    /// Set the storage path of the document.
    pub fn set_storage_path(&mut self, storage_path: StoragePathId) {
        self.data.storage_path = Some(storage_path);
        self.changed_values |= ChangedAttributes::StoragePath;
    }

    /// Returns `true` if the document has unsaved changes.
    #[inline]
    #[must_use]
    pub fn is_dirty(&self) -> bool {
        !self.changed_values.is_empty() && !self.changed_values.contains(ChangedAttributes::Deleted)
    }

    /// Returns `true` if the document was deleted.
    #[inline]
    #[must_use]
    pub fn is_deleted(&self) -> bool {
        self.changed_values.contains(ChangedAttributes::Deleted)
    }

    fn fail_if_deleted(&self) -> Result<()> {
        if self.is_deleted() {
            Err(Error::AlreadyDeleted)
        } else {
            Ok(())
        }
    }

    /// Refresh the document from the server.
    ///
    /// This will discard any local changes and replace them with the server's state.
    pub async fn refresh(&mut self) -> Result<()> {
        let document_data = self
            .client
            .as_ref()
            .get_document_data_by_id(self.data.id)
            .await?;

        self.data = document_data;

        self.changed_values = BitFlags::empty();
        self.content_is_truncated = false;
        Ok(())
    }

    /// Get the document thumbnail
    pub async fn tumb(&self) -> Result<Vec<u8>> {
        let resp = self
            .client
            .request(
                Method::GET,
                &format!("/api/documents/{}/thumb/", self.data.id),
                None,
                None,
            )
            .await?;

        Ok(resp
            .bytes()
            .await
            .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?
            .to_vec())
    }

    /// Update the document on the server.
    ///
    /// This applies the currently tracked local changes to the remote Paperless document.
    pub async fn patch(&mut self) -> Result<()> {
        if !self.is_dirty() {
            return Ok(());
        }

        self.fail_if_deleted()?;

        let patch = UpdateDocumentData {
            title: self
                .changed_values
                .contains(ChangedAttributes::Title)
                .then_some(self.data.title.clone()),

            archive_serial_number: self
                .changed_values
                .contains(ChangedAttributes::ArchiveSerialNumber)
                .then_some(self.data.archive_serial_number),

            content: self
                .changed_values
                .contains(ChangedAttributes::Content)
                .then_some(self.data.content.clone()),

            tags: self
                .changed_values
                .contains(ChangedAttributes::Tags)
                .then_some(self.data.tags.clone()),

            custom_fields: self
                .changed_values
                .contains(ChangedAttributes::CustomFields)
                .then_some(self.data.custom_fields.clone()),

            correspondent: self
                .changed_values
                .contains(ChangedAttributes::Correspondent)
                .then_some(self.data.correspondent),

            document_type: self
                .changed_values
                .contains(ChangedAttributes::DocumentType)
                .then_some(self.data.document_type),

            created: self
                .changed_values
                .contains(ChangedAttributes::Created)
                .then_some(self.data.created),

            owner: self
                .changed_values
                .contains(ChangedAttributes::Owner)
                .then_some(self.data.owner),

            storage_path: self
                .changed_values
                .contains(ChangedAttributes::StoragePath)
                .then_some(self.data.storage_path),
        };

        self.client
            .request(
                Method::PATCH,
                &format!("/api/documents/{}/", self.data.id),
                Some(&serde_json::to_value(&patch).map_err(|e| Error::Other(e.to_string()))?),
                None,
            )
            .await?;

        self.changed_values = BitFlags::empty();
        Ok(())
    }

    pub async fn delete(&mut self) -> Result<()> {
        self.client
            .request(
                Method::DELETE,
                &format!("/api/documents/{}/", self.data.id),
                None,
                None,
            )
            .await?;

        self.changed_values = BitFlags::from(ChangedAttributes::Deleted);
        Ok(())
    }

    /// Get the full content of the document, replacing any truncated content.
    pub async fn get_full_content(&mut self) -> Result<()> {
        self.fail_if_deleted()?;

        if !self.content_is_truncated {
            return Ok(());
        }

        let doc = self.client.get_document_data_by_id(self.data.id).await?;
        self.data.content = doc.content;
        self.content_is_truncated = false;
        Ok(())
    }

    /// Download the document to a buffer.
    pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
        self.fail_if_deleted()?;

        let resp = self
            .client
            .request(
                Method::GET,
                &format!("/api/documents/{}/download/", self.data.id),
                None,
                None,
            )
            .await?;

        if resp.status().is_success() {
            let bytes = resp
                .bytes()
                .await
                .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
            Ok(bytes.to_vec())
        } else {
            Err(Error::Other(format!(
                "Failed to download document: {}",
                resp.status()
            )))
        }
    }

    /// Download the document to a file, requires the `tokio-fs` feature.
    pub async fn download_to_file(&self, path: &std::path::Path) -> Result<()> {
        self.fail_if_deleted()?;

        let resp = self
            .client
            .request(
                Method::GET,
                &format!("/api/documents/{}/download/", self.data.id),
                None,
                None,
            )
            .await?;

        if !resp.status().is_success() {
            return Err(Error::Other(format!(
                "Failed to download document: {}",
                resp.status()
            )));
        }

        let mut file = tokio::fs::File::create(path)
            .await
            .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;

        resp.bytes_stream()
            .map_err(|e| Error::Other(format!("Failed to read document chunk: {e}")))
            .try_fold(&mut file, |file, chunk| async move {
                file.write_all(&chunk).await.map_err(|e| {
                    Error::Other(format!("Failed to save document chunk to file: {e}"))
                })?;
                Ok(file)
            })
            .await?;

        Ok(())
    }

    /// Generates a share link for the document that expires after the specified duration.
    pub fn generate_share_link_duration(
        &self,
        valid_for: Duration,
        version: ShareLinkFileVersion,
    ) -> impl Future<Output = Result<ShareLink>> {
        let expires = Utc::now() + valid_for;
        self.generate_share_link_expires(expires, version)
    }

    /// Generates a share link for the document that expires at the specified time.
    pub async fn generate_share_link_expires(
        &self,
        expires: DateTime<Utc>,
        version: ShareLinkFileVersion,
    ) -> Result<ShareLink> {
        self.fail_if_deleted()?;

        let mut share_link = self
            .client
            .request_json::<ShareLink>(
                Method::POST,
                "/api/share_links/",
                Some(
                    &serde_json::to_value(&CreateShareLink {
                        document: self.id(),
                        expiration: expires,
                        file_version: version,
                    })
                    .map_err(|e| Error::Other(e.to_string()))?,
                ),
                None,
            )
            .await?;

        share_link.base_url = self.client.base_url.clone();
        Ok(share_link)
    }
}

impl Display for Content<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Content::Full(text) => write!(f, "{text}"),
            Content::Truncated(text) => write!(f, "{text}..."),
        }
    }
}

impl AsRef<str> for Content<'_> {
    fn as_ref(&self) -> &str {
        match self {
            Content::Full(text) | Content::Truncated(text) => text,
        }
    }
}
