//
// ========================================================================
// 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.ee10.servlet;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Locale;

import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;
import jakarta.servlet.http.HttpServletRequest;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler.ServletContextScopeListener;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandler.ContextScopeListener;
import org.eclipse.jetty.util.DateCache;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.Name;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A Context Listener that produces additional debug.
 * This listener if added to a ContextHandler, will produce additional debug information to
 * either/or a specific log stream or the standard debug log.
 * The events produced by {@link ServletContextListener}, {@link ServletRequestListener},
 * {@link AsyncListener} and {@link ContextScopeListener} are logged.
 */
@ManagedObject("Debug Listener")
public class DebugListener extends AbstractLifeCycle implements ServletContextListener
{
    private static final Logger LOG = LoggerFactory.getLogger(DebugListener.class);
    private static final DateCache __date = new DateCache("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);

    private final String _attr = String.format("__R%s@%x", this.getClass().getSimpleName(), System.identityHashCode(this));

    private final PrintStream _out;
    private boolean _renameThread;
    private boolean _showHeaders;
    private boolean _dumpContext;

    public DebugListener()
    {
        this(null, false, false, false);
    }

    public DebugListener(@Name("renameThread") boolean renameThread, @Name("showHeaders") boolean showHeaders, @Name("dumpContext") boolean dumpContext)
    {
        this(null, renameThread, showHeaders, dumpContext);
    }

    public DebugListener(@Name("outputStream") OutputStream out, @Name("renameThread") boolean renameThread, @Name("showHeaders") boolean showHeaders, @Name("dumpContext") boolean dumpContext)
    {
        _out = out == null ? null : new PrintStream(out);
        _renameThread = renameThread;
        _showHeaders = showHeaders;
        _dumpContext = dumpContext;
    }

    @ManagedAttribute("Rename thread within context scope")
    public boolean isRenameThread()
    {
        return _renameThread;
    }

    public void setRenameThread(boolean renameThread)
    {
        _renameThread = renameThread;
    }

    @ManagedAttribute("Show request headers")
    public boolean isShowHeaders()
    {
        return _showHeaders;
    }

    public void setShowHeaders(boolean showHeaders)
    {
        _showHeaders = showHeaders;
    }

    @ManagedAttribute("Dump contexts at start")
    public boolean isDumpContext()
    {
        return _dumpContext;
    }

    public void setDumpContext(boolean dumpContext)
    {
        _dumpContext = dumpContext;
    }

    @Override
    public void contextInitialized(ServletContextEvent sce)
    {
        sce.getServletContext().addListener(_servletRequestListener);
        ContextHandler handler = ServletContextHandler.getServletContextHandler(sce.getServletContext());
        handler.addEventListener(_contextScopeListener);
        String cname = findContextName(sce.getServletContext());
        log("^  ctx=%s %s", cname, sce.getServletContext());
        if (_dumpContext)
        {
            if (_out == null)
            {
                handler.dumpStdErr();
            }
            else
            {
                try
                {
                    Dumpable.dump(handler, _out);
                }
                catch (Exception e)
                {
                    LOG.warn("Unable to dump {}", handler, e);
                }
            }
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce)
    {
        String cname = findContextName(sce.getServletContext());
        log("v  ctx=%s %s", cname, sce.getServletContext());
    }

    protected String findContextName(ServletContext context)
    {
        if (context == null)
            return null;
        String n = (String)context.getAttribute(_attr);
        if (n == null)
        {
            n = String.format("%s@%x", context.getContextPath(), context.hashCode());
            context.setAttribute(_attr, n);
        }
        return n;
    }

    protected String findRequestName(ServletContextRequest request)
    {
        if (request == null)
            return null;
        return request.getId();
    }

    protected String findRequestName(ServletRequest request)
    {
        return findRequestName(ServletContextRequest.getServletContextRequest(request));
    }

    protected void log(String format, Object... arg)
    {
        if (!isRunning())
            return;

        String s = String.format(format, arg);

        long now = System.currentTimeMillis();
        long ms = now % 1000;
        if (_out != null)
            _out.printf("%s.%03d:%s%n", __date.format(now), ms, s);
        if (LOG.isDebugEnabled())
            LOG.debug(s);
    }

    final AsyncListener _asyncListener = new AsyncListener()
    {
        @Override
        public void onTimeout(AsyncEvent event) throws IOException
        {
            String cname = findContextName(((AsyncContextEvent)event).getServletContext());
            String rname = findRequestName(event.getAsyncContext().getRequest());
            log("!  ctx=%s r=%s onTimeout %s", cname, rname, ((AsyncContextEvent)event).getServletRequestState());
        }

        @Override
        public void onStartAsync(AsyncEvent event) throws IOException
        {
            String cname = findContextName(((AsyncContextEvent)event).getServletContext());
            String rname = findRequestName(event.getAsyncContext().getRequest());
            log("!  ctx=%s r=%s onStartAsync %s", cname, rname, ((AsyncContextEvent)event).getServletRequestState());
        }

        @Override
        public void onError(AsyncEvent event) throws IOException
        {
            String cname = findContextName(((AsyncContextEvent)event).getServletContext());
            String rname = findRequestName(event.getAsyncContext().getRequest());
            log("!! ctx=%s r=%s onError %s %s", cname, rname, event.getThrowable(), ((AsyncContextEvent)event).getServletRequestState());
        }

        @Override
        public void onComplete(AsyncEvent event) throws IOException
        {
            AsyncContextEvent ace = (AsyncContextEvent)event;
            String cname = findContextName(ace.getServletContext());
            String rname = findRequestName(ace.getAsyncContext().getRequest());

            ServletContextRequest request = ServletContextRequest.getServletContextRequest(ace.getAsyncContext().getRequest());
            Response response = request.getServletContextResponse();
            String headers = _showHeaders ? ("\n" + response.getHeaders().toString()) : "";

            log("!  ctx=%s r=%s onComplete %s %d%s", cname, rname, ace.getServletRequestState(), response.getStatus(), headers);
        }
    };

    final ServletRequestListener _servletRequestListener = new ServletRequestListener()
    {
        @Override
        public void requestInitialized(ServletRequestEvent sre)
        {
            String cname = findContextName(sre.getServletContext());
            HttpServletRequest r = (HttpServletRequest)sre.getServletRequest();

            String rname = findRequestName(r);
            DispatcherType d = r.getDispatcherType();
            if (d == DispatcherType.REQUEST)
            {
                ServletContextRequest request = ServletContextRequest.getServletContextRequest(r);
                String headers = _showHeaders ? ("\n" + request.getHeaders().toString()) : "";
                StringBuffer url = r.getRequestURL();
                if (r.getQueryString() != null)
                    url.append('?').append(r.getQueryString());
                log(">> %s ctx=%s r=%s %s %s %s %s %s%s", d,
                    cname,
                    rname,
                    d,
                    r.getMethod(),
                    url.toString(),
                    r.getProtocol(),
                    request.getConnectionMetaData(),
                    headers);
            }
            else
            {
                log(">> %s ctx=%s r=%s", d, cname, rname);
            }
        }

        @Override
        public void requestDestroyed(ServletRequestEvent sre)
        {
            String cname = findContextName(sre.getServletContext());
            HttpServletRequest r = (HttpServletRequest)sre.getServletRequest();
            String rname = findRequestName(r);
            DispatcherType d = r.getDispatcherType();
            if (sre.getServletRequest().isAsyncStarted())
            {
                sre.getServletRequest().getAsyncContext().addListener(_asyncListener);
                log("<< %s ctx=%s r=%s async=true", d, cname, rname);
            }
            else
            {
                ServletContextRequest request = ServletContextRequest.getServletContextRequest(r);
                String headers = _showHeaders ? ("\n" + request.getServletContextResponse().getHeaders().toString()) : "";
                log("<< %s ctx=%s r=%s async=false %d%s", d, cname, rname, request.getServletContextResponse().getStatus(), headers);
            }
        }
    };

    final ServletContextScopeListener _contextScopeListener = new ServletContextScopeListener()
    {
        @Override
        public void enterScope(ServletContextHandler.ServletScopedContext context, ServletContextRequest request)
        {
            String cname = findContextName(context.getServletContext());
            if (request == null)
                log(">  ctx=%s", cname);
            else
            {
                String rname = findRequestName(request);

                if (_renameThread)
                {
                    Thread thread = Thread.currentThread();
                    thread.setName(String.format("%s#%s", thread.getName(), rname));
                }

                log(">  ctx=%s r=%s", cname, rname);
            }
        }

        @Override
        public void exitScope(ServletContextHandler.ServletScopedContext context, ServletContextRequest request)
        {
            String cname = findContextName(context.getServletContext());
            if (request == null)
                log("<  ctx=%s", cname);
            else
            {
                String rname = findRequestName(request);

                log("<  ctx=%s r=%s", cname, rname);
                if (_renameThread)
                {
                    Thread thread = Thread.currentThread();
                    if (thread.getName().endsWith(rname))
                        thread.setName(thread.getName().substring(0, thread.getName().length() - rname.length() - 1));
                }
            }
        }
    };
}
