/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model;

import static de.srsoftware.tools.Optionals.nullIfEmpty;
import static de.srsoftware.umbrella.core.Util.*;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static java.lang.System.Logger.Level.WARNING;

import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.constants.Field;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.*;
import org.json.JSONObject;

public class Task implements Mappable {
	public static final System.Logger LOG = System.getLogger(Task.class.getSimpleName());
	private final long id, projectId;
	private Long parentTaskId;
	private String description, name;
	private final Set<Long> requiredTasksIds;
	private int status, priority;
	private Double estimatedTime;
	private LocalDate dueDate, start;
	private boolean noIndex, showClosed;
	private final Map<Long, Member> members;
	private final Set<String> dirtyFields = new HashSet<>();
	private final Set<String> tags = new HashSet<>();

	public Task (long id, long projectId, Long parentTaskId, String name, String description, int status, Double estimatedTime, LocalDate start, LocalDate dueDate, boolean showClosed, boolean noIndex, Map<Long,Member> members, int priority){
		this.id = id;
		this.projectId = projectId;
		this.parentTaskId = parentTaskId;
		this.name = name;
		this.description = description;
		this.status = status;
		this.estimatedTime = estimatedTime;
		this.start = start;
		this.dueDate = dueDate;
		this.showClosed = showClosed;
		this.noIndex = noIndex;
		this.members = members;
		this.requiredTasksIds = new HashSet<>();
		this.priority = priority;
	}

	public Task clean() {
		dirtyFields.clear();
		return this;
	}

	public Task clean(String field){
		dirtyFields.remove(field);
		return this;
	}

	public String description(){
		return description;
	}

	public Task dirty(String field){
		dirtyFields.add(field);
		return this;
	}

	public Task dueDate(LocalDate newValue) {
		dueDate = newValue;
		return this;
	}

	public LocalDate dueDate(){
		return dueDate;
	}

	public Double estimatedTime(){
		return estimatedTime;
	}

	public boolean hasMember(UmbrellaUser user) {
		return members.containsKey(user.id());
	}

	public long id(){
		return id;
	}

	public boolean isDirty() {
		return !dirtyFields.isEmpty();
	}

	public boolean isDirty(String field){
		return dirtyFields.contains(field);
	}

	public Map<Long,Member> members(){
		return members;
	}

	public String name(){
		return name;
	}

	public boolean noIndex(){
		return noIndex;
	}

	public static Task of(ResultSet rs) throws SQLException {
		var estTime = rs.getDouble(EST_TIME);
		var parentTaskId = rs.getLong(PARENT_TASK_ID);
		var startDate = nullIfEmpty(rs.getString(START_DATE));
		var dueDate = nullIfEmpty(rs.getString(DUE_DATE));

		return new Task(
				rs.getLong(ID),
				rs.getLong(PROJECT_ID),
				parentTaskId == 0d ? null : parentTaskId,
				rs.getString(NAME),
				rs.getString(DESCRIPTION),
				rs.getInt(STATUS),
				estTime == 0d ? null : estTime,
				startDate != null ? LocalDate.parse(startDate) : null,
				dueDate != null ? LocalDate.parse(dueDate) : null,
				rs.getBoolean(SHOW_CLOSED),
				rs.getBoolean(NO_INDEX),
				new HashMap<>(),
				rs.getInt(PRIORITY)
		);
	}

	public static Task of(JSONObject json){
		if (!(json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number prjId)) throw missingField(PROJECT_ID);
		Long parentTaskId = json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number ptid ? ptid.longValue() : null;

		if (!(json.has(NAME) && json.get(NAME) instanceof String name && !name.isBlank())) throw missingField(NAME);
		if (!json.has(DESCRIPTION)) throw missingField(DESCRIPTION);
		var description = switch (json.get(DESCRIPTION)){
			case String d -> d;
			case JSONObject j -> j.getString(SOURCE);
			default -> throw invalidField(json.get(DESCRIPTION).getClass().getSimpleName(),"String or JSON");
		};

		var status = Status.OPEN.code();
		if (json.has(STATUS) && json.get(STATUS) instanceof JSONObject state){
			if (state.get(Field.CODE) instanceof Number code) status = code.intValue();
		}
		Double estimatedTime = null;
		if (json.has(EST_TIME)) {
			if (json.get(EST_TIME) instanceof Number est) estimatedTime = est.doubleValue();
			if (json.get(EST_TIME) instanceof String est) try {
				estimatedTime = Double.parseDouble(est);
			} catch (NumberFormatException ignored) {
			}
		}
		LocalDate startDate = json.has(START_DATE) && json.get(START_DATE) instanceof String d ? LocalDate.parse(d) : null;
		LocalDate dueDate = json.has(DUE_DATE) && json.get(DUE_DATE) instanceof String d ? LocalDate.parse(d) : null;
		var showClosed = json.has(SHOW_CLOSED) && json.get(SHOW_CLOSED) instanceof Boolean sc ? sc : false;
		var noIndex = json.has(NO_INDEX) && json.get(NO_INDEX) instanceof Boolean ni ? ni : false;
		var priority = json.has(PRIORITY) && json.get(PRIORITY) instanceof Number pr ? prjId.intValue() : 0;
		if (!(json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject)) throw missingField(MEMBERS);
		return new Task(0,prjId.longValue(),parentTaskId,name,description,status,estimatedTime,startDate,dueDate,showClosed,noIndex,new HashMap<>(),priority);
	}

