use axum::http::{header::{HeaderValue},uri::Uri};
use html_minifier::HTMLMinifier;
use std::{
    error::Error,
    collections::HashMap,
    str,
    io::{ErrorKind, Result},
};
use tera::Context;
use totp_rs::{Algorithm, TOTP, Secret};
use rand::Rng;

use crate::{
    defs::{
        AppDBs,
        ReqHeaderMap,
        Local,
        TraceData,
        TraceContent,
        TraceLogContent,
        AuthState,
        Random,
        TOKEN_AUTH_VALUE,
        TOKEN_KEY_VALUE,
    },
    users::{
        UserRole,
        User,
    },
};

use super::{CLAIM_UID, CLAIM_AUTH, CLAIM_APP_KEY};

pub struct ReqHandler<'a> {
    pub req_header: ReqHeaderMap,
    pub app_dbs: &'a AppDBs,
    pub lang: Local,
    pub uri:  &'a Uri,
    pub auth_state: AuthState,
    pub random: &'a Random,
    pub req_name: String,
    pub context: Context,
}

impl<'a> ReqHandler<'a> {
    pub fn new(req_header: ReqHeaderMap, app_dbs: &'a AppDBs, uri: &'a Uri, auth_state: &'a AuthState, random: &'a Random, req_name: &str) -> Self {
        let curr_lang = req_header.lang(&app_dbs.config);
        let lang = Local::get_lang(&app_dbs.config.locales,&curr_lang, &app_dbs.config.dflt_lang);
        let mut context = app_dbs.context.to_owned();
        context.insert("site_name",&app_dbs.config.name);
        context.insert("site_org", &app_dbs.config.org);
        context.insert("lang", &lang.itms);
        context.insert("req_name", &req_name);
        context.insert("main_url", &format!("{}://{}",&app_dbs.config.protocol, &app_dbs.config.hostport));
        context.insert("html_url", &app_dbs.config.html_url);
        context.insert("assets_url", &app_dbs.config.assets_url);
        context.insert("req_path", uri.path());
        context.insert("css_link", &app_dbs.config.ui.css_link);
        context.insert("js_link", &app_dbs.config.ui.js_link);
        context.insert("other_css_link", &app_dbs.config.ui.other_css_link);
        context.insert("other_js_link", &app_dbs.config.ui.other_js_link);
        context.insert("main_js_link", &app_dbs.config.ui.main_js_link);
        context.insert("utils_js_link", &app_dbs.config.ui.utils_js_link);
        context.insert("web_menu_items", &app_dbs.config.ui.web_menu_items);
        context.insert("main_name", &app_dbs.config.ui.main_name);
        context.insert("title", &app_dbs.config.ui.title);
        context.insert("subtitle", &app_dbs.config.ui.subtitle);
        context.insert("usr_roles", &auth_state.user_roles());
        let user_items = User::hash_items(&auth_state.user_items());
        context.insert("usr_items", &user_items);
        context.insert("usr_name", &auth_state.user_name());
        if auth_state.is_admin() {
            context.insert("isadmin", "TRUE");
        }
        context.insert("usr_email", &auth_state.user_email());
        context.insert("session_expire", &app_dbs.config.session_expire);
        context.insert("update_session_item", &format!("{}://{}/update_item",
            &app_dbs.config.protocol, &app_dbs.config.hostport));
        let user_id = auth_state.id();
        let sid = if user_id.is_empty() || user_id == "No ID" {
            String::from("") 
        } else {
            user_id.replace("-", "").to_owned()
        };
        context.insert("session_id",&sid);
        context.insert("signup_mode",&app_dbs.config.signup_mode);
        ReqHandler { 
            req_header,
            app_dbs,
            lang,
            uri,
            auth_state: auth_state.to_owned(),
            random,
            req_name: req_name.to_owned(),
            context,
        }
    }
    pub fn prepare_response(&mut self) {
        // self.req_header.header.contains_key(axum::http::header::CONTENT_TYPE)
        if self.req_header.is_browser() || self.req_header.is_wget() {
           self.req_header.header.append(axum::http::header::CONTENT_TYPE,
                HeaderValue::try_from("text/html; charset=utf-8")
                .expect("URI isn't a valid header value")
            );
        }
    }
   pub fn render_template(&mut self,template_file: &str, dflt_content: &str) -> String {    
        self.context.insert("page", &template_file.replace(".j2","").replace("html/", "").as_str());
        if template_file.contains("md") || template_file.contains("sample") || template_file.contains("code") {
           self.context.insert("with_code", "true");
        }
        match self.app_dbs.tera.render(&template_file, &self.context) {
            Ok(s) => {
                if self.req_header.is_browser() {
                    let mut html_minifier = HTMLMinifier::new();
                    match html_minifier.digest(s.as_str()) {
                        Ok(_) => { 
                            let Ok(result) = str::from_utf8(html_minifier.get_html()) else { return s };
                            // let result = str::from_utf8(html_minifier.get_html()).unwrap_or(s);
                            result.to_owned()
                        },
                        Err(e) => {
                            println!("Error minifier: {}",e);
                            s
                        },
                    }
                } else {
                   s
                }
            },
            Err(e) => {
                println!("Error: {}", &e);
                let mut cause = e.source();
                while let Some(e) = cause {
                    println!("Reason: {}", e);
                    cause = e.source();
                }
                dflt_content.to_owned()
            }
        }
    }
    #[allow(dead_code)]
    pub fn bad_request(&mut self,msg: &str) -> String {
        if let Some(tpl) = self.app_dbs.config.tpls.get("notfound") {
            self.render_template(tpl, msg)
        } else {
            msg.to_owned()
        }
    }
    #[allow(dead_code)]
    pub fn auth_role(&self) -> UserRole {
        #[cfg(feature = "authstore")]
        if let Some(authz) =  self.app_dbs.auth_store.authz.get(&self.notify_data.auth) {
            println!("aut: {}, name: {}, role: {}",&self.notify_data.auth, &authz.name,&authz.role);
            authz.role.to_owned()
        } else {
            UserRole::Anonymous
        }
        #[cfg(feature = "casbin")]
        UserRole::Anonymous
    }
    #[allow(dead_code)]
    pub fn get_lang(&self, key: &str) -> Local {
       Local::get_lang(&self.app_dbs.config.locales, key, &self.app_dbs.config.dflt_lang)
    }
    pub fn new_token(&self) -> String {
        if self.app_dbs.config.use_token {
            let data_claim = HashMap::from([
                (String::from(CLAIM_UID), self.auth_state.user_id()),
                (String::from(CLAIM_AUTH), TOKEN_AUTH_VALUE.to_owned()),
                (String::from(CLAIM_APP_KEY), TOKEN_KEY_VALUE.to_owned()),
            ]);
            let expire = false;
            self.app_dbs.config.paseto.generate_token("", &data_claim, expire).unwrap_or_else(|e|{
                eprintln!("Error generating token: {}", e);
                String::from("")
            })
        } else {
            let mut u128_pool = [0u8; 16];
            match self.random.lock() {
                Ok(mut r) => r.fill(&mut u128_pool),
                Err(e) => println!("Error random: {}",e),
            }
            u128::from_le_bytes(u128_pool).to_string()
        }
    }
    pub fn otp_make(&self, code: &str, defs: &str) -> Result<TOTP>  {
        let secret = match Secret::Encoded(code.to_owned()).to_bytes() {
            Ok(scrt) => scrt,
            Err(e) => 
                return Err(std::io::Error::new(
                ErrorKind::NotFound,
                format!("ERROR: Invalid Secret {}",&e)
                ))
        };
        let (digits, str_algorithm) = if defs.is_empty() {
            (   self.app_dbs.config.totp_digits,
                format!("{}",self.app_dbs.config.totp_algorithm)
            )
        } else {
            let arr_defs = defs.split(",").collect::<Vec<&str>>();
            if arr_defs.len() > 1 {
                (   arr_defs[0].parse().unwrap_or(self.app_dbs.config.totp_digits),
                    arr_defs[1].to_owned()
                )
            } else {
                (   self.app_dbs.config.totp_digits,
                    format!("{}",self.app_dbs.config.totp_algorithm)
                )
            }
        };
        let algorithm = match str_algorithm.to_uppercase().as_str() {
            "SHA1" => Algorithm::SHA1,
            "SHA256" => Algorithm::SHA256,
            "SHA512" => Algorithm::SHA512,
            _ => Algorithm::SHA1,
        };
        let totp = match TOTP::new(
            algorithm,
            digits,
            1,
            30,
            secret,
            Some(format!("{}",self.app_dbs.config.name)),
            format!("{}",self.app_dbs.config.org)
        ) {
            Ok(totp) => totp,
            Err(e) =>
                return Err(std::io::Error::new(
                ErrorKind::NotFound,
                format!("ERROR: Invalid Topt {}",&e)
                ))
        };
        Ok(totp)
    }
    pub fn otp_generate(&self) -> Result<TOTP> {
        let mut rng = rand::thread_rng();
        let data_byte: [u8; 21] = rng.gen();
        let base32_string = base32::encode(base32::Alphabet::RFC4648 { padding: false }, &data_byte);
        match self.otp_make(&base32_string, "") {
            Ok(totp) => Ok(totp),
            Err(e) =>
                Err(std::io::Error::new(
                ErrorKind::NotFound,
                format!("ERROR: Invalid TOTP {}",&e)
                ))
        }
    }
    pub fn otp_check(&self, code: &str, token: &str, defs: &str) -> Result<bool> {
        let totp = match self.otp_make(code, defs) {
            Ok(totp) => totp,
            Err(e) =>
                return Err(std::io::Error::new(
                ErrorKind::NotFound,
                format!("ERROR: Invalid TOTP {}",&e)
                ))
        };
        match totp.check_current(&token) {
            Ok(result) => Ok(result),
            Err(e) => 
                return Err(std::io::Error::new(
                ErrorKind::NotFound,
                format!("ERROR: check {}",&e)
                ))
        }
    } 
    pub fn trace_req(&self, info: String) -> Result<()> {
        let timestamp = chrono::Utc::now().timestamp().to_string();
        let user_id = self.auth_state.user_id(); 
        let trace_content = TraceContent{
            when: timestamp.to_owned(),
            sid:  self.auth_state.id(),
            origin: self.uri.path().to_owned(),
            trigger: String::from("req_handler"),
            id: user_id.to_owned(),
            info: info.to_owned(),
            context: self.req_name.to_owned(),
            role: self.auth_state.user_roles(),
            req: self.req_header.req_info(),
        };
        let trace_data = TraceData{
            user_id,
            timestamp,
            contents: vec![trace_content]
        };
        trace_data.save(&self.app_dbs.config, false)
    }
    pub fn logs(&self, userid: &str, human: bool, reverse: bool) -> Vec<TraceLogContent> {
        let timestamp = chrono::Utc::now().timestamp().to_string();
        let trace_data = TraceData{
            user_id: format!("{}",userid),
            timestamp,
            contents: Vec::new()
        };
        trace_data.load(&self.app_dbs.config, human, reverse)
    }
}