/* ************************************************************************
#
#  DivConq
#
#  http://divconq.com/
#
#  Copyright:
#    Copyright 2012 eTimeline, LLC. All rights reserved.
#
#  License:
#    See the license.txt file in the project's top-level directory for details.
#
#  Authors:
#    * Andy White
#
************************************************************************ */
package divconq.lang;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Instant;

import divconq.bus.Message;
import divconq.locale.LocaleUtil;
import divconq.log.DebugLevel;
import divconq.log.Logger;
import divconq.struct.RecordStruct;
import divconq.util.TimeUtil;

/**
 * Almost all code that executes after Hub.start should have a context.  The context
 * tells the code who the user responsible for the task is, what their access levels
 * are (at a high level), what language/locale/chronology(timezone) they use, how to log the 
 * debug messages for the task, and whether or not the user has been authenticated or not.
 * 
 * Although the task context is associated with the current thread, it is the task that the
 * context belongs to, not the thread.  If a task splits into multiple threads there is still
 * one TaskContext, even if the task makes a remote call on DivConq's bus that remote call
 * executes on the TaskContext.
 * 
 * As long as you use the built-in features - work pool, scheduler, bus, database - the task context
 * will smoothly come along with no effort from the app developer.
 *   
 * A quick guide to what context to use where:
 * 
 * Hub Context - useHubContext()
 * 
 * 		The code is running as part of the Hub core.
 * 
 * Root Context - useNewRoot()
 * 
 * 		Root context is the same identity as Hub, but with useNewRoot() you get a new log id.  
 * 		Use this with code running batch tasks that belong to the system rather than a specific
 * 		user.
 * 
 * Guest Context - useNewGuest()
 * 
 * 		Guest context is for use by an anonymous user.  For example a user through the interchange
 * 		(HTTP, FTP, SFTP, EDI, etc).
 * 
 *  User Contest - new TaskContext + set(tc)
 *  
 *  	When a user signs-in create and set a new context.  No need to authenticate against the database
 *  	that will happen automatically (as long as you follow DivConq development guidelines) so think
 *  	of creating the user Task Context as information gathering not authentication. 
 * 
 * @author Andy
 *
 */
public class TaskContext {
	static protected String runid = null;
	static protected String hubid = "00001";
	static protected TaskContext hubcontext = null;
	
	static protected ThreadLocal<TaskContext> context = new ThreadLocal<TaskContext>();
	static protected AtomicLong nextid = new AtomicLong();
	
	static {
		TaskContext.runid = TimeUtil.stampFmt.print(new DateTime(DateTimeZone.UTC));
		TaskContext.hubcontext = TaskContext.useNewRoot();
	}
	
	static public String getHubId() {
		return TaskContext.hubid;
	}
	
	static public void setHubId(String v) {
		TaskContext.hubid = v;
		TaskContext.hubcontext = TaskContext.useNewRoot();		// reset the hub context 
	}
	
	static public String getRunId() {
		return TaskContext.runid;
	}
	
	/**
	 * @return the hub context, use by code running in the "core" code of Hub (e.g. main thread)
	 */
	static public TaskContext getHubContext() {
		return TaskContext.hubcontext;
	}
	
	/**
	 * Sets the current thread to use the hub context
	 * 
	 * @return the hub context, use by code running in the "core" code of Hub (e.g. main thread)
	 */
	static public TaskContext useHubContext() {
		TaskContext.context.set(TaskContext.hubcontext);
		return TaskContext.hubcontext;
	}
	
	/**
	 * @return context of the current thread, if any
	 */
	static public TaskContext get() {
		return TaskContext.context.get();
	}
	
	/**
	 * @param v context for current thread to use
	 */
	static public void set(TaskContext v) {
		TaskContext.context.set(v);
	}
	
	/**
	 * @return an existing task context if the thread already has one, else a new clean context 
	 */
	static public TaskContext getOrCreate() {
		TaskContext tc = TaskContext.context.get();
		
		if (tc == null) {
			tc = new TaskContext();
			TaskContext.context.set(tc);
		}
		
		return tc;
	}
	
