//! API endpoint handlers
//!
//! This module contains actual implementations of endpoints with rust-style type signatures.
//!
//! The remaining functions here should probably be refactored away. The 'get_release_files' style
//! functions should go in entity_crud (or some new

use crate::database_models::*;
use crate::database_schema::*;
use crate::editing::*;
use crate::editing_crud::{EditgroupCrud, EditorCrud};
use crate::entity_crud::{EntityCrud, ExpandFlags, HideFlags};
use crate::errors::*;
use crate::identifiers::*;
use crate::server::*;
use diesel::prelude::*;
use fatcat_api_spec::models;
use fatcat_api_spec::models::*;

macro_rules! entity_auto_batch_handler {
    ($post_auto_batch_handler:ident, $model:ident) => {
        pub fn $post_auto_batch_handler(
            &self,
            conn: &DbConn,
            editgroup: Editgroup,
            entity_list: &[models::$model],
            editor_id: FatcatId,
        ) -> Result<Editgroup> {

            let editgroup_row = editgroup.db_create(conn, true)?;
            let editgroup_id = FatcatId::from_uuid(&editgroup_row.id);
            let edit_context = make_edit_context(editor_id, editgroup_id, true)?;
            edit_context.check(&conn)?;
            let model_list: Vec<&models::$model> = entity_list.iter().map(|e| e).collect();
            let _edits = $model::db_create_batch(conn, &edit_context, model_list.as_slice())?;

            let _clr: ChangelogRow = diesel::insert_into(changelog::table)
                .values((changelog::editgroup_id.eq(edit_context.editgroup_id.to_uuid()),))
                .get_result(conn)?;
            self.get_editgroup_handler(conn, editgroup_id)
        }
    }
}

pub fn get_release_files(
    conn: &DbConn,
    ident: FatcatId,
    hide_flags: HideFlags,
) -> Result<Vec<FileEntity>> {
    let rows: Vec<(FileRevRow, FileIdentRow, FileRevReleaseRow)> = file_rev::table
        .inner_join(file_ident::table)
        .inner_join(file_rev_release::table)
        .filter(file_rev_release::target_release_ident_id.eq(&ident.to_uuid()))
        .filter(file_ident::is_live.eq(true))
        .filter(file_ident::redirect_id.is_null())
        .load(conn)?;

    rows.into_iter()
        .map(|(rev, ident, _)| FileEntity::db_from_row(conn, rev, Some(ident), hide_flags))
        .collect()
}

pub fn get_release_filesets(
    conn: &DbConn,
    ident: FatcatId,
    hide_flags: HideFlags,
) -> Result<Vec<FilesetEntity>> {
    let rows: Vec<(FilesetRevRow, FilesetIdentRow, FilesetRevReleaseRow)> = fileset_rev::table
        .inner_join(fileset_ident::table)
        .inner_join(fileset_rev_release::table)
        .filter(fileset_rev_release::target_release_ident_id.eq(&ident.to_uuid()))
        .filter(fileset_ident::is_live.eq(true))
        .filter(fileset_ident::redirect_id.is_null())
        .load(conn)?;

    rows.into_iter()
        .map(|(rev, ident, _)| FilesetEntity::db_from_row(conn, rev, Some(ident), hide_flags))
        .collect()
}

pub fn get_release_webcaptures(
    conn: &DbConn,
    ident: FatcatId,
    hide_flags: HideFlags,
) -> Result<Vec<WebcaptureEntity>> {
    let rows: Vec<(
        WebcaptureRevRow,
        WebcaptureIdentRow,
        WebcaptureRevReleaseRow,
    )> = webcapture_rev::table
        .inner_join(webcapture_ident::table)
        .inner_join(webcapture_rev_release::table)
        .filter(webcapture_rev_release::target_release_ident_id.eq(&ident.to_uuid()))
        .filter(webcapture_ident::is_live.eq(true))
        .filter(webcapture_ident::redirect_id.is_null())
        .load(conn)?;

    rows.into_iter()
        .map(|(rev, ident, _)| WebcaptureEntity::db_from_row(conn, rev, Some(ident), hide_flags))
        .collect()
}

