package de.hsadmin.cli;

import java.io.BufferedReader;
import java.io.Console;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/** 
 * command line client for hsadmin.
 */
public class HSadmin {
	
	private static String version = "CLI Client 2.0.0 (2011/May/21 09:00 MEST)";
	
	private static String loginURL = "https://login.hostsharing.net/cas/v1/tickets";
	private static String servletURL = "https://admin.hostsharing.net/hsar/hsadmin/cli-interface/";
	private static String backendURL = "https://admin.hostsharing.net:443/hsar/backend";
	
	private static Properties config;
	private static String authenticatedUserName;
	private static String passWord; 
	private static String runAsUserName;
	private static int verbosity = 0;

	private static class PasswordMaskingThread extends Thread {

		private boolean stop = false;

		public void interrupt() {
			stop = true;
			super.interrupt();
		}

		public void run() {
			while (!stop) {
				try {
					Thread.sleep(50); // rate at which attempts to mask
				} catch (InterruptedException ie) {
				}
				// in case I was sleeping while stop was changed, don't print
				// I might be on the next line because of enter key being
				// pressed
				if (!stop) {
					System.out.print("\rPassword:                         "
								+ "\rPassword: ");
				}
				System.out.flush();
			}
		}
	}
	
	private static class HttpRequest {
		private URL url;
		private Map<String, List<String>> responseHeaders;
		private StringBuilder requestData;
		private StringBuilder responseData;
		private String password;
		private String username;
		private String responseMessage;

		public HttpRequest(String url) throws MalformedURLException {
			this.url = new URL(url);
		}

		public int post() throws IOException {
			return doRequest("POST");
		}

		public int put() throws IOException {
			return doRequest("PUT");
		}

		public int doRequest(String method) throws IOException {
			echoVerbose(method + ": " + url.toString());
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setDoOutput(true);
			conn.setDoInput(true);
			conn.setUseCaches(false);
			conn.setAllowUserInteraction(false);
			conn.setRequestProperty("Content-type",
					"application/x-www-form-urlencoded; charset=" + "UTF-8");
			if (username != null)
				conn.setRequestProperty("Authorization",
						createAuthorizationHeader(username, password));
			conn.setRequestMethod(method);
			OutputStreamWriter wr = new OutputStreamWriter(conn
					.getOutputStream(), "UTF-8");
			if (requestData == null) {
				if (username != null)
					addParam("username", username);
				if (password != null)
					addParam("password", password);
			}
			if (requestData != null)
				wr.write(requestData.toString());
			wr.flush();

			responseHeaders = conn.getHeaderFields();
			if (verbosity > 1)
				echoResponseHeaders(conn);

			responseData = new StringBuilder();
			try {
				BufferedReader rd = new BufferedReader(new InputStreamReader(
						conn.getInputStream(), "ISO-8859-1"));
				String line;
				while ((line = rd.readLine()) != null) {
					responseData.append(line + "\n");
				}
				rd.close();
			} catch (IOException exc) {
			}
			wr.close();
			responseMessage = conn.getResponseMessage();
			return conn.getResponseCode();
		}

		private String createAuthorizationHeader(String username,
				String password) throws IOException {
			if (password == null)
				password = getPassWord();
			String authValue = username + ":" + password;
			String encodedAuth = Base64.byteArrayToBase64(authValue.getBytes());
			return "Basic " + encodedAuth;
		}

		/**
		 * printing the response header.
		 */
		private void echoResponseHeaders(HttpURLConnection conn)
				throws IOException {
			// TODO: just for debugging - remove
			System.out.println("ResponseCode: " + conn.getResponseCode());
			System.out.println("ResponseMessage: " + conn.getResponseMessage());
			for (String key : responseHeaders.keySet()) {
				System.out.println(key + ": " + responseHeaders.get(key));
			}
		}

		public void addParam(String name, String value)
				throws UnsupportedEncodingException {
			if (requestData == null) {
				requestData = new StringBuilder();
			} else {
				requestData.append("&");
			}
			requestData.append(URLEncoder.encode(name, "UTF-8") + "="
					+ URLEncoder.encode(value, "UTF-8"));
		}

		public String getResponseData() {
			return responseData.toString();
		}

		public void setUsername(String username) {
			this.username = username;
		}

		public void setPassword(String password) {
			this.password = password;
		}

		public List<String> getResponseHeader(String key) {
			return responseHeaders.get(key);
		}