	/**
	 * @return context of the current thread, if any, otherwise the hub context
	 */
	static public TaskContext getOrHub() {
		TaskContext tc = TaskContext.context.get();
		
		if (tc == null)
			tc = TaskContext.hubcontext;
		
		return tc;
	}
	
	/**
	 * @return create a new guest context
	 */
	static public TaskContext allocateGuest() {
		TaskContext tc = new TaskContext();
		tc.setToGuest();
		return tc;
	}
	
	/**
	 * Sets the current thread to use a new guest context
	 * 
	 * @return create a new guest context
	 */
	static public TaskContext useNewGuest() {
		TaskContext tc = new TaskContext();
		tc.setToGuest();
		TaskContext.context.set(tc);
		return tc;
	}
	
	/**
	 * @return create a new root context
	 */
	static public TaskContext allocateRoot() {
		TaskContext tc = new TaskContext();
		tc.setToRoot();
		return tc;
	}
	
	/**
	 * Sets the current thread to use a new root context
	 * 
	 * @return create a new root context
	 */
	static public TaskContext useNewRoot() {
		TaskContext tc = new TaskContext();
		tc.setToRoot();
		TaskContext.context.set(tc);
		return tc;
	}
	
	/**
	 * set current thread context to null 
	 */
	static public void clear() {
		TaskContext.context.set(null);
	}
	
	protected String taskid = null; 
	protected String userid = "00000_000000000000001"; 
	protected String authToken = null;
	protected String locale = LocaleUtil.getDefaultLocale();
	protected String chrono = "/" + DateTimeZone.getDefault().getID();		// ISOChronology w/ default zone
	protected String origin = "hub:";
	protected DebugLevel level = null;
	protected HashMap<String, Object> params = null;
	protected Instant verifyExpires = null;
	protected List<String> authTags = null;				// cannot use ListStruct because Context must load before any Structs do
	protected RecordStruct credentials = null;
	protected boolean elevated = false;

	/**
	 * @return a unique task id - unique across all deployed hub, across runs of a hub
	 */
	public String getTaskId() {
		return this.taskid;
	}

	/**
	 * @return the id of the user, if guest then "00000_00000 00000 00001", if root then "00000_000000000000002"
	 */
	public String getUserId() {
		return this.userid;
	}

	/**
	 * User ids are expected in format of 5 digit hub id, underscore, 15 digit id unique (sequential) to 
	 * the given hub id. 
	 * 
	 * @param v user id to use for this context (do not set on HubContext)
	 */
	public void setUserId(String v) {
		if ((v != null) && (v.indexOf(" ") > -1))
			throw new IllegalArgumentException("Spaces not allowed in User Id");
		
		this.verifyExpires = null;
		this.userid = v;
	}

	/**
	 * @return the authentication token
	 */
	public String getAuthToken() {
		return this.authToken;
	}

	/**
	 * @param v the authentication token the user claims to have, if claimed - does not equate to authenticated (do not set on HubContext)
	 */
	public void setAuthToken(String v) {
		if ((v != null) && (v.indexOf(" ") > -1))
			throw new IllegalArgumentException("Spaces not allowed in Auth Token");
		
		this.verifyExpires = null;
		this.authToken = v;
	}
	
	/**
	 * @return logging level to use with this task
	 */
	public DebugLevel getLevel() {
		return this.level;
	}
	
	/**
	 * @param v logging level to use with this task
	 */
	public void setLevel(DebugLevel v) {
		this.level = v;
	}

	/**
	 * @return chronology to use with this task (for output)
	 */
	public String getChronology() {
		return this.chrono;
	}

	/**
	 * Chronology is [chronology] + "/" + [Timezone Id].  A "/" at start = default chronology (ISO).
	 * Therefore "/Europe/London" = "ISO/Europe/London" 
	 * 
	 * @param v  chronology to use with this task for output. do not set on HubContext.
	 */
	public void setChronology(String v) {
		//if ((v != null) && (v.indexOf(" ") > -1))
		//	throw new IllegalArgumentException("Spaces not allowed in chronology");
		
		// TODO improve
		
		this.chrono = v;
	}

