use std::fmt::Debug;
use serde::{Deserialize, Serialize};
use surrealdb::{
    engine::remote::ws::{Client, Ws}, error::Db, opt::auth::Root, sql::Thing, Response, Surreal
};
use tracing::{error, instrument, trace, warn};
use url::Url;

use crate::{Config, Timer};

#[derive(Serialize, 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,
    #[serde(skip_serializing)]
    id: Option<Thing>,
}

// manual impl to make tracing look nicer
impl Debug for Website {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let site = (self.site.domain().unwrap_or("n/a")).to_string() + self.site.path();
        f.debug_struct("Website").field("site", &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 {
            id: None,
            crawled,
            site,
        }
    }

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

    #[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.to_string();
        // let to = other.site.to_string();
        trace!("Linking {from} to {} other pages.", other.len());
        let msg = format!("Linked {len} pages");
        let timer = Timer::start(&msg);
        // prevent the timer from being dropped instantly.
        let _ = timer;
        match db
            .query("COUNT(RELATE (SELECT id FROM website WHERE site = $in) -> links_to -> $out)")
            .bind(("in", from))
            .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 OK");
                            return;
                        } else {
                            warn!("Didn't link all the records. {num}/{len}");
                            return;
                        }
                    }
                }
                warn!("Linking request succeeded but couldn't verify the results.");
            },
            Err(e) => {
                error!("{}", e.to_string());
            },
        }
    }

    #[instrument(skip_all)]
    pub async fn store(&self, db: &Surreal<Client>) -> Option<Thing> {
        let t = Timer::start("Stored page");
        let _ = t;
        // check if it's been gone thru before
        let mut response = db
            .query("SELECT * FROM ONLY website WHERE site = $site LIMIT 1")
            .bind(("site", self.site.to_string()))
            .await
            .expect("Failed to check surreal for duplicates!");

        if let Some(old) = response.take::<Option<Website>>(0).expect("Failed to read response from surreal for duplicates.") {
            // site exists already
            if let Some(id) = old.id {
                // make sure to preserve the "crawled status"
                let mut new = self.clone();
                new.crawled = old.crawled | new.crawled;

                // update the record
                match db.upsert((id.tb, id.id.to_string())).content(new).await {
                    Ok(e) => {
                        if let Some(a) = e {
                            let _: Record = a;
                            return Some(a.id);
                        }
                    }
                    Err(e) => {
                        match e {
                            surrealdb::Error::Db(error) => {
                                match error {
                                    Db::QueryCancelled => todo!(),
                                    Db::QueryNotExecuted => todo!(),
                                    Db::QueryNotExecutedDetail { message: _ } => todo!(),
                                   _=>{},
                                }
                            },
                            _=>{},
                        }
                        // error!("{}", e);
                    }
                };
            }
        } else {
            // sites hasn't existed yet
            match db.create("website").content(self.clone()).await {
                Ok(e) => {
                    let _: Option<Record> = e;
                    if let Some(a) = e {
                        let _: Record = a;
                        return Some(a.id);
                    }
                }
                Err(a) => error!("{:?}", a),
            };
        }
        None
    }
}

impl ToString for Website {
    fn to_string(&self) -> String {
        self.site.to_string()
    }
}

#[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)
}