impl Server {
    pub fn lookup_container_handler(
        &self,
        conn: &DbConn,
        issnl: &Option<String>,
        wikidata_qid: &Option<String>,
        expand_flags: ExpandFlags,
        hide_flags: HideFlags,
    ) -> Result<ContainerEntity> {
        let (ident, rev): (ContainerIdentRow, ContainerRevRow) = match (issnl, wikidata_qid) {
            (Some(issnl), None) => {
                check_issn(issnl)?;
                container_ident::table
                    .inner_join(container_rev::table)
                    .filter(container_rev::issnl.eq(&issnl))
                    .filter(container_ident::is_live.eq(true))
                    .filter(container_ident::redirect_id.is_null())
                    .first(conn)?
            }
            (None, Some(wikidata_qid)) => {
                check_wikidata_qid(wikidata_qid)?;
                container_ident::table
                    .inner_join(container_rev::table)
                    .filter(container_rev::wikidata_qid.eq(&wikidata_qid))
                    .filter(container_ident::is_live.eq(true))
                    .filter(container_ident::redirect_id.is_null())
                    .first(conn)?
            }
            _ => {
                return Err(
                    FatcatError::MissingOrMultipleExternalId("in lookup".to_string()).into(),
                );
            }
        };

        let mut entity = ContainerEntity::db_from_row(conn, rev, Some(ident), hide_flags)?;
        entity.db_expand(&conn, expand_flags)?;
        Ok(entity)
    }

    pub fn lookup_creator_handler(
        &self,
        conn: &DbConn,
        orcid: &Option<String>,
        wikidata_qid: &Option<String>,
        expand_flags: ExpandFlags,
        hide_flags: HideFlags,
    ) -> Result<CreatorEntity> {
        let (ident, rev): (CreatorIdentRow, CreatorRevRow) = match (orcid, wikidata_qid) {
            (Some(orcid), None) => {
                check_orcid(orcid)?;
                creator_ident::table
                    .inner_join(creator_rev::table)
                    .filter(creator_rev::orcid.eq(orcid))
                    .filter(creator_ident::is_live.eq(true))
                    .filter(creator_ident::redirect_id.is_null())
                    .first(conn)?
            }
            (None, Some(wikidata_qid)) => {
                check_wikidata_qid(wikidata_qid)?;
                creator_ident::table
                    .inner_join(creator_rev::table)
                    .filter(creator_rev::wikidata_qid.eq(wikidata_qid))
                    .filter(creator_ident::is_live.eq(true))
                    .filter(creator_ident::redirect_id.is_null())
                    .first(conn)?
            }
            _ => {
                return Err(
                    FatcatError::MissingOrMultipleExternalId("in lookup".to_string()).into(),
                );
            }
        };

        let mut entity = CreatorEntity::db_from_row(conn, rev, Some(ident), hide_flags)?;
        entity.db_expand(&conn, expand_flags)?;
        Ok(entity)
    }

    pub fn get_creator_releases_handler(
        &self,
        conn: &DbConn,
        ident: FatcatId,
        hide_flags: HideFlags,
    ) -> Result<Vec<ReleaseEntity>> {
        // TODO: some kind of unique or group-by?
        let rows: Vec<(ReleaseRevRow, ReleaseIdentRow, ReleaseContribRow)> = release_rev::table
            .inner_join(release_ident::table)
            .inner_join(release_contrib::table)
            .filter(release_contrib::creator_ident_id.eq(&ident.to_uuid()))
            .filter(release_ident::is_live.eq(true))
            .filter(release_ident::redirect_id.is_null())
            .load(conn)?;

        // TODO: from_rows, not from_row?
        rows.into_iter()
            .map(|(rev, ident, _)| ReleaseEntity::db_from_row(conn, rev, Some(ident), hide_flags))
            .collect()
    }

