import com.dvelop.d3.server.core.D3Interface;
import com.dvelop.d3.server.Document;
import com.dvelop.d3.server.User;
import com.dvelop.d3.server.DocumentType;
import com.dvelop.d3.server.ValueSet;
import com.dvelop.d3.server.Entrypoint;
import com.dvelop.d3.server.Condition;
import com.dvelop.d3.server.RepositoryField;

import groovy.transform.Field;
import groovy.json.JsonSlurper;
import groovy.sql.GroovyRowResult;
import java.net.http.*;
import java.net.URLEncoder;

// Duesseldorf Maps API
@Field final int MAX_RESULTS = 10;
@Field final String URL_MAPS_DOMAIN = 'maps.itk.local';
@Field final String URL_PATH_STREET_SEARCH = '/viewer/php/search/search.php'; 

// GeoServer API
@Field final String URL_GDI_DOMAIN = 'gdi.itk.local';
@Field final String URL_GDI_PATH_LIEKA = '/services/lieka/wfs';

// D3
@Field final String DOKART_KUERZEL = 'DSTRA';
@Field final int STRASSENNAME_POS = 22;
@Field final int HAUSNUMMER_POS = 24;
@Field final int STADTTEILNAME_POS = 29;
@Field final int BEZIRK_POS = 30;
@Field final int PLZ_POS = 34;
@Field final int HISTORIE_POS = 60;

@ValueSet(entrypoint = 'itk_STWM_strasse')
def itk_STWM_strasse(D3Interface d3, RepositoryField reposField, User user, DocumentType docType, int rowNo, int validate, Document doc){
    def input = doc.docFieldAsString(STRASSENNAME_POS);
    if (input == null || input.length() <= 2) {
        reposField.provideValuesForValueSet(['Mindestens 3 Zeichen eingeben!']);
        return 1;
    }

    def uri = buildUri(
        URL_MAPS_DOMAIN,
        URL_PATH_STREET_SEARCH,
        ['p': "de,s,$MAX_RESULTS,$input"]
    );
    def response = sendGetRequest(uri);
    def statusCode = response.statusCode();
    if (statusCode < 200 || statusCode >= 300) {
        d3.log.error("Request an Maps fehlgeschlagen! (Status: $statusCode, URI: ${response.uri()})");
        d3.log.error("Response: ${response.body()}");
        reposField.provideValuesForValueSet(["Fehler bei Anfrage an Maps ($statusCode)"]);
        return 1;
    }

    def body = response.body();
    def jsonObj = parseJson(body);
    if (jsonObj.errors == null || jsonObj.errors.size() > 0) {
        d3.log.error("[itk_STWM] Response enthaelt Fehler: ${jsonObj}");
        reposField.provideValuesForValueSet(['Fehler bei Antwort von Maps. Bitte wenden Sie sich an den Support.']);
        return 1;
    }

    if (jsonObj.count == 0) {
        d3.log.info("[itk_STWM] Keine Ergebnisse fuer '$input'");
        reposField.provideValuesForValueSet(["Keine Ergebnisse fuer '$input' gefunden!"]);
        return 0;
    }

    // TODO: "Dietrich" führt auch zu "Am Deich", aber d3 zeigt das nicht an, da
    // Suchbegriff nicht enthalten. Erst bei unfocus + focus wird "Am Deich"
    // angezeigt.

    def results = jsonObj.data*.text;
    d3.log.info("[itk_STWM] Results: $results");
    reposField.provideValuesForValueSet(results);
    return 0;
}

def buildUri(def baseUrl, def path, def queryParams) {
    def parts = queryParams.collect{"${it.key}=${it.value}"};
    def query = String.join('&', parts);
    return new URI('https', baseUrl, path, query, null);
}

def sendGetRequest(def uri) {
    def request = HttpRequest.newBuilder()
        .GET()
        .uri(uri)
        .headers('Accept', 'application/json')
        .build();

    def client = HttpClient.newHttpClient();
    // wir erwarten nur JSON, von daher passt der BodyHandler
    return client.send(request, HttpResponse.BodyHandlers.ofString());
}

def parseJson(def json) {
    def slurper = new JsonSlurper();
    return slurper.parseText(json);
}

@ValueSet(entrypoint = 'itk_STWM_hausnummer')
def itk_STWM_hausnummer(D3Interface d3, RepositoryField reposField, User user, DocumentType docType, int rowNo, int validate, Document doc) {
    def strassenName = doc.docFieldAsString(STRASSENNAME_POS);
    if (strassenName == null || strassenName.isBlank()) {
        reposField.provideValuesForValueSet(['Bitte geben Sie zuerst die Strasse an.']);
        return 0;
    }

    def jsonObj = queryGeoServer(d3, 'hausnummer', ['hausnummer', 'zusatz'], "strasse='$strassenName'");
    if (jsonObj == null) {
        reposField.provideValuesForValueSet(['Fehler bei Anfrage an GeoServer!']);
        return 1;
    }

    if (jsonObj.totalFeatures == 0) {
        d3.log.info("[itk_STWM] Keine Hausnummern fuer '$strassenName' gefunden!");
        reposField.provideValuesForValueSet(["Keine Hausnummern fuer '$strassenName' gefunden!"]);
        return 0;
    }

    def input = doc.docFieldAsString(HAUSNUMMER_POS) ?: ''; // leeres Feld ist null, aber leerer String zum Filtern nötig
    def hausnummern = jsonObj.features.stream()
                                      .map{String.join(' ', "${it.properties.hausnummer}", "${it.properties.zusatz ?: ''}").trim()}
                                      .filter{it.startsWith(input)}
                                      .sorted{a, b -> compareHausnummer(a, b)}
                                      .collect();

    reposField.provideValuesForValueSet(hausnummern);
    return 0;
}

