/*
 * 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.camel.spring.boot.actuate.endpoint;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.apache.camel.CamelContext;
import org.apache.camel.Route;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.StatefulService;
import org.apache.camel.api.management.ManagedCamelContext;
import org.apache.camel.api.management.mbean.ManagedRouteMBean;
import org.apache.camel.spi.RouteController;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.lang.Nullable;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;

/*
 * Spring Boot Management Endpoint to expose Camel Route information.
 */
@Endpoint(id = "camelroutes")
public class CamelRoutesEndpoint {

    private CamelContext camelContext;
    private ManagedCamelContext managedCamelContext;
    private CamelRoutesEndpointProperties properties;

    public CamelRoutesEndpoint(CamelContext camelContext, CamelRoutesEndpointProperties properties) {
        this.camelContext = camelContext;
        this.managedCamelContext = camelContext.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
        this.properties = properties;
    }

    @ReadOperation
    public List<RouteEndpointInfo> readRoutes() {
        return getRoutesInfo();
    }

    @ReadOperation
    public Object doReadAction(@Selector String id, @Selector ReadAction action) {
        switch (action) {
            case DETAIL:
                return getRouteDetailsInfo(id);
            case INFO:
                return getRouteInfo(id);
            default:
                throw new IllegalArgumentException("Unsupported read action " + action);
        }
    }

    @WriteOperation
    public void doWriteAction(@Selector String id, @Selector WriteAction action, @Nullable TimeInfo timeInfo) {
        if (this.properties.isReadOnly()) {
            throw new IllegalArgumentException(String.format("Read only: write action %s is not allowed", action));
        }

        switch (action) {
            case STOP:
                stopRoute(
                        id,
                        Optional.ofNullable(timeInfo).flatMap(ti -> Optional.ofNullable(ti.getTimeout())),
                        Optional.of(TimeUnit.SECONDS),
                        Optional.ofNullable(timeInfo).flatMap(ti -> Optional.ofNullable(ti.getAbortAfterTimeout())));
                break;
            case START:
                startRoute(id);
                break;
            case RESET:
                resetRoute(id);
                break;
            case SUSPEND:
                suspendRoute(id,
                        Optional.ofNullable(timeInfo).flatMap(ti -> Optional.ofNullable(ti.getTimeout())),
                        Optional.of(TimeUnit.SECONDS));
                break;
            case RESUME:
                resumeRoute(id);
                break;
            default:
                throw new IllegalArgumentException("Unsupported write action " + action);
        }
    }

    private RouteEndpointInfo getRouteInfo(String id) {
        Route route = getRoute(id);
        if (route != null) {
            return new RouteEndpointInfo(route);
        }
        return null;
    }

    private Collection<Route> getRoutes(){
        RouteController routeController = camelContext.getRouteController();
        Map<String,Route> routes = new HashMap<>();

        if (routeController != null ){
            routes.putAll(routeController.getControlledRoutes().stream().collect(toMap(Route::getId, identity())));
        }
        routes.putAll(camelContext.getRoutes().stream().collect(toMap(Route::getId, identity())));

        return routes.values();
    }
    private Route getRoute(String id){
        return getRoutes().stream().filter(x->x.getId().equals(id)).findAny().get();
    }

    private List<RouteEndpointInfo> getRoutesInfo() {
        return getRoutes().stream()
                .map(RouteEndpointInfo::new).collect(Collectors.toList());
    }

    private RouteDetailsEndpointInfo getRouteDetailsInfo(String id) {
        Route route = getRoute(id);
        if (route != null) {
            return new RouteDetailsEndpointInfo(camelContext, route);
        }

        return null;
    }

    private void startRoute(String id) {
        try {
            camelContext.getRouteController().startRoute(id);
        } catch (Exception e) {
            throw new RuntimeCamelException(e);
        }
    }

    private void resetRoute(String id) {
        try {
            ManagedRouteMBean managedRouteMBean = managedCamelContext.getManagedRoute(id, ManagedRouteMBean.class);
            if (managedRouteMBean != null) {
                managedRouteMBean.reset(true);
            }
        } catch (Exception e) {
            throw new RuntimeCamelException(e);
        }
    }

    private void stopRoute(String id, Optional<Long> timeout, Optional<TimeUnit> timeUnit, Optional<Boolean> abortAfterTimeout) {
        try {
            if (timeout.isPresent()) {
                camelContext.getRouteController().stopRoute(id, timeout.get(), timeUnit.orElse(TimeUnit.SECONDS), abortAfterTimeout.orElse(Boolean.TRUE));
            } else {
                camelContext.getRouteController().stopRoute(id);
            }
        } catch (Exception e) {
            throw new RuntimeCamelException(e);
        }
    }