    pub fn lookup_file_handler(
        &self,
        conn: &DbConn,
        md5: &Option<String>,
        sha1: &Option<String>,
        sha256: &Option<String>,
        expand_flags: ExpandFlags,
        hide_flags: HideFlags,
    ) -> Result<FileEntity> {
        let (ident, rev): (FileIdentRow, FileRevRow) = match (md5, sha1, sha256) {
            (Some(md5), None, None) => {
                check_md5(md5)?;
                file_ident::table
                    .inner_join(file_rev::table)
                    .filter(file_rev::md5.eq(md5))
                    .filter(file_ident::is_live.eq(true))
                    .filter(file_ident::redirect_id.is_null())
                    .first(conn)?
            }
            (None, Some(sha1), None) => {
                check_sha1(sha1)?;
                file_ident::table
                    .inner_join(file_rev::table)
                    .filter(file_rev::sha1.eq(sha1))
                    .filter(file_ident::is_live.eq(true))
                    .filter(file_ident::redirect_id.is_null())
                    .first(conn)?
            }
            (None, None, Some(sha256)) => {
                check_sha256(sha256)?;
                file_ident::table
                    .inner_join(file_rev::table)
                    .filter(file_rev::sha256.eq(sha256))
                    .filter(file_ident::is_live.eq(true))
                    .filter(file_ident::redirect_id.is_null())
                    .first(conn)?
            }
            _ => {
                return Err(
                    FatcatError::MissingOrMultipleExternalId("in lookup".to_string()).into(),
                );
            }
        };

        let mut entity = FileEntity::db_from_row(conn, rev, Some(ident), hide_flags)?;
        entity.db_expand(&conn, expand_flags)?;
        Ok(entity)
    }

