/* © SRSoftware 2025 */
package de.srsoftware.umbrella.user;

import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
import static de.srsoftware.tools.Optionals.*;
import static de.srsoftware.tools.Strings.uuid;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.postBox;
import static de.srsoftware.umbrella.core.ResponseCode.*;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR;
import static de.srsoftware.umbrella.core.Util.*;
import static de.srsoftware.umbrella.core.constants.Constants.CONFIG_SESSION_DURATION;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Field.PASSWORD;
import static de.srsoftware.umbrella.core.constants.Field.REDIRECT;
import static de.srsoftware.umbrella.core.constants.Field.STATE;
import static de.srsoftware.umbrella.core.constants.Field.USER;
import static de.srsoftware.umbrella.core.constants.Path.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.core.model.Translatable.t;
import static de.srsoftware.umbrella.user.Constants.*;
import static de.srsoftware.umbrella.user.Paths.*;
import static de.srsoftware.umbrella.user.Paths.IMPERSONATE;
import static de.srsoftware.umbrella.user.model.DbUser.Permission;
import static de.srsoftware.umbrella.user.model.DbUser.Permission.*;
import static java.lang.System.Logger.Level.*;
import static java.net.HttpURLConnection.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.text.MessageFormat.format;
import static java.time.temporal.ChronoUnit.DAYS;
import static java.util.Optional.empty;

import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.UserService;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.constants.Path;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.user.api.LoginServiceDb;
import de.srsoftware.umbrella.user.api.UserDb;
import de.srsoftware.umbrella.user.model.*;
import de.srsoftware.umbrella.user.model.DbUser;
import de.srsoftware.umbrella.user.model.Password;
import de.srsoftware.umbrella.user.sqlite.SqliteDB;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.*;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.*;
import org.jose4j.jwk.HttpsJwks;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
import org.json.JSONObject;


public class UserModule extends BaseHandler implements UserService {


	private record State(LoginService loginService, JSONObject config){

		public static State of(LoginService loginService, JSONObject config) {
			return new State(loginService,config);
		}
	}

	private static final BadHasher BAD_HASHER;
	private static final System.Logger LOG = System.getLogger("User");
	private final UserDb users;
	private final LoginServiceDb logins;
	private final HashMap<String, State> stateMap = new HashMap<>(); // map from state to OIDC provider name
	private final HashMap<String,String> tokenMap = new HashMap<>();

	static {
		try {
			BAD_HASHER = new BadHasher();
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
	}

	public UserModule(Configuration config) throws UmbrellaException {
		super();
		var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingConfig(CONFIG_DATABASE));
		var sqlite    = new SqliteDB(connect(dbFile));

		// may be splitted in separate db files later
		logins        = sqlite;
		users         = sqlite;

		Optional<Number> sessionDuration = config.get(CONFIG_SESSION_DURATION);
		sessionDuration.ifPresent(users::setSessionDuration);
		ModuleRegistry.add(this);
	}

