/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.android.vts.job;

import com.android.vts.entity.DeviceInfoEntity;
import com.android.vts.entity.ProfilingPointEntity;
import com.android.vts.entity.ProfilingPointRunEntity;
import com.android.vts.entity.ProfilingPointSummaryEntity;
import com.android.vts.util.DatastoreHelper;
import com.android.vts.util.PerformanceUtil;
import com.android.vts.util.TaskQueueHelper;
import com.android.vts.util.TimeUtil;
import com.google.appengine.api.datastore.DatastoreFailureException;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.DatastoreTimeoutException;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskOptions;
import java.io.IOException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/** Represents the notifications service which is automatically called on a fixed schedule. */
public class VtsProfilingStatsJobServlet extends BaseJobServlet {
    protected static final Logger logger =
            Logger.getLogger(VtsProfilingStatsJobServlet.class.getName());
    private static final String HIDL_HAL_OPTION = "hidl_hal_mode";
    private static final String[] splitKeysArray = new String[] {HIDL_HAL_OPTION};
    private static final Set<String> splitKeySet = new HashSet<>(Arrays.asList(splitKeysArray));

    public static final String PROFILING_STATS_JOB_URL = "/task/vts_profiling_stats_job";
    public static final String PROFILING_POINT_KEY = "profilingPointKey";
    public static final String QUEUE = "profilingStatsQueue";

    /**
     * Round the date down to the start of the day (PST).
     *
     * @param time The time in microseconds.
     * @return
     */
    public static long getCanonicalTime(long time) {
        long timeMillis = TimeUnit.MICROSECONDS.toMillis(time);
        ZonedDateTime zdt =
                ZonedDateTime.ofInstant(Instant.ofEpochMilli(timeMillis), TimeUtil.PT_ZONE);
        return TimeUnit.SECONDS.toMicros(
                zdt.withHour(0).withMinute(0).withSecond(0).toEpochSecond());
    }

    /**
     * Add tasks to process profiling run data
     *
     * @param profilingPointKeys The list of keys of the profiling point runs whose data process.
     */
    public static void addTasks(List<Key> profilingPointKeys) {
        Queue queue = QueueFactory.getQueue(QUEUE);
        List<TaskOptions> tasks = new ArrayList<>();
        for (Key key : profilingPointKeys) {
            String keyString = KeyFactory.keyToString(key);
            tasks.add(
                    TaskOptions.Builder.withUrl(PROFILING_STATS_JOB_URL)
                            .param(PROFILING_POINT_KEY, keyString)
                            .method(TaskOptions.Method.POST));
        }
        TaskQueueHelper.addToQueue(queue, tasks);
    }