    pub fn lookup_release_handler(
        &self,
        conn: &DbConn,
        doi: &Option<String>,
        wikidata_qid: &Option<String>,
        isbn13: &Option<String>,
        pmid: &Option<String>,
        pmcid: &Option<String>,
        core: &Option<String>,
        arxiv: &Option<String>,
        jstor: &Option<String>,
        ark: &Option<String>,
        mag: &Option<String>,
        expand_flags: ExpandFlags,
        hide_flags: HideFlags,
    ) -> Result<ReleaseEntity> {
        let (ident, rev): (ReleaseIdentRow, ReleaseRevRow) = match (
            doi,
            wikidata_qid,
            isbn13,
            pmid,
            pmcid,
            core,
            arxiv,
            jstor,
            ark,
            mag,
        ) {
            (Some(doi), None, None, None, None, None, None, None, None, None) => {
                // DOIs always stored lower-case; lookups are case-insensitive
                let doi = doi.to_lowercase();
                check_doi(&doi)?;
                release_ident::table
                    .inner_join(release_rev::table)
                    .filter(release_rev::doi.eq(&doi))
                    .filter(release_ident::is_live.eq(true))
                    .filter(release_ident::redirect_id.is_null())
                    .first(conn)?
            }
            (None, Some(wikidata_qid), None, None, None, None, None, None, None, None) => {
                check_wikidata_qid(wikidata_qid)?;
                release_ident::table
                    .inner_join(release_rev::table)
                    .filter(release_rev::wikidata_qid.eq(wikidata_qid))
                    .filter(release_ident::is_live.eq(true))
                    .filter(release_ident::redirect_id.is_null())
                    .first(conn)?
            }
            (None, None, Some(isbn13), None, None, None, None, None, None, None) => {
                check_isbn13(isbn13)?;
                let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) =
                    release_rev::table
                        .inner_join(release_ident::table)
                        .inner_join(release_rev_extid::table)
                        .filter(release_rev_extid::extid_type.eq("isbn13".to_string()))
                        .filter(release_rev_extid::value.eq(isbn13))
                        .filter(release_ident::is_live.eq(true))
                        .filter(release_ident::redirect_id.is_null())
                        .first(conn)?;
                (ident, rev)
            }
            (None, None, None, Some(pmid), None, None, None, None, None, None) => {
                check_pmid(pmid)?;
                release_ident::table
                    .inner_join(release_rev::table)
                    .filter(release_rev::pmid.eq(pmid))
                    .filter(release_ident::is_live.eq(true))
                    .filter(release_ident::redirect_id.is_null())
                    .first(conn)?
            }
            (None, None, None, None, Some(pmcid), None, None, None, None, None) => {
                check_pmcid(pmcid)?;
                release_ident::table
                    .inner_join(release_rev::table)
                    .filter(release_rev::pmcid.eq(pmcid))
                    .filter(release_ident::is_live.eq(true))
                    .filter(release_ident::redirect_id.is_null())
                    .first(conn)?
            }
            (None, None, None, None, None, Some(core), None, None, None, None) => {
                check_core_id(core)?;
                release_ident::table
                    .inner_join(release_rev::table)
                    .filter(release_rev::core_id.eq(core))
                    .filter(release_ident::is_live.eq(true))
                    .filter(release_ident::redirect_id.is_null())
                    .first(conn)?
            }
            (None, None, None, None, None, None, Some(arxiv), None, None, None) => {
                // TODO: this allows only lookup by full, versioned arxiv identifier. Probably also
                // want to allow lookup by "work" style identifier?
                check_arxiv_id(arxiv)?;
                let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) =
                    release_rev::table
                        .inner_join(release_ident::table)
                        .inner_join(release_rev_extid::table)
                        .filter(release_rev_extid::extid_type.eq("arxiv".to_string()))
                        .filter(release_rev_extid::value.eq(arxiv))
                        .filter(release_ident::is_live.eq(true))
                        .filter(release_ident::redirect_id.is_null())
                        .first(conn)?;
                (ident, rev)
            }
            (None, None, None, None, None, None, None, Some(jstor), None, None) => {
                check_jstor_id(jstor)?;
                let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) =
                    release_rev::table
                        .inner_join(release_ident::table)
                        .inner_join(release_rev_extid::table)
                        .filter(release_rev_extid::extid_type.eq("jstor".to_string()))
                        .filter(release_rev_extid::value.eq(jstor))
                        .filter(release_ident::is_live.eq(true))
                        .filter(release_ident::redirect_id.is_null())
                        .first(conn)?;
                (ident, rev)
            }
            (None, None, None, None, None, None, None, None, Some(ark), None) => {
                check_ark_id(ark)?;
                let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) =
                    release_rev::table
                        .inner_join(release_ident::table)
                        .inner_join(release_rev_extid::table)
                        .filter(release_rev_extid::extid_type.eq("ark".to_string()))
                        .filter(release_rev_extid::value.eq(ark))
                        .filter(release_ident::is_live.eq(true))
                        .filter(release_ident::redirect_id.is_null())
                        .first(conn)?;
                (ident, rev)
            }
            (None, None, None, None, None, None, None, None, None, Some(mag)) => {
                check_mag_id(mag)?;
                let (rev, ident, _extid): (ReleaseRevRow, ReleaseIdentRow, ReleaseExtidRow) =
                    release_rev::table
                        .inner_join(release_ident::table)
                        .inner_join(release_rev_extid::table)
                        .filter(release_rev_extid::extid_type.eq("mag".to_string()))
                        .filter(release_rev_extid::value.eq(mag))
                        .filter(release_ident::is_live.eq(true))
                        .filter(release_ident::redirect_id.is_null())
                        .first(conn)?;
                (ident, rev)
            }
            _ => {
                return Err(
                    FatcatError::MissingOrMultipleExternalId("in lookup".to_string()).into(),
                );
            }
        };

        let mut entity = ReleaseEntity::db_from_row(conn, rev, Some(ident), hide_flags)?;
        entity.db_expand(&conn, expand_flags)?;
        Ok(entity)
    }

    pub fn get_release_files_handler(
        &self,
        conn: &DbConn,
        ident: FatcatId,
        hide_flags: HideFlags,
    ) -> Result<Vec<FileEntity>> {
        get_release_files(conn, ident, hide_flags)
    }

    pub fn get_release_filesets_handler(
        &self,
        conn: &DbConn,
        ident: FatcatId,
        hide_flags: HideFlags,
    ) -> Result<Vec<FilesetEntity>> {
        get_release_filesets(conn, ident, hide_flags)
    }

    pub fn get_release_webcaptures_handler(
        &self,
        conn: &DbConn,
        ident: FatcatId,
        hide_flags: HideFlags,
    ) -> Result<Vec<WebcaptureEntity>> {
        get_release_webcaptures(conn, ident, hide_flags)
    }

    pub fn get_work_releases_handler(
        &self,
        conn: &DbConn,
        ident: FatcatId,
        hide_flags: HideFlags,
    ) -> Result<Vec<ReleaseEntity>> {
        let rows: Vec<(ReleaseRevRow, ReleaseIdentRow)> = release_rev::table
            .inner_join(release_ident::table)
            .filter(release_rev::work_ident_id.eq(&ident.to_uuid()))
            .filter(release_ident::is_live.eq(true))
            .filter(release_ident::redirect_id.is_null())
            .load(conn)?;

        rows.into_iter()
            .map(|(rev, ident)| ReleaseEntity::db_from_row(conn, rev, Some(ident), hide_flags))
            .collect()
    }

    pub fn accept_editgroup_handler(&self, conn: &DbConn, editgroup_id: FatcatId) -> Result<()> {
        accept_editgroup(conn, editgroup_id)?;
        Ok(())
    }

    pub fn create_editgroup_handler(
        &self,
        conn: &DbConn,
        editgroup: models::Editgroup,
    ) -> Result<Editgroup> {
        let row = editgroup.db_create(conn, false)?;
        Ok(row.into_model_partial(None, None))
    }

    pub fn get_editgroup_handler(
        &self,
        conn: &DbConn,
        editgroup_id: FatcatId,
    ) -> Result<Editgroup> {
        let (eg_row, cl_row) = Editgroup::db_get_with_changelog(conn, editgroup_id)?;
        let editor = Editor::db_get(&conn, FatcatId::from_uuid(&eg_row.editor_id))?.into_model();
        let mut editgroup = eg_row.into_model_partial(cl_row.map(|cl| cl.id), Some(editor));

        let edits = EditgroupEdits {
            containers: Some(
                container_edit::table
                    .filter(container_edit::editgroup_id.eq(editgroup_id.to_uuid()))
                    .get_results(conn)?
                    .into_iter()
                    .map(|e: ContainerEditRow| e.into_model().unwrap())
                    .collect(),
            ),
            creators: Some(
                creator_edit::table
                    .filter(creator_edit::editgroup_id.eq(editgroup_id.to_uuid()))
                    .get_results(conn)?
                    .into_iter()
                    .map(|e: CreatorEditRow| e.into_model().unwrap())
                    .collect(),
            ),
            files: Some(
                file_edit::table
                    .filter(file_edit::editgroup_id.eq(editgroup_id.to_uuid()))
                    .get_results(conn)?
                    .into_iter()
                    .map(|e: FileEditRow| e.into_model().unwrap())
                    .collect(),
            ),
            filesets: Some(
                fileset_edit::table
                    .filter(fileset_edit::editgroup_id.eq(editgroup_id.to_uuid()))
                    .get_results(conn)?
                    .into_iter()
                    .map(|e: FilesetEditRow| e.into_model().unwrap())
                    .collect(),
            ),
            webcaptures: Some(
                webcapture_edit::table
                    .filter(webcapture_edit::editgroup_id.eq(editgroup_id.to_uuid()))
                    .get_results(conn)?
                    .into_iter()
                    .map(|e: WebcaptureEditRow| e.into_model().unwrap())
                    .collect(),
            ),
            releases: Some(
                release_edit::table
                    .filter(release_edit::editgroup_id.eq(editgroup_id.to_uuid()))
                    .get_results(conn)?
                    .into_iter()
                    .map(|e: ReleaseEditRow| e.into_model().unwrap())
                    .collect(),
            ),
            works: Some(
                work_edit::table
                    .filter(work_edit::editgroup_id.eq(editgroup_id.to_uuid()))
                    .get_results(conn)?
                    .into_iter()
                    .map(|e: WorkEditRow| e.into_model().unwrap())
                    .collect(),
            ),
        };

        editgroup.edits = Some(edits);
        Ok(editgroup)
    }

    pub fn get_editor_handler(&self, conn: &DbConn, editor_id: FatcatId) -> Result<Editor> {
        let row: EditorRow = Editor::db_get(conn, editor_id)?;
        Ok(row.into_model())
    }

    pub fn get_changelog_handler(
        &self,
        conn: &DbConn,
        limit: Option<i64>,
    ) -> Result<Vec<ChangelogEntry>> {
        let limit = limit.unwrap_or(50);

        let changes: Vec<(EditgroupRow, ChangelogRow, EditorRow)> = editgroup::table
            .inner_join(changelog::table)
            .inner_join(editor::table)
            .order(changelog::id.desc())
            .limit(limit)
            .load(conn)?;

        let entries = changes
            .into_iter()
            .map(|(eg_row, cl_row, editor_row)| ChangelogEntry {
                index: cl_row.id,
                editgroup: Some(
                    eg_row.into_model_partial(Some(cl_row.id), Some(editor_row.into_model())),
                ),
                editgroup_id: uuid2fcid(&cl_row.editgroup_id),
                timestamp: chrono::DateTime::from_utc(cl_row.timestamp, chrono::Utc),
            })
            .collect();
        Ok(entries)
    }

    pub fn get_changelog_entry_handler(&self, conn: &DbConn, index: i64) -> Result<ChangelogEntry> {
        let cl_row: ChangelogRow = changelog::table.find(index).first(conn)?;
        let editgroup =
            self.get_editgroup_handler(conn, FatcatId::from_uuid(&cl_row.editgroup_id))?;

        let mut entry = cl_row.into_model();
        entry.editgroup = Some(editgroup);
        Ok(entry)
    }

    /// This helper either finds an Editor model by OIDC parameters (eg, remote domain and
    /// identifier), or creates one and inserts the appropriate auth rows. The semantics are
    /// basically an "upsert" of signup/account-creation.
    /// Returns an editor model and boolean flag indicating whether a new editor was created or
    /// not.
    /// If this function creates an editor, it sets the username to
    /// "{preferred_username}-{provider}"; the intent is for this to be temporary but unique. Might
    /// look like "bnewbold-github", or might look like "895139824-github". This is a hack to make
    /// check/creation idempotent.
    pub fn auth_oidc_handler(&self, conn: &DbConn, params: AuthOidc) -> Result<(Editor, bool)> {
        let existing: Vec<(EditorRow, AuthOidcRow)> = editor::table
            .inner_join(auth_oidc::table)
            .filter(auth_oidc::oidc_sub.eq(params.sub.clone()))
            .filter(auth_oidc::oidc_iss.eq(params.iss.clone()))
            .load(conn)?;

        let (editor_row, created): (EditorRow, bool) = match existing.first() {
            Some((editor, _)) => (editor.clone(), false),
            None => {
                let mut username = format!("{}-{}", params.preferred_username, params.provider);
                username.truncate(24);
                let editor = Editor {
                    editor_id: None,
                    username: username,
                    is_admin: Some(false),
                    is_bot: Some(false),
                    is_active: Some(true),
                };
                let row = editor.db_create(conn)?;
                // create an auth login row so the user can log back in
                diesel::insert_into(auth_oidc::table)
                    .values((
                        auth_oidc::editor_id.eq(row.id),
                        auth_oidc::provider.eq(params.provider),
                        auth_oidc::oidc_iss.eq(params.iss),
                        auth_oidc::oidc_sub.eq(params.sub),
                    ))
                    .execute(conn)?;
                (row, true)
            }
        };

        Ok((editor_row.into_model(), created))
    }

    entity_auto_batch_handler!(create_container_auto_batch_handler, ContainerEntity);
    entity_auto_batch_handler!(create_creator_auto_batch_handler, CreatorEntity);
    entity_auto_batch_handler!(create_file_auto_batch_handler, FileEntity);
    entity_auto_batch_handler!(create_fileset_auto_batch_handler, FilesetEntity);
    entity_auto_batch_handler!(create_webcapture_auto_batch_handler, WebcaptureEntity);
    entity_auto_batch_handler!(create_release_auto_batch_handler, ReleaseEntity);
    entity_auto_batch_handler!(create_work_auto_batch_handler, WorkEntity);
}