/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.brooklyn.rest.resources;

import static org.apache.brooklyn.rest.util.WebResourceUtils.notFound;

import java.net.URI;
import java.util.Date;
import java.util.List;
import java.util.Set;

import javax.annotation.Nullable;

import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
import org.apache.brooklyn.rest.util.WebResourceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.mgmt.usage.ApplicationUsage;
import org.apache.brooklyn.core.mgmt.usage.LocationUsage;
import org.apache.brooklyn.core.mgmt.usage.ApplicationUsage.ApplicationEvent;
import org.apache.brooklyn.rest.api.UsageApi;
import org.apache.brooklyn.rest.domain.UsageStatistic;
import org.apache.brooklyn.rest.domain.UsageStatistics;
import org.apache.brooklyn.rest.transform.ApplicationTransformer;
import org.apache.brooklyn.util.exceptions.UserFacingException;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Time;

import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;


public class UsageResource extends AbstractBrooklynRestResource implements UsageApi {

    private static final Logger log = LoggerFactory.getLogger(UsageResource.class);

    private static final Set<Lifecycle> WORKING_LIFECYCLES = ImmutableSet.of(Lifecycle.RUNNING, Lifecycle.CREATED, Lifecycle.STARTING);

    @Override
    public List<UsageStatistics> listApplicationsUsage(@Nullable String start, @Nullable String end) {
        log.debug("REST call to get application usage for all applications: dates {} -> {}", new Object[] {start, end});

        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.USAGE, null))
            throw WebResourceUtils.forbidden("User '%s' is not authorized to perform this operation", Entitlements.getEntitlementContext().user());

        List<UsageStatistics> response = Lists.newArrayList();
        
        Date startDate = parseDate(start, new Date(0));
        Date endDate = parseDate(end, new Date());
        
        checkDates(startDate, endDate);

        Set<ApplicationUsage> usages = mgmtInternal().getUsageManager().getApplicationUsage(Predicates.alwaysTrue());

        for (ApplicationUsage usage : usages) {
            List<UsageStatistic> statistics = retrieveApplicationUsage(usage, startDate, endDate);
            if (statistics.size() > 0) {
                response.add(new UsageStatistics(statistics, ImmutableMap.<String,URI>of()));
            }
        }
        return response;
    }
    
    @Override
    public UsageStatistics getApplicationUsage(String application, String start, String end) {
        log.debug("REST call to get application usage for application {}: dates {} -> {}", new Object[] {application, start, end});

        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.USAGE, null))
            throw WebResourceUtils.forbidden("User '%s' is not authorized to perform this operation", Entitlements.getEntitlementContext().user());

        Date startDate = parseDate(start, new Date(0));
        Date endDate = parseDate(end, new Date());

        checkDates(startDate, endDate);

        ApplicationUsage usage = mgmtInternal().getUsageManager().getApplicationUsage(application);
        if (usage != null) {
            List<UsageStatistic> statistics = retrieveApplicationUsage(usage, startDate, endDate);
            return new UsageStatistics(statistics, ImmutableMap.<String,URI>of());
        } else {
            throw notFound("Application '%s' not found", application);
        }
    }

    private List<UsageStatistic> retrieveApplicationUsage(ApplicationUsage usage, Date startDate, Date endDate) {
        log.debug("Determining application usage for application {}: dates {} -> {}", new Object[] {usage.getApplicationId(), startDate, endDate});
        log.trace("Considering application usage events of {}: {}", usage.getApplicationId(), usage.getEvents());

        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.USAGE, null))
            throw WebResourceUtils.forbidden("User '%s' is not authorized to perform this operation", Entitlements.getEntitlementContext().user());

        List<UsageStatistic> result = Lists.newArrayList();

        // Getting duration of state by comparing with next event (if next event is of same type, we just generate two statistics)...
        for (int i = 0; i < usage.getEvents().size(); i++) {
            ApplicationEvent current = usage.getEvents().get(i);
            Date eventStartDate = current.getDate();
            Date eventEndDate;

            if (i <  usage.getEvents().size() - 1) {
                ApplicationEvent next =  usage.getEvents().get(i + 1);
                eventEndDate = next.getDate();
            } else if (current.getState() == Lifecycle.DESTROYED) {
                eventEndDate = eventStartDate;
            } else {
                eventEndDate = new Date();
            }

            if (eventStartDate.compareTo(endDate) > 0 || eventEndDate.compareTo(startDate) < 0) {
                continue;
            }

            if (eventStartDate.compareTo(startDate) < 0) {
                eventStartDate = startDate;
            }
            if (eventEndDate.compareTo(endDate) > 0) {
                eventEndDate = endDate;
            }
            long duration = eventEndDate.getTime() - eventStartDate.getTime();
            UsageStatistic statistic = new UsageStatistic(ApplicationTransformer.statusFromLifecycle(current.getState()), usage.getApplicationId(), usage.getApplicationId(), format(eventStartDate), format(eventEndDate), duration, usage.getMetadata());
            log.trace("Adding application usage statistic to response for app {}: {}", usage.getApplicationId(), statistic);
            result.add(statistic);
        }
        
        return result;
    }

    @Override
    public List<UsageStatistics> listMachinesUsage(final String application, final String start, final String end) {
        log.debug("REST call to get machine usage for application {}: dates {} -> {}", new Object[] {application, start, end});

        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.USAGE, null))
            throw WebResourceUtils.forbidden("User '%s' is not authorized to perform this operation", Entitlements.getEntitlementContext().user());

        final Date startDate = parseDate(start, new Date(0));
        final Date endDate = parseDate(end, new Date());

        checkDates(startDate, endDate);
        
        // Note currently recording ALL metrics for a machine that contains an Event from given Application
        Set<LocationUsage> matches = mgmtInternal().getUsageManager().getLocationUsage(new Predicate<LocationUsage>() {
            @Override
            public boolean apply(LocationUsage input) {
                LocationUsage.LocationEvent first = input.getEvents().get(0);
                if (endDate.compareTo(first.getDate()) < 0) {
                    return false;
                }
                LocationUsage.LocationEvent last = input.getEvents().get(input.getEvents().size() - 1);
                if (!WORKING_LIFECYCLES.contains(last.getState()) && startDate.compareTo(last.getDate()) > 0) {
                    return false;
                }
                if (application != null) {
                    for (LocationUsage.LocationEvent e : input.getEvents()) {
                        if (Objects.equal(application, e.getApplicationId())) {
                            return true;
                        }
                    }
                    return false;
                }
                return true;
            }
        });
        
        List<UsageStatistics> response = Lists.newArrayList();
        for (LocationUsage usage : matches) {
            List<UsageStatistic> statistics = retrieveMachineUsage(usage, startDate, endDate);
            if (statistics.size() > 0) {
                response.add(new UsageStatistics(statistics, ImmutableMap.<String,URI>of()));
            }
        }
        return response;
    }

    @Override
    public UsageStatistics getMachineUsage(final String machine, final String start, final String end) {
        log.debug("REST call to get machine usage for machine {}: dates {} -> {}", new Object[] {machine, start, end});

        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.USAGE, null))
            throw WebResourceUtils.forbidden("User '%s' is not authorized to perform this operation", Entitlements.getEntitlementContext().user());

        final Date startDate = parseDate(start, new Date(0));
        final Date endDate = parseDate(end, new Date());

        checkDates(startDate, endDate);
        
        // Note currently recording ALL metrics for a machine that contains an Event from given Application
        LocationUsage usage = mgmtInternal().getUsageManager().getLocationUsage(machine);
        
        if (usage == null) {
            throw notFound("Machine '%s' not found", machine);
        }
        
        List<UsageStatistic> statistics = retrieveMachineUsage(usage, startDate, endDate);
        return new UsageStatistics(statistics, ImmutableMap.<String,URI>of());
    }

    private List<UsageStatistic> retrieveMachineUsage(LocationUsage usage, Date startDate, Date endDate) {
        log.debug("Determining machine usage for location {}", usage.getLocationId());
        log.trace("Considering machine usage events of {}: {}", usage.getLocationId(), usage.getEvents());

        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.USAGE, null))
            throw WebResourceUtils.forbidden("User '%s' is not authorized to perform this operation", Entitlements.getEntitlementContext().user());

        List<UsageStatistic> result = Lists.newArrayList();

        // Getting duration of state by comparing with next event (if next event is of same type, we just generate two statistics)...
        for (int i = 0; i < usage.getEvents().size(); i++) {
            LocationUsage.LocationEvent current = usage.getEvents().get(i);
            Date eventStartDate = current.getDate();
            Date eventEndDate;

            if (i <  usage.getEvents().size() - 1) {
                LocationUsage.LocationEvent next =  usage.getEvents().get(i + 1);
                eventEndDate = next.getDate();
            } else if (current.getState() == Lifecycle.DESTROYED || current.getState() == Lifecycle.STOPPED) {
                eventEndDate = eventStartDate;
            } else {
                eventEndDate = new Date();
            }

            if (eventStartDate.compareTo(endDate) > 0 || eventEndDate.compareTo(startDate) < 0) {
                continue;
            }

            if (eventStartDate.compareTo(startDate) < 0) {
                eventStartDate = startDate;
            }
            if (eventEndDate.compareTo(endDate) > 0) {
                eventEndDate = endDate;
            }
            long duration = eventEndDate.getTime() - eventStartDate.getTime();
            UsageStatistic statistic = new UsageStatistic(ApplicationTransformer.statusFromLifecycle(current.getState()), usage.getLocationId(), current.getApplicationId(), format(eventStartDate), format(eventEndDate), duration, usage.getMetadata());
            log.trace("Adding machine usage statistic to response for app {}: {}", usage.getLocationId(), statistic);
            result.add(statistic);
        }
        
        return result;
    }
    
    private void checkDates(Date startDate, Date endDate) {
        if (startDate.compareTo(endDate) > 0) {
            throw new UserFacingException(new IllegalArgumentException("Start must be less than or equal to end: " + startDate + " > " + endDate + 
                    " (" + startDate.getTime() + " > " + endDate.getTime() + ")"));
        }
    }

    private Date parseDate(String toParse, Date def) {
        return Strings.isBlank(toParse) ? def : Time.parseDate(toParse);
    }
    
    private String format(Date date) {
        return Time.makeDateString(date, Time.DATE_FORMAT_ISO8601_NO_MILLIS, Time.TIME_ZONE_UTC);
    }
}
