use anyhow::{Context as _, anyhow};
use axum::{
    Extension, Json,
    extract::{Path, State},
};
use axum_extra::TypedHeader;
use axum_typed_multipart::TypedMultipart;
use log::info;
use reconcile_text::{BuiltinTokenizer, is_binary, reconcile};
use serde::Deserialize;

use super::{
    device_id_header::DeviceIdHeader, requests::UpdateDocumentVersion,
    responses::DocumentUpdateResponse,
};
use crate::{
    app_state::{
        AppState,
        database::models::{DocumentId, StoredDocumentVersion, VaultId},
    },
    config::user_config::User,
    errors::{SyncServerError, not_found_error, server_error},
    utils::{
        dedup_paths::dedup_paths, is_file_type_mergable::is_file_type_mergable,
        normalize::normalize, sanitize_path::sanitize_path,
    },
};

#[derive(Deserialize)]
pub struct UpdateDocumentPathParams {
    #[serde(deserialize_with = "normalize")]
    vault_id: VaultId,

    document_id: DocumentId,
}

#[axum::debug_handler]
#[allow(clippy::too_many_lines)]
pub async fn update_document(
    Path(UpdateDocumentPathParams {
        vault_id,
        document_id,
    }): Path<UpdateDocumentPathParams>,
    Extension(user): Extension<User>,
    TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
    State(state): State<AppState>,
    TypedMultipart(request): TypedMultipart<UpdateDocumentVersion>,
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
    // No need for a transaction as document versions are immutable
    let parent_document = state
        .database
        .get_document_version(&vault_id, request.parent_version_id, None)
        .await
        .map_err(server_error)?
        .map_or_else(
            || {
                Err(not_found_error(anyhow!(
                    "Parent version with id `{}` not found",
                    request.parent_version_id
                )))
            },
            Ok,
        )?;

    let sanitized_relative_path = sanitize_path(&request.relative_path);

    let mut transaction = state
        .database
        .create_write_transaction(&vault_id)
        .await
        .map_err(server_error)?;

    let last_update_id = state
        .database
        .get_max_update_id_in_vault(&vault_id, Some(&mut transaction))
        .await
        .map_err(server_error)?;

    let latest_version = state
        .database
        .get_latest_document(&vault_id, &document_id, Some(&mut transaction))
        .await
        .map_err(server_error)?
        .map_or_else(
            || {
                Err(not_found_error(anyhow!(
                    "Document with id `{document_id}` not found",
                )))
            },
            Ok,
        )?;

    if latest_version.is_deleted {
        transaction
            .rollback()
            .await
            .context("Failed to roll back transaction")
            .map_err(server_error)?;

        return Ok(Json(DocumentUpdateResponse::FastForwardUpdate(
            latest_version.into(),
        )));
    }

    let content = request.content.contents.to_vec();

    // Return the latest version if the content and path are the same as the latest
    // version
    if content == latest_version.content && sanitized_relative_path == latest_version.relative_path
    {
        info!("Document content is the same as the latest version, skipping update");
        transaction
            .rollback()
            .await
            .context("Failed to roll back transaction")
            .map_err(server_error)?;

        return Ok(Json(DocumentUpdateResponse::FastForwardUpdate(
            latest_version.into(),
        )));
    }

    let merged_content = if is_file_type_mergable(&sanitized_relative_path)
        && !is_binary(&parent_document.content)
        && !is_binary(&latest_version.content)
        && !is_binary(&content)
    {
        reconcile(
            str::from_utf8(&parent_document.content)
                .expect("parent must be valid UTF-8 because it's not binary"),
            &str::from_utf8(&latest_version.content)
                .expect("latest_version must be valid UTF-8 because it's not binary")
                .into(),
            &str::from_utf8(&content)
                .expect("content must be valid UTF-8 because it's not binary")
                .into(),
            &*BuiltinTokenizer::Word,
        )
        .apply()
        .text()
        .into_bytes()
    } else {
        content.clone()
    };

    let is_different_from_request_content = merged_content != content;

    // We can only update the relative path if we're the first one to do so
    let new_relative_path = if parent_document.relative_path == latest_version.relative_path
        && latest_version.relative_path != sanitized_relative_path
    {
        let mut new_relative_path = String::default();
        for candidate in dedup_paths(&sanitized_relative_path) {
            if state
                .database
                .get_latest_document_by_path(&vault_id, &candidate, Some(&mut transaction))
                .await
                .map_err(server_error)?
                .is_none()
            {
                new_relative_path = candidate;
                break;
            }
        }

        new_relative_path
    } else {
        latest_version.relative_path.clone()
    };

    let new_version = StoredDocumentVersion {
        document_id,
        vault_update_id: last_update_id + 1,
        relative_path: new_relative_path,
        content: merged_content,
        updated_date: chrono::Utc::now(),
        is_deleted: false,
        user_id: user.name,
        device_id: device_id.0,
    };

    state
        .database
        .insert_document_version(&vault_id, &new_version, Some(&mut transaction))
        .await
        .map_err(server_error)?;

    transaction
        .commit()
        .await
        .context("Failed to commit successful transaction")
        .map_err(server_error)?;

    Ok(Json(if is_different_from_request_content {
        DocumentUpdateResponse::MergingUpdate(new_version.into())
    } else {
        DocumentUpdateResponse::FastForwardUpdate(new_version.into())
    }))
}