    /**
     * Update the profiling summaries with the information from a profiling point run.
     *
     * @param testKey The key to the TestEntity whose profiling data to analyze.
     * @param profilingPointRun The profiling data to analyze.
     * @param devices The list of devices used in the profiling run.
     * @param time The canonical timestamp of the summary to update.
     * @return true if the update succeeds, false otherwise.
     */
    public static boolean updateSummaries(
            Key testKey,
            ProfilingPointRunEntity profilingPointRun,
            List<DeviceInfoEntity> devices,
            long time) {
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
        Transaction tx = datastore.beginTransaction();
        try {
            List<Entity> puts = new ArrayList<>();

            ProfilingPointEntity profilingPoint =
                    new ProfilingPointEntity(
                            testKey.getName(),
                            profilingPointRun.getName(),
                            profilingPointRun.getType(),
                            profilingPointRun.getRegressionMode(),
                            profilingPointRun.getXLabel(),
                            profilingPointRun.getYLabel());
            puts.add(profilingPoint.toEntity());

            String option = PerformanceUtil.getOptionAlias(profilingPointRun, splitKeySet);

            Set<String> branches = new HashSet<>();
            Set<String> deviceNames = new HashSet<>();

            branches.add(ProfilingPointSummaryEntity.ALL);
            deviceNames.add(ProfilingPointSummaryEntity.ALL);

            for (DeviceInfoEntity d : devices) {
                branches.add(d.getBranch());
                deviceNames.add(d.getBuildFlavor());
            }

            List<Key> summaryGets = new ArrayList<>();
            for (String branch : branches) {
                for (String device : deviceNames) {
                    summaryGets.add(
                            ProfilingPointSummaryEntity.createKey(
                                    profilingPoint.getKey(), branch, device, option, time));
                }
            }

            Map<Key, Entity> summaries = datastore.get(tx, summaryGets);
            Map<String, Map<String, ProfilingPointSummaryEntity>> summaryMap = new HashMap<>();
            for (Key key : summaries.keySet()) {
                Entity e = summaries.get(key);
                ProfilingPointSummaryEntity profilingPointSummary =
                        ProfilingPointSummaryEntity.fromEntity(e);
                if (profilingPointSummary == null) {
                    logger.log(Level.WARNING, "Invalid profiling point summary: " + e.getKey());
                    continue;
                }
                if (!summaryMap.containsKey(profilingPointSummary.getBranch())) {
                    summaryMap.put(profilingPointSummary.getBranch(), new HashMap<>());
                }
                Map<String, ProfilingPointSummaryEntity> deviceMap =
                        summaryMap.get(profilingPointSummary.getBranch());
                deviceMap.put(profilingPointSummary.getBuildFlavor(), profilingPointSummary);
            }

            Set<ProfilingPointSummaryEntity> modifiedEntities = new HashSet<>();

            for (String branch : branches) {
                if (!summaryMap.containsKey(branch)) {
                    summaryMap.put(branch, new HashMap<>());
                }
                Map<String, ProfilingPointSummaryEntity> deviceMap = summaryMap.get(branch);

                for (String device : deviceNames) {
                    ProfilingPointSummaryEntity summary;
                    if (deviceMap.containsKey(device)) {
                        summary = deviceMap.get(device);
                    } else {
                        summary =
                                new ProfilingPointSummaryEntity(
                                        profilingPoint.getKey(), branch, device, option, time);
                        deviceMap.put(device, summary);
                    }
                    summary.update(profilingPointRun);
                    modifiedEntities.add(summary);
                }
            }

            for (ProfilingPointSummaryEntity profilingPointSummary : modifiedEntities) {
                puts.add(profilingPointSummary.toEntity());
            }
            datastore.put(tx, puts);
            tx.commit();
        } catch (ConcurrentModificationException
                | DatastoreFailureException
                | DatastoreTimeoutException e) {
            return false;
        } finally {
            if (tx.isActive()) {
                tx.rollback();
                logger.log(
                        Level.WARNING,
                        "Profiling stats job transaction still active: "
                                + profilingPointRun.getKey());
                return false;
            }
        }
        return true;
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
        String profilingPointKeyString = request.getParameter(PROFILING_POINT_KEY);

        Key profilingPointRunKey;
        try {
            profilingPointRunKey = KeyFactory.stringToKey(profilingPointKeyString);
        } catch (IllegalArgumentException e) {
            logger.log(Level.WARNING, "Invalid key specified: " + profilingPointKeyString);
            return;
        }
        Key testKey = profilingPointRunKey.getParent().getParent();

        ProfilingPointRunEntity profilingPointRun = null;
        try {
            Entity profilingPointRunEntity = datastore.get(profilingPointRunKey);
            profilingPointRun = ProfilingPointRunEntity.fromEntity(profilingPointRunEntity);
        } catch (EntityNotFoundException e) {
            // no run found
        }
        if (profilingPointRun == null) {
            return;
        }

        Query deviceQuery =
                new Query(DeviceInfoEntity.KIND).setAncestor(profilingPointRunKey.getParent());

        List<DeviceInfoEntity> devices = new ArrayList<>();
        for (Entity e : datastore.prepare(deviceQuery).asIterable()) {
            DeviceInfoEntity deviceInfoEntity = DeviceInfoEntity.fromEntity(e);
            if (e == null) continue;
            devices.add(deviceInfoEntity);
        }

        long canonicalTime = getCanonicalTime(profilingPointRunKey.getParent().getId());
        int retryCount = 0;
        while (retryCount++ <= DatastoreHelper.MAX_WRITE_RETRIES) {
            boolean result = updateSummaries(testKey, profilingPointRun, devices, canonicalTime);
            if (!result) {
                logger.log(
                        Level.WARNING, "Retrying profiling stats update: " + profilingPointRunKey);
                continue;
            }
            break;
        }
        if (retryCount > DatastoreHelper.MAX_WRITE_RETRIES) {
            logger.log(Level.SEVERE, "Could not update profiling stats: " + profilingPointRunKey);
        }
    }
}
