package com.akdeniz.googleplaycrawler;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;

import com.akdeniz.googleplaycrawler.misc.Base64;

/**
 * ClientLogin implementation.
 * 
 * @author patrick
 * 
 */
class Identity {

	private static final String LOGIN_URL = "https://android.clients.google.com/auth";
	private static final String PUBKEY = "AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pKRI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/6rmf5AAAAAwEAAQ==";
	private static final String SERVICE = "androidmarket";

	private String firstName;
	private String lastName;
	private String email;
	private String services;
	private String authToken;

	private Identity() {
	}

	/**
	 * User's first name
	 * 
	 * @return the first name, retrieved from Play
	 */
	public String getFirstName() {
		return firstName;
	}

	/**
	 * User's last name
	 * 
	 * @return the lastname, retrieved from Play
	 */
	public String getLastName() {
		return lastName;
	}

	/**
	 * User's current email address
	 * 
	 * @return the email address, retrieved from Play.
	 */
	public String getEmail() {
		return email;
	}

	/**
	 * List the services, the user is clear for.
	 * 
	 * @return list of service names. Potentially empty, never null
	 */
	public List<String> listServices() {
		ArrayList<String> ret = new ArrayList<String>();
		if (services != null) {
			String[] tmp = services.split(" *, *");
			for (String s : tmp) {
				ret.add(s);
			}
		}
		return ret;
	}

	/**
	 * Get the session cookie
	 * 
	 * @return a token that can be used for getting access.
	 */
	public String getAuthToken() {
		return authToken;
	}

	/**
	 * Sing into Play
	 * 
	 * @param req
	 *          parameter object with username, password and locale.
	 * @return new instance
	 * @throws BadAuthenticationException
	 * @throws ClientProtocolException
	 * @throws HttpResponseException
	 * @throws IOException
	 */
	public static Identity signIn(HttpClient client, String uid, String pwd)
			throws ClientProtocolException,
			HttpResponseException, IOException {

		
		
		Locale loc = Locale.getDefault();
		String epwd = null;
		try {
			epwd = encryptString(uid + "\u0000" + pwd);
		}
		catch (Exception e) {
			// Should not happen unless the user is in a country with an embargo on
			// cryptography. In which case, we are screwed anyway.
			throw new RuntimeException("Could not encrypt password", e);
		}

		List<NameValuePair> params = new ArrayList<NameValuePair>();
		params.add(new BasicNameValuePair("Email", uid));
		params.add(new BasicNameValuePair("EncryptedPasswd", epwd));
		params.add(new BasicNameValuePair("service", SERVICE));
		params.add(new BasicNameValuePair("add_account", "1"));
		params.add(new BasicNameValuePair("accountType", "HOSTED_OR_GOOGLE"));
		params.add(new BasicNameValuePair("hasPermission", "1"));
		params.add(new BasicNameValuePair("source", "android"));
		params.add(new BasicNameValuePair("app", "com.android.vending"));
		if (loc != null) {
			params.add(new BasicNameValuePair("device_country", loc.getLanguage()));
			params.add(new BasicNameValuePair("lang", loc.getLanguage()));
		}

		Map<String, String> map = doPost(client, params);
		Identity ret = new Identity();
		ret.firstName = map.get("firstName");
		ret.lastName = map.get("lastName");
		ret.email = map.get("Email");
		ret.services = map.get("services");
		ret.authToken = map.get("Auth");
		String tok = map.get("Token");

		if (tok != null && tok.length() > 0) {
			// Since mid Oct/2017, "Token" must be sent back if account details
			// (first-, lastname, services) are requested by "add_account".
			// Otherwise the "Auth" cookie will have a short TTL.
			params = new ArrayList<NameValuePair>();
			params.add(new BasicNameValuePair("Authorization", "GoogleLogin auth="
					+ map.get("Auth")));
			params.add(new BasicNameValuePair("Token", tok));
			params.add(new BasicNameValuePair("token_request_options", "CAA4AQ=="));
			params.add(new BasicNameValuePair("service", SERVICE));
			params.add(new BasicNameValuePair("accountType", "HOSTED_OR_GOOGLE"));
			params.add(new BasicNameValuePair("app", "com.android.vending"));
			map = doPost(client, params);
			ret.authToken = map.get("Auth");
		}
		return ret;
	}

