use base64::{
    alphabet,
    engine::{self, general_purpose},
    Engine,
};
use metrics::counter;
use serde::{ser::SerializeStruct, Deserialize, Serialize};
use std::{fmt::Debug, sync::LazyLock, time::Instant};
use surrealdb::{
    engine::remote::ws::{Client, Ws},
    opt::auth::Root,
    sql::Thing,
    Error::Api,
    Response, Surreal,
};
use tokio::sync::Mutex;
use tracing::{error, instrument, trace, warn};
use url::Url;

use crate::{Config, Timer};

// static LOCK: LazyLock<Arc<Mutex<bool>>> = LazyLock::new(|| Arc::new(Mutex::new(true)));
static LOCK: LazyLock<Mutex<bool>> = LazyLock::new(|| Mutex::new(true));

const CUSTOM_ENGINE: engine::GeneralPurpose =
    engine::GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD);

const TIME_SPENT_ON_LOCK: &'static str = "surql_lock_waiting_ms";
const STORE: &'static str = "surql_store_calls";
const LINK: &'static str = "surql_link_calls";

#[derive(Deserialize, Clone)]
pub struct Website {
    /// The url that this data is found at
    pub site: Url,
    /// Wether or not this link has been crawled yet
    pub crawled: bool,
}

impl Serialize for Website {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer {
            let mut state = serializer.serialize_struct("Website", 2)?;
            state.serialize_field("crawled", &self.crawled)?;
            // to_string() calls the correct naming of site
            state.serialize_field("site", &self.site.to_string())?;
            state.end()
    }
}

// manual impl to make tracing look nicer
impl Debug for Website {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Website").field("site", &self.site).finish()
    }
}

impl Website {
    /// Creates a blank site (assumes that url param is site's root)
    pub fn new(url: &str, crawled: bool) -> Self {
        let site = match Url::parse(url) {
            Ok(a) => a,
            Err(_) => todo!(),
        };
        Self {
            crawled,
            site
        }
    }

    pub fn set_crawled(&mut self) {
        trace!("Set crawled to true");
        self.crawled = true
    }

    pub fn get_url_as_string(site: &Url) -> String {
        let domain = match site.domain() {
            Some(s) => s.to_string(),
            None => {
                warn!("Failed to get domain of URL: {}, falling back to 'localhost'", site.to_string());
                "localhost".to_string()
            }
        };
        let path = site.path();

        domain + path
    }
    pub fn get_url_as_b64_path(site: &Url) -> String {
        let domain = site.domain().unwrap_or("DOMAIN").to_string();
        let path = &CUSTOM_ENGINE.encode(site.path());

        domain + path
    }

    #[instrument(skip_all)]
    pub async fn links_to(&self, other: Vec<Thing>, db: &Surreal<Client>) {
        let len = other.len();
        if len == 0 {
            return;
        }
        
        let from = &self.site;

        // let to = other.site.to_string();
        trace!("Linking {} pages to {from}", other.len());
        let msg = format!("Linked {len} pages to {from}");
        let timer = Timer::start(&msg);
        // prevent the timer from being dropped instantly.
        let _ = timer;
        counter!(LINK).increment(1);
        match db
            .query("COUNT(RELATE (SELECT id FROM website WHERE site = $in) -> links_to -> $out)")
            .bind(("in", from.clone()))
            .bind(("out", other))
            .await
        {
            Ok(mut e) => {
                // The relate could technically "fail" (not relate anything), this just means that
                // the query was ok.
                let _: Response = e;
                if let Ok(vec) = e.take(0) {
                    let _: Vec<usize> = vec;
                    if let Some(num) = vec.get(0) {
                        if *num == len {
                            trace!("Link for {from} OK - {num}/{len}");
                            return;
                        } else {
                            error!("Didn't link all the records. {num}/{len}. Surreal response: {:?}", e);
                            return;
                        }
                    }
                }
                error!("Linking request succeeded but couldn't verify the results.");
            }
            Err(e) => {
                error!("{}", e.to_string());
            }
        }
    }

    // Insert ever item in the vec into surreal, crawled state will be preserved as TRUE
    // if already in the database as such or incoming data is TRUE.
    pub async fn store_all(all: Vec<Self>, db: &Surreal<Client>) -> Vec<Thing> {
        counter!(STORE).increment(1);
        let mut things = Vec::with_capacity(all.len());

        // TODO this only allows for one thread to be in the database at a time.
        // This is currently required since otherwise we get write errors.
        // If the default `crawled` is set to false, we might not need to write any more
        // than just the name. `accessed_at` is fun but not needed.
        let now = Instant::now();
        let lock = LOCK.lock().await;
        counter!(TIME_SPENT_ON_LOCK).increment(now.elapsed().as_millis() as u64);

        match db
            .query(
                "INSERT INTO website $array
                    ON DUPLICATE KEY UPDATE
                        accessed_at = time::now(),
                        crawled = crawled OR $input.crawled
                    RETURN VALUE id;
                 ",
            )
            .bind(("array", all))
            .await
        {
            Ok(mut id) => match id.take::<Vec<Thing>>(0) {
                Ok(mut x) => things.append(&mut x),
                Err(err) => match err {
                    Api(error) => {
                        eprintln!("{:?}", error);
                        error!("{:?}", error);
                    }
                    _ => error!("{:?}", err),
                },
            },
            Err(err) => {
                error!("{:?}", err);
            }
        }
        drop(lock);
        things
    }
}

#[derive(Debug, Serialize)]
pub struct Email {
    pub email: String,
    pub on: String,
}

#[derive(Debug, Deserialize)]
pub struct Record {
    #[allow(dead_code)]
    pub id: Thing,
}

#[instrument(skip_all, name = "SurrealDB")]
pub async fn connect(config: &Config) -> surrealdb::Result<Surreal<Client>> {
    trace!("Establishing connection to surreal...");
    // Connect to the server
    let db = Surreal::new::<Ws>(&config.surreal_url).await?;

    trace!("Logging in...");
    // Signin as a namespace, database, or root user
    db.signin(Root {
        username: &config.surreal_username,
        password: &config.surreal_password,
    })
    .await?;

    // Select a specific namespace / database
    db.use_ns(&config.surreal_ns)
        .use_db(&config.surreal_db)
        .await?;

    let setup = include_bytes!("setup.surql");
    let file = setup.iter().map(|c| *c as char).collect::<String>();

    db.query(file)
        .await
        .expect("Failed to setup surreal tables.");

    Ok(db)
}
