package com.jdblabs.timestamper.core

import com.jdbernard.util.SmartConfig
import groovyx.net.http.HTTPBuilder
import java.text.SimpleDateFormat
import java.util.UUID;

import static groovyx.net.http.ContentType.*
import static groovyx.net.http.Method.*

public class JDBLabsWebTimelineSource extends TimelineSource {

    private static final String CONFIG_UNAME = "web.username"
    private static final String CONFIG_PWD = "web.password"
    private static final String CONFIG_TIMELINE = "web.timelineId"

    private static isoDateFormat

    static {
        isoDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
        isoDateFormat.timeZone = TimeZone.getTimeZone("UTC")
    }

    private String username
    private String password
    private String timelineId

    private URI baseUri
    private HTTPBuilder http

    private Map serverEntryIds

    public JDBLabsWebTimelineSource(URI uri, SmartConfig config) {
        super(uri)

        // load web-timeline specific properties
        username = config.getProperty(CONFIG_UNAME, "")
        password = config.getProperty(CONFIG_PWD, "")
        timelineId = config.getProperty(CONFIG_TIMELINE, "")

        baseUri = new URI(uri.scheme + "://" + uri.authority + "/ts_api/")
        http = new HTTPBuilder(baseUri)

        // set some default error handlers
        http.handler.'500' = { resp, json ->
            throw new IOException("The web timeline reported an internal " +
                "error: ${json.error ?: 'no details available'}") }

        http.handler.failure = {resp, json ->
            throw new IOException("Unable to complete the operation: error " +
                "communicating to the web timeline.\n" +
                "${resp.statusLine}: ${json}") }

        // init our map of known entries
        serverEntryIds = [:]
    }

    public Timeline read() {
        def timelineJSON
        def entryListJSON
        def startDate, endDate
        Timeline timeline

        // make sure we have a fresh session
        authenticate()

        // load the timeline information
        http.request(GET, JSON) {
            uri.path = "timelines/${username}/${timelineId}"

            response.'404' = { resp ->
                throw new IOException("No timeline '${timelineId}' for user " +
                    "'${username}'") }

            response.success = { resp, json -> timelineJSON = json }
        }

        timeline = new Timeline()

        // create start and end times to use as parameters to the web service
        startDate = Calendar.getInstance()
        startDate.timeInMillis = 0  // Beginning of the epoch
        startDate = isoDateFormat.format(startDate.time)

        // Until now
        endDate = isoDateFormat.format(Calendar.getInstance().time)

        // load the timeline entries
        entryListJSON = http.get(
            path: "entries/${username}/${timelineId}",
            contentType: JSON,
            query: [
                byDate: true,
                startDate: startDate,
                endDate: endDate ] ) { resp, json -> json }

        entryListJSON.each { entry ->
            
            // parse and create the timeline marker
            def timestamp = isoDateFormat.parse(entry.timestamp)
            def marker = new TimelineMarker(timestamp, entry.mark, entry.notes,
                UUID.fromString(entry.uuid));

            // add it to the timeline and our map of hashes
            timeline.addMarker(marker)
            serverEntryIds[marker.uuid] = entry.id
        }

        // return the created timeline
        return timeline
    }

    public void persist(Timeline t) {
        Map deletedEntries
        List newEntries
        
        // make sure we have a fresh session
        authenticate()

        // Find differences since we last persisted.
        
        // We want to identify the set of entries that were previously present
        // on the server but are no longer present locally and delete them from
        // the server. We start with the set of all entries last seen on the
        // server. We look through our local entries and if we find the entry
        // locally we remove it from our set of deletion candidates.
        
        // We will also be constructing a set of entries that we have locally
        // which were not present on the server. This set of entries will be
        // added to the server.

        deletedEntries = serverEntryIds.clone()    // shallow copy
        newEntries = []
        
        t.each { marker ->
            // This marker is present on the server and is still present
            // locally, remove from deletedEntries
            if (deletedEntries.containsKey(marker.uuid)) {
                deletedEntries.remove(marker.uuid) }

            // This marker is not present on the server, add to newEntries.
            else { newEntries << marker }
        }

        // Delete all entries that used to be present but are not any longer
        deletedEntries.each { uuid, entryId ->
            http.request(DELETE) {
                uri.path = "entries/${username}/${timelineId}/${entryId}"

                response.'404' = { resp -> serverEntryIds.remove(uuid) }
                response.success = { resp -> serverEntryIds.remove(uuid) }
            }
        }

        // add all new entries
        newEntries.each { entry ->

            def entryBody = [
                mark: entry.mark,
                notes: entry.notes,
                uuid: entry.uuid.toString(),
                timestamp: isoDateFormat.format(entry.timestamp)
            ]

            http.request(POST, JSON) {
                uri.path = "entries/${username}/${timelineId}"
                requestContentType = JSON
                body = entryBody

                response.success = { resp, json ->
                    serverEntryIds.put(entry.uuid, json?.id ?: 0) }
            }
        }
    }

    public boolean isAuthenticated() { return false; }

    public void authenticate() throws AuthenticationException {
        http.request(POST, JSON) { req ->
            uri.path = '/ts_api/login'
            requestContentType = JSON
            body = [ username: this.username,
                    password: this.password ]

            response.'401' = { resp ->
                throw new AuthenticationException(
                    "Unable to connect to ${baseUri}: " +
                    "Invalid username/password combination.") }
        }
    }

    protected int fullHash(TimelineMarker tm) {
        return 61 * tm.hashCode() + (tm.mark != null ? tm.mark.hashCode() : 0);
    }
}
