//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.server.handler;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.LongAdder;

import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.statistic.CounterStatistic;
import org.eclipse.jetty.util.statistic.SampleStatistic;

public class StatisticsHandler extends EventsHandler
{
    private final CounterStatistic _requestStats = new CounterStatistic(); // how many requests are being handled (full lifecycle)
    private final SampleStatistic _requestTimeStats = new SampleStatistic(); // latencies of requests (full lifecycle)
    private final CounterStatistic _handleStats = new CounterStatistic(); // how many requests are in handle()
    private final SampleStatistic _handleTimeStats = new SampleStatistic(); // latencies of requests in handle()
    private final LongAdder _failures = new LongAdder();
    private final LongAdder _handlingFailures = new LongAdder();
    private final LongAdder _responses1xx = new LongAdder();
    private final LongAdder _responses2xx = new LongAdder();
    private final LongAdder _responses3xx = new LongAdder();
    private final LongAdder _responses4xx = new LongAdder();
    private final LongAdder _responses5xx = new LongAdder();
    private final LongAdder _bytesRead = new LongAdder();
    private final LongAdder _bytesWritten = new LongAdder();
    private long _startTime = NanoTime.now();

    public StatisticsHandler()
    {
    }

    public StatisticsHandler(Handler handler)
    {
        super(handler);
    }

    @Override
    protected void doStart() throws Exception
    {
        reset();
        super.doStart();
    }

    @Override
    protected void onBeforeHandling(Request request)
    {
        _requestStats.increment();
        _handleStats.increment();
    }

    @Override
    protected void onAfterHandling(Request request, boolean handled, Throwable failure)
    {
        if (failure != null)
            _handlingFailures.increment();
        _handleStats.decrement();
        _handleTimeStats.record(NanoTime.since(request.getHeadersNanoTime()));
    }

    @Override
    protected void onRequestRead(Request request, Content.Chunk chunk)
    {
        if (chunk != null)
            _bytesRead.add(chunk.remaining());
    }

    @Override
    protected void onResponseWrite(Request request, boolean last, ByteBuffer content)
    {
        int length = BufferUtil.length(content);
        if (length > 0)
            _bytesWritten.add(length);
    }

    @Override
    protected void onComplete(Request request, int status, HttpFields headers, Throwable failure)
    {
        if (failure != null)
            _failures.increment();
        _requestTimeStats.record(NanoTime.since(request.getBeginNanoTime()));
        _requestStats.decrement();
        switch (status / 100)
        {
            case 1 -> _responses1xx.increment();
            case 2 -> _responses2xx.increment();
            case 3 -> _responses3xx.increment();
            case 4 -> _responses4xx.increment();
            case 5 -> _responses5xx.increment();
        }
    }

    @Override
    public void dump(Appendable out, String indent) throws IOException
    {
        dumpObjects(out, indent,
            Dumpable.named("requestStats", _requestStats),
            Dumpable.named("requestTimeStats", _requestTimeStats),
            Dumpable.named("handleStats", _handleStats),
            Dumpable.named("handleTimeStats", _handleTimeStats),
            Dumpable.named("failures", _failures),
            Dumpable.named("handlingFailures", _handlingFailures),
            Dumpable.named("1xxResponses", _responses1xx),
            Dumpable.named("2xxResponses", _responses2xx),
            Dumpable.named("3xxResponses", _responses3xx),
            Dumpable.named("4xxResponses", _responses4xx),
            Dumpable.named("5xxResponses", _responses5xx),
            Dumpable.named("bytesRead", _bytesRead),
            Dumpable.named("bytesWritten", _bytesWritten)
        );
    }

    @ManagedOperation(value = "resets the statistics", impact = "ACTION")
    public void reset()
    {
        _startTime = NanoTime.now();
        _requestStats.reset();
        _requestTimeStats.reset();
        _handleStats.reset();
        _handleTimeStats.reset();
        _failures.reset();
        _handlingFailures.reset();
        _responses1xx.reset();
        _responses2xx.reset();
        _responses3xx.reset();
        _responses4xx.reset();
        _responses5xx.reset();
        _bytesRead.reset();
        _bytesWritten.reset();
    }

