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

import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
import static de.srsoftware.tools.Query.decode;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.Util.request;
import static de.srsoftware.umbrella.core.constants.Constants.HOME;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Field.REDIRECT;
import static de.srsoftware.umbrella.core.constants.Field.TOKEN;
import static de.srsoftware.umbrella.core.constants.Path.*;
import static de.srsoftware.umbrella.user.Paths.*;
import static java.lang.Long.parseLong;
import static java.lang.System.Logger.Level.*;
import static java.lang.System.Logger.Level.DEBUG;
import static java.net.HttpURLConnection.*;
import static java.time.temporal.ChronoUnit.DAYS;

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.constants.Path;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Session;
import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.io.IOException;
import java.time.Instant;
import java.util.*;
import org.json.JSONObject;

public class UserLegacy extends BaseHandler {

	private final Configuration config;
	private final String messageUrl;

	public UserLegacy(Configuration config)  {
		super();
		this.config = config.subset("umbrella.modules").orElseThrow(() -> new RuntimeException("Missing configuration: umbrella.modules"));
		this.messageUrl = null;
	}

	@Override
	public boolean doGet(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		var head = path.pop();
		return switch (head){
			case null -> sendRedirect(ex, url(ex).replaceAll("/legacy/.*",""));
			case COMMON_TEMPLATES -> {
				allowOrigin(ex, "*"); // add CORS header
				yield load(path,ex);
			}
			case Path.LOGIN -> getLogin(ex);
			case LOGOUT-> logout(ex);
			case MODULES -> getModules(ex);
			case SEARCH -> sendRedirect(ex,url(ex).replaceAll("/legacy/","/"));
			default -> super.doGet(path,ex);
		};
	}

