/*
 * Copyright (C) 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.route;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import org.aglais.zms.db.core.Database;
import org.aglais.zms.db.core.model.User;
import org.aglais.zms.server.AglaisHttpAuthenticator;
import org.aglais.zms.server.ChannelUtil;
import org.aglais.zms.server.routing.HttpMethod;
import org.aglais.zms.server.routing.HttpRoute;

import java.util.ArrayList;
import java.util.List;

import static org.aglais.zms.api.core.APIConstants.*;
import static org.aglais.zms.server.route.util.RouteUtil.readJson;
import static org.aglais.zms.server.route.util.RouteUtil.writeJson;

/**
 * Route handler for /db/user/* endpoints
 */
public class DbUserRoute {

    private final Database db;
    private final ChannelGroup allChannels;

    /**
     * Create a new user route handler
     *
     * @param db          The {@link Database} to operate on
     * @param allChannels {@link ChannelGroup} containing all the channels currently connected
     */
    public DbUserRoute(Database db, ChannelGroup allChannels) {
        this.db = db;
        this.allChannels = allChannels;
    }

    /**
     * Load the password for a given user from the DB, if it is null. This allows to update a user over the API without
     * knowing or changing the password.
     *
     * @param user The {@link User} to load the password for
     * @return The input object
     */
    private User loadPasswordFromDbIfNeeded(User user) {
        if (user != null && user.getPassword() == null) {
            User dbUser = db.getUserByID(user.getID());

            if (dbUser != null)
                user.setPassword(dbUser.getPassword());
        }

        return user;
    }

    /**
     * Get all active channels having a valid logged in {@link User} matching the given user
     *
     * @param user The user to search for
     * @return All channels where the given user is logged in
     */
    private List<Channel> getUserChannels(User user) {
        List<Channel> result = new ArrayList<>();

        for (Channel ch : allChannels) {
            User chUser = ch.attr(AglaisHttpAuthenticator.USER_KEY).get();

            if (chUser != null && user.getID() == chUser.getID()) {
                result.add(ch);
            }
        }

        return result;
    }

    /**
     * Check if two channels are part of the same connection
     * <p>
     * The following cases are being taken care of:<br>
     * 1. The channels are directly equal<br>
     * 2. One of the channels has {@link ChannelUtil#isChildConnectionChannel} return true and the other channel
     * is its parent<br>
     * 3. Both channels have {@link ChannelUtil#isChildConnectionChannel} return true and have the same parent
     *
     * @param ch1 The first {@link Channel}
     * @param ch2 The second {@link Channel}
     * @return True if the channels are part of the same connection, false otherwise
     */
    private boolean isSameConnection(Channel ch1, Channel ch2) {
        if (ch2 == ch1)
            return true;

        boolean ch1IsChild = ChannelUtil.isChildConnectionChannel(ch1);
        boolean ch2IsChild = ChannelUtil.isChildConnectionChannel(ch2);

        if (ch1IsChild && ch1.parent() == ch2)
            return true;

        if (ch2IsChild && ch2.parent() == ch1)
            return true;

        return ch1IsChild && ch2IsChild && ch1.parent() == ch2.parent();
    }

    /**
     * Disconnect all connections by a given {@link User}
     *
     * @param ownCh Own {@link Channel}, this will not be closed even if the user matches
     * @param user  The user to disconnect
     */
    private void kickUser(Channel ownCh, User user) {
        for (Channel ch : getUserChannels(user)) {
            if (!isSameConnection(ownCh, ch))
                ch.close();
        }
    }

    /**
     * Update the {@link AglaisHttpAuthenticator#USER_KEY} attribute for the given {@link User}
     *
     * @param user The user to update
     */
    private void updateUser(User user) {
        for (Channel ch : getUserChannels(user))
            ch.attr(AglaisHttpAuthenticator.USER_KEY).set(user);
    }

    @HttpRoute(path = URL_DB_ADD_USER, method = HttpMethod.POST)
    public void addUser(ChannelHandlerContext ctx, HttpRequest request, FullHttpResponse response) {
        writeJson(response.content(), db.addUser(readJson(request, User.class)));
    }

    @HttpRoute(path = URL_DB_ALL_USERS)
    public void allUsers(ChannelHandlerContext ctx, HttpRequest request, FullHttpResponse response) {
        writeJson(response.content(), db.getAllUsers());
    }

    @HttpRoute(path = URL_DB_DELETE_USER, method = HttpMethod.POST)
    public void deleteUser(ChannelHandlerContext ctx, HttpRequest request, FullHttpResponse response) {
        User user = readJson(request, User.class);
        boolean result = db.deleteUser(user);

        if (result)
            kickUser(ctx.channel(), user);

        writeJson(response.content(), result);
    }

    @HttpRoute(path = URL_DB_UPDATE_USER, method = HttpMethod.PUT)
    public void updateUser(ChannelHandlerContext ctx, HttpRequest request, FullHttpResponse response) {
        User user = readJson(request, User.class);
        // Check if the username is allowed
        boolean result = user.getName().matches("^[^:]+$");

        // Check that no other user is using this name yet
        User existingUser = db.getUserByName(user.getName());
        result = result && (existingUser == null || existingUser.getID() == user.getID());

        result = result && db.updateUser(loadPasswordFromDbIfNeeded(user));

        // Update user attribute
        if (result)
            updateUser(user);

        writeJson(response.content(), db.updateUser(user));
    }

    @HttpRoute(path = URL_DB_USER_BY_NAME, method = HttpMethod.POST)
    public void userByName(ChannelHandlerContext ctx, HttpRequest request, FullHttpResponse response) {
        writeJson(response.content(), db.getUserByName(readJson(request, String.class)));
    }

    @HttpRoute(path = URL_DB_USER_BY_ID, method = HttpMethod.POST)
    public void userById(ChannelHandlerContext ctx, HttpRequest request, FullHttpResponse response) {
        writeJson(response.content(), db.getUserByID(readJson(request, Integer.class)));
    }

}