	/**
	 * @return locale to use with this task for output
	 */
	public String getLocale() {
		return this.locale;
	}

	/**
	 * @param v locale to use with this task for output.  do not set on HubContext
	 */
	public void setLocale(String v) {
		if ((v != null) && (v.indexOf(" ") > -1))
			throw new IllegalArgumentException("Spaces not allowed in Locale");
		
		this.locale = v;
	}

	/**
	 * Origin indicates where this task originated from.  "hub:" means it was started by
	 * the a hub (task id gives away which hub).  "http:[ip address]" means the task
	 * was started in response to a web request.  "ws:[ip address]" means the task
	 * was started in response to a web scoket request.  "ftp:[ip address]" means the task
	 * was started in response to a ftp request.  Etc.
	 * 
	 * @return origin string 
	 */
	public String getOrigin() {
		return this.origin;
	}

	/**
	 * Origin indicates where this task originated from.  "hub:" means it was started by
	 * the a hub (task id gives away which hub).  "http:[ip address]" means the task
	 * was started in response to a web request.  "ws:[ip address]" means the task
	 * was started in response to a web scoket request.  "ftp:[ip address]" means the task
	 * was started in response to a ftp request.  Etc.
	 * 
	 * @param v origin string
	 */
	public void setOrigin(String v) {
		// spaces not allowed because of how context is passed to the database
		if ((v != null) && (v.indexOf(" ") > -1))
			throw new IllegalArgumentException("Spaces not allowed in Origin");
		
		this.origin = v;
	}
	
	/**
	 * Elevated tasks have been a) authenticated and b) passed successfully into
	 * a service.  Once elevated all subsequent calls with the task no longer need
	 * to be authenticated or authorized by DivConq framework (individual services/modules
	 * may require it).  Meaning that "guest" cannot call "SendMail" unless it first 
	 * goes through a service that is open to guests, such as password recovery.  
	 * 
	 * @return true if task has been elevated
	 */
	public boolean isElevated() {
		return this.elevated;
	}
	
	/**
	 * Elevated tasks have been a) authenticated and b) passed successfully into
	 * a service.  Once elevated all subsequent calls with the task no longer need
	 * to be authenticated or authorized by DivConq framework (individual services/modules
	 * may require it).  Meaning that "guest" cannot call "SendMail" unless it first 
	 * goes through a service that is open to guests, such as password recovery.  
	 * 
	 * Mark the task context as elevated - typically app code does not need to call
	 * this because the services and scheduler handlers decide when a task has met
	 * the desired state. 
	 */
	public void elevate() {
		this.elevated = true;
	}
	
	public TaskContext() {
		this.taskid = TaskContext.getHubId() + "_" + TaskContext.getRunId() 
			+ "_000000" + String.format("%09d", TaskContext.nextid.getAndIncrement());		// TODO support longs
		
		this.level = Logger.getGlobalLevel();
	}
	
	/**
	 * @param m create a task context from a message (RPC calls to dcBus), keep in mind
	 * this is info gathering only, message must not be allowed to force an 
	 * authenticated/elevated state inappropriately - from RPC clear "Elevated"
	 * field before calling this
	 */
	public TaskContext(Message m) {
		this.thaw(m);
	}
	
	/**
	 * store a key value pair with this task context
	 * 
	 * @param name 
	 * @param value
	 */
	public void putParam(String name, Object value) {
		this.params.put(name, value);
	}
	
	/**
	 * @param name key for a pair stored with this context
	 * @return the value for this pair
	 */
	public Object getParam(String name) {
		return this.params.get(name);
	}
	
	/**
	 * @param m store task context into a message - for context transfer over bus
	 */
	public void freeze(Message m) {
		m.setField("TaskId", this.taskid);
		m.setField("UserId", this.userid);
		m.setField("AuthToken", this.authToken);
		m.setField("Credentials", this.credentials);
		m.setField("DebugLevel", this.level.getCode());		// TODO maybe string not code
		m.setField("Elevated", this.elevated);
	}
	
