package com.github.steveice10.mc.auth.service;

import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException;
import com.github.steveice10.mc.auth.exception.request.RequestException;
import com.github.steveice10.mc.auth.util.HTTP;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

/**
 * Service used for authenticating users.
 */
public class AuthenticationService extends Service {
    private static final URI DEFAULT_BASE_URI = URI.create("https://authserver.mojang.com/");
    private static final String AUTHENTICATE_ENDPOINT = "authenticate";
    private static final String REFRESH_ENDPOINT = "refresh";
    private static final String INVALIDATE_ENDPOINT = "invalidate";

    private String clientToken;

    private String username;
    private String password;
    private String accessToken;

    private boolean loggedIn;
    private String id;
    private List<GameProfile.Property> properties = new ArrayList<>();
    private List<GameProfile> profiles = new ArrayList<>();
    private GameProfile selectedProfile;

    /**
     * Creates a new AuthenticationService instance.
     */
    public AuthenticationService() {
        this(UUID.randomUUID().toString());
    }

    /**
     * Creates a new AuthenticationService instance.
     *
     * @param clientToken Client token to use when making authentication requests.
     */
    public AuthenticationService(String clientToken) {
        super(DEFAULT_BASE_URI);

        if(clientToken == null) {
            throw new IllegalArgumentException("ClientToken cannot be null.");
        }

        this.clientToken = clientToken;
    }

    /**
     * Gets the client token of the service.
     *
     * @return The service's client token.
     */
    public String getClientToken() {
        return this.clientToken;
    }

    /**
     * Gets the username of the service.
     *
     * @return The service's username.
     */
    public String getUsername() {
        return this.id;
    }

    /**
     * Gets the password of the service.
     *
     * @return The user's ID.
     */
    public String getPassword() {
        return this.password;
    }

    /**
     * Gets the access token of the service.
     *
     * @return The user's access token.
     */
    public String getAccessToken() {
        return this.accessToken;
    }

    /**
     * Gets whether the service has been used to log in.
     *
     * @return Whether the service is logged in.
     */
    public boolean isLoggedIn() {
        return this.loggedIn;
    }

    /**
     * Gets the ID of the user logged in with the service.
     *
     * @return The user's ID.
     */
    public String getId() {
        return this.id;
    }

    /**
     * Gets the properties of the user logged in with the service.
     *
     * @return The user's properties.
     */
    public List<GameProfile.Property> getProperties() {
        return Collections.unmodifiableList(this.properties);
    }

    /**
     * Gets the available profiles of the user logged in with the service.
     *
     * @return The user's available profiles.
     */
    public List<GameProfile> getAvailableProfiles() {
        return Collections.unmodifiableList(this.profiles);
    }

    /**
     * Gets the selected profile of the user logged in with the service.
     *
     * @return The user's selected profile.
     */
    public GameProfile getSelectedProfile() {
        return this.selectedProfile;
    }

    /**
     * Sets the username of the service.
     *
     * @param username Username to set.
     */
    public void setUsername(String username) {
        if(this.loggedIn && this.selectedProfile != null) {
            throw new IllegalStateException("Cannot change username while user is logged in and profile is selected.");
        } else {
            this.username = username;
        }
    }

    /**
     * Sets the password of the service.
     *
     * @param password Password to set.
     */
    public void setPassword(String password) {
        if(this.loggedIn && this.selectedProfile != null) {
            throw new IllegalStateException("Cannot change password while user is logged in and profile is selected.");
        } else {
            this.password = password;
        }
    }

    /**
     * Sets the access token of the service.
     *
     * @param accessToken Access token to set.
     */
    public void setAccessToken(String accessToken) {
        if(this.loggedIn && this.selectedProfile != null) {
            throw new IllegalStateException("Cannot change access token while user is logged in and profile is selected.");
        } else {
            this.accessToken = accessToken;
        }
    }

