use std::collections::HashMap;
use serde::{Serialize, Deserialize};
// use std::io::{Result, ErrorKind};
use std::error::Error;
//use std::str::FromStr;
type BoxResult<T> = Result<T,Box<dyn Error>>;
use url::Url;
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use openidconnect::{
	core::{
		CoreClient,
		CoreProviderMetadata,
		CoreResponseType,
	},
	reqwest::async_http_client, HttpRequest,
	AuthenticationFlow,
	ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce,
	RedirectUrl, Scope, PkceCodeChallenge, PkceCodeVerifier,
};

use axum::{http::method::Method, http::HeaderMap, http::header::CONTENT_TYPE};

use crate::defs::Config;

fn default_empty() -> String { String::from("") }
fn default_expire_in() -> u64 { 0 }
fn default_algorithm() -> String { String::from("RS256") }
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct OpenidClaims {
	pub name: String,
	pub owner: String,
	pub email: String,
	#[serde(default = "default_empty", rename(deserialize = "signupApplication"))]
	pub signup_application: String,
//	exp: i64,
}
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
pub struct OpenidTokens {
	# [serde(default = "default_empty")]
	pub access_token: String,
	# [serde(default = "default_empty")]
	pub id_token: String,
	# [serde(default = "default_empty")]
	pub refresh_token: String,
	# [serde(default = "default_empty")]
	pub token_type: String,
	# [serde(default = "default_expire_in")]
	pub expire_in: u64,
	# [serde(default = "default_empty")]
	pub scope: String,
}
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
pub struct OpenidData {
	#[serde(default = "default_empty")]
	pub code: String,
	#[serde(default = "default_empty")]
	pub state: String,
}

#[derive(Debug, Clone, Serialize, Deserialize,Default)]
pub struct OpenidConf {
	#[serde(default = "default_algorithm")]
	pub algorithm: String,
	pub access_token_url: String,
	pub disable_signature: bool,
	pub endpoint: String,
	pub client_id: String,
	pub client_secret: String,
	pub certificate: String,
	pub org_name: String,
	pub app_name: String,
	pub signup_application: String,
}

pub struct OpenidCli {
   pub client: CoreClient,
   pub pk_challenge: PkceCodeChallenge,
   pub pk_verifier: PkceCodeVerifier,
}
pub struct OpenidClient {
	pub url: Url,
	pub token: CsrfToken,
	pub nonce: Nonce,
}
/*
fn handle_error<T: std::error::Error>(fail: &T, msg: &'static str) {
	let mut err_msg = format!("ERROR: {}", msg);
	let mut cur_fail: Option<&dyn std::error::Error> = Some(fail);
	while let Some(cause) = cur_fail {
		err_msg += &format!("\n    caused by: {}", cause);
		cur_fail = cause.source();
	}
	println!("{}", err_msg);
	// exit(1);
}
*/
impl OpenidConf {
	pub async fn client(&self, return_url: String) -> BoxResult<OpenidCli> {
		let app_client_id = ClientId::new(self.client_id.to_owned());
		let app_client_secret = ClientSecret::new(self.client_secret.to_owned());
		let issuer_url = IssuerUrl::new(self.endpoint.to_owned())?;
		let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, async_http_client).await?;
		let redirect_url = RedirectUrl::new(return_url.to_owned())?;
		let (pk_challenge, pk_verifier) = PkceCodeChallenge::new_random_sha256();
		// Set up the config for the App OAuth2 process.
		Ok( OpenidCli {
			client: CoreClient::from_provider_metadata(
					provider_metadata,
					app_client_id,
		   		Some(app_client_secret),
				).set_redirect_uri(redirect_url),
			pk_challenge,
			pk_verifier,
			}
		)
	}
	pub async fn get_auth(&self, openid_cli: &OpenidCli) -> BoxResult<OpenidClient> {
		// Generate the authorization URL to which we'll redirect the user.
		//let pk_challenge = openid_cli.pk_challenge.clone();
		let (url, token, nonce) =
			openid_cli.client
				.authorize_url(
					AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
					CsrfToken::new_random,
					Nonce::new_random,
				)
				// This example is requesting access to the the user's profile including email.
				.add_scope(Scope::new("name".to_string()))
				.add_scope(Scope::new("email".to_string()))
				// Set the PKCE code challenge.
				// .set_pkce_challenge(pk_challenge)
				.url();
    	Ok(OpenidClient { url, token, nonce })
	}
	pub async fn get_token(&self, _openid_cli: &OpenidCli, auth_code: String, _str_nonce: String) -> BoxResult<OpenidClaims> {
		// let nonce = Nonce::new(str_nonce);
		// this is because PkceCodeVerifier des not have clone derive
		// let str_pk_verifier = serde_json::to_string(&openid_cli.pk_verifier).unwrap_or_default();
		// let pk_verifier: PkceCodeVerifier = serde_json::from_str(&str_pk_verifier)?;
		
		let mut header_map = HeaderMap::new();
		header_map.insert(CONTENT_TYPE, "application/json".parse()?);
		let mut body_data: HashMap<String, String> = HashMap::new();
		body_data.insert(String::from("grant_type"), String::from("authorization_code"));
		body_data.insert(String::from("client_id"), self.client_id.to_owned());
		body_data.insert(String::from("client_secret"), self.client_secret.to_owned());
		body_data.insert(String::from("code"), auth_code.to_owned());
		let str_body = serde_json::to_string(&body_data).unwrap_or_default();
		let body = str_body.as_bytes().to_vec();
		if self.access_token_url.is_empty() {
			return Err(Box::try_from(format!("Invalid openid access token url"))?);
		}
		let openid_request = HttpRequest{
			url: Url::parse(&self.access_token_url)?,
			method: Method::POST,
			headers: header_map,
			body
		};
		let openid_response = async_http_client(openid_request).await?;
		// dbg!(&openid_response.headers);
		let vec_to_string = String::from_utf8(openid_response.body)?;
		let  openid_tokens: OpenidTokens = serde_json::from_str(&vec_to_string).unwrap_or(OpenidTokens::default());
		let algorithm = match self.algorithm.as_str() {
			"RS256" => Algorithm::RS256,
			_ => Algorithm::RS256,
		};
		let mut validation = Validation::new(algorithm);
		if self.disable_signature {
			validation.insecure_disable_signature_validation();
		}
		let res = jsonwebtoken::decode::<OpenidClaims>(
			&openid_tokens.id_token,
	   &DecodingKey::from_secret(&[]),
			&validation
		)?;
		Ok(res.claims)
	}
}
pub async fn collect_openid_clients(config: Config, return_url: &str) ->  HashMap<String,OpenidCli> {
	let mut clients = HashMap::new();
	for (key, openid_conf) in config.openid_auths.clone().into_iter() {
	   match openid_conf.client(return_url.to_owned()).await {
		   Ok(cli) => {
			   clients.insert(key.to_owned(),cli);
			   println!("OpenID {} added", key)
		   },
		   Err(e) => println!("Openid {} error: {} ",key, e),
	   }
	}
	clients
}