    /**
     * @deprecated use {@link #getRequestTotal()} instead.
     */
    @Deprecated
    @ManagedAttribute("number of requests")
    public int getRequests()
    {
        return (int)_requestStats.getTotal();
    }

    @ManagedAttribute("total number of requests")
    public int getRequestTotal()
    {
        return (int)_requestStats.getTotal();
    }

    @ManagedAttribute("current number of active requests")
    public int getRequestsActive()
    {
        return (int)_requestStats.getCurrent();
    }

    @ManagedAttribute("maximum number of active requests")
    public int getRequestsActiveMax()
    {
        return (int)_requestStats.getMax();
    }

    @ManagedAttribute("total time spent in request execution (in ns)")
    public long getRequestTimeTotal()
    {
        return _requestTimeStats.getTotal();
    }

    @ManagedAttribute("maximum request execution time (in ns)")
    public long getRequestTimeMax()
    {
        return _requestTimeStats.getMax();
    }

    @ManagedAttribute("mean request execution time (in ns)")
    public double getRequestTimeMean()
    {
        return _requestTimeStats.getMean();
    }

    @ManagedAttribute("standard deviation for request execution time (in ns)")
    public double getRequestTimeStdDev()
    {
        return _requestTimeStats.getStdDev();
    }

    @ManagedAttribute("total number of calls to handle()")
    public int getHandleTotal()
    {
        return (int)_handleStats.getTotal();
    }

    @ManagedAttribute("current number of requests in handle()")
    public int getHandleActive()
    {
        return (int)_handleStats.getCurrent();
    }

    @ManagedAttribute("maximum number of requests in handle()")
    public int getHandleActiveMax()
    {
        return (int)_handleStats.getMax();
    }

    @ManagedAttribute("maximum handle() execution time (in ns)")
    public long getHandleTimeMax()
    {
        return _handleTimeStats.getMax();
    }

    @ManagedAttribute("total time spent in handle() execution (in ns)")
    public long getHandleTimeTotal()
    {
        return _handleTimeStats.getTotal();
    }

    @ManagedAttribute("mean handle() execution time (in ns)")
    public double getHandleTimeMean()
    {
        return _handleTimeStats.getMean();
    }

    @ManagedAttribute("standard deviation for handle() execution time (in ns)")
    public double getHandleTimeStdDev()
    {
        return _handleTimeStats.getStdDev();
    }

    @ManagedAttribute("number of failed requests")
    public int getFailures()
    {
        return _failures.intValue();
    }

    @ManagedAttribute("number of requests with 1xx response status")
    public int getResponses1xx()
    {
        return _responses1xx.intValue();
    }

    @ManagedAttribute("number of requests with 2xx response status")
    public int getResponses2xx()
    {
        return _responses2xx.intValue();
    }

    @ManagedAttribute("number of requests with 3xx response status")
    public int getResponses3xx()
    {
        return _responses3xx.intValue();
    }

    @ManagedAttribute("number of requests with 4xx response status")
    public int getResponses4xx()
    {
        return _responses4xx.intValue();
    }

    @ManagedAttribute("number of requests with 5xx response status")
    public int getResponses5xx()
    {
        return _responses5xx.intValue();
    }

    @ManagedAttribute("number of requests that threw an exception from handle()")
    public int getHandlingFailures()
    {
        return _handlingFailures.intValue();
    }

    @ManagedAttribute("bytes read count")
    public long getBytesRead()
    {
        return _bytesRead.longValue();
    }

    @ManagedAttribute("bytes written count")
    public long getBytesWritten()
    {
        return _bytesWritten.longValue();
    }

    @ManagedAttribute("duration for which statistics have been collected")
    public Duration getStatisticsDuration()
    {
        return Duration.ofNanos(NanoTime.since(_startTime));
    }

