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

import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.*;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Field.PERMISSION;
import static de.srsoftware.umbrella.core.constants.Field.SETTINGS;
import static de.srsoftware.umbrella.core.constants.Field.SOURCE;
import static de.srsoftware.umbrella.core.constants.Field.TAGS;
import static de.srsoftware.umbrella.core.constants.Module.PROJECT;
import static de.srsoftware.umbrella.core.constants.Path.*;
import static de.srsoftware.umbrella.core.constants.Text.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.core.model.Permission.*;
import static de.srsoftware.umbrella.core.model.Permission.OWNER;
import static de.srsoftware.umbrella.core.model.Status.OPEN;
import static de.srsoftware.umbrella.core.model.Status.PREDEFINED;
import static de.srsoftware.umbrella.core.model.Translatable.t;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE;
import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
import static java.lang.Boolean.TRUE;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;

import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.*;
import de.srsoftware.umbrella.core.api.ProjectService;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.constants.Path;
import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.messagebus.events.ProjectEvent;
import java.io.IOException;
import java.util.*;
import org.json.JSONArray;
import org.json.JSONObject;

public class ProjectModule extends BaseHandler implements ProjectService {

	private final ProjectDb projectDb;

	public ProjectModule(Configuration config) throws UmbrellaException {
		super();
		var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE));
		projectDb = new SqliteDb(connect(dbFile));
		ModuleRegistry.add(this);
	}

	private UmbrellaUser addMember(Project project, long userId) {
		var user = userService().loadUser(userId);
		var member = new Member(user,READ_ONLY);
		project.members().put(userId,member);
		project.dirty(MEMBERS);
		return user;
	}

	@Override
	public boolean doGet(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		addCors(ex);
		try {
			Optional<Token> token = SessionToken.from(ex).map(Token::of);
			var user = userService().loadUser(token);
			if (user.isEmpty()) return unauthorized(ex);
			var head = path.pop();
			return switch (head) {
				case null -> super.doGet(path, ex);
				default -> {
					var projectId = Long.parseLong(head);
					head = path.pop();
					yield switch (head) {
						case null -> getProject(ex, projectId, user.get());
						default -> super.doGet(path, ex);
					};
				}
			};
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	@Override
	public boolean doPatch(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		addCors(ex);
		try {
			Optional<Token> token = SessionToken.from(ex).map(Token::of);
			var user = userService().loadUser(token);
			if (user.isEmpty()) return unauthorized(ex);
			var head = path.pop();
			return switch (head) {
				default -> {
					var projectId = Long.parseLong(head);
					head = path.pop();
					yield switch (head){
						case null -> patchProject(ex,projectId,user.get());
						case Path.STATE -> patchProjectState(ex,projectId,user.get());
						default -> super.doPatch(path,ex);
					};
				}
			};
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	@Override
	public boolean doPost(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		addCors(ex);
		try {
			Optional<Token> token = SessionToken.from(ex).map(Token::of);
			var user = userService().loadUser(token);
			if (user.isEmpty()) return unauthorized(ex);
			var head = path.pop();
			return switch (head) {
				case LIST -> postProjectList(ex, user.get());
				case SEARCH -> postSearch(user.get(),ex);
				case null -> postProject(ex, user.get());
				default -> {
					var projectId = Long.parseLong(head);
					head = path.pop();
					yield switch (head){
						case Path.STATE -> postNewState(ex,projectId,user.get());
						case null, default -> super.doGet(path, ex);
					};
				}
			};
		} catch (NumberFormatException e){
			return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	private void dropMember(Project project, long userId) {
		if (project.members().get(userId).permission() == OWNER) throw forbidden("You may not remove the owner of the project");
		projectDb.dropMember(project.id(),userId);
		project.members().remove(userId);
	}

	private boolean getProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException {
		var project = loadMembers(projectDb.load(projectId));
		if (!project.hasMember(user)) throw notAmember(t(PROJECT_WITH_ID,ID,project.name()));
		var map = project.toMap();
		project.companyId().map(companyService()::get).map(Company::toMap).ifPresent(data -> map.put(Field.COMPANY,data));
		return sendContent(ex,map);
	}

	public Map<Long, Project> listCompanyProjects(long companyId, boolean includeClosed) throws UmbrellaException {
		var projectList = projectDb.ofCompany(companyId, includeClosed);
		loadMembers(projectList.values());
		return projectList;
	}

	private boolean listCompanyProjects(HttpExchange ex, UmbrellaUser user, long companyId) throws IOException, UmbrellaException {
		var company = companyService().get(companyId);
		if (!companyService().membership(companyId,user.id())) throw notAmember(t(COMPANY_WITH_ID,ID,company.name()));
		var projects = listCompanyProjects(companyId,false);
		return sendContent(ex,mapValues(projects));
	}

	@Override
	public Map<Long, Project> listUserProjects(long userId, boolean includeClosed) throws UmbrellaException {
		var projectMap = projectDb.ofUser(userId, includeClosed);
		loadMembers(projectMap.values());
		return projectMap;
	}

	private boolean listUserProjects(HttpExchange ex, UmbrellaUser user, boolean showClosed) throws IOException, UmbrellaException {
		var projects = listUserProjects(user.id(),showClosed);
		return sendContent(ex,mapValues(projects));
	}

	@Override
	public Project load(long projectId) {
		return projectDb.load(projectId);
	}

	@Override
	public Collection<Project> loadMembers(Collection<Project> projectList) {
		var userMap = new HashMap<Long,UmbrellaUser>();
		for (var project : projectList){
			for (var entry : projectDb.getMembers(project).entrySet()){
				var userId = entry.getKey();
				var permission = entry.getValue();
				var user = userMap.computeIfAbsent(userId,k -> userService().loadUser(userId));
				project.members().put(userId,new Member(user,permission));
			}
		}
		return projectList;
	}


	private void patchMembers(Project project, JSONObject json) {
		var members = project.members();
		for (var key : json.keySet()){
			long userId;
			try {
				userId = Long.parseLong(key);
			} catch (NumberFormatException e) {
				throw invalidField(USER_ID,t(LONG));
			}
			if (!(json.get(key) instanceof Number number)) throw invalidField(PERMISSION,t(Text.NUMBER));
			var permission = Permission.of(number.intValue());
			if (permission == OWNER) { // if a new person is about to become the project owner
				for (var member : members.values()){ // alter the previous owners to editors
					if (member.permission() == OWNER) members.put(member.user().id(),new Member(member.user(),EDIT));
				}
			}
			members.put(userId,new Member(userService().loadUser(userId),permission));
			project.dirty(MEMBERS);
		}
	}

	private boolean patchProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException {
		var project = loadMembers(projectDb.load(projectId));
		if (!project.hasMember(user)) throw notAmember(t(PROJECT_WITH_ID,ID,project.name()));
		var old = project.toMap();
		var json = json(ex);
		UmbrellaUser newMember = null;
		if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(project,id.longValue());
		if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(project,memberJson);
		if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) newMember = addMember(project,num.longValue());

		project = projectDb.save(project.patch(json), user);
		messageBus().dispatch(newMember != null ? new ProjectEvent(user,project,newMember) : new ProjectEvent(user,project, old));
		return sendContent(ex,project.toMap());
	}

	private boolean patchProjectState(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException {
		var project = loadMembers(projectDb.load(projectId));
		if (!project.hasMember(user)) throw notAmember(t(PROJECT_WITH_ID,ID,project.name()));
		var json = json(ex);
		if (!json.has(ID)) throw missingField(ID);
		if (!json.has(NAME)) throw missingField(NAME);
		if (!(json.get(ID) instanceof Number fieldId)) throw invalidField(ID,Text.NUMBER);
		var newName = json.getString(NAME);
		if (newName.isBlank()) throw invalidField(NAME, STRING);
		var newState = new Status(newName,fieldId.intValue());
		return sendContent(ex, projectDb.save(projectId,newState));

	}

	private boolean postNewState(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException {
		var project = loadMembers(load(projectId));
		if (!project.hasMember(user)) throw notAmember(t(PROJECT_WITH_ID,ID,project.name()));
		var json = json(ex);
		if (!(json.has(Field.CODE) && json.get(Field.CODE) instanceof Number code)) throw missingField(Field.CODE);
		if (!(json.has(NAME) && json.get(NAME) instanceof String name)) throw missingField(NAME);
		var newState = new Status(name,code.intValue());
		return sendContent(ex, projectDb.save(projectId,newState));
	}

	private boolean postProject(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
		var json = json(ex);
		if (!(json.has(NAME) && json.get(NAME) instanceof String name)) throw missingField(NAME);
		String description = null;
		if (json.has(DESCRIPTION)){
			var desc = json.get(DESCRIPTION);
			if (desc instanceof String d) description = d;
			if (desc instanceof JSONObject nested && nested.has(SOURCE) && nested.get(SOURCE) instanceof String d) description = d;
		}
		Long companyId = null;
		if (json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number number){
			if (!companyService().membership(number.longValue(), user.id())) throw notAmember(t(COMPANY_WITH_ID, ID,number));
			companyId = number.longValue();
		}
		var showClosed = false;
		if (json.has(SETTINGS) && json.get(SETTINGS) instanceof JSONObject settingsJson){
			showClosed = settingsJson.has(SHOW_CLOSED) && settingsJson.get(SHOW_CLOSED) == TRUE;
		}
		var owner = Map.of(user.id(),new Member(user,OWNER));
		var prj = new Project(0,name,description, OPEN.code(),companyId,showClosed, owner, PREDEFINED);
		prj = projectDb.save(prj,user);

		if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray arr){
			var tagList = arr.toList().stream().filter(elem -> elem instanceof String).map(String.class::cast).toList();
			tagService().save(PROJECT,prj.id(),null,tagList);
		}
		messageBus().dispatch(new ProjectEvent(user,prj, CREATE));
		return sendContent(ex,prj);
	}

	private boolean postProjectList(HttpExchange ex, UmbrellaUser user) throws IOException {
		var json = json(ex);
		var showClosed = json.has(SHOW_CLOSED) && json.get(SHOW_CLOSED) instanceof Boolean bool ? bool : false;
		if (json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number companyId) return listCompanyProjects(ex, user, companyId.longValue());
		return listUserProjects(ex,user,showClosed);
	}


	private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingField(KEY);
		var keys = Arrays.asList(key.split(" "));
		var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
		var projects = projectDb.find(user.id(),keys,fulltext);
		return sendContent(ex,mapValues(projects));
	}
}