	/*	TODO thaw/freeze the locale and chron as well - then places where we use these from msg change to use from context...
	 * 
			<Field Name="Locale" Type="dcTinyString" />
			<Field Name="Chronology" Type="dcTinyString" />
			<Field Name="Credentials" Type="Any" />
	 * 
	 * 
	 */
	
	/**
	 * @param m revive the task context from a message
	 */
	public void thaw(Message m) {
		if (m.hasField("UserId"))
			this.userid = m.getFieldAsString("UserId");
		
		if (m.hasField("AuthToken"))
			this.authToken = m.getFieldAsString("AuthToken");
		
		if (m.hasField("DebugLevel"))
			this.level = DebugLevel.parseCode(m.getFieldAsInteger("DebugLevel"));	// TODO maybe string not code
		
		if (m.hasField("TaskId"))
			this.taskid = m.getFieldAsString("TaskId");
		
		if (m.hasField("Elevated")) 
			this.elevated = m.getFieldAsBoolean("Elevated");
		
		if (m.hasField("Credentials"))
			this.credentials = m.getFieldAsRecord("Credentials");
	}
	
	/**
	 * @return check to see if the current task has guest access.  returns true even if user falsely claims to be authenticated
	 */
	public boolean isGuest() {
		if ("00000_000000000000001".equals(this.userid))
			return true;
		
		this.verify();
		
		return "00000_000000000000001".equals(this.userid);
	}
	
	/*
	public boolean isRootUser() {
		return false;  // "00000_000000000000000".equals(this.userid);		// TODO figure out how to verify root user
	}
	*/
	
	// TODO make async and also make remote call if DB not local
	// verify says - the given auth token, if any, is valid - if there is none then you are a guest and that is valid
	public boolean verify() {
		if (this.elevated)
			return true;
		
		/* TODO
		
		//  if blank then this really is a guest, which is fine and counts as verified
		if (StringUtil.isBlank(this.authToken)) {
			this.setToGuest();
			return true;
		}
		
		// how long has it been since the last verify - if more than 5 minutes run verify again
		if ((this.verifyExpires != null) && this.verifyExpires.isAfterNow())
			return true;
		
		// put this here so that "verify" call in dbman doesn't go into infinite loop
		this.verifyExpires = new DateTime(DateTimeZone.UTC).plusMinutes(5).toInstant();  	// TODO configure # minutes
	
    	ResultInfo res2 = Hub.instance.dbman.callForLocal("ftcVerify", new RecordStruct(
				new FieldStruct("AuthToken", this.authToken)
			)
		);
    	
    	// if the session is missing, try to login again
    	if ((res2.getCode() == 51) && (this.credentials != null)) {
    		// TODO should just call ftcUser service, for now we make some working assumptions
    		
    		String password = this.credentials.getFieldAsString("Password");
    		
    		if (this.credentials.hasField("UserName") && StringUtil.isNotBlank(password)) {
				RecordStruct params = new RecordStruct(
						new FieldStruct("User", this.credentials.getFieldAsString("UserName")),
						new FieldStruct("At", new DateTime()),
						new FieldStruct("Password", Hub.instance.getClock().h2(password).toString())
				);
							
				res2 = Hub.instance.dbman.callForLocal("ftcSignOn", params);
				
				System.out.println("RESTORE session: " + res2.getCode());
    		}
    	}
    	
    	// TODO logging - may count as warning against this IP address
		if (res2.hasErrors()) {
			// default to guest account  
			this.setToGuest();
			return false;
		}

		RecordStruct res = (RecordStruct) res2.getLocalResult();
		
		if (res == null) {
			this.setToGuest();
			return false;
		}
		
    	String uid = res.getFieldAsString("UserId");
    	
    	// TODO logging - may count as warning against this IP address
		if (StringUtil.isBlank(uid) || !uid.equals(this.userid)) {
			// default to guest account  
			this.setToGuest();
			return false;
		}
		
		//this.userid = uid;
    	this.authTags = res.getFieldAsList("UserTags");
		
    	// if restore then we have AuthToken
    	if (res.hasField("AuthToken"))
    		this.authToken = res.getFieldAsString("AuthToken");
    	
    	*/
    	
    	// authenticated but not elevated
		return true;
	}
	
