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

import static de.srsoftware.document.mustang.Constants.*;
import static de.srsoftware.document.mustang.Constants.KEY_DATA;
import static de.srsoftware.document.mustang.Constants.KEY_PDF;
import static de.srsoftware.document.mustang.Constants.KEY_PRODUCER;
import static de.srsoftware.document.mustang.Constants.KEY_TEMPLATE;
import static de.srsoftware.document.zugferd.data.UnitCode.*;
import static de.srsoftware.tools.MimeType.MIME_PDF;
import static de.srsoftware.tools.Optionals.isSet;
import static de.srsoftware.tools.Strings.escapeHtmlEntities;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.Errors.ADDRESS_MISSING;
import static de.srsoftware.umbrella.core.ModuleRegistry.*;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Field.AMOUNT;
import static de.srsoftware.umbrella.core.constants.Field.COMPANY;
import static de.srsoftware.umbrella.core.constants.Field.CUSTOMER;
import static de.srsoftware.umbrella.core.constants.Field.DOCUMENT;
import static de.srsoftware.umbrella.core.constants.Field.NUMBER;
import static de.srsoftware.umbrella.core.constants.Field.SENDER;
import static de.srsoftware.umbrella.core.constants.Field.STATE;
import static de.srsoftware.umbrella.core.constants.Field.TYPE;
import static de.srsoftware.umbrella.core.constants.Field.UNIT;
import static de.srsoftware.umbrella.core.constants.Field.UNIT_PRICE;
import static de.srsoftware.umbrella.core.constants.Field.USER;
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.Document.State.NEW;
import static de.srsoftware.umbrella.core.model.Document.State.SENT;
import static de.srsoftware.umbrella.core.model.Translatable.t;
import static de.srsoftware.umbrella.documents.Constants.*;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.WARNING;
import static java.net.HttpURLConnection.*;
import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.toMap;

import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.document.api.Content;
import de.srsoftware.document.api.DocumentRegistry;
import de.srsoftware.document.api.RenderError;
import de.srsoftware.document.files.DocumentDirectory;
import de.srsoftware.document.processor.latex.LatexFactory;
import de.srsoftware.document.processor.weasyprint.WeasyFactory;
import de.srsoftware.document.zugferd.ZugferdFactory;
import de.srsoftware.document.zugferd.data.*;
import de.srsoftware.document.zugferd.data.Currency;
import de.srsoftware.tools.*;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.*;
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.core.model.Customer;
import de.srsoftware.umbrella.documents.model.*;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.json.JSONObject;

public class DocumentApi extends BaseHandler implements DocumentService {
	private static final Predicate<de.srsoftware.document.api.Document> ZUGFERD_FILTER = document -> document.id().equals("Zugferd");
	private final DocumentRegistry registry = new DocumentRegistry();

	private final Configuration config;
	private final DocumentDb db;

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

		Optional<String> templates = config.get(CONFIG_TEMPLATES);
		if (templates.isEmpty()) throw missingField(CONFIG_TEMPLATES);
		this.registry.add(new DocumentDirectory(new File(templates.get())));
		this.registry.add(new TemplateProcessor(), new LatexFactory(), new WeasyFactory(), new ZugferdFactory());

