package ru.yandex.webmaster3.admin.security.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import ru.yandex.webmaster3.admin.security.IDMRole;
import ru.yandex.webmaster3.admin.service.AdminUsersHostsService;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.blackbox.UserWithLogin;
import ru.yandex.webmaster3.core.blackbox.service.BlackboxUsersService;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.host.verification.VerificationType;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;
import ru.yandex.webmaster3.storage.admin.AdminUserInfo;
import ru.yandex.webmaster3.storage.admin.IDMVerificationRecord;
import ru.yandex.webmaster3.storage.admin.UserRoleRecord;
import ru.yandex.webmaster3.storage.admin.dao.AdminUsersYDao;
import ru.yandex.webmaster3.storage.admin.dao.IDMVerificationsYDao;
import ru.yandex.webmaster3.storage.admin.dao.UserRolesYDao;
import ru.yandex.webmaster3.storage.admin.security.Role;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author avhaliullin
 */
@Slf4j
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class IDMUsersService {

    private final List<RoleRemoveObserver> observers = new ArrayList<>();

    private final BlackboxUsersService blackboxYandexTeamUsersService;
    private final BlackboxUsersService blackboxExternalYandexUsersService;
    private final AdminUsersYDao adminUsersYDao;
    private final UserRolesYDao userRolesYDao;
    private final IDMVerificationsYDao iDMVerificationsYDao;
    private final AdminUsersHostsService adminUsersHostsService;

    @PostConstruct
    public void init() {
        registerRemoveRoleObserver(adminUsersHostsService);
    }

    public Map<AdminUserInfo, Set<IDMRole>> listAllUsersRoles() {
        try {
            List<UserRoleRecord> simpleRoles = userRolesYDao.getAllUsersRoles();
            Map<Long, Set<IDMRole>> usersRoles = new HashMap<>();
            for (UserRoleRecord record : simpleRoles) {
                Set<IDMRole> userRolesSet = usersRoles.computeIfAbsent(record.getUserId(), ignored -> new HashSet<>());
                userRolesSet.add(new IDMRole.SimpleRole(record.getRole(), record.getRequestedLogin()));
            }
            List<IDMVerificationRecord> idmVerificationRecords = iDMVerificationsYDao.listAllVerifications();
            for (IDMVerificationRecord record : idmVerificationRecords) {
                Set<IDMRole> userRolesSet = usersRoles.computeIfAbsent(record.getAdminUserId(), ignored -> new HashSet<>());
                userRolesSet.add(new IDMRole.HostOwnerRole(record.getRequestedLogin(), record.getRequestedHost()));
            }
            Map<AdminUserInfo, Set<IDMRole>> result = new HashMap<>();
            for (Map.Entry<Long, Set<IDMRole>> entry : usersRoles.entrySet()) {
                AdminUserInfo adminUserInfo = adminUsersYDao.getUserInfoById(entry.getKey());
                result.put(adminUserInfo, entry.getValue());
            }
            return result;
        } catch (WebmasterYdbException e) {
            throw new WebmasterException("Unable to list all users with roles",
                    new WebmasterErrorResponse.YDBErrorResponse(this.getClass(), e), e);
        }
    }

    private Set<IDMRole> listUserIDMRoles(AdminUserInfo userInfo) {
        try {
            long adminUserId = userInfo.getAdminUser().getUserId();
            long passportUserId = userInfo.getPassportUser().getUserId();
            List<UserRoleRecord> simpleRoles = userRolesYDao.getUserRoles(adminUserId);
            Set<IDMRole> result = new HashSet<>();
            for (UserRoleRecord userRoleRecord : simpleRoles) {
                result.add(new IDMRole.SimpleRole(userRoleRecord.getRole(), userRoleRecord.getRequestedLogin()));
            }
            List<IDMVerificationRecord> idmVerificationRecords =
                    iDMVerificationsYDao.listVerificationsForUser(passportUserId);
            for (IDMVerificationRecord record : idmVerificationRecords) {
                result.add(new IDMRole.HostOwnerRole(record.getRequestedLogin(), record.getRequestedHost()));
            }
            return result;
        } catch (WebmasterYdbException e) {
            throw new WebmasterException("Unable to list all roles for user",
                    new WebmasterErrorResponse.YDBErrorResponse(this.getClass(), e), e);
        }
    }

    public Set<IDMRole> listUserIDMRoles(String login) {
        try {
            AdminUserInfo user = adminUsersYDao.getUserInfoByLogin(login);
            if (user == null) {
                return Collections.emptySet();
            }
            return listUserIDMRoles(user);
        } catch (WebmasterYdbException e) {
            throw new WebmasterException("Unable to list all roles for user",
                    new WebmasterErrorResponse.YDBErrorResponse(this.getClass(), e), e);
        }
    }

    public Optional<String> addUserRole(String adminUserLogin, IDMRole role) {
        log.info("IDM request Add Role: login - {}", adminUserLogin);
        AdminUserInfo adminUser = adminUsersYDao.getUserInfoByLogin(adminUserLogin);
        UserWithLogin ytUser;
        UserWithLogin extUser = getUserForLogin(role.getRequestedLogin(), BlackboxType.EXTERNAL_YANDEX);
        if (adminUser == null) {
            ytUser = getUserForLogin(adminUserLogin, BlackboxType.YANDEX_TEAM);
            adminUser = new AdminUserInfo(ytUser, extUser);
            adminUsersYDao.addUserInfo(adminUser);
        } else {
            ytUser = adminUser.getAdminUser();
            if (!adminUser.getPassportUser().getLogin().equals(extUser.getLogin())) {
                if (userRolesYDao.getUserRoles(adminUser.getAdminUser().getUserId()).isEmpty()) {
                    adminUsersYDao.addUserInfo(new AdminUserInfo(ytUser, extUser));
                } else {
                    return Optional.of("IDM cannot add role for user " + adminUserLogin + ". " +
                            "New passport login (" + role.getRequestedLogin() + ") " +
                            "differs from old one (" + adminUser.getPassportUser().getLogin());
                }
            }
        }

        if (role instanceof IDMRole.SimpleRole) {
            IDMRole.SimpleRole simpleRole = (IDMRole.SimpleRole) role;
            log.info("IDM request Add Role: roleId - {}, roleRequestedLogin - {}, role - {}", role.getId(), role.getRequestedLogin(), ((IDMRole.SimpleRole) role).getRole());
            return addSimpleRole(adminUser, simpleRole);
        } else if (role instanceof IDMRole.HostOwnerRole) {
            log.info("IDM request Add Role: roleId - {}, roleRequestedLogin - {}, roleRequestedHost - {}", role.getId(), role.getRequestedLogin(), ((IDMRole.HostOwnerRole) role).getRequestedHost());
            return addOwnHostRole(adminUser, (IDMRole.HostOwnerRole) role);
        } else {
            throw new RuntimeException("Unknown role class " + role.getClass());
        }
    }

    private Optional<String> addOwnHostRole(AdminUserInfo adminUserInfo, IDMRole.HostOwnerRole role)  {
        WebmasterHostId hostId;
        try {
            hostId = IdUtils.urlToHostId(role.getRequestedHost());
        } catch (Exception e) {
            log.warn("Bad host passed in host owner role", e);
            return Optional.of("Bad host name passed: \"" + role.getRequestedHost() + "\"");
        }
        long passportUserId = adminUserInfo.getPassportUser().getUserId();
        IDMVerificationRecord oldRecord =
                iDMVerificationsYDao.getVerificationForUserAndHost(passportUserId, hostId);
        if (oldRecord != null) {
            return Optional.of("Already have role for user " + adminUserInfo.getPassportUser().getLogin() +
                    " and host " + IdUtils.hostIdToUrl(hostId));
        }
        iDMVerificationsYDao.addRecord(
                new IDMVerificationRecord(
                        passportUserId,
                        hostId,
                        adminUserInfo.getAdminUser().getUserId(),
                        role.getRequestedHost(),
                        role.getRequestedLogin(),
                        DateTime.now()
                )
        );
        adminUsersHostsService.startVerification(passportUserId, hostId, VerificationType.IDM);
        return Optional.empty();
    }

    private Optional<String> addSimpleRole(AdminUserInfo adminUserInfo, IDMRole.SimpleRole role)  {
        userRolesYDao.addRole(adminUserInfo.getAdminUser().getUserId(), role.getRole(), role.getRequestedLogin());
        return Optional.empty();
    }

    public Optional<String> removeUserRole(String userLogin, IDMRole role) {
        AdminUserInfo user = adminUsersYDao.getUserInfoByLogin(userLogin);
        if (user == null) {
            return Optional.of("User " + userLogin + " not found in system");
        }
        if (role instanceof IDMRole.SimpleRole) {
            return removeSimpleRole(user, (IDMRole.SimpleRole) role);
        } else if (role instanceof IDMRole.HostOwnerRole) {
            return removeOwnHostRole(user, (IDMRole.HostOwnerRole) role);
        } else {
            throw new RuntimeException("Unknown role class " + role.getClass());
        }
    }

    private Optional<String> removeOwnHostRole(AdminUserInfo user, IDMRole.HostOwnerRole role) {
        long passportUserId = user.getPassportUser().getUserId();
        WebmasterHostId hostId = IdUtils.urlToHostId(role.getRequestedHost());
        IDMVerificationRecord verificationRecord = iDMVerificationsYDao.getVerificationForUserAndHost(passportUserId, hostId);
        iDMVerificationsYDao.removeRecord(passportUserId, hostId);
        if (verificationRecord != null) {
            adminUsersHostsService.startVerification(passportUserId, hostId, VerificationType.IDM);
        }
        return Optional.empty();
    }

    private Optional<String> removeSimpleRole(AdminUserInfo user, IDMRole.SimpleRole role)  {
        Set<Role> roles = userRolesYDao.getUserRoles(user.getAdminUser().getUserId())
                .stream().map(UserRoleRecord::getRole).collect(Collectors.toSet());
        roles.remove(role.getRole());

        for (RoleRemoveObserver observer : observers) {
            observer.observeUserRolesChange(user, roles);
        }
        userRolesYDao.removeRole(user.getAdminUser().getUserId(), role.getRole());
        return Optional.empty();
    }

    public UserWithLogin getAssociatedExternalUserInfo(long adminUserId) {
        AdminUserInfo userInfo = adminUsersYDao.getUserInfoById(adminUserId);
        if (userInfo == null) {
            throw new WebmasterException("User " + adminUserId + " not found in DB",
                    new WebmasterErrorResponse.DataConsistencyErrorResponse(getClass(), null));
        }
        return userInfo.getPassportUser();
    }

    public long getAssociatedExternalUserId(long adminUserId) {
        return getAssociatedExternalUserInfo(adminUserId).getUserId();
    }

    public AdminUserInfo getUserInfoByLogin(String login) {
        return adminUsersYDao.getUserInfoByLogin(login);
    }

    private UserWithLogin getUserForLogin(String login, BlackboxType blackboxType) {
        UserWithLogin user = blackboxType == BlackboxType.EXTERNAL_YANDEX ?
                blackboxExternalYandexUsersService.getUserByLogin(login) :
                blackboxYandexTeamUsersService.getUserByLogin(login);
        if (user == null) {
            throw new WebmasterException("User '" + login + "' not found in " + blackboxType + " blackbox",
                    new WebmasterErrorResponse.BlackboxErrorResponse(getClass(), null));
        }
        return user;
    }

    private static Role roleId2Role(String id) {
        return Role.valueOf(id.toUpperCase());
    }


    private enum BlackboxType {
        YANDEX_TEAM,
        EXTERNAL_YANDEX
    }

    public void registerRemoveRoleObserver(RoleRemoveObserver observer) {
        observers.add(observer);
    }
}