	private boolean deleteOIDC(HttpExchange ex, UmbrellaUser user, de.srsoftware.tools.Path path) throws IOException {
		var head = path.pop();
		try {
			return switch (head) {
				case CONNECTED -> deleteServiceConnection(ex, user);
				case null -> super.doGet(path, ex);
				default -> deleteService(ex, user, head);
			};
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	private boolean deleteService(HttpExchange ex, UmbrellaUser user, String serviceName) throws IOException {
		if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return unauthorized(ex);
		try {
			logins.delete(serviceName);
			return ok(ex);
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	private boolean deleteServiceConnection(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
		if (user == null) return sendContent(ex,HTTP_SERVER_ERROR,"Expected user object to be of type DbUser");
		JSONObject json;
		try {
			json = json(ex);
		} catch (Exception e){
			LOG.log(WARNING,"Request does not contain valid JSON",e);
			return sendContent(ex,HTTP_FAILED_DEPENDENCY,"Request does not contain valid JSON");
		}
		if (!(json.has(FOREIGN_ID) && json.get(FOREIGN_ID) instanceof String foreignId && !foreignId.isBlank())) throw missingField(FOREIGN_ID);
		if (!(json.has(SERVICE_ID) && json.get(SERVICE_ID) instanceof String serviceId && !serviceId.isBlank())) throw missingField(SERVICE_ID);


		try {
			logins.unlink(ForeignLogin.of(serviceId,foreignId,user.id()));
			return ok(ex);
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	@Override
	public Map<Long, UmbrellaUser> list(Integer start, Integer limit, Collection<Long> ids) throws UmbrellaException {
		return users.list(start, limit, ids);
	}

	public UmbrellaUser load(Session session) throws UmbrellaException{
		return users.load(session);
	}

	public Optional<UmbrellaUser> load(EmailAddress email){
		try {
			return Optional.of(users.load(email));
		} catch (UmbrellaException ignored){
			return empty();
		}
	}

	@Override
	public UmbrellaUser loadUser(long userId) throws UmbrellaException {
		return users.load(userId);
	}

	public Optional<UmbrellaUser> loadUser(Optional<Token> sessionToken) throws UmbrellaException {
		try {
			if (sessionToken.isEmpty()) return empty();
			var session = load(sessionToken.get());
			return Optional.of(load(session));
		} catch (UmbrellaException e) {
			return empty();
		}
	}

	public Optional<UmbrellaUser> loadUser(HttpExchange ex) throws UmbrellaException {
		return loadUser(SessionToken.from(ex).map(Token::of));
	}

	@Override
	public boolean doDelete(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		try {
			var user = loadUser(ex);
			addCors(ex);
			var head = path.pop();
			if (OIDC.equals(head)) return user.isEmpty() ? unauthorized(ex) : deleteOIDC(ex, user.get(), path);
		} catch (UmbrellaException e){
			return send(ex,e);
		}
		return super.doDelete(path, ex);
	}

	@Override
	public boolean doGet(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		var sessionToken = SessionToken.from(ex).map(Token::of);
		try {
			var user = loadUser(sessionToken);
			addCors(ex);
			var head = path.pop();
			switch (head) {
				case LIST: return user.isEmpty() ? unauthorized(ex) : getUserList(ex, user.get());
				case LOGOUT: return logout(ex, sessionToken);
				case OIDC: return getOIDC(ex,user.orElse(null),path);
				case VALIDATE_OTP: return validateOtpToken(ex,path.pop());
				case WHOAMI: return user.isEmpty() ? unauthorized(ex) : sendContent(ex,user.get());

			};
			long userId = Long.parseLong(head);
			if (user.isEmpty()) return unauthorized(ex);
			if (!(user.get() instanceof DbUser dbUser)) return unauthorized(ex);
			if (!(dbUser.id() == userId || dbUser.permissions().contains(LIST_USERS))) throw forbidden("You are not allowed to access that user!");
			return sendContent(ex,loadUser(userId));
		} catch (UmbrellaException e) {
			return send(ex,e);
		} catch (NumberFormatException ignored) {}
		return super.doGet(path, ex);

	}

	public boolean doPatch(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		addCors(ex);
		String head = null;
		try {
			var requestingUser = loadUser(ex);
			head = path.pop();
			long userId;
			if (head == null || head.isBlank()) return sendContent(ex, HTTP_UNPROCESSABLE,"User id missing!");
			if (requestingUser.isEmpty())       return unauthorized(ex);
			if (Path.PASSWORD.equals(head))          return patchPassword(ex,requestingUser.get());
			if (OIDC.equals(head))              return patchService(ex,path.pop(),requestingUser.get());
			userId = Long.parseLong(head);
			DbUser editedUser = (DbUser) loadUser(userId);

			if (!(requestingUser.get() instanceof DbUser dbUser) || !(dbUser.id() == userId || dbUser.permissions().contains(UPDATE_USERS))) throw forbidden("You are not allowed to update user "+editedUser.name());

			JSONObject json;
			try {
				json = json(ex);
			} catch (Exception e){
				LOG.log(WARNING,"Request does not contain valid JSON",e);
				return sendContent(ex,HTTP_BAD_REQUEST,"Body contains no JSON data");
			}
			return update(ex, editedUser,json);

		} catch (NumberFormatException e) {
			return sendContent(ex, HTTP_UNPROCESSABLE,"Invalid user id: "+head);
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	@Override
	public boolean doPost(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		addCors(ex);
		var head = path.pop();
		Long targetId = null;
		try {
			targetId = Long.parseLong(head);
			head = path.pop();
		} catch (NumberFormatException ignored) {}
		try {
			return switch (head) {
				case CREATE -> postCreate(ex);
				case OIDC -> postOIDC(ex, path);
				case IMPERSONATE -> impersonate(ex, targetId);
				case Path.LOGIN -> postLogin(ex);
				case SEARCH -> postSearch(ex);
				case RESET_PW -> postResetPassword(ex);
				case null, default -> super.doPost(path,ex);
			};
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	@Override
	public void dropSession(Token token) throws UmbrellaException{
		users.dropSession(token);
	}


	private boolean exchangeToken(HttpExchange ex) throws IOException {
		JSONObject params;
		try {
			params = json(ex);
		} catch (Exception e) {
			LOG.log(WARNING, "Request does not seem to contain JSON data!", e);
			return sendContent(ex,HTTP_BAD_REQUEST,"Request does not seem to contain JSON data!");
		}
		if (!params.has(Field.CODE)) return sendContent(ex,HTTP_BAD_REQUEST,"missing auth code");
		if (!params.has(STATE)) return sendContent(ex,HTTP_BAD_REQUEST,"no state submitted");
		var code = params.getString(Field.CODE);
		var state = stateMap.remove(params.getString(STATE));
		if (state == null) return sendContent(ex,HTTP_BAD_REQUEST,"no state submitted");
		var redirect = url(ex).replaceAll("/api/.*","/oidc_callback");
		var location = state.config.getString(TOKEN_ENDPOINT);

		try {
			var data = Map.of(GRANT_TYPE,AUTHORIZATION_CODE, Field.CODE,code,REDIRECT_URI,redirect,CLIENT_ID,state.loginService.clientId(),CLIENT_SECRET,state.loginService.clientSecret());
			var resp = request(location,data,MIME_FORM_URL,state.loginService.basicAuth());
			if (!(resp instanceof JSONObject json)) return sendContent(ex,HTTP_BAD_REQUEST,format("{0} did not return JSON!",location));
			if (!json.has(ID_TOKEN)) return sendContent(ex,HTTP_FAILED_DEPENDENCY,"Missing ID token – token exchange failed!");
			var idToken = json.getString(ID_TOKEN);
			var oidcUserId = verifyAndGetUserId(idToken, state);
			var optUser = loadUser(ex);
			if (optUser.isPresent()){ // user already logged in – this is the case when the new id gets assigned
				var currentUser = optUser.get();
				var assignment = new ForeignLogin(state.loginService.name(),oidcUserId,currentUser.id());
				logins.save(assignment);
			}
			var user = loadUser(logins.getUserId(state.loginService.name(), oidcUserId));
			var session = users.getSession(user);
			var returnTo = "/user";
			if (state.config.has("returnTo")) returnTo = state.config.getString("returnTo");
			session.cookie().addTo(ex);
			return sendContent(ex,Map.of(REDIRECT,returnTo));
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	public Session extend(Session session) throws UmbrellaException{
		return users.extend(session);
	}


	private boolean getConnectedServices(HttpExchange ex, UmbrellaUser user) throws IOException {
		if (user == null) return unauthorized(ex);
		try {
			var connections = logins.listAssignments(user.id()).stream().map(ForeignLogin::toMap);
			return sendContent(ex,connections);
		} catch (UmbrellaException e) {
			return send(ex,e);
		}

	}

	private boolean getOIDC(HttpExchange ex, UmbrellaUser user, de.srsoftware.tools.Path path) throws IOException, UmbrellaException {
		var head = path.pop();
		return switch (head){
			case BUTTONS -> getOidcButtons(ex);
			case LIST -> getServiceList(ex,user);
			case CONNECTED -> getConnectedServices(ex,user);
			case Path.REDIRECT -> getOidcRedirect(ex,path.pop());
			case null -> super.doGet(path,ex);
			default -> getOIDC(ex,user,head);
		};
	}

	private boolean getOIDC(HttpExchange ex, UmbrellaUser user, String serviceId) throws IOException, UmbrellaException {
		if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) throw forbidden("You are not allowed to manage that service!");
		return sendContent(ex,logins.loadLoginService(serviceId).toMap());
	}

	private JSONObject getOidcConfig(LoginService service) throws UmbrellaException {
		URL url = null;
		try {
			url = new URI(service.url()+"/.well-known/openid-configuration").toURL();
			var is = open(url).getInputStream();
			var bos = new ByteArrayOutputStream();
			is.transferTo(bos);
			is.close();
			return new JSONObject(bos.toString(UTF_8));
		} catch (MalformedURLException | URISyntaxException e) {
			throw new UmbrellaException(500,"Failed to create URL from \"{0}\"!",service.url()).causedBy(e);
		} catch (IOException e) {
			throw new UmbrellaException(500,"Failed to read from \"{0}\"!",url).causedBy(e);
		}
	}

	private boolean getOidcRedirect(HttpExchange ex, String name) throws IOException {
		try {
			var loginService = logins.loadLoginService(name);
			var config = getOidcConfig(loginService);
			if (!config.has(AUTH_ENDPOINT)) {
				LOG.log(WARNING, "OpenID-Configuration does not declare an {0}!", AUTH_ENDPOINT);
				throw new UmbrellaException(424, "No authorization endpoint configured for {0}", loginService.name());
			}
			var returnTo = queryParam(ex).get("returnTo");
			if (isSet(returnTo)) config.put("returnTo",returnTo);
			var url = url(ex);
			var callback = url.replaceAll("/api/.*",  "/oidc_callback"); // TODO: frontendPath an zweiter stelle
			var authEndpoint = config.getString(AUTH_ENDPOINT);
			var clientId = loginService.clientId();
			var state = UUID.randomUUID().toString();
			stateMap.put(state, State.of(loginService, config));
			return sendContent(ex,Map.of(OIDC_CALLBACK, callback, AUTH_ENDPOINT, authEndpoint, SCOPE, OIDC_SCOPE, CLIENT_ID, clientId, RESPONSE_TYPE, Field.CODE, STATE, state));
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	private boolean getOidcButtons(HttpExchange ex) throws IOException {
		try {
			var services = logins.listLoginServices().stream().map(LoginService::name);
			return sendContent(ex,services);
		} catch (UmbrellaException e) {
			return send(ex,e);
		} catch (IOException e) {
			return sendContent(ex,HTTP_SERVER_ERROR,e.getMessage());
		}
	}

	private boolean getServiceList(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
		if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) throw forbidden("You are not allowed to manage that service!");
		var services = logins.listLoginServices().stream().map(LoginService::toMap);
		return sendContent(ex,services);
	}

	private boolean getUserList(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
		if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(LIST_USERS))) throw forbidden("You are not allowed to list users!");
		return sendContent(ex,users.list(0, null,null).values().stream().map(UmbrellaUser::toMap));
	}

	private boolean impersonate(HttpExchange ex, Long targetId) throws IOException, UmbrellaException {
		var requestingUser = loadUser(ex);
		if (!(requestingUser.isPresent() && requestingUser.get() instanceof DbUser dbUser)) return unauthorized(ex);
		if (!dbUser.permissions().contains(Permission.IMPERSONATE)) throw forbidden("You are not allowed to impersonate other users!");
		if (targetId == null) return sendContent(ex,HTTP_UNPROCESSABLE,"user id missing");
		var targetUser = loadUser(targetId);
		users.getSession(targetUser).cookie().addTo(ex);
		return sendContent(ex,targetUser.toMap());
	}

	public Session load(Token token) throws UmbrellaException{
		return  users.load(token);
	}


	public boolean logout(HttpExchange ex, Optional<Token> optToken) throws IOException {
		if (optToken.isPresent()){
			var token = optToken.get();
			try {
				users.dropSession(token);
			} catch (UmbrellaException ignored){

			}
			new SessionToken(token.toString(),"/", Instant.now().minus(1, DAYS),true).addTo(ex);
			return ok(ex);
		}
		return unauthorized(ex);
	}


	private boolean postOIDC(HttpExchange ex, de.srsoftware.tools.Path path) throws IOException {
		return switch (path.pop()){
			case Path.TOKEN -> exchangeToken(ex);
			case null, default -> super.doPost(path,ex);
		};
	}

	private boolean patchPassword(HttpExchange ex, UmbrellaUser requestingUser) throws IOException {
		if (!(requestingUser instanceof DbUser user)) return sendContent(ex, HTTP_SERVER_ERROR,"DbUser expected");
		JSONObject json;
		try {
			json = json(ex);
		} catch (Exception e){
			LOG.log(WARNING,"Request does not contain valid JSON",e);
			return sendContent(ex,HTTP_BAD_REQUEST,"Body contains no JSON data");
		}
		if (!json.has("old") || !(json.get("old") instanceof String oldpass) || oldpass.isBlank()) return sendContent(ex, HTTP_UNPROCESSABLE,"old password missing!");
		if (!json.has("new") || !(json.get("new") instanceof String newpass) || newpass.isBlank()) return sendContent(ex, HTTP_UNPROCESSABLE,"new password missing!");
		var old = Password.of(BAD_HASHER.hash(oldpass,null));
		if (!user.hashedPassword().equals(old)) return sendContent(ex,HTTP_UNAUTHORIZED,"Wrong password (old)");
		if (weak(newpass)) return sendContent(ex,HTTP_BAD_REQUEST,"New password too weak!");
		var pass = Password.of(BAD_HASHER.hash(newpass,null));
		try {
			var updated = users.save(new DbUser(user.id(), user.name(), user.email(), pass, user.theme(), user.language(), user.permissions(), null));
			return sendContent(ex, updated);
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	private boolean patchService(HttpExchange ex, String serviceName, UmbrellaUser requestingUser) throws IOException, UmbrellaException {
		if (!(requestingUser instanceof DbUser user && user.permissions().contains(MANAGE_LOGIN_SERVICES))) throw forbidden("You are not allowed to manage that service!");
		var json = json(ex);
		if (!json.has(NAME)          ||          !(json.get(NAME) instanceof String name)     || name.isBlank())     throw missingField(NAME);
		if (!json.has(URL)           ||           !(json.get(URL) instanceof String url)      || url.isBlank())      throw missingField(URL);
		if (!json.has(CLIENT_ID)     ||     !(json.get(CLIENT_ID) instanceof String clientId) || clientId.isBlank()) throw missingField(CLIENT_ID);
		if (!json.has(CLIENT_SECRET) || !(json.get(CLIENT_SECRET) instanceof String secret)   || secret.isBlank())   throw missingField(CLIENT_SECRET);
		var service = logins.save(new LoginService(name,url,clientId,secret, DEFAULT_FIELD));
		return sendContent(ex,service.toMap());
	}

	private boolean postCreate(HttpExchange ex) throws IOException, UmbrellaException {
		var optUser = loadUser(ex);
		if (!(optUser.isPresent() && optUser.get() instanceof DbUser dbUser)) return unauthorized(ex);
		if (!dbUser.permissions().contains(Permission.CREATE_USERS)) throw forbidden("You are not allowed to create new users!");
		var json = json(ex);

		if (json.has(USER)) json = json.getJSONObject(USER);
		var name = json.has(NAME) ? json.getString(NAME) : null;
		var email = json.has(EMAIL) ? new EmailAddress(json.getString(EMAIL)) : null;
		var theme = json.has(THEME) ? json.getString(THEME) : null;
		var lang = json.has(LANGUAGE) ? json.getString(LANGUAGE) : null;
		var pass = json.has(PASSWORD) ? json.getString(PASSWORD) : null;
		var hashedPass = Password.of(BAD_HASHER.hash(pass,null));
		var newUser = new DbUser(0, name, email, hashedPass, theme, lang, Set.of(), null);

		var user = users.save(newUser);
		return sendContent(ex,HTTP_OK,user);
	}

	private boolean postResetPassword(HttpExchange ex) throws IOException {
		try {
			var email = body(ex);
			var addr = new EmailAddress(email);
			var user = users.load(addr);
			var token = uuid();
			var oldToken = tokenMap.get(email);
			if (oldToken != null) tokenMap.remove(oldToken);
			tokenMap.put(token,email);
			tokenMap.put(email,token);
			var url = url(ex).replace("/api/user/reset_pw","/user/reset/pw")+"?token="+token;
			var subject = t("Your token to create a new password");
			var content = t("To receive a new password, open the following link: {url}",URL,url);
			var message = new TranslatableMessage(user,subject,content,null);
			var envelope = new Envelope<>(0, message, user);
			postBox().send(envelope);
		} catch (UmbrellaException e){
			return send(ex,e);
		}
		return ok(ex);
	}

	private boolean postLogin(HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!(json.has(USERNAME) && json.get(USERNAME) instanceof String username)) return sendContent(ex, HTTP_UNPROCESSABLE,"Username missing");
		var optLock = Lock.get(username);
		if (optLock.isPresent()) {
			var lock = optLock.get();
			LOG.log(WARNING, "{0} is locked after {1} failed logins. Lock will be released at {2}", username, lock.attempts(), lock.releaseTime());
			return sendContent(ex,HTTP_UNAUTHORIZED,lock);
		}

		if (!(json.has(PASSWORD) && json.get(PASSWORD) instanceof String password)) return sendContent(ex, HTTP_UNPROCESSABLE,"Password missing");
		if (password.isBlank()) return sendContent(ex,HTTP_UNAUTHORIZED,"Password must not be blank");
		var hashedPass = Password.of(BAD_HASHER.hash(password,null));
		try {
			var user = users.load(username, hashedPass);
			Lock.unlock(username);
			users.getSession(user).cookie().addTo(ex);
			return sendContent(ex,user);
		} catch (UmbrellaException e){
			if (e.statusCode() == HTTP_UNAUTHORIZED) return sendContent(ex,HTTP_UNAUTHORIZED,Lock.lock(username));
			return send(ex,e);
		}
	}

	private boolean postSearch(HttpExchange ex) throws IOException {
		var requestingUser = loadUser(ex);
		if (!(requestingUser.isPresent() && requestingUser.get() instanceof DbUser dbUser)) return unauthorized(ex);
		var key = body(ex);
		return sendContent(ex,mapValues(search(key)));
	}

	@Override
	public Optional<UmbrellaUser> refreshSession(HttpExchange ex) {
		Optional<Token> optToken = SessionToken.from(ex).map(Token::of);
		var user = loadUser(optToken);
		user.map(users::getSession).map(users::extend).map(Session::cookie).ifPresent(token -> token.addTo(ex));
		return user;
	}

	static int score(String password){
		if (password == null) return 0;
		var score = 0;
		for (int i=0; i<password.length(); i++){
			int c = password.charAt(i);
			score += Character.isDigit(c) || Character.isLetter(c) ? 1 : 3;
		}
		return score;
	}

	@Override
	public Map<Long, ? extends UmbrellaUser> search(String key) {
		return users.search(key);
	}

	private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException {
		var id    = user.id();
		var name  = json.has(NAME)     && json.get(NAME)     instanceof String s && !s.isBlank() ? s : user.name();
		var email = json.has(EMAIL)    && json.get(EMAIL)    instanceof String e && !e.isBlank() ? new EmailAddress(e) : user.email();
		var pass  = json.has(PASSWORD) && json.get(PASSWORD) instanceof String p && !p.isBlank() ? Password.of(BAD_HASHER.hash(p,null)) : user.hashedPassword();
		var theme = json.has(THEME)    && json.get(THEME)    instanceof String t && !t.isBlank() ? t : user.theme();
		var lang  = json.has(LANGUAGE) && json.get(LANGUAGE) instanceof String l && !l.isBlank() ? l : user.language();
		var saved = users.save(new DbUser(id,name,email,pass,theme,lang, user.permissions(),null));
		return sendContent(ex,HTTP_OK,saved);
	}

	private boolean validateOtpToken(HttpExchange ex, String token) throws IOException {
		if (token == null) return sendContent(ex,HTTP_UNPROCESSABLE,"No token provided!");
		var email = tokenMap.get(token);
		tokenMap.remove(token);
		if (email == null) return sendContent(ex,HTTP_UNAUTHORIZED,"Unknown token!");
		try {
			var user = users.load(new EmailAddress(email));
			users.getSession(user).cookie().addTo(ex);
			return sendContent(ex,user);
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}


	private String verifyAndGetUserId(String jwt, State state) throws UmbrellaException {
		var jwksEndpoint = state.config.getString(JWKS_ENDPOINT);
		var audience = state.loginService.clientId();
		var httpJwks = new HttpsJwks(jwksEndpoint);
		var resolver = new HttpsJwksVerificationKeyResolver(httpJwks);
		JwtConsumer consumer = new JwtConsumerBuilder()
				.setVerificationKeyResolver(resolver)
				.setExpectedAudience(audience)
				.build();
		try {
			var claims = consumer.processToClaims(jwt);
			return claims.getSubject();
		} catch (InvalidJwtException | MalformedClaimException e) {
			throw new UmbrellaException(500,"Failed to verify JWT!").causedBy(e);
		}
	}

	private static boolean weak(String password){
		return score(password) < 14;
	};
}