		this.registry.documents().forEach(d -> LOG.log(DEBUG,"found template: {0}",d));
	}

	private DocumentData convert(Document document) throws UmbrellaException {
		var currency = switch (document.currency()){
			case "€" -> Currency.EUR;
			case "$" -> Currency.USD;
			default -> throw unprocessable("Unsupported currency: {currency}",CURRENCY,document.currency());
		};
		var typeCode = switch (document.type().name()){
			case "invoice" -> TypeCode.HANDELSRECHNUNG;
			default -> throw unprocessable("Unsupported document type: {type}",TYPE,document.type().name());
		};
		var countryID = CountryCode.DE;
		LOG.log(WARNING,"countryID is hardcoded to be \"DE\", should be field of company!");
		var sender = document.sender();

		var match = POST_CODE.matcher(sender.name());
		if (!match.find()) throw unprocessable(ADDRESS_MISSING, OBJECT, t(Text.SENDER));
		var name = match.group(1).trim();
		var streetAddress = match.group(2).trim();
		var postCode = match.group(3);
		var city = match.group(4).trim();
		var taxNumber = sender.taxNumber();
		if (!taxNumber.startsWith("DE")) throw unprocessable("Invalid sender tax number ({number})!", NUMBER,taxNumber);
		var author = new Author(name,countryID,postCode,city,streetAddress,taxNumber);

		match = POST_CODE.matcher(document.customer().name());
		if (!match.find()) throw unprocessable(ADDRESS_MISSING, OBJECT,t(Text.CUSTOMER));
		name = escapeHtmlEntities(match.group(1).trim());
		streetAddress = escapeHtmlEntities(match.group(2).trim());
		postCode = match.group(3);
		city = match.group(4).trim();
		var customer = new de.srsoftware.document.zugferd.data.Customer(name,countryID,postCode,streetAddress,city);
		var footer = document.footer();
		var head = document.head();

		var notes = new ArrayList<String>();
		if (!head.isBlank()) notes.add(head);
		if (!footer.isBlank()) notes.add(footer);
		LocalDate deliveryDate;
		try {
			deliveryDate = LocalDate.parse(document.delivery());
		} catch (RuntimeException ex) {
			throw unprocessable("\"{date}\" is not a valid delivery date (cannot be parsed)!", DATE,document.delivery());
		}
		var lineItems = new ArrayList<LineItem>();
		for (var entry : document.positions().entrySet()){
			var number = entry.getKey();
			var pos = entry.getValue();

			UnitCode unit = switch (pos.unit()){
				case "d" -> per_day;
				case "h", "Stunden" -> hours;
				case "jährlich" -> per_year;
				case "pauschal" -> fixed;
				case "Stück" -> pieces;
				default -> throw unprocessable("No unit code defined for {unit}",UNIT,pos.unit());
			};
			var percent = pos.tax();
			var taxType = percent == 0 ? TaxType.Z : TaxType.S;
			LOG.log(WARNING,"tax type is hardcoded to be \"{0}\", should be field of document / document position!",taxType.name());

			var taxSet = new LineItemTaxSet(percent*100L,taxType);
			// bei Gutschriften soll die Menge negativ sein, nicht aber der Einheitspreis. Deshalb wird das ggf. invertiert.
			var neg = pos.unitPrice() < 0;
			var unitPrice = (neg ? -1 : 1) * pos.unitPrice();
			var amount = (neg ? -1 : 1) * pos.amount();
			lineItems.add(new LineItem(number,pos.itemCode(),pos.title(),pos.description(),null,unitPrice,unit,amount,taxSet));
		}
		String terms = footer;
		return new DocumentData(document.number(),currency,typeCode,document.date(),author,customer,notes,deliveryDate,null,terms,lineItems);
	}

	private boolean deleteDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
		var doc = db.loadDoc(docId);
		var companyId = doc.companyId();
		if (!companyService().membership(companyId,user.id())) throw notAmember(Translatable.t(COMPANY_WITH_ID, ID,doc.companyId()));
		if (doc.state() != NEW) throw new UmbrellaException(HTTP_BAD_REQUEST,"This document has already been sent");
		return sendContent(ex,db.deleteDoc(docId));
	}

	private boolean deletePosition(HttpExchange ex, long docId, UmbrellaUser user) throws UmbrellaException, IOException {
		var doc = db.loadDoc(docId);
		var companyId = doc.companyId();
		if (!companyService().membership(companyId,user.id())) throw notAmember(Translatable.t(COMPANY_WITH_ID, ID,doc.companyId()));
		if (doc.state() != NEW) throw new UmbrellaException(HTTP_BAD_REQUEST,"This document has already been sent");
		var json = json(ex);
		if (!(json.has(POSITION) && json.get(POSITION) instanceof Number number)) throw missingField(POSITION);
		db.dropPosition(docId,number.longValue());
		return send(ex,db.loadDoc(docId).positions());
	}

	@Override
	public Map<Long, Map<Long, String>> docsReferencedByTimes(Set<Long> timeIds) throws UmbrellaException {
		return db.docReferencedByTimes(timeIds);
	}

	@Override
	public boolean doDelete(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();
			long docId = Long.parseLong(head);
			return switch (path.pop()){
				case POSITION -> deletePosition(ex,docId,user.get());
				case null -> deleteDocument(ex,docId,user.get());
				default -> super.doDelete(path,ex);
			};
		} catch (NumberFormatException ignored) {
			return super.doDelete(path,ex);
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	@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 PATH_TYPES -> getDocTypes(ex);
				case STATES -> getDocStates(ex);
				case null -> super.doGet(path,ex);
				default -> {
					var docId = Long.parseLong(head);
					head = path.pop();
					yield switch (head){
						case null -> getDocument(ex,docId,user.get());
						case PATH_PDF -> getRenderedDocument(ex,docId,user.get());
						case Path.SETTINGS -> getDocumentSettings(ex,docId,user.get());
						default -> super.doGet(path,ex);
					};
				}
			};
		} catch (NumberFormatException ignored) {
			return 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();
			var docId = Long.parseLong(head);
			head = path.pop();
			return switch (head){
				case POSITION -> patchDocumentPosition(docId,user.get(),ex);
				case null -> patchDocument(docId,user.get(),ex);
				default -> super.doPatch(path,ex);
			};
		} catch (NumberFormatException n){
			return sendContent(ex,HTTP_UNPROCESSABLE,"Invalid document id");
		} 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 -> listCompaniesDocuments(ex,user.get());
				case SEARCH -> postSearch(ex,user.get());
				case TEMPLATES -> postTemplateList(ex,user.get());
				case null -> postDocument(ex,user.get());
				default -> postToDocument(ex,path,user.get(),Long.parseLong(head));
			};
		} catch (NumberFormatException ignored) {
			return super.doPost(path,ex);
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	private boolean getDocStates(HttpExchange ex) throws IOException {
		var map = Stream.of(Document.State.values()).collect(toMap(Document.State::code, Document.State::name));
		return sendContent(ex,map);
	}

	private boolean getDocTypes(HttpExchange ex) throws UmbrellaException, IOException {
		var types = db.listTypes();
		var map = types.values().stream().collect(toMap(Type::id, Type::name));
		return sendContent(ex,map);
	}

	private boolean getDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
		var doc = getDocumentWithCompanyData(docId,user);
		return sendContent(ex,doc.renderToMap());
	}

	private Tuple<Document,Company> getDocument(long docId, UmbrellaUser user) throws UmbrellaException {
		var doc = db.loadDoc(docId);
		var companyId = doc.companyId();
		var company = companyService().get(companyId);
		if (!companyService().membership(companyId,user.id())) throw notAmember(Translatable.t(COMPANY_WITH_ID,ID,company.name()));
		return Tuple.of(doc,company);
	}

	private boolean getDocumentSettings(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
		var tuple = getDocument(docId,user);
		var doc = tuple.a;
		var company = tuple.b;
		var settings = db.getCustomerSettings(company.id(),doc.type(),doc.customer().id());
		return sendContent(ex,settings);
	}

	private Document getDocumentWithCompanyData(long docId, UmbrellaUser user) throws UmbrellaException {
		var tuple = getDocument(docId,user);
		var company = tuple.b;
		var sep = company.decimalSeparator();

		var doc = tuple.a;
		if (sep != null) doc.setDecimalSeparator(sep);
		doc.setCompanyName(company.name());

		return doc;
	}

	private boolean getRenderedDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
		var document = getDocumentWithCompanyData(docId,user);
		var content  = renderDocument(document,user);
		var headers  = ex.getResponseHeaders();
		headers.add(CONTENT_TYPE, MIME_PDF);
		headers.add(CONTENT_DISPOSITION,"attachment; filename=\""+document.number()+".pdf\"");
		return sendContent(ex,content.bytes());
	}

	public Map<Long, Document> list(long companyId) throws UmbrellaException{
		return db.listDocs(companyId);
	}

	private boolean listCompaniesDocuments(HttpExchange ex, UmbrellaUser user) throws UmbrellaException {
		try {
			var json = json(ex);
			if (!json.has(COMPANY)) throw missingField(COMPANY);
			long companyId = json.getLong(COMPANY);
			var company    = companyService().get(companyId);
			if (!companyService().membership(companyId,user.id())) throw notAmember(Translatable.t(COMPANY_WITH_ID,ID,company.name()));
			var docs = list(companyId);
			var map  = new HashMap<Long,Object>();
			for (var entry : docs.entrySet()) map.put(entry.getKey(),entry.getValue().summary());
			return sendContent(ex,new JSONObject(map).toString(2));
		} catch (IOException e) {
			LOG.log(WARNING,"Failed to parse JSON data from request",e);
			throw new UmbrellaException( 500,"Failed to parse JSON data from request").causedBy(e);
		}
	}

	private boolean patchDocument(long docId, UmbrellaUser user, HttpExchange ex) throws UmbrellaException, IOException {
		var doc = getDocument(docId,user).a;
		var data = json(ex);
		doc.patch(data);
		if (doc.isDirty(FOOTER,HEAD)) {
			var settings = db.getCustomerSettings(doc.companyId(),doc.type(),doc.customer().id());
			if (settings == null) settings = CustomerSettings.empty();
			db.save(doc.companyId(),doc.type(),doc.customer().id(), settings.patch(data));
		}
		var stateChanged = doc.isDirty(STATE);
		db.save(doc);
		if (stateChanged) updateTimes(doc);
		return ok(ex);
	}

	private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException {
		var template = document.template();
		var templateName = template+".html.pdf";
		var type = document.type().name();
		var zugferd = "invoice".equals(type);
		Predicate<de.srsoftware.document.api.Document> filter = zugferd ? ZUGFERD_FILTER : doc -> doc.name().equals(templateName);
		var optDoc = registry.documents()
				.filter(filter)
				.findAny();
		if (optDoc.isEmpty()) throw UmbrellaException.notFound("Cannot render {type} {id}: Missing template \"{template}\"",TYPE,type, ID,document.number(),TEMPLATE,template);
		Function<String,String> translate = text -> translator().translate(user.language(),text);
		var pdfData = new HashMap<String,Object>();
		pdfData.put(DOCUMENT,document.renderToMap());
		pdfData.put("translate",translate);
		pdfData.put(USER,user);
		pdfData.put(PRICE_FORMAT, priceFormat(document.currency(),user.language()));
		Map<String,Object> data = pdfData;
		if (zugferd) {// create additional data
			data = Map.of(
					KEY_XML,Map.of(
							KEY_PROFILE,"EN16931",
							KEY_DATA,convert(document)
					),
					KEY_PDF,Map.of(
							KEY_TEMPLATE, templateName,
							KEY_DATA, pdfData
					),
					KEY_PRODUCER,"SRSoftware Umbrella"
			);
		}
		var source   =  optDoc.get();
		var rendered = source.render(data);
		if (rendered instanceof RenderError err) throw new UmbrellaException(500,"Failed to render {0}: {1}",source.name(),err.toString());
		if (!(rendered instanceof Content content)) throw new UmbrellaException(500,"Unknown result type ({0}) returned from render process!",rendered.getClass().getSimpleName());

		return content;

	}

	private boolean send(HttpExchange ex,PositionList positions) throws IOException {
		return sendContent(ex,positions.entrySet().stream().collect(toMap(Map.Entry::getKey,entry -> entry.getValue().renderToMap())));
	}

	private boolean patchDocumentPosition(long docId, UmbrellaUser user, HttpExchange ex) throws UmbrellaException, IOException {
		var doc = getDocument(docId,user).a;
		if (doc.state() != NEW) throw forbidden("Document has already been send and is write-protected!");
		var json = json(ex);
		var step = json.has(MOVE) && json.get(MOVE) instanceof Number num ? num.intValue() : 0;
		Integer number = json.has(POSITION) && json.get(POSITION) instanceof Number num ? num.intValue() : null;
		if (isSet(number) && step != 0) {
			var pos2 = number+step;
			if (number >0 && pos2>0 && number <=doc.positions().size() && pos2<=doc.positions().size()){
				db.switchPositions(docId,new Pair<>(number,pos2));
				doc = db.loadDoc(docId);
			}
		}

		return send(ex,db.save(doc).positions());
	}

	private boolean postCloneDoc(long docId, HttpExchange ex, UmbrellaUser user) throws IOException {
		Type docType = null;
		try {
			docType = db.getType(Integer.parseInt(body(ex)));
		} catch (NumberFormatException nfe){
			throw invalidField(BODY,DOCUMENT_TYPE_ID);
		}

		Document doc = getDocument(docId, user).a;

		var nextNumber = db.nextDocId(user.language(),doc.companyId(),docType);

		Document clone = new Document(
				0,
				doc.companyId(),
				nextNumber,
				docType,
				LocalDate.now(),
				NEW,
				doc.template(),
				doc.delivery(),
				doc.head(),
				doc.footer(),
				doc.currency(),
				doc.decimalSeparator(),
				doc.sender(),
				doc.customer(),
				new PositionList()
		);
		clone = db.save(clone);
		doc.positions().values().forEach(clone.positions()::add);
		return sendContent(ex,db.save(clone));
	}

	private boolean postDocument(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
		var json = json(ex);
		if (!(json.has(SENDER) && json.get(SENDER) instanceof JSONObject senderData)) throw missingField(SENDER);
		if (!senderData.has(COMPANY) || !(senderData.get(COMPANY) instanceof Number rawCompId)) throw missingField(COMPANY);
		var companyId = rawCompId.longValue();
		var company = companyService().get(companyId);
		if (!companyService().membership(companyId,user.id())) throw notAmember(Translatable.t(COMPANY_WITH_ID,ID,company.name()));

		if (!json.has(CUSTOMER) || !(json.get(CUSTOMER) instanceof JSONObject customerData)) throw missingField(CUSTOMER);
		if (!json.has(TYPE) || !(json.get(TYPE) instanceof Number docTypeId)) throw missingField(TYPE);
		var type = db.getType(docTypeId.intValue());
		var customer = Customer.of(customerData);
		String currency = company.currency();
		String sep = company.decimalSeparator();
		var settings = db.getCustomerSettings(companyId,type,customer.id());
		var newCustomer = settings == null;
		if (newCustomer) settings = CustomerSettings.empty();
		var nextNumber = db.nextDocId(user.language(), companyId,type);
		String lastHead = settings.header();
		String lastFooter = settings.footer();
		var sender = Sender.of(senderData);
		LOG.log(DEBUG,json.toString(2));
		var doc = new Document(0,companyId,nextNumber,type, LocalDate.now(), NEW,null,null,lastHead,lastFooter,currency,sep,sender,customer,new PositionList());
		var saved = db.save(doc);
		if (newCustomer) {
			if (customerData.get(CONTACT_ID) instanceof Number contactId) {
				var contacts = contactService();
				var contact = contacts.load(user.id(), contactId.longValue());
				contacts.save(contact.setCustomerNumber(customer.id()));
			}
			companyService().saveNewCustomer(companyId,customer.id());
		}
		return sendContent(ex,saved);
	}

	private boolean postDocumentPosition(long docId, HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
		var doc = getDocument(docId,user).a;

		var json = json(ex);
		if (!(json.has(AMOUNT) && json.get(AMOUNT) instanceof Number amount)) throw missingField(AMOUNT);
		if (!(json.has(DESCRIPTION) && json.get(DESCRIPTION) instanceof String description)) throw missingField(DESCRIPTION);
		if (!(json.has(ITEM_CODE) && json.get(ITEM_CODE) instanceof String itemCode)) throw missingField(ITEM_CODE);
		if (!(json.has(TITLE) && json.get(TITLE) instanceof String title)) throw missingField(TITLE);
		if (!(json.has(UNIT) && json.get(UNIT) instanceof String unit)) throw missingField(UNIT);
		var unitPrice = json.has(UNIT_PRICE) && json.get(UNIT_PRICE) instanceof Number num ? num : 0L;
		try {
			unitPrice = db.getCustomerPrice(doc.companyId(),doc.customer().id(),itemCode);
		} catch (UmbrellaException ignored) {}
		int tax = json.has(TAX) && json.get(TAX) instanceof Number t ? t.intValue() : 19; // TODO should not be hard-coded
		Long timeId = json.has(TIME_ID) && json.get(TIME_ID) instanceof Number t ? t.longValue() : null;
		var pos = new Position(doc.positions().size()+1,itemCode,amount.doubleValue(),unit,title,description,unitPrice.longValue(),tax,timeId,false);
		doc.positions().add(pos);

		return send(ex,db.save(doc).positions());
	}

	private boolean postTemplateList(HttpExchange ex, UmbrellaUser user) throws UmbrellaException, IOException {
		var json = json(ex);
		if (!(json.has(COMPANY) && json.get(COMPANY) instanceof Number companyId)) throw missingField(COMPANY);
		var company = companyService().get(companyId.longValue());
		if (!companyService().membership(companyId.longValue(),user.id())) throw notAmember(Translatable.t(COMPANY_WITH_ID,ID,company.name()));
		var templates = registry.documents()
				.filter(d -> d.name().endsWith(".template"))
				.map(d -> d.name().replaceAll("(\\.[^.]+)?\\.template$",""));
		return sendContent(ex,templates);
	}

	private boolean postSearch(HttpExchange ex, UmbrellaUser user) 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 userCompanyIds = companyService().listCompaniesOf(user).keySet();

		var documents = db.find(userCompanyIds,keys,fulltext);
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		return sendContent(ex,mapValues(documents));
	}

	private boolean postToDocument(HttpExchange ex, de.srsoftware.tools.Path path, UmbrellaUser user, long docId) throws IOException, UmbrellaException {
		var head = path.pop();
		return switch (head){
			case Path.CLONE -> postCloneDoc(docId,ex,user);
			case POSITION -> postDocumentPosition(docId,ex,user);
			case PATH_SEND -> sendDocument(ex,path,user,docId);
			case null, default -> super.doPost(path,ex);
		};
	}

	private PriceFormat priceFormat(String currency, String language) {
		var pattern = switch (currency){
			case "$" -> "$&nbsp;{0,number,#,###.00}";
			default -> "{0,number,#,###.00}&nbsp;"+currency;
		};
		var message = new MessageFormat(pattern, "de".equals(language)? Locale.GERMAN:Locale.US);
		return val -> message.format(new Object[]{val/100d});
	}

	private boolean sendDocument(HttpExchange ex, de.srsoftware.tools.Path path, UmbrellaUser user, long docId) throws IOException, UmbrellaException {
		var doc = getDocumentWithCompanyData(docId,user);
		var rendered = renderDocument(doc,user);
		var json = json(ex);
		if (!(json.has(EMAIL) && json.get(EMAIL) instanceof String email)) throw missingField(EMAIL);
		if (!(json.has(SUBJECT) && json.get(SUBJECT) instanceof String subject)) throw missingField(SUBJECT);
		if (!(json.has(CONTENT) && json.get(CONTENT) instanceof String content)) throw missingField(CONTENT);
		var settings = new CustomerSettings(doc.head(),doc.footer(),content);
		try {
			db.save(settings,doc);
		} catch (UmbrellaException e) {
			LOG.log(WARNING,e);
		}
		var attachment = new Attachment(doc.number()+".pdf",rendered.mimeType(),rendered.bytes());
		var message = new TranslatedMessage(user,subject,content,List.of(attachment));
		var envelope = new Envelope<>(0, message,new User(doc.customer().shortName(),new EmailAddress(email),doc.customer().language()));
		postBox().send(envelope);
		db.save(doc.set(SENT));
		updateTimes(doc);
		return ok(ex);
	}

	private void updateTimes(Document doc) {
		var timeState = switch (doc.state()){
			case DELAYED, SENT -> Time.State.Pending;
			case ERROR -> Time.State.Open;
			case DECLINED, NEW -> Time.State.Open;
			case PAYED -> Time.State.Complete;
		};
		var timeIds = doc.positions().values().stream().map(Position::timeId).filter(not(Optionals::is0)).toList();
		timeService().updateStates(timeIds,timeState);
	}
}