def queryGeoServer(def d3, def layer, def properties, def filter) {
    def uri = buildUri(
        URL_GDI_DOMAIN,
        URL_GDI_PATH_LIEKA,
        [
            'service': 'wfs',
            'version': '2.0.0',
            'request': 'GetFeature',
            'srsName': 'EPSG:25832',
            'typename': "lieka:$layer",
            'outputformat': 'application/json',
            'propertyname': String.join(',', properties),
            'cql_filter': filter
        ]
    );

    def response = sendGetRequest(uri);
    def statusCode = response.statusCode();
    if (statusCode < 200 || statusCode >= 300) {
        d3.log.error("[itk_STWM] Request an GeoServer fehlgeschlagen! (Status: $statusCode, URI: ${response.uri()})");
        d3.log.error("[itk_STWM] Response: ${response.body()}");
        return null;
    }

    def body = response.body();
    return parseJson(body);
}

def compareHausnummer(def a, def b) {
    // Parsen der Zahlen, da Sortierung '1', '11', '2', während 1, 2, 11 bei Ints
    def nummerA = Integer.parseInt(a.split(' ')[0]);
    def nummerB = Integer.parseInt(b.split(' ')[0]);
    def result = nummerA <=> nummerB;
    if (result != 0) {
        return result;
    }

    def zusatzA = a.contains(' ') ? a.split(' ')[1] : '';
    def zusatzB = b.contains(' ') ? b.split(' ')[1] : '';
    return zusatzA <=> zusatzB;
}

@ValueSet(entrypoint = 'itk_STWM_stadtteil')
def itk_STWM_stadtteil(D3Interface d3, RepositoryField reposField, User user, DocumentType docType, int rowNo, int validate, Document doc) {
    def stadtteile = queryByAddress(d3, doc, 'stadtteilname');
    reposField.provideValuesForValueSet(stadtteile);
    return 0;
}

def queryByAddress(def d3, def doc, def propertyName) {
    def strassenName = doc.docFieldAsString(STRASSENNAME_POS);
    def hausnummer = doc.docFieldAsString(HAUSNUMMER_POS);
    if (strassenName?.isBlank() || hausnummer?.isBlank()) {
        return ['Bitte geben Sie zuerst Strasse und Hausnummer an.'];
    }

    def filter = buildCqlFilter(doc);
    def jsonObj = queryGeoServer(d3, 'hausnummer', ['stadtteilnummer', 'stadtteilname', 'plz'], filter);
    if (jsonObj == null) {
        return ['Fehler bei Anfrage an GeoServer!'];
    }

    if (jsonObj.totalFeatures == 0) {
        return ["Kein Ergebnisse fuer '$strassenName $hausnummer' gefunden!"];
    }

    return jsonObj.features.collect{ feature -> feature.properties.get(propertyName)};
}

def buildCqlFilter(def doc) {
    def strassenName = doc.docFieldAsString(STRASSENNAME_POS);
    def hausnummerField = doc.docFieldAsString(HAUSNUMMER_POS);
    def parts = hausnummerField.split(' ');
    def hausnummer = parts[0];
    def zusatz = parts.size() == 2 ? parts[1] : '';
    return "strasse='$strassenName' and hausnummer=$hausnummer${!zusatz.isBlank() ? " and zusatz='$zusatz'" : ''}";
}

@ValueSet(entrypoint = 'itk_STWM_bezirk')
def itk_STWM_bezirk(D3Interface d3, RepositoryField reposField, User user, DocumentType docType, int rowNo, int validate, Document doc) {
    // stadtteilnummer = bezirk + zahl -> 123: bezirk = 12
    def bezirke = queryByAddress(d3, doc, 'stadtteilnummer').collect{it.intdiv(10)};  
    reposField.provideValuesForValueSet(bezirke);
    return 0;
}

@ValueSet(entrypoint = 'itk_STWM_plz')
def itk_STWM_plz(D3Interface d3, RepositoryField reposField, User user, DocumentType docType, int rowNo, int validate, Document doc) {
    def plz = queryByAddress(d3, doc, 'plz');
    reposField.provideValuesForValueSet(plz);
    return 0;
}