		public void setRequestData(String data) {
			requestData = new StringBuilder(data);
		}

		public String getResponseMessage() {
			return responseMessage;
		}
	}
	
	/**
	 * returns the TGT, either from a special user config file or from stdin
	 */
	private static String getTGT() throws IOException {
		HttpRequest postReq = new HttpRequest(loginURL);
		echoVerbose("using userName=" + authenticatedUserName + " for login");
		postReq.setUsername(authenticatedUserName);
		String pw = getPassWord();
		postReq.setPassword(pw);
		if (201 == postReq.post()) {
			String tgt = postReq.getResponseHeader("Location").get(0);
			echoVerbose("TGT: " + tgt);
			return tgt;
		} else {
			return null;
		}
	}
	
	/**
	 * retrieves a new session ticket using the given ticket granting ticket
	 */
	private static String getST(String tgt) {
		String st = null;
		try {
			HttpRequest httpReq = new HttpRequest(tgt);
			httpReq.setRequestData("service=" + backendURL);
			if (200 == httpReq.post()) {
				st = httpReq.getResponseData();
			}
		} catch (Exception exc) { /* ignore */
		}
		echoVerbose("ST: " + st);
		return st;
	}

	/**
	 * returns the password, either from parameter, user config file or from
	 * stdin.
	 */
	private static String getPassWord() throws IOException {
		if (passWord == null)
			passWord = config.getProperty("passWord." + authenticatedUserName,
					config.getProperty("passWord"));
		if (passWord == null) {
			Console console = System.console();
			if (console != null) {
				char[] input = console.readPassword("Password: ");
				passWord = new String(input);
			} else {
				System.out.print("Password: ");
				System.out.flush();
				BufferedReader reader = new BufferedReader(
						new InputStreamReader(System.in));

				PasswordMaskingThread thread = new PasswordMaskingThread();
				thread.start();
				passWord = reader.readLine();
				thread.interrupt();
			}
		}
		return passWord;
	}

	/**
	 * calls the remote service using the given service ticket 'st'.
	 */
	private static String exec(String st, String[] args) throws IOException {
		HttpRequest httpReq = new HttpRequest(servletURL);
		if (runAsUserName == null) {
			runAsUserName = authenticatedUserName;
		}
		echoVerbose("using userName=" + runAsUserName + " for exec");
		httpReq.setUsername(runAsUserName);
		httpReq.setPassword(st);
		StringBuilder argsAsString = new StringBuilder();
		for (String arg : args) {
			argsAsString.append(arg + "\n");
		}
		echoVerbose("--- remote args: ---\n" + argsAsString.toString()
				+ "\n--- end of args. ---");
		httpReq.setRequestData(argsAsString.toString());
		int code = httpReq.put();
		List<String> hsadminErrors = httpReq.getResponseHeader("X-hsadmin-error");
		if (hsadminErrors != null && hsadminErrors.size() > 0 && hsadminErrors.get(0) != null) {
			throw new RuntimeException(hsadminErrors.get(0));
		}
		else {
			if (200 == code) return httpReq.getResponseData();
		}
		throw new RuntimeException("HTTP error #" + code + ": "
				+ httpReq.getResponseMessage());
	}

	/**
	 * reads session information (in users home dir).
	 */
	private static String loadTGT(String user, String fileName)
			throws IOException {
		Properties properties = new Properties();
		try {
			properties.load(new FileInputStream(new File(System
					.getProperty("user.home"), fileName)));
			return properties.getProperty(user);
		} catch (FileNotFoundException exc) {
			return null;
		}
	}

	/**
	 * stores session information (in users home dir).
	 */
	private static void storeTGT(String fileName, String user, String tgt)
			throws IOException {
		Properties properties = new Properties();
		try {
			properties.load(new FileInputStream(new File(System
					.getProperty("user.home"), fileName)));
		} catch (Exception exc) { /* ignore */
		}
		properties.setProperty(user, tgt);
		properties.store(new FileOutputStream(new File(System
				.getProperty("user.home"), fileName)), "");
	}

	/**
	 * reads the settings from the given config file (looked up in users home
	 * dir).
	 */
	private static void readUserConfig(String string) throws IOException {
		// Read properties file.
		config = new Properties();
		try {
			config.load(
				new FileInputStream(
					new File(System.getProperty("user.home"), ".hsadmin.conf")));
		} catch (FileNotFoundException e) {
		}
		authenticatedUserName = config.getProperty("userName", System.getenv("USER"));
		loginURL = config.getProperty("loginURL", loginURL);
		servletURL = config.getProperty("servletURL", servletURL);
		backendURL = config.getProperty("backendURL", backendURL);
		verbosity = Integer.parseInt(config.getProperty("verbosity", "0"));
	}