    /**
     * Logs the service in.
     * The current access token will be used if set. Otherwise, password-based authentication will be used.
     *
     * @throws RequestException If an error occurs while making the request.
     */
    public void login() throws RequestException {
        if(this.username == null || this.username.equals("")) {
            throw new InvalidCredentialsException("Invalid username.");
        }

        boolean token = this.accessToken != null && !this.accessToken.equals("");
        boolean password = this.password != null && !this.password.equals("");
        if(!token && !password) {
            throw new InvalidCredentialsException("Invalid password or access token.");
        }

        AuthenticateRefreshResponse response;
        if(token) {
            RefreshRequest request = new RefreshRequest(this.clientToken, this.accessToken, null);
            response = HTTP.makeRequest(this.getProxy(), this.getEndpointUri(REFRESH_ENDPOINT), request, AuthenticateRefreshResponse.class);
        } else {
            AuthenticationRequest request = new AuthenticationRequest(this.username, this.password, this.clientToken);
            response = HTTP.makeRequest(this.getProxy(), this.getEndpointUri(AUTHENTICATE_ENDPOINT), request, AuthenticateRefreshResponse.class);
        }

        if(response == null) {
            throw new RequestException("Server returned invalid response.");
        } else if(!response.clientToken.equals(this.clientToken)) {
            throw new RequestException("Server responded with incorrect client token.");
        }

        if(response.user != null && response.user.id != null) {
            this.id = response.user.id;
        } else {
            this.id = this.username;
        }

        this.accessToken = response.accessToken;
        this.profiles = response.availableProfiles != null ? Arrays.asList(response.availableProfiles) : Collections.<GameProfile>emptyList();
        this.selectedProfile = response.selectedProfile;

        this.properties.clear();
        if(response.user != null && response.user.properties != null) {
            this.properties.addAll(response.user.properties);
        }

        this.loggedIn = true;
    }

    /**
     * Logs the service out.
     *
     * @throws RequestException If an error occurs while making the request.
     */
    public void logout() throws RequestException {
        if(!this.loggedIn) {
            throw new IllegalStateException("Cannot log out while not logged in.");
        }

        InvalidateRequest request = new InvalidateRequest(this.clientToken, this.accessToken);
        HTTP.makeRequest(this.getProxy(), this.getEndpointUri(INVALIDATE_ENDPOINT), request);

        this.accessToken = null;
        this.loggedIn = false;
        this.id = null;
        this.properties.clear();
        this.profiles.clear();
        this.selectedProfile = null;
    }

    /**
     * Selects a game profile.
     *
     * @param profile Profile to select.
     * @throws RequestException If an error occurs while making the request.
     */
    public void selectGameProfile(GameProfile profile) throws RequestException {
        if(!this.loggedIn) {
            throw new RequestException("Cannot change game profile while not logged in.");
        } else if(this.selectedProfile != null) {
            throw new RequestException("Cannot change game profile when it is already selected.");
        } else if(profile == null || !this.profiles.contains(profile)) {
            throw new IllegalArgumentException("Invalid profile '" + profile + "'.");
        }

        RefreshRequest request = new RefreshRequest(this.clientToken, this.accessToken, profile);
        AuthenticateRefreshResponse response = HTTP.makeRequest(this.getProxy(), this.getEndpointUri(REFRESH_ENDPOINT), request, AuthenticateRefreshResponse.class);
        if(response == null) {
            throw new RequestException("Server returned invalid response.");
        } else if(!response.clientToken.equals(this.clientToken)) {
            throw new RequestException("Server responded with incorrect client token.");
        }

        this.accessToken = response.accessToken;
        this.selectedProfile = response.selectedProfile;
    }

    @Override
    public String toString() {
        return "UserAuthentication{clientToken=" + this.clientToken + ", username=" + this.username + ", accessToken=" + this.accessToken + ", loggedIn=" + this.loggedIn + ", profiles=" + this.profiles + ", selectedProfile=" + this.selectedProfile + "}";
    }

    private static class Agent {
        private String name;
        private int version;

        protected Agent(String name, int version) {
            this.name = name;
            this.version = version;
        }
    }

    private static class User {
        public String id;
        public List<GameProfile.Property> properties;
    }

    private static class AuthenticationRequest {
        private Agent agent;
        private String username;
        private String password;
        private String clientToken;
        private boolean requestUser;

        protected AuthenticationRequest(String username, String password, String clientToken) {
            this.agent = new Agent("Minecraft", 1);
            this.username = username;
            this.password = password;
            this.clientToken = clientToken;
            this.requestUser = true;
        }
    }

    private static class RefreshRequest {
        private String clientToken;
        private String accessToken;
        private GameProfile selectedProfile;
        private boolean requestUser;

        protected RefreshRequest(String clientToken, String accessToken, GameProfile selectedProfile) {
            this.clientToken = clientToken;
            this.accessToken = accessToken;
            this.selectedProfile = selectedProfile;
            this.requestUser = true;
        }
    }

    private static class InvalidateRequest {
        private String clientToken;
        private String accessToken;

        protected InvalidateRequest(String clientToken, String accessToken) {
            this.clientToken = clientToken;
            this.accessToken = accessToken;
        }
    }

    private static class AuthenticateRefreshResponse {
        public String accessToken;
        public String clientToken;
        public GameProfile selectedProfile;
        public GameProfile[] availableProfiles;
        public User user;
    }
}