	@Override
	public boolean doPost(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		try {
			return switch (path.pop()) {
				case JSON -> jsonUser(ex);
				case NOTIFY -> legacyNotify(ex);
				case SEARCH -> postSearch(ex);
				case VALIDATE_TOKEN -> validateToken(ex);
				default -> super.doPost(path,ex);
			};
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	private boolean getLogin(HttpExchange ex) throws IOException {
		var returnTo = queryParam(ex).get("returnTo");
		if (returnTo instanceof String url) {
			var token = SessionToken.from(ex).map(SessionToken::sessionId)
					.or(() -> getBearer(ex))
					.map(Token::of);
			if (token.isEmpty()) return sendRedirect(ex,"/user/login?returnTo="+url);
			return sendRedirect(ex, url + (url.contains("?") ? "&" : "?") + "token=" + token.get());
		}

		var location = url(ex);
		location = location.replaceAll("/api/.*","/login");
		if (returnTo != null) location += "?returnTo="+returnTo;
		return sendRedirect(ex,location);
	}

	public boolean getModules(HttpExchange ex) throws IOException {
		var optToken = SessionToken.from(ex).map(Token::of);
		if (optToken.isEmpty()) return unauthorized(ex);
		var list = new ArrayList<Map<String,String>>();
		for (var module : config.keys()){
			Optional<String> url = config.get(module+".baseUrl");
			url.ifPresent(u -> list.add(Map.of("module",module,URL,u)));
		}
		return sendContent(addCors(ex),list);
	}

	private boolean jsonUser(HttpExchange ex) throws UmbrellaException, IOException {
			Map<String, Object> data = null;
		try {
			data = decode(body(ex));
		} catch (IOException e) {
			LOG.log(ERROR,"Failed to extract body of request");
			throw new UmbrellaException(400,"Failed to extract body of request").causedBy(e);
		}
		var arrayPassed = false;
		var ids = new ArrayList<Long>();
		for (var entry : data.entrySet()){
			var key = entry.getKey();
			var val = entry.getValue();
			if (key.startsWith("ids[") && (arrayPassed = true)) ids.add(parseLong(entry.getValue().toString()));
			if ("ids".equals(key) && val instanceof Map<?,?> idMap) {
				for (var o : idMap.values()){
					ids.add(Long.parseLong(o.toString()));
				}
				arrayPassed = true;
			}
		}
		if (ids.isEmpty() && data.get("id") instanceof String idString) ids.add(parseLong(idString));
		var related = "true".equals(data.get("related"));
		if (related) {
			LOG.log(WARNING,"Fetching related users not implemented, yet!");
			throw new UmbrellaException(400,"Fetching related users not implemented, yet!");
		}

		Map<Long, UmbrellaUser> userMap = userService().list(0, null, ids);
		if (arrayPassed || userMap.size() != 1) {
			var userData = new HashMap<Long, Map<String, Object>>();
			for (var entry : userMap.entrySet()) userData.put(entry.getKey(),entry.getValue().toMap());
			return sendContent(ex,userData);
		}
		for (var entry : userMap.entrySet()){
			return sendContent(ex,entry.getValue());
		}
		throw UmbrellaException.notFound("Failed to load user(s) for id(s): {id}", ID,ids);
	}

	private boolean legacyNotify(HttpExchange ex) throws UmbrellaException, IOException {
		if (messageUrl == null) throw new UmbrellaException(500,"missing configuration: umbrella.modules.message.baseUrl");
		var mime = contentType(ex).orElse(null);
		JSONObject data;
		try {
			data = switch (mime) {
				case MIME_FORM_URL -> new JSONObject(decode(body(ex)));
				case null, default -> json(ex);
			};
		} catch (Exception e){
			throw new UmbrellaException(400,"Failed to fetch content of request").causedBy(e);
		}
		if (!data.has(TOKEN)) throw new UmbrellaException(400,"token missing in request data!");
		var dropKeys = new HashSet<String>();
		dropKeys.add(TOKEN);
		var token = Token.of(data.getString(TOKEN));
		var userSession = requestSession(token);
		var user = userSession.user();
		LOG.log(DEBUG,"Token belongs to {0}.",user);

		// recipients is a field used by legacy code.
		// it may be a single user id or a list of user ids.
		var recipients = new HashSet<Long>();
		for (var key : data.keySet()){
			if (key.startsWith("recipients[")) {
				recipients.add(data.getLong(key));
				dropKeys.add(key);
				break;
			}
			if (key.equals("recipients")){
				var list = data.getJSONObject(key);
				for (var idx : list.keySet()){
					var id = list.getLong(idx);
					recipients.add(id);
				}
				dropKeys.add(key);
				break;
			}
		}
		if (!recipients.isEmpty()){ // replace legacy user ids by user objects in receivers field
			Map<Long, UmbrellaUser> resp = userService().list(0, null, recipients);
			data.put("receivers",resp.values().stream().map(UmbrellaUser::toMap).toList());
		}

		if (!data.has(SENDER)) data.put(SENDER,user.toMap());

		dropKeys.forEach(data::remove);

		LOG.log(DEBUG,"received legacy notification: {0}",data);

		var messageList = Map.of("messages",List.of(data.toMap()));
		var resp = request(messageUrl, messageList, null, token.asBearer());
		if (!(resp instanceof JSONObject json)) throw new UmbrellaException(500,"{0} did not return JSON!",messageUrl);

		// TODO: should we return json?
		return ok(ex);
	}

	private boolean logout(HttpExchange ex) throws IOException {
		var returnTo = queryParam(ex).get("returnTo");
		var optToken = SessionToken.from(ex).map(Token::of);
		if (optToken.isPresent()) try{
			var token = optToken.get();
			userService().dropSession(token);
			var expiredToken = new SessionToken(token.toString(),"/", Instant.now().minus(1, DAYS),true);
			expiredToken.addTo(ex);
			if (returnTo instanceof String location) return sendRedirect(ex,location);
			return sendContent(ex, Map.of(REDIRECT,HOME));
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
		if (returnTo instanceof String location) return sendRedirect(ex,location);
		return sendContent(ex,Map.of(REDIRECT,HOME));
	}

	private boolean postSearch(HttpExchange ex) throws IOException, UmbrellaException {
		var optToken = SessionToken.from(ex).map(Token::of);
		if (optToken.isEmpty()) return unauthorized(ex);
		var token = optToken.get();
		var json  = json(ex);
		if (!(json.has(KEY) && json.get(KEY) instanceof String key) || key.isBlank()) return sendContent(ex,HTTP_BAD_REQUEST,"No search key given");
		var fulltext = key.contains("+") || (json.has("fulltext") && "on".equals(json.get("fulltext")));
		StringBuilder searchResult = new StringBuilder();
		if (fulltext){
			for (var module : config.keys()){
				var baseUrl = config.get(module + ".baseUrl");
				if (baseUrl.isEmpty()) continue;

				var res = request(baseUrl.get()+"/search",token.asMap().plus(KEY,key),MIME_FORM_URL,token.asBearer());
				if (!(res instanceof String content) || content.isBlank()) continue;
				searchResult.append("<fieldset><label>").append(module).append("</label>");
				searchResult.append(content).append("</fieldset>\n");
			}
		} else {
			var bookmark = config.get("bookmark.baseUrl");
			if (bookmark.isEmpty()) throw new UmbrellaException(500,"Tag search not available: Bookmark module not configured!");
			var res = request(bookmark.get()+"/search",token.asMap().plus(KEY,key),MIME_FORM_URL,null);
			if (!(res instanceof String content)) throw new UmbrellaException(500,"Search did not return html content!");
			searchResult.append(content);
		}
		addCors(ex);
		return sendContent(ex,searchResult.toString());
	};

	protected Session requestSession(Token token) throws UmbrellaException {
		var session = userService().load(token);
		session = userService().extend(session);
		return session;
	}

	private static String stripTrailingSlash(Object o){
		String url = o.toString();
		if (url.endsWith("/")) return url.substring(0,url.length()-1);
		return url;
	}

	private boolean validateToken(HttpExchange ex) throws UmbrellaException, IOException {
		String body;
		try {
			body = body(ex);
		} catch (Exception e){
			throw new UmbrellaException(400,"Failed to read request body").causedBy(e);
		}
		var map = decode(body);
		LOG.log(DEBUG,"validateToken(…, {0}), data: {1}",map);

		String domain = stripTrailingSlash(map.get(DOMAIN) instanceof String s ? s : "");
		var keys = config.keys();
		var match = false;
		for (var key : keys){
			var baseUrl = config.get(key + ".baseUrl").map(UserLegacy::stripTrailingSlash).orElse(null);
			if (domain.equals(baseUrl)){
				match = true;
				break;
			}
		}
		if (!match) throw new UmbrellaException(500,"Failed to verify request domain!");

		var o = map.get(TOKEN);
		if (!(o instanceof String token)) throw new UmbrellaException(500,"Request did not contain token!");
		var session = userService().load(Token.of(token));
		var user = userService().load(session);
		var userMap = user.toMap();
		userMap.put(TOKEN,Map.of(TOKEN,token, EXPIRATION,session.expiration().getEpochSecond()));
		return sendContent(ex,userMap);
	}
}
