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

import static de.srsoftware.tools.Optionals.emptyIfNull;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE;
import static de.srsoftware.umbrella.core.Util.mapMarkdown;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static java.util.Optional.empty;

import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import org.json.JSONObject;


public final class Document implements Mappable {
	public static final String DEFAULT_THOUSANDS_SEPARATOR = ".";

	public enum State {

		NEW(1), SENT(2), DELAYED(3), PAYED(4), DECLINED(5), ERROR(99);

		private final int code;

		State(int code) {
			this.code = code;
		}

		public int code() {
			return code;
		}

		public static Map<Integer, Object> names() {
			return Arrays.stream(values()).collect(Collectors.toMap(state -> state.code, state -> Map.of("name", state.toString(), "display", "<? " + state + " ?>")));
		}

		public static Optional<State> of(int code) {
			for (var s : State.values()) {
				if (s.code == code) return Optional.of(s);
			}
			return empty();
		}

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

	private final long companyId, id;
	private String companyName, currency, decimalSeparator, delivery, footer, head, number;

	private final Type type;
	private LocalDate date;
	private State state;
	private String template;
	private final Sender sender;
	private final Customer customer;
	private final PositionList positions;
	private final Set<String> dirtyFields = new HashSet<>();


	public Document(long id, long companyId, String number, Type type, LocalDate date, State state, String template, String delivery, String head, String footer, String currency, String decimalSeparator, Sender sender, Customer customer, PositionList positions) {
		this.id = id;
		this.companyId = companyId;
		this.number = number;
		this.type = type;
		this.date = date;
		this.state = state;
		this.template = template;
		this.delivery = delivery;
		this.head = head;
		this.footer = footer;
		this.currency = currency;
		this.sender = sender;
		this.customer = customer;
		this.positions = positions;
		this.decimalSeparator = decimalSeparator;
		positions.setDocId(id);
		if (id == 0) dirtyFields.add(ID);
	}

	public void clean() {
		dirtyFields.clear();
	}

	public long companyId() {
		return companyId;
	}

	public String currency() {
		return currency;
	}

	public Customer customer() {
		return customer;
	}

	public LocalDate date() {
		return date;
	}

	public String decimalSeparator() {
		return decimalSeparator;
	}

	public String delivery() {
		return delivery;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == this) return true;
		if (obj == null || obj.getClass() != this.getClass()) return false;
		var that = (Document) obj;
		return this.id == that.id &&
				this.companyId == that.companyId &&
				Objects.equals(this.number, that.number) &&
				Objects.equals(this.type, that.type) &&
				Objects.equals(this.date, that.date) &&
				Objects.equals(this.state, that.state) &&
				Objects.equals(this.template, that.template) &&
				Objects.equals(this.delivery, that.delivery) &&
				Objects.equals(this.head, that.head) &&
				Objects.equals(this.footer, that.footer) &&
				Objects.equals(this.currency, that.currency) &&
				Objects.equals(this.sender, that.sender) &&
				Objects.equals(this.customer, that.customer) &&
				Objects.equals(this.positions, that.positions);
	}

	public String footer() {
		return footer;
	}

	public long grossSum() {
		var taxMap = new HashMap<Integer, Double>();
		for (var pos : positions.values()) {
			var taxRate = pos.tax();
			var sum = taxMap.get(taxRate);
			if (sum == null) sum = 0d;
			sum += pos.unitPrice() * pos.amount();
			taxMap.put(taxRate, sum);
		}
		var sum = 0d;
		for (var entry : taxMap.entrySet()) {
			var taxRate = entry.getKey();
			var taxSum = entry.getValue();
			sum += taxSum * (100 + taxRate) / 100d;
		}
		return Math.round(sum);
	}

	@Override
	public int hashCode() {
		return Objects.hash(id, companyId, number, type, date, state, template, delivery, head, footer, currency, sender, customer, positions);
	}

	public String head() {
		return head;
	}

	public long id() {
		return id;
	}

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

	public boolean isDirty(String ... fields){
		for (var field : fields){
			if (dirtyFields.contains(field)) return true;
		}
		return false;
	}

	public boolean isNew(){
		return dirtyFields.contains(ID);
	}

	public long netSum() {
		var sum = 0d;
		for (var pos : positions.values()) sum += pos.unitPrice() * pos.amount();
		return Math.round(sum);
	}

	public String number() {
		return number;
	}