	private static Map<String, String> doPost(HttpClient client,
			List<NameValuePair> params) throws ClientProtocolException,
			 IOException {
		HttpPost httppost = new HttpPost(LOGIN_URL);
		httppost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
		HttpResponse response = client.execute(httppost);
		Map<String, String> map = parseContent(response.getEntity().getContent());
		if (response.getStatusLine().getStatusCode() == 200) {
			return map;
		}

		if (map.containsKey("Error")) {
			throw new ClientProtocolException(map.get("Error"));
		}
		throw new HttpResponseException(response.getStatusLine().getStatusCode(),
				response.getStatusLine().getReasonPhrase());
	}

	private static Map<String, String> parseContent(InputStream in)
			throws IOException {
		HashMap<String, String> ret = new HashMap<String, String>();
		int k = 0;
		boolean value = false; // Simple state machine.
		StringBuilder key = new StringBuilder();
		StringBuilder val = new StringBuilder();
		while (true) {
			k = in.read();
			if (k == -1) { // EOF
				ret.put(key.toString(), val.toString());
				break;
			}
			if (k == '=') { // Skip symbol; toggle state
				value = true;
				continue;
			}
			if (k == '\n') { // End of value -> commit key/value pair
				value = false;
				ret.put(key.toString(), val.toString());
				key.setLength(0);
				val.setLength(0);
				continue;
			}
			if (k == '\r') { // Skip line end symbol.
				continue;
			}
			if (value) { // Depending on state either file into key or value
				val.append((char) k);
			}
			else {
				key.append((char) k);
			}
		}
		return ret;
	}

	private static String encryptString(String str)
			throws NoSuchAlgorithmException, InvalidKeySpecException,
			NoSuchPaddingException, UnsupportedEncodingException,
			InvalidKeyException, IllegalBlockSizeException, BadPaddingException {

		int i = 0;

		byte[] obj = new byte[5];
		Key createKeyFromString = createKeyFromString(PUBKEY, obj);
		if (createKeyFromString == null) {
			return null;
		}

		Cipher instance = Cipher.getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING");
		byte[] bytes = str.getBytes("UTF-8");
		int length = ((bytes.length - 1) / 86) + 1;
		byte[] obj2 = new byte[(length * 133)];
		while (i < length) {
			instance.init(1, createKeyFromString);
			byte[] doFinal = instance.doFinal(bytes, i * 86,
					i == length + -1 ? bytes.length - (i * 86) : 86);
			System.arraycopy(obj, 0, obj2, i * 133, obj.length);
			System
					.arraycopy(doFinal, 0, obj2, (i * 133) + obj.length, doFinal.length);
			i++;
		}
		return Base64.encodeToString(obj2, 10);
	}

	private static PublicKey createKeyFromString(String str, byte[] bArr)
			throws NoSuchAlgorithmException, InvalidKeySpecException {
		byte[] decode = Base64.decode(str, 0);
		int readInt = readInt(decode, 0);
		byte[] obj = new byte[readInt];
		System.arraycopy(decode, 4, obj, 0, readInt);
		BigInteger bigInteger = new BigInteger(1, obj);
		int readInt2 = readInt(decode, readInt + 4);
		byte[] obj2 = new byte[readInt2];
		System.arraycopy(decode, readInt + 8, obj2, 0, readInt2);
		BigInteger bigInteger2 = new BigInteger(1, obj2);
		decode = MessageDigest.getInstance("SHA-1").digest(decode);
		bArr[0] = (byte) 0;
		System.arraycopy(decode, 0, bArr, 1, 4);
		return KeyFactory.getInstance("RSA").generatePublic(
				new RSAPublicKeySpec(bigInteger, bigInteger2));

	}

	private static int readInt(byte[] bArr, int i) {
		return (((((bArr[i] & 255) << 24) | 0) | ((bArr[i + 1] & 255) << 16)) | ((bArr[i + 2] & 255) << 8))
				| (bArr[i + 3] & 255);
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(firstName);
		sb.append(' ');
		sb.append(lastName);
		sb.append('<');
		sb.append(email);
		sb.append('>');
		sb.append('\t');
		sb.append(services);
		sb.append('\t');
		sb.append(authToken);
		return sb.toString();
	}

}