	public Task patch(JSONObject json) {
		for (var key : json.keySet()){
			switch (key){
				case DESCRIPTION:    description   = json.getString(key); break;
				case DUE_DATE:       dueDate       = json.isNull(DUE_DATE) || json.getString(DUE_DATE).isBlank() ? null : LocalDate.parse(json.getString(DUE_DATE)); break;
				case EST_TIME: estimatedTime = json.isNull(EST_TIME) ? null : json.getDouble(EST_TIME); break;
				case MEMBERS: continue;
				case NAME:           name          = json.getString(key); break;
				case NO_INDEX:       noIndex       = json.getBoolean(NO_INDEX); break;
				case PARENT_TASK_ID: parentTaskId  = json.isNull(PARENT_TASK_ID) ? null : json.getLong(PARENT_TASK_ID); break;
				case PRIORITY:       priority      = json.getInt(PRIORITY); break;
				case REQUIRED_TASKS_IDS:
					requiredTasksIds.clear();
					requiredTasksIds.addAll(json.getJSONArray(REQUIRED_TASKS_IDS).toList().stream().map(entry -> Long.parseLong(entry.toString())).toList());
					break;
				case SHOW_CLOSED:    showClosed    = json.getBoolean(SHOW_CLOSED); break;
				case START_DATE:     start         = json.isNull(START_DATE) || json.getString(START_DATE).isBlank() ? null : LocalDate.parse(json.getString(START_DATE)); break;
				case STATUS:         status        = json.getInt(key); break;
				default: {
					LOG.log(WARNING,"Tried to patch field ''{0}'' of task, which is not implemented!",key);
					key = null;
				}
			}
			if (key != null) dirtyFields.add(key);
		}
		return this;
	}

	public Long parentTaskId(){
		return parentTaskId;
	}

	public int priority(){
		return priority;
	}

	public long projectId(){
		return projectId;
	}

	public Set<Long> requiredTasksIds(){
		return requiredTasksIds;
	}

	public boolean showClosed(){
		return showClosed;
	}

	public LocalDate start(){
		return start;
	}

	public int status(){
		return status;
	}

	public Task tags(Collection<String> newValue){
		tags.clear();
		if (newValue != null) tags.addAll(newValue);
		return this;
	}

	public Set<String> tags(){
		return tags;
	}

	public Map<String,Object> toMap(boolean renderMarkdown){
		var map = new HashMap<String,Object>();
		var memberMap = new HashMap<Long,Map<String,Object>>();
		if (members != null) for (var entry : members.entrySet()){
			memberMap.put(entry.getKey(),entry.getValue().toMap());
		}
		map.put(ID, id);
		map.put(PROJECT_ID, projectId);
		map.put(PARENT_TASK_ID, parentTaskId);
		map.put(PRIORITY,priority);
		map.put(NAME, name);
		map.put(DESCRIPTION, renderMarkdown ? mapMarkdown(description) : Map.of(SOURCE,description));
		map.put(STATUS, status);
		map.put(EST_TIME, estimatedTime);
		map.put(START_DATE,start);
		map.put(DUE_DATE,dueDate);
		map.put(NO_INDEX,noIndex);
		map.put(MEMBERS,memberMap);
		map.put(REQUIRED_TASKS_IDS,requiredTasksIds);
		map.put(SHOW_CLOSED,showClosed);
		map.put(TOTAL_PRIO,totalPrio());
		map.put(TAGS,tags);
		return map;
	}

	@Override
	public Map<String, Object> toMap() {
		return toMap(true);
	}

	private int totalPrio() {
		if (status >= Status.COMPLETE.code()) return 0; // task is done, do no longer highlight
		int total = priority;
		if (dueDate != null) {
			var diff = (int) (dueDate.toEpochDay()-LocalDate.now().toEpochDay());
			if (diff <= 0) total += 100; // due date has passed
			if (diff < 100) total += 100 - diff; // due within the next 100 days
		}
		return Math.min(100,total);
	}


	@Override
	public String toString() {
		return name;
	}
}
