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

import static de.srsoftware.tools.MimeType.*;
import static de.srsoftware.umbrella.core.Errors.MISSING_FIELD;
import static de.srsoftware.umbrella.core.constants.Field.PRICE_FORMAT;
import static de.srsoftware.umbrella.core.constants.Field.USER;
import static java.lang.System.Logger.Level.TRACE;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.text.MessageFormat.format;

import de.srsoftware.configuration.JsonConfig;
import de.srsoftware.document.api.*;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.json.JSONObject;

public abstract class TemplateDoc implements Document {
	private static final Pattern TOKEN_PATTERN = Pattern.compile("<\\? (([^?]|\\?[^>])+) \\?>");
	private static final Pattern POSITION_PATTERN = Pattern.compile("<!-- positions -->(.*)<!-- positions -->",Pattern.DOTALL);
	private static final Pattern TAXES_PATTERN = Pattern.compile("<!-- tax list -->(.*)<!-- tax list -->",Pattern.DOTALL);
	private String id, mime;

	@Override
	public String description() {
		return format("File created by TemplateProcessor from \"{0}\"",source().name());
	}

	@Override
	public String id() {
		if (id == null) {
			id = source().name();
			if (id.endsWith(".template")) id = id.substring(0,id.length()-9);
		}
		return id;
	}

	@Override
	public String name() {
		return "processed "+id();
	}

	@Override
	public String mimeType() {
		if (mime == null) {
			mime = MIME_UNKNOWN;
			if (id().endsWith("html")) mime = MIME_HTML;
			if (id().endsWith("tex")) mime = MIME_LATEX;
		}
		return mime;
	}

	@Override
	public RenderResult render(Map<String, Object> data) {
		data = new HashMap<>(data);
		if (!(data.get(PRICE_FORMAT) instanceof PriceFormat price)) return RenderError.of(MISSING_FIELD,PRICE_FORMAT);
		if (data.get(USER) instanceof UmbrellaUser user) data.put(USER,user.toMap());
		var precursor = source().render(data);
		if (precursor instanceof Content content){
			var source = new String(content.bytes(), UTF_8);

			var config = new JsonConfig(new JSONObject(data));
			source = renderPositions(source,config.subset("document.positions"),price);
			source = renderTaxes(source,config.subset("document.taxes"),price);

			var opt = data.get("translate");
			@SuppressWarnings("unchecked")
			Function<String,String> translate = opt instanceof Function<?,?> fun ? (Function<String,String>) fun : s -> s;

			var tokens = findTokensIn(source);
			for (var token : tokens){
				var val = config.get(token).map(Object::toString);
				if (val.isPresent()) {
					var value = val.get().trim();
					value = switch (token){
						case "document.net_sum","document.gross_sum" -> price.format(Long.parseLong(value));
						default -> value;
					};
					if (MIME_HTML.equals(mimeType())) value = value.replace("\n","<span class=\"break\"></span>\n");
					source = source.replace("<? "+token+" ?>",translate.apply(value));
				}
			}
			tokens = findTokensIn(source);
			for (var token : tokens){
				var value = translate.apply(token);
				if (MIME_HTML.equals(mimeType())) value = value.replace("\n","<span class=\"break\"></span>\n");
				source = source.replace("<? "+token+" ?>",value);
			}
			return new StringContent(source,content.mimeType());
		}
		return precursor;
	}

	private String renderPositions(String source, Optional<JsonConfig> opt, PriceFormat price) {
		if (opt.isPresent()){
			var positions = opt.get();
			System.getLogger(id()).log(TRACE,"Positions: {0}",positions);

			var matcher = POSITION_PATTERN.matcher(source);
			if (matcher.find()){
				var posCode = matcher.group(1).trim();
				var start = matcher.start(1);
				var end = matcher.end(1);
				var tokens = findTokensIn(posCode);
				StringBuilder sb = new StringBuilder();
				sb.append(source, 0, start);
				for (var pos : positions.keys().stream().sorted(this::byIntValue).toList()){
					var copy = posCode;
					for (var token : tokens){
						var key = pos+"."+token;
						var val = positions.get(key).orElse(token);
						String value = switch (token){
							case "amount" -> ""+(Math.round(1000d*(double)val)/1000d);
							case "net_price", "unit_price" -> price.format((long)val);
							case "optional" -> val == Boolean.TRUE ? "optional" : "";
							case null -> "";
							default -> val+"";
						};
						copy = copy.replace("<? "+token+" ?>",value);
					}
					sb.append(copy).append("\n");
				}
				sb.append(source.substring(end));
				return sb.toString();
			}
		}
		return source;
	}

	private String renderTaxes(String source, Optional<JsonConfig> opt, PriceFormat price) {
		if (opt.isPresent()){
			var taxes = opt.get();
			System.getLogger(id()).log(TRACE,"Taxes: {0}",taxes);

			var matcher = TAXES_PATTERN.matcher(source);
			if (matcher.find()){
				var taxCode = matcher.group(1).trim();
				var start = matcher.start(1);
				var end = matcher.end(1);
				var tokens = findTokensIn(taxCode);
				StringBuilder sb = new StringBuilder();
				sb.append(source, 0, start);

				for (var rate : taxes.keys()){
					var percent = Integer.parseInt(rate);
					Optional<Long> o = taxes.get(rate);
					long netSum = o.orElse(0L);
					long tax = Math.round(percent * netSum / 100d);
					long grossSum = netSum + tax;

					var copy = taxCode;

					for (var token : tokens){
						var value = switch (token){
							case "net_sum" -> price.format(netSum);
							case "rate" -> rate+" %";
							case "amount" -> price.format(tax);
							case "gross_sum" -> price.format(grossSum);
							default -> null;
						};
						if (value != null) copy = copy.replace("<? "+token+" ?>",value);
					}
					sb.append(copy).append("\n");
				}

				sb.append(source.substring(end));
				return sb.toString();
			}
		}
		return source;
	}

	private int byIntValue(String a, String b) {
		try {
			var i1 = Integer.parseInt(a);
			var i2 = Integer.parseInt(b);
			return i1-i2;
		} catch (NumberFormatException ignored){
			return a.compareTo(b);
		}
	}

	private static Set<String> findTokensIn(String content) {
		var matcher = TOKEN_PATTERN.matcher(content);
		var token = new HashSet<String>();
		while (matcher.find()) token.add(matcher.group(1));
		return token;
	}

	protected abstract Document source();
}