	/**
	 * echoes the string to stdout depending on verbosity level.
	 */
	private static void echoVerbose(String string) {
		if (verbosity > 0) {
			System.out.println("]" + string);
		}
	}

	private static void printHelp() {
		System.out.println("syntax:  hsadmin [-h|--help] [-V (0|1|2)|--verbosity=(0|1|2)] [-v|--version]\n"
						+ "                 [-u USER|--user=USER] [-|REMOTE-ARGUMENTS]");
		System.out.println("sample:  hsadmin -V 0 -u xyz00 -c user.search");
		System.out.println("version: " + version);
		System.out.println("");
		System.out.println("REMOTE-ARGUMENTS ('-' on command line => read from stdin):");
	}

	/**
	 * reads remote arguments from stdin into 'remoteArgs'.
	 */
	private static void readArgsFromStdIn(ArrayList<String> remoteArgs)
			throws IOException {
		echoVerbose("READING ARGS FROM STDIN");
		BufferedReader rd = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = rd.readLine()) != null) {
			echoVerbose("ADDING REMOTE ARG:" + line);
			remoteArgs.add(line);
		}
		rd.close();
	}

	private static String createSessionTicket() throws IOException {
		String tgt = loadTGT(authenticatedUserName, ".hsadmin.tgt");
		String st = getST(tgt);
		if (st == null) {
			tgt = getTGT();
			if (tgt == null) {
				System.err.println("ERROR: login failure - cannot get ticket granting ticket");
				System.exit(1);
			}
			storeTGT(".hsadmin.tgt", authenticatedUserName, tgt);
			st = getST(tgt);
		}
		return st;
	}

	private static ArrayList<String> createRemoteArgs(String[] args)
			throws IOException {
		ArrayList<String> remoteArgs = new ArrayList<String>();
		for (int n = 0; n < args.length; ++n) {
			String arg = args[n];
			if (arg.equals("-u")) {
				// TODO: out of bounds error
				authenticatedUserName = args[++n];
			} else if (arg.startsWith("--user=") || arg.startsWith("--user:")) {
				authenticatedUserName = args[n].substring(7);
			} else if (arg.equals("-r")) {
				// TODO: out of bounds error
				runAsUserName  = args[++n];
			} else if (arg.toLowerCase().startsWith("--runas=") || arg.toLowerCase().startsWith("--runas:")) {
				runAsUserName = args[n].substring(8);
			} else if (arg.equals("-V")) {
				// TODO: out of bounds error
				verbosity = Integer.parseInt(args[++n]);
			} else if (arg.startsWith("--verbosity:")
					|| arg.startsWith("--verbosity=")) {
				verbosity = Integer.parseInt(args[n].substring(12));
			} else if (arg.equals("-v")) {
				System.out.println(version);
				remoteArgs = new ArrayList<String>();
				break;
			} else if (arg.startsWith("--version")) {
				System.out.println(version);
				remoteArgs = new ArrayList<String>();
				break;
			} else if (arg.equals("-h")) {
				// TODO: out of bounds error
				printHelp();
				remoteArgs = new ArrayList<String>();
				remoteArgs.add("-h");
				break;
			} else if (arg.startsWith("--help")) {
				printHelp();
				remoteArgs = new ArrayList<String>();
				remoteArgs.add("-h");
				break;
			} else if (arg.equals("-"))
				readArgsFromStdIn(remoteArgs);
			else {
				echoVerbose("ADDING REMOTE ARG:" + args[n]);
				remoteArgs.add(args[n]);
			}
		}
		return remoteArgs;
	}

	/**
	 * main program.
	 */
	public static void main(String[] args) throws IOException {
		readUserConfig(".hsadmin.conf");
		ArrayList<String> remoteArgs = createRemoteArgs(args);
		if (remoteArgs.size() > 0) {
			try {
				String st = createSessionTicket();
				String response = exec(st, remoteArgs.toArray(new String[remoteArgs.size()]));
				if (response != null)
					System.out.print(response);
			} catch (Exception exc) {
				System.err.println(exc.getMessage());
				System.exit(1); // TODO: be more specific, 
								// like: 1=unknown, 2=remoting, 3=functional
			}
		}
	}

}
