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

import static de.srsoftware.tools.Optionals.*;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.*;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_NOT_IMPLEMENTED;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Field.PROJECT;
import static de.srsoftware.umbrella.core.constants.Field.TAGS;
import static de.srsoftware.umbrella.core.constants.Field.TASKS;
import static de.srsoftware.umbrella.core.constants.Module.TASK;
import static de.srsoftware.umbrella.core.constants.Path.*;
import static de.srsoftware.umbrella.core.constants.Text.LONG;
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.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.PERMISSIONS;
import static de.srsoftware.umbrella.task.Constants.*;
import static java.lang.System.Logger.Level.WARNING;
import static java.net.URLDecoder.decode;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.*;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.core.model.Task;
import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.messagebus.events.Event;
import de.srsoftware.umbrella.messagebus.events.TaskEvent;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import org.json.JSONArray;
import org.json.JSONObject;

public class TaskModule extends BaseHandler implements TaskService {

	private final TaskDb taskDb;

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

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

	private boolean deleteTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException {
		var task = loadMembers(taskDb.load(taskId));
		var member = task.members().get(user.id());
		if (member == null || !member.mayWrite()) throw forbidden("You are not allowed to delete {object}", OBJECT, task.name());
		taskDb.delete(task);
		noteService().deleteEntity(TASK, "" + taskId);
		tagService().deleteEntity(TASK, taskId);
		messageBus().dispatch(new TaskEvent(user,task,Event.EventType.DELETE));
		return sendContent(ex, Map.of(DELETED, taskId));
	}