    private void suspendRoute(String id, Optional<Long> timeout, Optional<TimeUnit> timeUnit) {
        try {
            if (timeout.isPresent()) {
                camelContext.getRouteController().suspendRoute(id, timeout.get(), timeUnit.orElse(TimeUnit.SECONDS));
            } else {
                camelContext.getRouteController().suspendRoute(id);
            }
        } catch (Exception e) {
            throw new RuntimeCamelException(e);
        }
    }

    private void resumeRoute(String id) {
        try {
            camelContext.getRouteController().resumeRoute(id);
        } catch (Exception e) {
            throw new RuntimeCamelException(e);
        }
    }

    /*
     * Container for exposing {@link org.apache.camel.Route} information as JSON.
     */
    @JsonPropertyOrder({"id", "group", "description", "uptime", "uptimeMillis"})
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    public static class RouteEndpointInfo {

        private final String id;
        private final String group;
        // excludes "route.start.exception" which includes stacktrace.
        @JsonIgnoreProperties(value = {"route.start.exception"} )
        private final Map<String, Object> properties;
        private final String description;
        private final String uptime;
        private final long uptimeMillis;
        private final String status;

        public RouteEndpointInfo(Route route) {
            this.id = route.getId();
            this.group = route.getGroup();
            this.description = route.getDescription();
            this.uptime = route.getUptime();
            this.uptimeMillis = route.getUptimeMillis();

            if (route.getProperties() != null) {
                this.properties = new HashMap<>(route.getProperties());
            } else {
                this.properties = Collections.emptyMap();
            }

            if (route instanceof StatefulService) {
                this.status = ((StatefulService) route).getStatus().name();
            } else {
                this.status = null;
            }
        }

        public String getId() {
            return id;
        }

        public String getGroup() {
            return group;
        }

        public Map<String, Object> getProperties() {
            return properties;
        }

        public String getDescription() {
            return description;
        }

        public String getUptime() {
            return uptime;
        }

        public long getUptimeMillis() {
            return uptimeMillis;
        }

        public String getStatus() {
            return status;
        }
    }

    /*
     * Container for exposing {@link org.apache.camel.Route} information
     * with route details as JSON. Route details are retrieved from JMX.
     */
    public static class RouteDetailsEndpointInfo extends RouteEndpointInfo {

        @JsonProperty("details")
        private RouteDetails routeDetails;
        private final Map<String, Object> properties;

        public RouteDetailsEndpointInfo(final CamelContext camelContext, final Route route) {
            super(route);
            if (route.getProperties() != null) {
                this.properties = new HashMap<>(route.getProperties());
            } else {
                this.properties = Collections.emptyMap();
            }
            if (camelContext.getManagementStrategy().getManagementAgent() != null) {
                ManagedCamelContext mcc = camelContext.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
                this.routeDetails = new RouteDetails(mcc.getManagedRoute(route.getId(), ManagedRouteMBean.class));
            }
        }

        public Map<String, Object> getProperties() {
            return properties;
        }

        @JsonInclude(JsonInclude.Include.NON_EMPTY)
        static class RouteDetails {

            private long deltaProcessingTime;
            private long exchangesInflight;
            private long exchangesTotal;
            private long externalRedeliveries;
            private long failuresHandled;
            private String firstExchangeCompletedExchangeId;
            private Date firstExchangeCompletedTimestamp;
            private String firstExchangeFailureExchangeId;
            private Date firstExchangeFailureTimestamp;
            private String lastExchangeCompletedExchangeId;
            private Date lastExchangeCompletedTimestamp;
            private String lastExchangeFailureExchangeId;
            private Date lastExchangeFailureTimestamp;
            private long lastProcessingTime;
            private String load01;
            private String load05;
            private String load15;
            private long maxProcessingTime;
            private long meanProcessingTime;
            private long minProcessingTime;
            private Long oldestInflightDuration;
            private String oldestInflightExchangeId;
            private long redeliveries;
            private long totalProcessingTime;
            private boolean hasRouteController;

