package lc.esp.engine;

import jakarta.servlet.http.HttpServletResponse;
import lc.mecha.fabric.HandlerStatus;
import lc.mecha.http.server.HttpTransaction;
import lc.mecha.http.server.PrefixedHandler;
import lc.mecha.log.MechaLogger;
import lc.mecha.log.MechaLoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;

/**
 * This handler implements authentication mechanics in a generic manner, so that implementing handlers do not need
 * to concern themselves with the details of the authentication. This implementation supports only
 * HTTP Basic authentication.
 *
 * @author Alex Leigh
 * @since mk19
 */
public abstract class AuthenticatedHandler extends PrefixedHandler {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(AuthenticatedHandler.class);
    private final Directory dir;

    private final HashSet<String> groupIds = new HashSet<>();

    public AuthenticatedHandler(Set<String> path, Directory dir) {
        super(path);
        this.dir = dir;
    }

    /**
     * Handle the given request and attempt to perform basic authentication against the configured directory store.
     * If basic authentication is unsuccessful, 403 forbidden is returned to the caller. If an identity is successfully
     * established, the <code>handleAuthenticated()</code> method is called.
     */
    @Override
    public HandlerStatus handlePrefixedWebRequest(HttpTransaction request) throws Throwable {
        try {
            final String authorization = request.httpServletRequest.getHeader("Authorization");

            if (authorization != null && authorization.toLowerCase().startsWith("basic")) {
                String base64Credentials = authorization.substring("Basic".length()).trim();
                byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
                String credentials = new String(credDecoded, StandardCharsets.UTF_8);
                final String[] values = credentials.split(":", 2);

                String userId = dir.authenticate(values[0], values[1]);

                logger.info("AD returns: {}", userId);

                if (groupIds.size() > 0) {
                    Set<String> groups = dir.findGroups(userId);
                    for (String requireId : groupIds) {
                        if (!groups.contains(requireId)) {
                            request.httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
                            return HandlerStatus.BREAK;
                        }
                    }
                }

                if (userId != null) {
                    return handleAuthenticated(request, dir, userId);
                }
            }

            request.httpServletResponse.setHeader("WWW-Authenticate", "BASIC realm=\"LEIGH&CO\"");
            request.httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return HandlerStatus.BREAK;
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }

    /**
     * Add a group ID to the list of permitted groups. If there are no groups permitted, then authentication
     * will succeed if the provided credentials are valid. If at least one group is provided, then the principal
     * must be a member of that group. If multiple groups are provided, then the principal must be a member of
     * all groups.
     *
     * @param groupId
     */
    public void addGroupId(String groupId) {
        groupIds.add(groupId);
    }

    @Override
    public String toString() {
        return "AuthenticatedHandler{" +
                "dir=" + dir +
                ", groups=" + groupIds +
                '}';
    }

    /**
     * Handle the connection which has now been authenticated. The directory service matching the request is passed,
     * as well as the userId for the user.
     */
    public abstract HandlerStatus handleAuthenticated(HttpTransaction request, Directory dir, String userId) throws Exception;
}