	public void patch(JSONObject json) throws UmbrellaException {
		for (var key : json.keySet()){
			switch (key){
				case CUSTOMER: if (json.get(key) instanceof JSONObject nested) customer.patch(nested); break;
				case CURRENCY: currency = json.getString(key); break;
				case DATE:           date = LocalDate.parse(json.getString(key)); break;
				case DELIVERY: delivery = json.getString(key); break;
				case FOOTER:   footer = json.getString(key); break;
				case HEAD:     head = json.getString(key); break;
				case NUMBER:         number = json.getString(key); break;
				case SENDER:         if (json.get(key) instanceof JSONObject nested) sender.patch(nested); break;
				case STATE:          state = State.of(json.getInt(key)).orElseThrow(() -> new UmbrellaException(HTTP_UNPROCESSABLE, Translatable.t("Invalid state"))); break;
				case POS:      if (json.get(key) instanceof JSONObject nested) positions.patch(nested); break;
				case TEMPLATE: if (json.get(key) instanceof String templateId) template = templateId; break;
				default:             key = null;
			}
			if (key != null) dirtyFields.add(key);
		}
	}

	public PositionList positions() {
		return positions;
	}

	public Map<String, Object> renderToMap() {
		var map = new HashMap<String, Object>();
		map.put(ID, id);
		map.put(COMPANY, Map.of(ID,companyId, NAME,companyName));
		map.put(NUMBER, number);
		map.put(TYPE, type.name());
		map.put(DATE, date);
		map.put(STATE, state.code);
		map.put(DELIVERY, emptyIfNull(delivery));
		map.put(HEAD, mapMarkdown(head));
		map.put(FOOTER, mapMarkdown(footer));
		map.put(CURRENCY, currency);
		map.put(SENDER, sender.toMap());
		map.put(CUSTOMER, customer.toMap());
		map.put(POSITIONS, positions.asMap(true));
		map.put("taxes",positions.taxNetSums(true));
		map.put(NET_SUM, netSum());
		map.put(GROSS_SUM, grossSum());
		map.put("template", emptyIfNull(template));
		return map;
	}

	public Sender sender() {
		return sender;
	}

	public Document set(State newState) {
		state = newState;
		dirtyFields.add(STATE);
		return this;
	}

	/**
	 * alter the separator for decimals
	 * @param newValue new value for the separator
	 * @return this document instance
	 */
	public Document setDecimalSeparator(String newValue) {
		decimalSeparator = newValue;
		return this;
	}

	/**
	 * set the company name
	 * @param newValue new value for the company name
	 * @return this document instance
	 */
	public Document setCompanyName(String newValue){
		companyName = newValue;
		return this;
	}

	public State state() {
		return state;
	}

	public Map<String, Object> summary() {
		return Map.of(
				ID, id,
				NUMBER, number,
				"type", type.name(),
				STATE, Map.of(NAME,state.toString(), ID,state.code),
				DATE, date,
				CURRENCY, currency,
				CUSTOMER, customer.toMap(),
				"sum", positions.grossSum(true)
		);
	}

	public String template() {
		return template;
	}

	@Override
	public Map<String, Object> toMap() {
		var map = new HashMap<String, Object>();
		map.put(ID, id);
		map.put(COMPANY, companyId);
		map.put(NUMBER, number);
		map.put(TYPE, type);
		map.put(DATE, date);
		map.put(STATE, state.code);
		map.put(DELIVERY, delivery);
		map.put(HEAD, head);
		map.put(FOOTER, footer);
		map.put(CURRENCY, currency);
		map.put(SENDER, sender.toMap());
		map.put(CUSTOMER, customer.toMap());
		map.put(POSITIONS, positions.asMap(false));
		map.put("taxes",positions.taxNetSums(true));
		map.put(NET_SUM, netSum());
		map.put(GROSS_SUM, grossSum());
		if (template != null) map.put("template", template);
		return map;
	}


	@Override
	public String toString() {
		return "Document[" +
				"id=" + id + ", " +
				"company=" + companyId + ", " +
				"number=" + number + ", " +
				"type=" + type + ", " +
				"date=" + date + ", " +
				"state=" + state + ", " +
				"template=" + template + ", " +
				"delivery=" + delivery + ", " +
				"head=" + head + ", " +
				"footer=" + footer + ", " +
				"currency=" + currency + ", " +
				"sender=" + sender + ", " +
				"customer=" + customer + ", " +
				"positions=" + positions + ']';
	}

	public Type type() {
		return type;
	}

}
