package ru.yandex.webmaster3.admin.security.idm.http;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import ru.yandex.webmaster3.admin.security.IDMRole;
import ru.yandex.webmaster3.admin.security.service.IDMUsersService;
import ru.yandex.webmaster3.core.WebmasterCommonErrorType;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.security.tvm.TVM2AuthService;
import ru.yandex.webmaster3.core.security.tvm.TVMServiceTicket;
import ru.yandex.webmaster3.core.security.tvm.TVMTokenService;
import ru.yandex.webmaster3.storage.admin.security.Role;

/**
 * @author avhaliullin
 */
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class IDMHttpHandler extends AbstractHandler {
    private static final Logger log = LoggerFactory.getLogger(IDMHttpHandler.class);

    private static final ObjectMapper OM = new ObjectMapper();

    private static final String HOST_FIELD = "host";
    private static final String PASSPORT_LOGIN_FIELD = "passport-login";

    private static final String TARGET_INFO = "info";
    private static final String TARGET_GET_USER_ROLES = "get-user-roles";
    private static final String TARGET_GET_ALL_ROLES = "get-all-roles";
    private static final String TARGET_ADD_ROLE = "add-role";
    private static final String TARGET_REMOVE_ROLE = "remove-role";

    private static final String HEADER_SSL_VERIFIED = "X-Qloud-SSL-Verified";
    private static final String HEADER_SSL_ISSUER = "X-Qloud-SSL-Issuer";
    private static final String HEADER_SSL_SUBJECT = "X-Qloud-SSL-Subject";

    private static final String ROLE_SLUG = "role";

    private static final OKResponse OK_RESPONSE = new OKResponse();

    private static final int MAX_REQUEST_FORM_SIZE = 1024 * 1024;

    private Set<String> allowedSSLVerifiedValues;
    private Set<String> allowedSSLIssuerValues;
    private Set<String> allowedSSLSubjectValues;

    private final IDMUsersService idmUsersService;
    private final TVM2AuthService tvm2AuthService;
    private final String basePath;
    private final String tmpFolder;
    private final String clientCertSubject;
    private final boolean enableClientCertValidation;
    private final Set<Integer> authorizedClientIds;

    public void init() {
        allowedSSLVerifiedValues = new HashSet<>(Arrays.asList(
                "true",
                "SUCCESS"
        ));

        allowedSSLIssuerValues = new HashSet<>(Arrays.asList(
                "/DC=ru/DC=yandex/DC=ld/CN=YandexInternalCA",
                "CN=YandexInternalCA,DC=ld,DC=yandex,DC=ru"
        ));

        allowedSSLSubjectValues = new HashSet<>(Arrays.asList(clientCertSubject.split("\n")));
    }

    @Override
    public void handle(String target, Request request, HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse) throws IOException, ServletException {
        if (!target.startsWith(basePath)) {
            return;
        }
        target = target.substring(basePath.length());
        if (target.endsWith("/")) {
            target = target.substring(0, target.length() - 1);
        }
        log.info("Processing IDM request: '{}', method type {}", target, request.getMethod());
        if (enableClientCertValidation && !requestAllowed(httpServletRequest)) {
            request.setHandled(true);
            httpServletResponse.setStatus(403);
            return;
        }
        if (!enableClientCertValidation && !tvm2Authorized(request)) {
            request.setHandled(true);
            httpServletResponse.setStatus(403);
            return;
        }
        try {
            switch (target) {
                case TARGET_INFO:
                    handleInfo(request, httpServletResponse);
                    break;
                case TARGET_GET_USER_ROLES:
                    handleUserRoles(request, httpServletResponse);
                    break;
                case TARGET_GET_ALL_ROLES:
                    handleAllRoles(request, httpServletResponse);
                    break;
                case TARGET_ADD_ROLE:
                    handleAddRole(request, httpServletResponse);
                    break;
                case TARGET_REMOVE_ROLE:
                    handleRemoveRole(request, httpServletResponse);
                    break;
            }
        } catch (IDMException e) {
            respondWithError(request, httpServletResponse, e, e.isFatal());
        } catch (RuntimeException e) {
            boolean fatal = e instanceof WebmasterException &&
                    ((WebmasterException) e).getError().getCode() == WebmasterCommonErrorType.REQUEST__ILLEGAL_PARAMETER_VALUE;
            respondWithError(request, httpServletResponse, e, fatal);
        } finally {
            log.info("IDM request '{}' processed", target);
        }
    }

    private boolean requestAllowed(HttpServletRequest request) {
        String verified = request.getHeader(HEADER_SSL_VERIFIED);
        if (!allowedSSLVerifiedValues.contains(verified)) {
            log.warn(HEADER_SSL_VERIFIED + "=" + verified);
            return false;
        }
        String issuer = request.getHeader(HEADER_SSL_ISSUER);
        if (!allowedSSLIssuerValues.contains(issuer)) {
            log.warn(HEADER_SSL_ISSUER + "=" + issuer);
            return false;
        }
        String subject = request.getHeader(HEADER_SSL_SUBJECT);
        if (!allowedSSLSubjectValues.contains(subject)) {
            log.warn(HEADER_SSL_SUBJECT + "=" + subject);
            return false;
        }
        return true;
    }

    /**
     * Аутентификация по TVM2
     * @param request
     * @return
     */
    private boolean tvm2Authorized(HttpServletRequest request) {
        String tvm2ticketString = request.getHeader(TVMTokenService.TVM2_TICKET_HEADER);
        if (Strings.isNullOrEmpty(tvm2ticketString)) {
            log.warn("Header {} is missing", TVMTokenService.TVM2_TICKET_HEADER);
            return false;
        }
        TVMServiceTicket ticket = tvm2AuthService.validateTicket(tvm2ticketString.getBytes(StandardCharsets.US_ASCII));
        if (ticket == null) {
            log.warn("TVM 2.0 Ticket validation failed");
            return false;
        }
        log.info("Authenticated client {}", ticket.getClientId());
        if (!authorizedClientIds.contains(ticket.getClientId())) {
            log.warn("TVM 2.0 client {} not authorized for IDM handling", ticket.getClientId());
            return false;
        }
        return true;
    }

    private void handleInfo(Request request, HttpServletResponse httpServletResponse) throws IOException {
        List<RoleInfo> roles = new ArrayList<>();
        for (Role role : Role.values()) {
            roles.add(new RoleInfo(role.getId(), role.getDescription()));
        }
        roles.add(new RoleInfo(IDMRole.HOST_OWNER_ROLE_ID, "Host owner"));
        respond(request, httpServletResponse, new InfoResponse(roles));
    }

    private void handleUserRoles(Request request, HttpServletResponse httpServletResponse) throws IOException {
        String login = getNonEmptyParam(request, "login");
        respond(request, httpServletResponse,
                new GetUserRolesResponse(
                        prepareUserRolesList(idmUsersService.listUserIDMRoles(login))
                )
        );
    }

    private void handleAllRoles(Request request, HttpServletResponse httpServletResponse) throws IOException {
        List<UserWithRoles> usersWithRoles = idmUsersService.listAllUsersRoles().entrySet().stream()
                .map(entry -> new UserWithRoles(entry.getKey().getAdminUser().getLogin(), prepareUserRolesList(entry.getValue())))
                .collect(Collectors.toList());
        respond(request, httpServletResponse, new AllRolesResponse(usersWithRoles));
    }

    private Map<String, String> idmRole2Fields(IDMRole role) {
        Map<String, String> result = new HashMap<>();
        result.put(PASSPORT_LOGIN_FIELD, role.getRequestedLogin());
        if (role instanceof IDMRole.HostOwnerRole) {
            result.put(HOST_FIELD, ((IDMRole.HostOwnerRole) role).getRequestedHost());
        }
        return result;
    }

    private List<List<Object>> prepareUserRolesList(Collection<IDMRole> roles) {
        return roles.stream().map(role -> {
            List<Object> roleDesc = new ArrayList<>(2);
            roleDesc.add(new UserRole(role.getId()));
            roleDesc.add(idmRole2Fields(role));
            return roleDesc;
        }).collect(Collectors.toList());
    }

    private void handleAddRole(Request request, HttpServletResponse httpServletResponse) throws IOException {
        if (isPost(request)) {
            extractParameters(request);
            String login = getNonEmptyParam(request, "login");
            IDMRole idmRole = extractRole(request);
            Optional<String> errorOpt = idmUsersService.addUserRole(login, idmRole);
            if (errorOpt.isPresent()) {
                respondWithFatalError(request, httpServletResponse, errorOpt.get());
            } else {
                respond(request, httpServletResponse, new AddRoleResponse(idmRole));
            }
        }
    }

    private void handleRemoveRole(Request request, HttpServletResponse httpServletResponse) throws IOException {
        if (isPost(request)) {
            extractParameters(request);
            String login = getNonEmptyParam(request, "login");
            String roleId = extractRoleId(request);
            Map<String, Object> fields = extractFields(request, "data");
            IDMRole role;
            String passportLogin = extractAndRemovePassportLogin(fields);
            if (IDMRole.HOST_OWNER_ROLE_ID.equals(roleId)) {
                String hostName = extractAndRemoveHostName(fields);
                log.info("IDM request Remove Role: roleId - {}, login - {}, hostName - {}", roleId, login, hostName);
                role = new IDMRole.HostOwnerRole(passportLogin, hostName);
            } else {
                log.info("IDM request Remove Role: roleId - {}, login - {}", roleId, login);
                role = new IDMRole.SimpleRole(Role.fromId(roleId), passportLogin);
            }
            Optional<String> errorOpt = idmUsersService.removeUserRole(login, role);
            if (errorOpt.isPresent()) {
                respondWithFatalError(request, httpServletResponse, errorOpt.get());
            } else {
                respond(request, httpServletResponse, OK_RESPONSE);
            }
        }
    }

    private boolean isPost(Request request) {
        return "POST".equals(request.getMethod());
    }

    private String getNonEmptyParam(Request request, String name) {
        String value = request.getParameter(name);
        if (StringUtils.isEmpty(value)) {
            throw new IDMException(true, "Parameter '" + name + "' should not be empty");
        }
        return value;
    }

    private IDMRole extractRole(Request request) {
        String roleId = extractRoleId(request);
        Map<String, Object> fieldsMap = extractFields(request, "fields");
        String passportLogin = extractAndRemovePassportLogin(fieldsMap);
        IDMRole result;
        if (IDMRole.HOST_OWNER_ROLE_ID.equals(roleId)) {
            String hostName = extractAndRemoveHostName(fieldsMap);
            result = new IDMRole.HostOwnerRole(passportLogin, hostName);
        } else {
            Object hostField = fieldsMap.get(HOST_FIELD);
            if (hostField != null && !StringUtils.isEmpty(hostField.toString())) {
                throw new IDMException(true, "\"Site\" parameter used only with \"Host owner\" role");
            }
            Role role = Role.fromId(roleId);
            if (role == null) {
                throw new IDMException(true, "Unknown role " + roleId);
            }
            result = new IDMRole.SimpleRole(role, passportLogin);
        }
        if (fieldsMap.values().stream().filter(v -> !v.toString().isEmpty()).findAny().isPresent()) {
            throw new IDMException(true, "Unknown extra fields: " + String.join(", ", fieldsMap.keySet()));
        }
        return result;
    }

    private Map<String, Object> extractFields(Request request, String paramName) {
        String fields = getNonEmptyParam(request, paramName);
        try {
            return OM.readValue(fields, new TypeReference<Map<String, Object>>() {
            });
        } catch (IOException e) {
            throw new IDMException(true, "Failed to parse json from 'fields' parameter " + fields);
        }
    }

    private String extractAndRemovePassportLogin(Map<String, Object> fieldsMap) {
        Object passportLoginObject = fieldsMap.get(PASSPORT_LOGIN_FIELD);
        if (!(passportLoginObject instanceof String)) {
            throw new IDMException(true, "Field '" + PASSPORT_LOGIN_FIELD + "' not found");
        }
        fieldsMap.remove(PASSPORT_LOGIN_FIELD);
        return (String) passportLoginObject;
    }

    private String extractAndRemoveHostName(Map<String, Object> fieldsMap) {
        Object hostObject = fieldsMap.get(HOST_FIELD);
        if (!(hostObject instanceof String)) {
            throw new IDMException(true, "Host parameter is required for this role");
        }
        fieldsMap.remove(HOST_FIELD);
        return (String) hostObject;
    }

    private String extractRoleId(Request request) {
        String role = getNonEmptyParam(request, "role");
        RoleRequestParam roleRequest;
        try {
            roleRequest = OM.readValue(role, RoleRequestParam.class);

        } catch (IOException e) {
            throw new IDMException(true, "Failed to parse json from 'role' parameter");
        }
        String roleId = roleRequest.role;
        if (StringUtils.isEmpty(roleId)) {
            throw new IDMException(true, "Parameter 'role." + ROLE_SLUG + "' should not be empty");
        }
        return roleId;
    }

    private void extractParameters(Request request) {
        request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT,
                new MultipartConfigElement(tmpFolder,
                        MAX_REQUEST_FORM_SIZE,
                        MAX_REQUEST_FORM_SIZE,
                        MAX_REQUEST_FORM_SIZE)
        );
        request.extractParameters();
    }

    private void respondWithError(Request request, HttpServletResponse httpServletResponse, Exception e, boolean fatal) throws IOException {
        String message = "Request " + request.getPathInfo() + " failed: " + e.getMessage();
        log.error(message, e);
        Response response = fatal ? new FatalResponse(message) : new ErrorResponse(message);
        respond(request, httpServletResponse, response);
    }

    private void respondWithFatalError(Request request, HttpServletResponse httpServletResponse, String message) throws IOException {
        log.error("Fatal idm error: " + message);
        Response response = new FatalResponse(message);
        respond(request, httpServletResponse, response);
    }

    private void respond(Request request, HttpServletResponse httpServletResponse, Response response) throws IOException {
        request.setHandled(true);
        httpServletResponse.setHeader("Content-Type", "application/json; charset=utf8");
        try (OutputStream os = httpServletResponse.getOutputStream()) {
            OM.writeValue(os, response);
        }
        httpServletResponse.setStatus(200);
    }

    public static class Response {
        public final int code;

        public Response(int code) {
            this.code = code;
        }
    }

    public static class OKResponse extends Response {
        public OKResponse() {
            super(0);
        }
    }

    public static class ErrorResponse extends Response {
        public final String error;

        public ErrorResponse(String message) {
            super(1);
            this.error = message;
        }
    }

    public static class FatalResponse extends Response {
        public final String fatal;

        public FatalResponse(String message) {
            super(1);
            this.fatal = message;
        }
    }

    public static class UserRole {
        @JsonProperty(value = ROLE_SLUG)
        public final String role;

        public UserRole(String role) {
            this.role = role;
        }
    }

    public static class FieldDescriptor {
        public final String slug;
        public final String name;
        public final boolean required;

        public FieldDescriptor(String slug, String name, boolean required) {
            this.slug = slug;
            this.name = name;
            this.required = required;
        }
    }

    public static class InfoResponse extends OKResponse {
        public final RolesDescription roles;
        public final List<FieldDescriptor> fields = new ArrayList<>();

        {
            fields.add(new FieldDescriptor(PASSPORT_LOGIN_FIELD, "Паспортный логин", true));
            fields.add(new FieldDescriptor(HOST_FIELD, "Сайт для подтверждения (только для роли \"Host owner\")", false));
        }

        public InfoResponse(Collection<RoleInfo> roles) {
            this.roles = new RolesDescription(roles);
        }
    }

    public static class AddRoleResponse extends OKResponse {
        public final Map<String, Object> data;

        public AddRoleResponse(IDMRole role) {
            this.data = new HashMap<>();
            data.put(PASSPORT_LOGIN_FIELD, role.getRequestedLogin());
            if (role instanceof IDMRole.HostOwnerRole) {
                data.put(HOST_FIELD, ((IDMRole.HostOwnerRole) role).getRequestedHost());
            }
        }
    }

    public static class RolesDescription {
        public final Map<String, String> values;
        public final String slug = ROLE_SLUG;
        public final String name = "Роль";

        public RolesDescription(Collection<RoleInfo> roles) {
            this.values = new HashMap<>();
            for (RoleInfo role : roles) {
                values.put(role.id, role.description);
            }
        }
    }

    public static class RoleInfo {
        private final String id;
        private final String description;

        public RoleInfo(String id, String description) {
            this.id = id;
            this.description = description;
        }
    }

    public static class GetUserRolesResponse extends OKResponse {
        public final List<List<Object>> roles;

        public GetUserRolesResponse(List<List<Object>> roles) {
            this.roles = roles;
        }
    }

    public static class UserWithRoles {
        public final List<List<Object>> roles;
        public final String login;

        public UserWithRoles(String login, List<List<Object>> roles) {
            this.roles = roles;
            this.login = login;
        }
    }

    public static class AllRolesResponse extends OKResponse {
        public final List<UserWithRoles> users;

        public AllRolesResponse(List<UserWithRoles> users) {
            this.users = users;
        }
    }

    public static class RoleRequestParam {
        public final String role;

        public RoleRequestParam(@JsonProperty(value = ROLE_SLUG) String role) {
            this.role = role;
        }
    }
}
