/*
 * Copyright (C) 2025-2026  Tim Zimmermann <tim@aglais.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.aglais.zms.server.routing;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.unix.Errors;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.util.AttributeKey;
import org.aglais.zms.db.core.Database;
import org.aglais.zms.server.AglaisBaseHttpRequestHandler;
import org.aglais.zms.server.ChannelUtil;
import org.aglais.zms.server.route.ApiRoute;
import org.aglais.zms.server.route.DbAdminRoute;
import org.aglais.zms.server.route.DbSpeciesRoute;
import org.aglais.zms.server.route.DbUserRoute;
import org.aglais.zms.util.Log;
import org.apache.logging.log4j.util.TriConsumer;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;

import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static org.aglais.zms.api.core.APIConstants.USER_AGENT;

/**
 * HTTP request handler implementation
 */
@ChannelHandler.Sharable
public class AglaisHttpRouteHandler extends AglaisBaseHttpRequestHandler {

    /**
     * {@link AttributeKey} to retrieve the last received {@link HttpRequest}
     */
    private static final AttributeKey<HttpRequest> LAST_REQUEST_KEY = AttributeKey.valueOf("last_request");

    private static final String LOG_TAG = AglaisHttpRouteHandler.class.getSimpleName();

    private final Database db;
    private final ChannelGroup allChannels;
    private final HashMap<String, HashMap<io.netty.handler.codec.http.HttpMethod,
            TriConsumer<ChannelHandlerContext, HttpRequest, FullHttpResponse>>> handlers = new HashMap<>();

    /**
     * Create a new HTTP request handler operating on the specified {@link Database}
     *
     * @param advertiseQuic True if support for QUIC should be advertised using the Alt-Svc header
     * @param db            {@link Database} to operate on
     * @param allChannels   {@link ChannelGroup} containing all currently connected clients
     */
    public AglaisHttpRouteHandler(boolean advertiseQuic, Database db, ChannelGroup allChannels) {
        super(advertiseQuic);
        this.db = db;
        this.allChannels = allChannels;

        registerAnnotatedHandlers(new ApiRoute());
        registerAnnotatedHandlers(new DbAdminRoute(db));
        registerAnnotatedHandlers(new DbSpeciesRoute(db));
        registerAnnotatedHandlers(new DbUserRoute(db, allChannels));
    }

    /**
     * Register all handler methods annotated with {@link HttpRoute}
     *
     * @param handlerInstance The object containing handler methods
     */
    private void registerAnnotatedHandlers(Object handlerInstance) {
        for (Method method : handlerInstance.getClass().getDeclaredMethods()) {
            if (method.isAnnotationPresent(HttpRoute.class)) {
                HttpRoute route = method.getAnnotation(HttpRoute.class);
                handlers.computeIfAbsent(route.path(), k -> new HashMap<>())
                        .put(io.netty.handler.codec.http.HttpMethod.valueOf(route.method().name()),
                                (ctx, request, response) -> {
                                    try {
                                        method.invoke(handlerInstance, ctx, request, response);
                                        response.headers().set(HttpHeaderNames.CONTENT_TYPE, route.contentType());
                                    } catch (IllegalAccessException | InvocationTargetException e) {
                                        throw new RuntimeException(e);
                                    }
                                });
            }
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        if (ChannelUtil.isChildConnectionChannel(ctx.channel()))
            allChannels.add(ctx.channel().parent());
        allChannels.add(ctx.channel());
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) {
        ctx.channel().attr(LAST_REQUEST_KEY).set(msg);

        if (!db.isConnected()) {
            db.connect();
        }

        FullHttpResponse httpResponse = new DefaultFullHttpResponse(msg.protocolVersion(), OK,
                ctx.channel().alloc().buffer());
        httpResponse.headers().set(HttpHeaderNames.SERVER, USER_AGENT);

        if (handlers.containsKey(msg.uri())) {
            io.netty.handler.codec.http.HttpMethod handlerMethod = msg.method();
            // Allow HEAD for all GET endpoints even if not explicitly specified
            if (handlerMethod == io.netty.handler.codec.http.HttpMethod.HEAD && !handlers.get(msg.uri()).containsKey(handlerMethod))
                handlerMethod = HttpMethod.GET;
            TriConsumer<ChannelHandlerContext, HttpRequest, FullHttpResponse> handler = handlers.get(msg.uri())
                    .get(handlerMethod);
            if (handler != null) {
                try {
                    handler.accept(ctx, msg, httpResponse);
                } catch (Throwable t) {
                    Log.e(LOG_TAG, "Failed to invoke handler for " + msg.uri() + " method " + handlerMethod, t);

                    httpResponse.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
                }
            } else {
                httpResponse.setStatus(METHOD_NOT_ALLOWED);
            }
        } else {
            httpResponse.setStatus(NOT_FOUND);
        }

        writeResponse(ctx, msg, httpResponse);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        if (cause instanceof Errors.NativeIoException e) {
            // When using epoll on linux in combination with SSL we'll end up receiving
            // -ECONNRESET whenever a client disconnects
            // No need to do anything other than closing the connection in this case
            if (e.expectedErr() == -104 /* -ECONNRESET */) {
                ctx.close();
                return;
            }
        }

        Log.e(LOG_TAG, "Exception in HTTP handler", cause);

        HttpRequest lastRequest = ctx.channel().attr(LAST_REQUEST_KEY).get();
        if (lastRequest == null) {
            ctx.close();
            return;
        }

        FullHttpResponse httpResponse = new DefaultFullHttpResponse(lastRequest.protocolVersion(), INTERNAL_SERVER_ERROR);
        writeResponse(ctx, lastRequest, httpResponse);
    }
}
