/*
 * Copyright (C) 2023  Aravinth Manivannan <realaravinth@batsense.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
use url::Url;

use crate::api::v1::get_random;
use crate::errors::*;
use crate::mcaptcha::PerformanceAnalytics;
use crate::Data;

impl Data {
    /// Check if an mCaptcha instance is registered on the database
    pub async fn mcaptcha_url_exists(&self, url: &str) -> ServiceResult<bool> {
        let res = sqlx::query!(
            "SELECT EXISTS (SELECT 1 from survey_mcaptcha_hostname WHERE url = $1)",
            url
        )
        .fetch_one(&self.db)
        .await?;

        let mut resp = false;
        if let Some(x) = res.exists {
            if x {
                resp = true;
            }
        }

        Ok(resp)
    }

    /// Register an mCaptcha instance
    pub async fn mcaptcha_register_instance(&self, url: &str) -> ServiceResult<String> {
        let secret = get_random(32);
        sqlx::query!(
            "INSERT INTO survey_mcaptcha_hostname (url, secret) VALUES ($1, $2)",
            url,
            &secret,
        )
        .execute(&self.db)
        .await?;
        Ok(secret)
    }

    /// Update the secret of an mCaptcha instance
    pub async fn mcaptcha_update_secret(&self, url: &str) -> ServiceResult<String> {
        let secret = get_random(32);
        sqlx::query!(
            "UPDATE survey_mcaptcha_hostname set secret = $1 WHERE url = $2",
            &secret,
            url
        )
        .execute(&self.db)
        .await?;
        Ok(secret)
    }

    /// Authenticate an mCaptcha instance and return its URL
    pub async fn mcaptcha_authenticate_and_get_url(
        &self,
        secret: &str,
    ) -> ServiceResult<Url> {
        struct U {
            url: String,
        }

        let res = sqlx::query!(
            "SELECT EXISTS (
        SELECT
            url
        FROM
            survey_mcaptcha_hostname
        WHERE secret = $1
        )",
            secret
        )
        .fetch_one(&self.db)
        .await?;

        if !match res.exists {
            Some(true) => true,
            _ => false,
        } {
            return Err(ServiceError::WrongPassword);
        }

        let url = sqlx::query_as!(
            U,
            "SELECT
            url
        FROM
            survey_mcaptcha_hostname
        WHERE
            secret = $1; ",
            secret
        )
        .fetch_one(&self.db)
        .await?;

        Ok(Url::parse(&url.url).unwrap())
    }

    /// Delete mCaptcha instance from database
    pub async fn mcaptcha_delete_mcaptcha_instance(
        &self,
        url: &str,
        secret: &str,
    ) -> ServiceResult<()> {
        sqlx::query!(
            "DELETE FROM survey_mcaptcha_hostname WHERE secret = $1 AND url =$2",
            secret,
            url
        )
        .execute(&self.db)
        .await?;
        Ok(())
    }
    /// Delete mCaptcha camapign from database
    pub async fn mcaptcha_delete_mcaptcha_campaign(
        &self,
        campaign_id: &uuid::Uuid,
        secret: &str,
    ) -> ServiceResult<()> {
        let campaign_str = campaign_id.to_string();
        let res = sqlx::query!(
            "DELETE FROM
                survey_mcaptcha_campaign
            WHERE
                campaign_id = $1
            AND
                url_id = (
                    SELECT
                        ID
                    FROM
                        survey_mcaptcha_hostname
                    WHERE
                        secret = $2
                    )",
            &campaign_str,
            secret
        )
        .execute(&self.db)
        .await?;

        Ok(())
    }

    /// Check if an mCaptcha instance campaign is registered on DB
    pub async fn mcaptcha_campaign_is_registered(
        &self,
        campaign_id: &uuid::Uuid,
        secret: &str,
    ) -> ServiceResult<bool> {
        let campaign_str = campaign_id.to_string();

        let res = sqlx::query!(
            "SELECT EXISTS (
        SELECT
            ID
        FROM
            survey_mcaptcha_campaign
        WHERE
            campaign_id = $1
        AND
            url_id = (
                SELECT
                    ID
                FROM
                    survey_mcaptcha_hostname
                WHERE
                    secret = $2
                )
        )",
            &campaign_str,
            secret
        )
        .fetch_one(&self.db)
        .await?;

        let mut resp = false;
        if let Some(x) = res.exists {
            if x {
                resp = true;
            }
        }
        Ok(resp)
    }

    /// Register an mCaptcha instance campaign on DB
    pub async fn mcaptcha_register_campaign(
        &self,
        campaign_id: &uuid::Uuid,
        secret: &str,
    ) -> ServiceResult<()> {
        let campaign_str = campaign_id.to_string();
        let public_id = uuid::Uuid::new_v4();

        sqlx::query!(
            "INSERT INTO
                survey_mcaptcha_campaign (campaign_id, public_id, url_id)
            VALUES ($1, $2, (SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $3));",
            &campaign_str,
            &public_id.to_string(),
            secret,
        )
        .execute(&self.db)
        .await?;
        Ok(())
    }

    /// Register an mCaptcha instance campaign on DB
    pub async fn mcaptcha_get_campaign_public_id(
        &self,
        campaign_id: &uuid::Uuid,
        secret: &str,
    ) -> ServiceResult<uuid::Uuid> {
        let campaign_str = campaign_id.to_string();
        struct S {
            public_id: String,
        }

        let res = sqlx::query_as!(
            S,
            "SELECT
                public_id
            FROM
                survey_mcaptcha_campaign
            WHERE
                campaign_id = $1
            AND
                url_id = (SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $2);",
            &campaign_str,
            secret,
        )
        .fetch_one(&self.db)
        .await?;

        Ok(uuid::Uuid::parse_str(&res.public_id).unwrap())
    }

    /// Get an mCaptcha instance campaign checkpoint
    pub async fn mcaptcha_get_checkpoint(
        &self,
        campaign_id: &uuid::Uuid,
        secret: &str,
    ) -> ServiceResult<usize> {
        let campaign_str = campaign_id.to_string();

        struct CheckPoint {
            synced_till: i32,
        }

        let checkpoint = sqlx::query_as!(
            CheckPoint,
            "SELECT
            synced_till
        FROM
            survey_mcaptcha_campaign
        WHERE 
            campaign_id = $1
        AND
            url_id = (
                SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $2
            );",
            &campaign_str,
            secret
        )
        .fetch_one(&self.db)
        .await?;
        let checkpoint = checkpoint.synced_till as usize;
        Ok(checkpoint)
    }

    /// Set an mCaptcha instance campaign checkpoint
    pub async fn mcaptcha_set_checkpoint(
        &self,
        campaign_id: &uuid::Uuid,
        secret: &str,
        checkpoint: usize,
    ) -> ServiceResult<()> {
        let campaign_str = campaign_id.to_string();
        sqlx::query!(
            "UPDATE
                survey_mcaptcha_campaign
            SET
                synced_till = $1
            WHERE 
                campaign_id = $2
            AND
                url_id = (
                    SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $3
                )
                ",
            checkpoint as i32,
            &campaign_str,
            secret
        )
        .execute(&self.db)
        .await?;

        Ok(())
    }

    /// Store mCaptcha instance campaign analytics
    pub async fn mcaptcha_insert_analytics(
        &self,
        campaign_id: &uuid::Uuid,
        secret: &str,
        r: &PerformanceAnalytics,
    ) -> ServiceResult<()> {
        let campaign_str = campaign_id.to_string();
        sqlx::query!(
            "INSERT INTO
                survey_mcaptcha_analytics (
                    campaign_id, time, difficulty_factor, worker_type
                )
                VALUES ((
                    SELECT
                        ID
                    FROM
                        survey_mcaptcha_campaign
                    WHERE 
                        campaign_id = $1
                    AND
                        url_id = (
                            SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $2
                        )
                    ), $3, $4, $5
                );",
            &campaign_str,
            secret,
            r.time as i32,
            r.difficulty_factor as i32,
            &r.worker_type,
        )
        .execute(&self.db)
        .await?;
        Ok(())
    }

    /// fetch PoW analytics
    pub async fn mcaptcha_analytics_fetch(
        &self,
        public_id: &uuid::Uuid,
        limit: usize,
        offset: usize,
    ) -> ServiceResult<Vec<PerformanceAnalytics>> {
        let public_id_str = public_id.to_string();
        struct P {
            id: i32,
            time: i32,
            difficulty_factor: i32,
            worker_type: String,
        }

        impl From<P> for PerformanceAnalytics {
            fn from(v: P) -> Self {
                Self {
                    time: v.time as u32,
                    difficulty_factor: v.difficulty_factor as u32,
                    worker_type: v.worker_type,
                    id: v.id as usize,
                }
            }
        }

        let mut c = sqlx::query_as!(
            P,
            "SELECT id, time, difficulty_factor, worker_type FROM survey_mcaptcha_analytics
            WHERE 
                campaign_id = (
                    SELECT 
                        ID FROM survey_mcaptcha_campaign 
                    WHERE 
                        public_id = $1
                )
                ORDER BY ID
                OFFSET $2 LIMIT $3
                ",
            &public_id_str,
            offset as i32,
            limit as i32
        )
        .fetch_all(&self.db)
        .await?;
        let mut res = Vec::with_capacity(c.len());
        for i in c.drain(0..) {
            res.push(i.into())
        }

        Ok(res)
    }
}

#[cfg(test)]
mod tests {
    use crate::{mcaptcha::PerformanceAnalytics, tests::*};

    use url::Url;

    #[actix_rt::test]
    async fn test_db_mcaptcha_works() {
        let url = Url::parse("http://test_add_campaign.example").unwrap();
        let data = get_test_data().await;
        let url_str = url.to_string();
        if data.mcaptcha_url_exists(&url_str).await.unwrap() {
            let secret = data.mcaptcha_update_secret(&url_str).await.unwrap();
            data.mcaptcha_delete_mcaptcha_instance(&url_str, &secret)
                .await
                .unwrap();
        }
        assert!(!data.mcaptcha_url_exists(&url_str).await.unwrap());

        let secret = data.mcaptcha_register_instance(&url_str).await.unwrap();
        assert!(data.mcaptcha_url_exists(&url_str).await.unwrap());
        let secret2 = data.mcaptcha_update_secret(&url_str).await.unwrap();
        assert_ne!(secret2, secret);
        let secret = secret2;

        assert_eq!(
            data.mcaptcha_authenticate_and_get_url(&secret)
                .await
                .unwrap(),
            url
        );

        let uuid = uuid::Uuid::new_v4();

        if data
            .mcaptcha_campaign_is_registered(&uuid, &secret)
            .await
            .unwrap()
        {
            data.mcaptcha_delete_mcaptcha_campaign(&uuid, &secret)
                .await
                .unwrap();
        }

        assert!(!data
            .mcaptcha_campaign_is_registered(&uuid, &secret)
            .await
            .unwrap());
        data.mcaptcha_register_campaign(&uuid, &secret)
            .await
            .unwrap();
        assert!(data
            .mcaptcha_campaign_is_registered(&uuid, &secret)
            .await
            .unwrap());

        assert_eq!(
            data.mcaptcha_get_checkpoint(&uuid, &secret).await.unwrap(),
            0
        );
        data.mcaptcha_set_checkpoint(&uuid, &secret, 1)
            .await
            .unwrap();
        assert_eq!(
            data.mcaptcha_get_checkpoint(&uuid, &secret).await.unwrap(),
            1
        );

        let analytics = PerformanceAnalytics {
            id: 1,
            time: 1,
            difficulty_factor: 1,
            worker_type: "foo".to_string(),
        };
        data.mcaptcha_insert_analytics(&uuid, &secret, &analytics)
            .await
            .unwrap();

        let public_id = data
            .mcaptcha_get_campaign_public_id(&uuid, &secret)
            .await
            .unwrap();
        let db_analytics = data
            .mcaptcha_analytics_fetch(&public_id, 50, 0)
            .await
            .unwrap();

        assert_eq!(db_analytics.len(), 1);
        assert_eq!(db_analytics[0].time, analytics.time);
        assert_eq!(
            db_analytics[0].difficulty_factor,
            analytics.difficulty_factor
        );
        assert_eq!(db_analytics[0].worker_type, analytics.worker_type);

        assert_eq!(
            data.mcaptcha_analytics_fetch(&public_id, 50, 1)
                .await
                .unwrap(),
            vec![]
        );
    }
}