	/**
	 * Builds a guest context
	 */
	public void setToGuest() {
		this.userid = "00000_000000000000001";
		this.verifyExpires = null;
		this.authToken = null;
		this.authTags = new ArrayList<String>( );
		this.authTags.add("Guest");
		this.credentials = null;
		this.elevated = false;
	}
	
	/**
	 * Builds a root context
	 */
	public void setToRoot() {
		this.userid = "00000_000000000000002";
		this.verifyExpires = null;
		this.authToken = null;
		this.authTags = new ArrayList<String>( );
		this.authTags.add("SysAdmin");
		this.authTags.add("AppAdmin");
		this.authTags.add("PowerUser");
		this.authTags.add("User");
		this.credentials = null;
		this.elevated = true;
	}
	
	/**
	 * @param tags to search for with this user
	 * @return true if this user has one of the requested authorization tags  (does not check authentication)
	 */
	public boolean isAuthorized(String... tags) {
		if (this.elevated)
			return true;
		
		if (this.authTags == null)
			return false;
		
		for (String has : this.authTags) {
			for (String wants : tags) {
				if (has.equals(wants))
					return true;
			}
		}
		
		return false;
	}
    
    public void error(String message, String... tags) {
    	Logger.log(this, DebugLevel.Error, message, tags);
    }
    
    public void warn(String message, String... tags) {
    	Logger.log(this, DebugLevel.Warn, message, tags);
    }
    
    public void info(String message, String... tags) {
    	Logger.log(this, DebugLevel.Info, message, tags);
    }
    
    public void trace(String message, String... tags) {
    	Logger.log(this, DebugLevel.Trace, message, tags);
    }
    
    public void logBoundary(String... tags) {
    	Logger.boundary(this, tags);
    }
    
    public void log(DebugLevel level, String message, String... tags) {
    	Logger.log(this, level, message, tags);
    }
	
	/**
	 * @param code for message translation token
	 * @param params for message translation
	 */
	public void traceTr(long code, Object... params) {
		String msg = this.tr("_code_" + code, params);
		this.log(DebugLevel.Trace, msg, "Code", code + "");
	}
	
	/**
	 * @param code for message translation token
	 * @param params for message translation
	 */
	public void infoTr(long code, Object... params) {		
		String msg = this.tr("_code_" + code, params);
		this.log(DebugLevel.Info, msg, "Code", code + "");
	}
	
	/**
	 * @param code for message translation token
	 * @param params for message translation
	 */
	public void warnTr(long code, Object... params) {
		String msg = this.tr("_code_" + code, params);
		this.log(DebugLevel.Warn, msg, "Code", code + "");
	}
	
	/**
	 * @param code for message translation token
	 * @param params for message translation
	 */
	public void errorTr(long code, Object... params) {
		String msg = this.tr("_code_" + code, params);
		this.log(DebugLevel.Error, msg, "Code", code + "");
	}
    
	public String tr(String token, Object... params) {
		return LocaleUtil.tr(this.locale, token, params);		
	}
		
	public String trp(String pluraltoken, String singulartoken, Object... params) {
		return LocaleUtil.trp(this.locale, pluraltoken, singulartoken, params);		
	}
	
	/*
	public boolean isAuthorized(String wants) {
		if (this.authTags == null)
			return false;
		
		for (Object o : this.authTags.list()) {
			if (o instanceof String) {
				String has = (String)o;
				
				// system always passes
				if ("System".equals(has))
					return true;
				
				if (has.equals(wants))
					return true;
			}
		}
		
		return false;
	}
	*/
}