    /**
     * Checks that the wrapped handler can read/write at a minimal rate of N bytes per second.
     * When reading or writing does not conform to the specified rates, this handler prevents
     * further reads or writes by making them immediately fail.
     */
    public static class MinimumDataRateHandler extends StatisticsHandler
    {
        private final long _minimumReadRate;
        private final long _minimumWriteRate;

        /**
         * Creates a {@code MinimumDataRateHandler} with the specified read and write rates.
         * @param minimumReadRate the minimum number of bytes to be read per second, or 0 for not checking the read rate.
         * @param minimumWriteRate the minimum number of bytes to be written per second, or 0 for not checking the write rate.
         */
        public MinimumDataRateHandler(long minimumReadRate, long minimumWriteRate)
        {
            this(null, minimumReadRate, minimumWriteRate);
        }

        /**
         * Creates a {@code MinimumDataRateHandler} with the specified read and write rates.
         *
         * @param handler the handler to wrap.
         * @param minimumReadRate the minimum number of bytes to be read per second, or 0 for not checking the read rate.
         * @param minimumWriteRate the minimum number of bytes to be written per second, or 0 for not checking the write rate.
         */
        public MinimumDataRateHandler(Handler handler, long minimumReadRate, long minimumWriteRate)
        {
            super(handler);
            _minimumReadRate = minimumReadRate;
            _minimumWriteRate = minimumWriteRate;
        }

        @Override
        public boolean handle(Request request, Response response, Callback callback) throws Exception
        {
            MinimumDataRateRequest wrappedRequest = new MinimumDataRateRequest(request);
            MinimumDataRateResponse wrappedResponse = new MinimumDataRateResponse(wrappedRequest, response);
            return super.handle(wrappedRequest, wrappedResponse, callback);
        }

        protected class MinimumDataRateRequest extends Request.Wrapper
        {
            private Content.Chunk _errorContent;

            private MinimumDataRateRequest(Request request)
            {
                super(request);
            }

            private long dataRatePerSecond(long dataCount)
            {
                if (dataCount == 0L)
                    return 0L;
                long delayInNs = NanoTime.since(getHeadersNanoTime());
                // If you read 1 byte or more in 0ns or less, you have infinite bandwidth.
                if (delayInNs <= 0L)
                    return Long.MAX_VALUE;
                return dataCount * 1_000_000_000 / delayInNs;
            }

            @Override
            public void demand(Runnable demandCallback)
            {
                if (_minimumReadRate > 0)
                {
                    long rr = dataRatePerSecond(getBytesRead());
                    if (rr < _minimumReadRate)
                    {
                        _errorContent = Content.Chunk.from(new TimeoutException("read rate is too low: " + rr));
                        demandCallback.run();
                        return;
                    }
                }
                super.demand(demandCallback);
            }

            @Override
            public Content.Chunk read()
            {
                return _errorContent != null ? _errorContent : super.read();
            }
        }

        protected class MinimumDataRateResponse extends Response.Wrapper
        {
            public MinimumDataRateResponse(MinimumDataRateRequest request, Response wrapped)
            {
                super(request, wrapped);
            }

            @Override
            public MinimumDataRateRequest getRequest()
            {
                return (MinimumDataRateRequest)super.getRequest();
            }

            @Override
            public void write(boolean last, ByteBuffer byteBuffer, Callback callback)
            {
                if (_minimumWriteRate > 0)
                {
                    long bytesWritten = getBytesWritten();
                    if (bytesWritten > 0L)
                    {
                        long wr = getRequest().dataRatePerSecond(bytesWritten);
                        if (wr < _minimumWriteRate)
                        {
                            TimeoutException cause = new TimeoutException("write rate is too low: " + wr);
                            getRequest()._errorContent = Content.Chunk.from(cause);
                            callback.failed(cause);
                            return;
                        }
                    }
                }
                super.write(last, byteBuffer, callback);
            }
        }
    }
}