	@Override
	public boolean doDelete(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 taskId = Long.parseLong(head);
					head = path.pop();
					yield head == null ? deleteTask(ex, taskId, user.get()) : super.doDelete(path, ex);
				}
			};
		} catch (UmbrellaException e) {
			return send(ex, e);
		}
	}

	@Override
	public boolean doGet(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 PERMISSIONS -> getPermissionList(ex);
				case TAGGED -> getTaggedTasks(path, user.get(), ex);
				case null -> getUserTasks(user.get(), ex);
				default -> {
					var taskId = Long.parseLong(head);
					yield switch (path.pop()){
						case null -> getTask(ex,taskId,user.get());
						case PARENT_CANDIDATES -> getParentCandidates(ex,taskId, user.get());
						default -> super.doGet(path,ex);
					};
				}
			};
		} catch (UmbrellaException e) {
			return send(ex, e);
		}
	}

	@Override
	public boolean doPatch(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 taskId = Long.parseLong(head);
					head = path.pop();
					yield head == null ? patchTask(ex, taskId, user.get()) : super.doPatch(path, ex);
				}
			};
		} catch (UmbrellaException e){
			return send(ex, e);
		}
	}

	@Override
	public boolean doPost(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 ADD -> postNewTask(user.get(), ex);
				case ESTIMATED_TIMES -> estimatedTimes(user.get(), ex);
				case LIST -> postTaskList(user.get(), ex);
				case SEARCH -> postSearch(user.get(), ex);
				default -> super.doPost(path, ex);
			};
		} catch (UmbrellaException e) {
			return send(ex, e);
		}
	}

	private void dropMember(Task task, long userId) {
		if (task.members().get(userId).permission() == OWNER) throw forbidden("You may not remove the owner of {object}", OBJECT,task.name());
		taskDb.dropMember(task.id(), userId);
		task.members().remove(userId);
	}

	private boolean estimatedTimes(UmbrellaUser user, HttpExchange ex) throws IOException, UmbrellaException {
		var json = json(ex);
		if (!(json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number cid)) throw missingField(COMPANY_ID);
		var companyId = cid.longValue();
		var company = companyService().get(companyId);
		if (!companyService().membership(companyId,user.id())) throw notAmember(company.name());
		var projectMap = projectService().listCompanyProjects(companyId, false);
		var taskMap = taskDb.listTasks(projectMap.keySet());
		var taskTree = new HashMap<Long, Map<String, Object>>();
		taskMap.values().stream().filter(task -> !is0(task.estimatedTime())).forEach(task -> placeInTree(task, taskTree, taskMap));
		var result = new ArrayList<Map<String, Object>>();
		projectMap.values().forEach(project -> {
			var mappedProject = new HashMap<>(project.toMap());
			var children = taskTree.values().stream().filter(root -> project.id() == (Long) root.get(PROJECT_ID)).toList();
			if (!children.isEmpty()) {
				mappedProject.put(TASKS, children);
				result.add(mappedProject);
			}
		});
		return sendContent(ex, result);
	}

	private boolean getParentCandidates(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException {
		var task = taskDb.load(taskId);
		var project = projectService().load(task.projectId());
		var projectTasks = taskDb.listProjectTasks(project.id(),null, false);
		var mapped = projectTasks.values().stream().collect(Collectors.toMap(Task::id,Task::toMap));
		var roots = new HashMap<Long,Map<String,Object>>();
		for (var map : mapped.values()){
			if (!(map.get(ID) instanceof Long id)) continue;
			if (id == taskId) continue;
			if (map.get(PARENT_TASK_ID) instanceof Long parentId) {
				var parent = mapped.get(parentId);
				if (parent != null) {
					var o = parent.get(Field.CHILDREN);
					Map<Long,Object> children;
					if (o == null) {
						children = new HashMap<>();
						parent.put(Field.CHILDREN,children);
					} else children = (Map<Long, Object>) o;
					children.put(id, map);
				}
			} else {
				roots.put(id, map);
			}
		}
		return sendContent(ex,roots);
	}

	private boolean getPermissionList(HttpExchange ex) throws IOException {
		var map = new HashMap<Integer, String>();
		for (var permission : Permission.values()) map.put(permission.code(),permission.name());
		return sendContent(ex, map);
	}

	private boolean getTaggedTasks(Path path, UmbrellaUser user, HttpExchange ex) throws IOException {
		var tag = decode(path.toString(), UTF_8);
		var tags = tagService().getTagUses(user,tag);
		var taskIds = nullable(tags.get(TASK)).orElseGet(ArrayList::new);
		var tasks = mapValues(taskDb.load(taskIds));
		var taskTags = tagService().getTags(TASK,taskIds,user);
		for (var entry : tasks.entrySet()){
			var list = taskTags.get(entry.getKey());
			entry.getValue().put(TAGS,list==null?List.of():list);
		}
		return sendContent(ex, tasks);
	}

	private boolean getTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException {
		var task = loadMembers(taskDb.load(taskId));
		if (!task.hasMember(user)) throw notAmember(task.name());
		return sendContent(ex, task);
	}

	private boolean getUserTasks(UmbrellaUser user, HttpExchange ex) throws IOException {
		long offset = 0;
		Long limit = null;
		var params = queryParam(ex);
		if (params.get(OFFSET) instanceof String o) try {
			offset = Long.parseLong(o);
		} catch (NumberFormatException e) {
			throw invalidField(OFFSET, t(Text.NUMBER));
		}
		if (params.get(LIMIT) instanceof String l) try {
			limit = Long.parseLong(l);
		} catch (NumberFormatException e) {
			throw invalidField(LIMIT, t(Text.NUMBER));
		}
		var tasks  = taskDb.listUserTasks(user.id(), limit, offset, false);
		var mapped = loadMembers(tasks).stream().map(Task::toMap);
		return sendContent(ex, mapped);
	}

	@Override
	public Map<Long, Task> listCompanyTasks(long companyId) throws UmbrellaException {
		var projectList = projectService().listCompanyProjects(companyId, false);
		return taskDb.listTasks(projectList.keySet());
	}

	@Override
	public Map<Long, Task> listProjectTasks(long projectId) throws UmbrellaException {
		return taskDb.listTasks(List.of(projectId));
	}

	private Task loadTaskOrNull(long taskId) {
		try {
			return taskDb.load(taskId);
		} catch (UmbrellaException e) {
			LOG.log(WARNING, e.getMessage());
			return null;
		}
	}

	@Override
	public Map<Long, Task> load(Collection<Long> taskIds) {
		return taskDb.load(taskIds);
	}

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

	private Map<Long,Map<String,Object>> mapTasks(Map<Long,Task> tasks, boolean render){
		if (render) return mapValues(tasks);
		return tasks.entrySet().stream()
				.collect(Collectors.toMap(Map.Entry::getKey,e -> e.getValue().toMap(false)));
	}

	private boolean newParentIsSubtask(Task task, long newParent) {
		var parent = taskDb.load(newParent);
		while (parent != null) {
			if (task.id() == parent.id()) return true;
			if (parent.parentTaskId() == null) break;
			parent = taskDb.load(parent.parentTaskId());
		}
		return false;
	}

	private Map<String, Object> placeInTree(Task task, HashMap<Long, Map<String, Object>> taskTree, Map<Long, Task> taskMap) {
		var mappedTask = task.toMap();
		if (task.parentTaskId() != null) {
			Task parent = taskMap.get(task.parentTaskId());
			var trunk = placeInTree(parent, taskTree, taskMap);
			@SuppressWarnings("unchecked")
			ArrayList<Object> children = (ArrayList<Object>) trunk.computeIfAbsent(Field.CHILDREN, k -> new ArrayList<>());
			children.add(mappedTask);
			return mappedTask;
		}
		taskTree.put(task.id(), mappedTask);
		return mappedTask;
	}

	private void patchMembers(Task task, JSONObject json) {
		var members = task.members();
		for (var key : json.keySet()) {
			long userId;
			try {
				userId = Long.parseLong(key);
			} catch (NumberFormatException e) {
				throw invalidField(USER_ID, t(LONG));
			}
			var permission = switch (json.get(key)) {
				case Number code -> Permission.of(code.intValue());
				case String name -> Permission.valueOf(name);
				default -> throw invalidField(PERMISSION, "int / String");
			};
			if (permission == OWNER) { // if a new person is about to become the task 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));
				}
			}
			if (permission == ASSIGNEE) { // if a new person is about to become the task owner
				for (var member : members.values()) { // alter the previous owners to editors
					if (member.permission() == ASSIGNEE) members.put(member.user().id(), new Member(member.user(), EDIT));
				}
			}
			members.put(userId, new Member(userService().loadUser(userId), permission));
			task.dirty(MEMBERS);
		}
	}

	private boolean patchTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException {
		var task = loadMembers(taskDb.load(taskId));
		var old = task.toMap();
		var member = task.members().get(user.id());
		if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not a allowed to edit {object}!", OBJECT, task.name());
		var json = json(ex);
		if (json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number ptid && newParentIsSubtask(task, ptid.longValue())) throw forbidden("Task must not be sub-task of itself.");

		UmbrellaUser newMember = null;
		if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(task, id.longValue());
		if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(task, memberJson);
		if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) newMember = addMember(task, num.longValue());

		task = taskDb.save(task.patch(json)).tags(tagService().getTags(TASK, taskId, user));
		messageBus().dispatch(newMember != null ? new TaskEvent(user, task, newMember) : new TaskEvent(user, task, old));
		return sendContent(ex, task);
	}

	private boolean postNewTask(UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!(json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number pid)) throw missingField(PROJECT_ID);

		long projectId = pid.longValue();
		var project = projectService().load(projectId);
		projectService().loadMembers(List.of(project));
		var members = project.members();
		var member = members.get(user.id());
		if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not allowed to create new tasks in this project");

		var parentTask = json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number par ? taskService().load(Set.of(par.longValue())).get(par.longValue()) : null;
		if (parentTask != null) {
			taskService().loadMembers(parentTask);
			members = parentTask.members();
			member = members.get(user.id());
			if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not allowed to add sub-stasks to {object}", OBJECT, parentTask.name());
		}

		var newMembers = new HashMap<Long, Permission>();
		for (var mem : members.values()) { // Assign members from project or parent task
			var permission = mem.permission() == OWNER ? EDIT : mem.permission();
			newMembers.put(mem.user().id(), permission);
		}

		if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject mems) {
			// check of assignee has been set by client
			for (var k : mems.keySet()) {
				try {
					var userId = Long.parseLong(k);
					var permName = mems.getJSONObject(k).getJSONObject(PERMISSION).getString(NAME);
					if (Permission.valueOf(permName) == ASSIGNEE) newMembers.put(userId, ASSIGNEE);
				} catch (Exception ignored) {
					LOG.log(WARNING, "Failed to parse {0}", mems.get(k));
				}
			}
		}
		// set ownership to current user
		newMembers.put(user.id(), OWNER);


		json.put(MEMBERS, Map.of()); // reset member map for task-to-be-created
		Task task = Task.of(json);
		if (parentTask != null && parentTask.dueDate() != null && task.dueDate() == null) task.dueDate(parentTask.dueDate());
		task = taskDb.save(task);

		// do actual member assignment
		for (var entry : newMembers.entrySet()) taskDb.setMember(task.id(), entry.getKey(), entry.getValue());

		Collection<String> tagList = null;
		if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray arr) {
			tagList = arr.toList().stream().filter(e -> e instanceof String).map(String.class::cast).toList();
		}
		if ((tagList == null || tagList.isEmpty()) && parentTask != null) tagList = tagService().getTags(TASK, parentTask.id(), user);
		if ((tagList == null || tagList.isEmpty())) tagList = tagService().getTags(PROJECT, projectId, user);
		if (tagList != null && !tagList.isEmpty()) tagService().save(TASK, task.id(), null, tagList);
		task = loadMembers(task);
		task.tags(tagList);
		messageBus().dispatch(new TaskEvent(user, task, CREATE));
		return sendContent(ex, task);
	}

	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 projectId = json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number pid ? pid.longValue() : null;
		var keys = Arrays.asList(key.split(" "));
		var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
		var tasks = taskDb.find(user.id(), keys, fulltext);
		if (projectId != null) tasks = tasks.values().stream().filter(task -> task.projectId() == projectId).collect(Collectors.toMap(Task::id, t -> t));
		return sendContent(ex, mapValues(tasks));
	}

	private boolean postTaskList(UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		LOG.log(WARNING, "Missing permission check in {0}.postTaskList!", getClass().getSimpleName());
		var showClosed   = json.has(SHOW_CLOSED)    && json.get(SHOW_CLOSED)    instanceof Boolean bool  ? bool : false;
		var noIndex      = json.has(NO_INDEX)       && json.get(NO_INDEX)       instanceof Boolean bool  ? bool : false;
		var projectId    = json.has(PROJECT_ID)     && json.get(PROJECT_ID)     instanceof Number number ? number.longValue() : null;
		var parentTaskId = json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number number ? number.longValue() : null;
		var markdown     = !json.has(RENDERED)    || !(json.get(RENDERED)       instanceof Boolean render) || render;
		if (isSet(projectId)) {
			if (parentTaskId == null) {
				var list = taskDb.listRootTasks(projectId, user, showClosed);
				return sendContent(ex, mapTasks(list,markdown));
			}
			var projectTasks = taskDb.listProjectTasks(projectId, parentTaskId, noIndex);
			loadMembers(projectTasks.values());
			var tags = tagService().getTags(TASK,projectTasks.keySet(),user);

			projectTasks = addTags(projectTasks, tags);
			return sendContent(ex, mapTasks(projectTasks, markdown));
		}
		if (isSet(parentTaskId)) return sendContent(ex, mapValues(taskDb.listChildrenOf(parentTaskId, user, showClosed)));
		var taskIds = json.has(IDS) && json.get(IDS) instanceof JSONArray ids ? ids.toList().stream().map(Object::toString).map(Long::parseLong).toList() : null;
		var tasks = taskDb.load(taskIds);
		if (isSet(taskIds)) return sendContent(ex, mapTasks(tasks,markdown));
		return sendEmptyResponse(HTTP_NOT_IMPLEMENTED, ex);
	}

	private Map<Long, Task> addTags(Map<Long, Task> taskList, Map<Long, ? extends Collection<String>> tags) {
		return taskList.values().stream().map(task -> task.tags(tags.get(task.id()))).collect(Collectors.toMap(Task::id, t -> t));
	}
}