@Entrypoint(entrypoint = 'hook_validate_import_entry_10')
@Condition(doctype = 'DSTRA')
def validateStrassenImport(D3Interface d3, User user, DocumentType docType, Document doc, String nextCall) {
	// TODO: do some checks
	String strasse = doc.docFieldAsString(STRASSENNAME_POS);
	String hausnummer = doc.docFieldAsString(HAUSNUMMER_POS);
	if (strasse && hausnummer) {
        tryFillDerivedValues(d3, doc);
    }
	
	return 0
}

@Entrypoint(entrypoint = 'hook_insert_entry_10')
@Condition(doctype = 'DSTRA')
def validateStrassendokument(D3Interface d3, User user, DocumentType docType, Document doc, String nextcall) {
    def strassenName = doc.docFieldAsString(STRASSENNAME_POS);
    def hausnummer = doc.docFieldAsString(HAUSNUMMER_POS);
    if (strassenName && hausnummer) {
        tryFillDerivedValues(d3, doc);
    }

    // keine andere Validierung, damit Angaben außerhalb der API trotzdem noch möglich sind.
    return 0;
}

def tryFillDerivedValues(def d3, def doc) {
    def filter = buildCqlFilter(doc);
    def jsonObj = queryGeoServer(d3, 'hausnummer', ['stadtteilnummer', 'stadtteilname', 'plz'], filter);
    if (!jsonObj?.totalFeatures || jsonObj.totalFeatures != 1) {
        return;
    }

    def result = jsonObj.features[0];
    def stadtteilname = result.properties.stadtteilname;
    def bezirk = result.properties.stadtteilnummer.intdiv(10);
    def plz = result.properties.plz;
    doc.field[STADTTEILNAME_POS] = stadtteilname;
    doc.field[BEZIRK_POS] = String.valueOf(bezirk);
    doc.field[PLZ_POS] = String.valueOf(plz);
}

@Entrypoint(entrypoint = 'hook_validate_update_entry_10')
@Condition(doctype = 'DSTRA')
def validateStrassenUpdate(D3Interface d3, User user, DocumentType docType, Document doc) {
	// TODO: do some checks
	// TODO: wird beim update nochmal aufgerufen, also unnötige doppelte API request
	//       lässt sichd as irgendwie verhindern?
	clearCurrentValues(doc);
	
	String strasse = doc.docFieldAsString(STRASSENNAME_POS);
	String hausnummer = doc.docFieldAsString(HAUSNUMMER_POS);
	d3.log.error("$strasse $hausnummer")
	if (strasse && hausnummer) {
        tryFillDerivedValues(d3, doc);
    }
	
	return 0
}

/**
 * Leeren der aktuellen von Straße & Hausnummer abhängigen Werte, um 
 * "ungültiger Wert"-Fehler zu vermeiden.
 */
def clearCurrentValues(Document doc) {
	doc.docField(BEZIRK_POS, '');
	doc.docField(STADTTEILNAME_POS, '');
	doc.docField(PLZ_POS, '');
}

@Entrypoint(entrypoint = 'hook_upd_attrib_entry_20')
@Condition(doctype = 'DSTRA')
def logAddressChangesInHistory(D3Interface d3, Document doc, User user, DocumentType docTypeShort, DocumentType docTypeShortNew) {
	if (docTypeShort.getId() != docTypeShortNew.getId()) {
		// Dokumentartwechsel hat stattgefunden
		return 0;
	}
	
	String newStreet = doc.docFieldAsString(STRASSENNAME_POS);
	String newNumber = doc.docFieldAsString(HAUSNUMMER_POS);
	if (newStreet && newNumber) {
        tryFillDerivedValues(d3, doc);
    }
	
	String docId = doc.getNumber();
	String currAddress = getCurrentAddressFromDb(d3, docId);
	String newPlz = doc.docFieldAsString(PLZ_POS);
	String newAddress = "${newStreet} ${newNumber}, ${newPlz}"
	if (currAddress == null || currAddress == newAddress) {
		// no changes or address added
		return 0;
	}
	
	int multFieldIndex = getNextMultiFieldIndex(d3, docId, HISTORIE_POS);
	doc.docMultField(HISTORIE_POS, multFieldIndex, currAddress);
	return 0;
}

def getCurrentAddressFromDb(D3Interface d3, String docId) {
	String query = """\
		SELECT dok_dat_feld_${STRASSENNAME_POS} + ' ' +
		       dok_dat_feld_${HAUSNUMMER_POS} + ', ' +
			   dok_dat_feld_${PLZ_POS} AS address
		FROM firmen_spezifisch
		WHERE doku_id = ?
	""".stripIndent();
	
	GroovyRowResult result = d3.sql.firstRow(query, [docId]);
	if (!result || !result.address) {
		return null;
	}
	
	return result.address;
}

def getNextMultiFieldIndex(D3Interface d3, String docId, int fieldPos) {
	String query = """\
		SELECT ISNULL(MAX(row_number) + 1, 1) AS rownr 
		FROM firm_spez_mult_val 
		WHERE doku_id = ?  and field_no = ?
	""".stripIndent();
	
	GroovyRowResult result = d3.sql.firstRow(query, [docId, fieldPos]);
	if (!result) {
		return 1;
	}
	
	return result.rownr;
}