import java.util.concurrent.Callable
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit

import groovy.transform.TupleConstructor

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.github.fge.jsonschema.main.JsonSchemaFactory

import org.apache.http.HttpStatus
import org.apache.http.client.config.CookieSpecs
import org.apache.http.client.config.RequestConfig
import org.apache.http.client.methods.HttpHead
import org.apache.http.impl.client.HttpClients
import org.yaml.snakeyaml.LoaderOptions
import org.yaml.snakeyaml.Yaml

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.8.1'
        classpath group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.8.8'
        classpath group: 'com.github.fge', name: 'json-schema-validator', version: '2.2.6'
        classpath group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.3'
        classpath group: 'org.yaml', name: 'snakeyaml', version: '1.18'
    }
}

@TupleConstructor
class UriProbe {
    String uri
    Future<Integer> future
}

def checkUriProbes(probes) {
    probes.forEach {
        try {
            def statusCode = it.future.get()
            if (statusCode != HttpStatus.SC_OK) {
                throw new GradleException("$it.uri: map resource not available ($statusCode)")
            }
        } catch (ExecutionException e) {
            throw new GradleException("$it.uri: error requesting map resource ($e.cause.message)", e.cause)
        }
    }
}

def newHttpClient() {
    return HttpClients.custom()
            .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES).build())
            .build()
}

def parseMapsYamlUris(yamlFile) {
    def yaml = new Yaml()
    def maps = yaml.load(yamlFile.text)
    def downloadUris = maps.collect { it['url'] }
    def thumbnailUris = maps.collect { it['img'] }
    return (downloadUris + thumbnailUris).findAll { it != null }
}

def probeUris(uris) {
    def maxInFlightProbes = 5
    def aggregateProbeTimeoutInMinutes = 5

    newHttpClient().withCloseable { client ->
        def probes = []

        def executor = Executors.newFixedThreadPool(maxInFlightProbes)
        uris.each { uri ->
            probes << new UriProbe(uri, executor.submit({
                def request = new HttpHead(uri)
                client.execute(request).withCloseable { response ->
                    def statusCode = response.statusLine.statusCode
                    logger.info("URI probe result for '$uri': $statusCode")
                    return statusCode
                }
            } as Callable))
        }
        executor.shutdown()
        if (!executor.awaitTermination(aggregateProbeTimeoutInMinutes, TimeUnit.MINUTES)) {
            throw new GradleException("timed out waiting for URI probes to complete")
        }

        return probes
    }
}

// Temporary until SnakeYAML defaults to no duplicate keys per YAML 1.2 spec
// https://bitbucket.org/asomov/snakeyaml/issues/337/option-to-disallow-duplicate-keys
def validateHasNoDuplicateKeys(yamlFile) {
    def loaderOptions = new LoaderOptions()
    loaderOptions.setAllowDuplicateKeys(false)
    def yaml = new Yaml(loaderOptions)
    try {
        yamlFile.withInputStream { yaml.load(it) }
    } catch (Exception e) {
        throw new GradleException("$yamlFile: ${e.message}", e)
    }
}

def validateSchema(yamlFile, jsonSchemaFile) {
    def yamlMapper = new ObjectMapper(new YAMLFactory())
    def yaml = yamlMapper.readTree(yamlFile)

    def jsonMapper = new ObjectMapper()
    def yamlSchema = jsonMapper.readTree(jsonSchemaFile)
    def schema = JsonSchemaFactory.byDefault().getJsonSchema(yamlSchema)

    def report = schema.validate(yaml, true)
    if (!report.success) {
        throw new GradleException("$yamlFile: $report")
    }
}

ext.validateMapsYamlUris = { yamlFile ->
    def uris = parseMapsYamlUris(yamlFile)
    def probes = probeUris(uris)
    checkUriProbes(probes)
}

ext.validateYaml = { yamlFile, jsonSchemaFile ->
    validateHasNoDuplicateKeys(yamlFile)
    validateSchema(yamlFile, jsonSchemaFile)
}
