package cz.senslog.fetch.senslog.telemetry;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import cz.senslog.connector.fetch.api.ConnectorFetcher;
import cz.senslog.connector.model.api.GeoJsonModel;
import cz.senslog.connector.tools.http.HttpClient;
import cz.senslog.connector.tools.http.HttpRequest;
import cz.senslog.connector.tools.http.HttpResponse;
import cz.senslog.connector.tools.http.URLBuilder;
import cz.senslog.connector.tools.json.BasicJson;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Optional;

import static cz.senslog.connector.tools.http.HttpContentType.APPLICATION_GEO_JSON;
import static cz.senslog.connector.tools.http.HttpContentType.APPLICATION_JSON;
import static cz.senslog.connector.tools.http.HttpHeader.*;

public class SensLogTelemetryFetcher implements ConnectorFetcher<SensLogTelemetrySession, GeoJsonModel> {

    private static final Logger logger = LogManager.getLogger(SensLogTelemetryFetcher.class);

    private static final class CampaignInfo {
        int id;
        String name, description;
        OffsetDateTime fromTime, toTime;
    }

    private final SensLogTelemetryConfig config;

    private final HttpClient httpClient;

    private CampaignInfo campaignInfo;

    public SensLogTelemetryFetcher() {
        this(null, null);
    }

    public SensLogTelemetryFetcher(SensLogTelemetryConfig config, HttpClient httpClient) {
        this.config = config;
        this.httpClient = httpClient;
    }

    @Override
    public void init() throws Exception {
        assert config != null; assert httpClient != null;

        HttpRequest request = HttpRequest.newBuilder().GET()
                .url(URLBuilder.newBuilder(config.getBaseUrl(), String.format("campaigns/%d", config.getCampaignId()))
                        .addParam("zone", "UTC")
                        .addParam("navigationLinks", "false")
                        .build())
                .header(ACCEPT, APPLICATION_JSON)
                .header(AUTHORIZATION, "Bearer " + config.getBearerToken())
                .build();

        HttpResponse response = httpClient.send(request);

        if (response.isError()) {
            throw logger.throwing(new RuntimeException(response.getBody()));
        }

        campaignInfo = BasicJson.jsonToObject(response.getBody(), CampaignInfo.class);
        if (campaignInfo.id != config.getCampaignId()) {
            throw logger.throwing(new RuntimeException("Invalid campaign id: " + config.getCampaignId()));
        }

        logger.info("Initialized fetching telemetries from the campaign '{}'", campaignInfo.name);
    }

    @Override
    public GeoJsonModel fetch(Optional<SensLogTelemetrySession> sessionOpt) {

        final OffsetDateTime now = OffsetDateTime.ofInstant(Instant.now(), ZoneOffset.UTC);

        SensLogTelemetrySession session = sessionOpt.orElse(null);
        assert session != null;

        if (session.hasNotNext()) {
            if (session.getFromTime() == null) {
                if (config.getStartAt() != null) {
                    session.setFromTime(OffsetDateTime.of(config.getStartAt(), ZoneOffset.UTC));
                } else {
                    session.setFromTime(campaignInfo.fromTime);
                }
                session.setToTime(session.getFromTime().plusHours(config.getInterval()));
            }

            if (session.getToTime().isAfter(now)) {
                logger.info("To early for the new data, the next interval is in {} minutes.", Duration.between(now, session.getToTime()).toMinutes());
                return GeoJsonModel.empty();
            }
        }

        HttpRequest request = HttpRequest.newBuilder().GET()
                .url(URLBuilder.newBuilder(config.getBaseUrl(), String.format("campaigns/%d/units/observations", config.getCampaignId()))
                        .addParam("limit", config.getLimit())
                        .addParam("format", "geojson")
                        .addParam("from", session.getFromTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
                        .addParam("to", session.getToTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
                        .addParam("zone", "UTC")
                        .addParam("offset", session.getOffset())
                        .addParam("navigationLinks", "false")
                        .build())
                .header(ACCEPT, APPLICATION_GEO_JSON)
                .header(AUTHORIZATION, "Bearer " + config.getBearerToken())
                .build();

        HttpResponse response = httpClient.send(request);

        if (response.isError()) {
            logger.error(response.getBody());
            return GeoJsonModel.empty();
        }

        JsonObject geoJson = BasicJson.jsonToObject(response.getBody(), JsonObject.class);

        JsonObject metadata = geoJson.remove("metadata").getAsJsonObject();
        int geoSize = metadata.get("size").getAsInt();
        session.setOffset(geoSize);
        session.hasNext(metadata.get("hasNext").getAsBoolean());

        if (session.hasNotNext()) {
            session.setFromTime(session.getToTime());
            session.setToTime(session.getToTime().plusHours(config.getInterval()));
            session.setOffset(0);
        }

        JsonArray features = geoJson.getAsJsonArray("features");
        if (features.size() != geoSize) {
            logger.warn("Retrieved metadata size of features differs to " + geoSize + " from " + features.size());
            geoSize = features.size();
        }

        if (geoSize <= 0) {
            return GeoJsonModel.empty();
        }

        OffsetDateTime fTimestampFirst = OffsetDateTime.parse(features.get(0).getAsJsonObject().getAsJsonObject("properties").get("timestamp").getAsString());
        OffsetDateTime fTimestampLast = OffsetDateTime.parse(features.get(geoSize-1).getAsJsonObject().getAsJsonObject("properties").get("timestamp").getAsString());

        return new GeoJsonModel(geoJson, fTimestampFirst, fTimestampLast);
    }
}