            RouteDetails(ManagedRouteMBean managedRoute) {
                try {
                    this.deltaProcessingTime = managedRoute.getDeltaProcessingTime();
                    this.exchangesInflight = managedRoute.getExchangesInflight();
                    this.exchangesTotal = managedRoute.getExchangesTotal();
                    this.externalRedeliveries = managedRoute.getExternalRedeliveries();
                    this.failuresHandled = managedRoute.getFailuresHandled();
                    this.firstExchangeCompletedExchangeId = managedRoute.getFirstExchangeCompletedExchangeId();
                    this.firstExchangeCompletedTimestamp = managedRoute.getFirstExchangeCompletedTimestamp();
                    this.firstExchangeFailureExchangeId = managedRoute.getFirstExchangeFailureExchangeId();
                    this.firstExchangeFailureTimestamp = managedRoute.getFirstExchangeFailureTimestamp();
                    this.lastExchangeCompletedExchangeId = managedRoute.getLastExchangeCompletedExchangeId();
                    this.lastExchangeCompletedTimestamp = managedRoute.getLastExchangeCompletedTimestamp();
                    this.lastExchangeFailureExchangeId = managedRoute.getLastExchangeFailureExchangeId();
                    this.lastExchangeFailureTimestamp = managedRoute.getLastExchangeFailureTimestamp();
                    this.lastProcessingTime = managedRoute.getLastProcessingTime();
                    this.load01 = managedRoute.getLoad01();
                    this.load05 = managedRoute.getLoad05();
                    this.load15 = managedRoute.getLoad15();
                    this.maxProcessingTime = managedRoute.getMaxProcessingTime();
                    this.meanProcessingTime = managedRoute.getMeanProcessingTime();
                    this.minProcessingTime = managedRoute.getMinProcessingTime();
                    this.oldestInflightDuration = managedRoute.getOldestInflightDuration();
                    this.oldestInflightExchangeId = managedRoute.getOldestInflightExchangeId();
                    this.redeliveries = managedRoute.getRedeliveries();
                    this.totalProcessingTime = managedRoute.getTotalProcessingTime();
                    this.hasRouteController = managedRoute.getHasRouteController();
                } catch (Exception e) {
                    // Ignore
                }
            }

            public long getDeltaProcessingTime() {
                return deltaProcessingTime;
            }

            public long getExchangesInflight() {
                return exchangesInflight;
            }

            public long getExchangesTotal() {
                return exchangesTotal;
            }

            public long getExternalRedeliveries() {
                return externalRedeliveries;
            }

            public long getFailuresHandled() {
                return failuresHandled;
            }

            public String getFirstExchangeCompletedExchangeId() {
                return firstExchangeCompletedExchangeId;
            }

            public Date getFirstExchangeCompletedTimestamp() {
                return firstExchangeCompletedTimestamp;
            }

            public String getFirstExchangeFailureExchangeId() {
                return firstExchangeFailureExchangeId;
            }

            public Date getFirstExchangeFailureTimestamp() {
                return firstExchangeFailureTimestamp;
            }

            public String getLastExchangeCompletedExchangeId() {
                return lastExchangeCompletedExchangeId;
            }

            public Date getLastExchangeCompletedTimestamp() {
                return lastExchangeCompletedTimestamp;
            }

            public String getLastExchangeFailureExchangeId() {
                return lastExchangeFailureExchangeId;
            }

            public Date getLastExchangeFailureTimestamp() {
                return lastExchangeFailureTimestamp;
            }

            public long getLastProcessingTime() {
                return lastProcessingTime;
            }

            public String getLoad01() {
                return load01;
            }

            public String getLoad05() {
                return load05;
            }

            public String getLoad15() {
                return load15;
            }

            public long getMaxProcessingTime() {
                return maxProcessingTime;
            }

            public long getMeanProcessingTime() {
                return meanProcessingTime;
            }

            public long getMinProcessingTime() {
                return minProcessingTime;
            }

            public Long getOldestInflightDuration() {
                return oldestInflightDuration;
            }

            public String getOldestInflightExchangeId() {
                return oldestInflightExchangeId;
            }

            public long getRedeliveries() {
                return redeliveries;
            }

            public long getTotalProcessingTime() {
                return totalProcessingTime;
            }

            public boolean getHasRouteController() {
                return hasRouteController;
            }
        }
    }

    /*
     * List of write actions available for the endpoint
     */
    public enum WriteAction {
        STOP,
        START,
        RESET,
        SUSPEND,
        RESUME
    }

    /*
     * List of read actions available for the endpoint
     */
    public enum ReadAction {
        DETAIL,
        INFO
    }

    /*
     * Optional time information for the actions
     */
    public static class TimeInfo {
        private Long timeout;
        private Boolean abortAfterTimeout;

        public Long getTimeout() {
            return timeout;
        }

        public void setTimeout(Long timeout) {
            this.timeout = timeout;
        }

        public Boolean getAbortAfterTimeout() {
            return abortAfterTimeout;
        }

        public void setAbortAfterTimeout(Boolean abortAfterTimeout) {
            this.abortAfterTimeout = abortAfterTimeout;
        }
    }

}
