using System;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections;
using System.Web;
using System.Net;

namespace Orciid.Core
{
	/// <summary>
	/// Single Sign-on support class
	/// </summary>
	/// <remarks>
	/// This class holds all methods required to tie user authentication into
	/// an external system
	/// </remarks>
	public class SingleSignOn
	{
		#region XSD
		private const string XSD = @"
<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' elementFormDefault='qualified' attributeFormDefault='unqualified'>
  <xs:element name='singlesignon'>
    <xs:complexType>
      <xs:sequence>
        <xs:element name='site' maxOccurs='unbounded'>
          <xs:complexType>
            <xs:sequence>
              <xs:element name='validate' minOccurs='0' maxOccurs='unbounded'>
                <xs:complexType>
                  <xs:simpleContent>
                    <xs:extension base='xs:string'>
                      <xs:attribute name='parameter' type='xs:string' use='required'/>
                    </xs:extension>
                  </xs:simpleContent>
                </xs:complexType>
              </xs:element>
              <xs:element name='success' type='xs:string' maxOccurs='unbounded'/>
              <xs:element name='login'>
                <xs:complexType>
                  <xs:simpleContent>
                    <xs:extension base='xs:string'>
                      <xs:attribute name='from' use='required'>
                        <xs:simpleType>
                          <xs:restriction base='xs:string'>
                            <xs:enumeration value='request'/>
                            <xs:enumeration value='result'/>
                          </xs:restriction>
                        </xs:simpleType>
                      </xs:attribute>
                    </xs:extension>
                  </xs:simpleContent>
                </xs:complexType>
              </xs:element>
              <xs:element name='attribute' minOccurs='0' maxOccurs='unbounded'>
                <xs:complexType>
                  <xs:simpleContent>
                    <xs:extension base='xs:string'>
                      <xs:attribute name='name' type='xs:string' use='required'/>
                    </xs:extension>
                  </xs:simpleContent>
                </xs:complexType>
              </xs:element>
            </xs:sequence>
            <xs:attribute name='url' type='xs:string' use='required'/>
            <xs:attribute name='type' use='required'>
              <xs:simpleType>
                <xs:restriction base='xs:string'>
                  <xs:enumeration value='text/xml'/>
                  <xs:enumeration value='text/plain'/>
                </xs:restriction>
              </xs:simpleType>
            </xs:attribute>
            <xs:attribute name='timeout' use='required'>
              <xs:simpleType>
                <xs:restriction base='xs:int'>
                  <xs:minInclusive value='1'/>
                </xs:restriction>
              </xs:simpleType>
            </xs:attribute>
            <xs:attribute name='id' type='xs:string' use='required'/>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>
";
		#endregion

		private string siteid;
		private Hashtable parameters;
		private bool valid = true;
		private string url;
		private string type;
		private int timeout;
		private Hashtable validations = new Hashtable();
		private ArrayList success = new ArrayList();
		private string loginpattern;
		private string loginfrom;
		private string login = null;
		private Hashtable attributes = null;
		private Hashtable attributepatterns = new Hashtable();

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Creates a new single sign-on object for the site specified.
		/// </remarks>
		/// <param name="siteid">The identifier of the site sending the single sign-on request</param>
		/// <param name="parameters">Any parameters that were submitted with the request</param>
		public SingleSignOn(string siteid, Hashtable parameters)
		{
			this.siteid = siteid;
			this.parameters = parameters;
			if (!Configuration.Instance.ContainsKey("authentication.singlesignonconfig"))
			{
				valid = false;
				return;
			}
			string configfile = Configuration.Instance.GetString("authentication.singlesignonconfig");
			try
			{
				XmlValidatingReader reader = new XmlValidatingReader(
					new XmlTextReader(new StreamReader(configfile)));
				try
				{
					XmlSchemaCollection schemas = new XmlSchemaCollection();
					schemas.Add(null, new XmlTextReader(new StringReader(XSD)));
					reader.ValidationType = ValidationType.Schema;
					reader.Schemas.Add(schemas);
					XmlDocument doc = new XmlDocument();
					doc.Load(reader);
				
					XmlNode sitenode = doc.SelectSingleNode(
						String.Format("/singlesignon/site[@id=\"{0}\"]", siteid));
					url = sitenode.Attributes["url"].Value;
					type = sitenode.Attributes["type"].Value;
					timeout = Int32.Parse(sitenode.Attributes["timeout"].Value);
					foreach (XmlNode node in sitenode.SelectNodes("validate"))
						validations.Add(node.Attributes["parameter"].Value, node.InnerText);
					foreach (XmlNode node in sitenode.SelectNodes("success"))
						success.Add(node.InnerText);
					XmlNode loginnode = sitenode.SelectSingleNode("login");
					loginpattern = loginnode.InnerText;
					loginfrom = loginnode.Attributes["from"].Value;
					foreach (XmlNode node in sitenode.SelectNodes("attribute"))
						attributepatterns.Add(node.Attributes["name"].Value, node.InnerText);
					ValidateParameters();
				}
				catch (Exception ex)
				{
					TransactionLog.Instance.AddException("Single sign-on error", ex);
					valid = false;
				}
				finally
				{
					reader.Close();
				}
			}
			catch (Exception ex)
			{
				TransactionLog.Instance.AddException("Single sign-on error", 
					"Configuration file not found: " + configfile, ex);
				valid = false;
			}
		}

		internal bool ValidateParameters()
		{
			foreach (string var in validations.Keys)
			{
				Regex regex = new Regex((string)validations[var]);
				if (!regex.IsMatch((string)parameters[var]))
					return false;
			}
			return true;
		}

		/// <summary>
		/// Validity flag
		/// </summary>
		/// <remarks>
		/// Indicates if the single sign-on request was valid.
		/// </remarks>
		/// <value>
		/// <c>true</c> if the request is valid, <c>false</c> otherwise.
		/// </value>
		public bool Valid
		{
			get
			{
				return valid;
			}
		}

		/// <summary>
		/// Site identifier
		/// </summary>
		/// <remarks>
		/// The site identifier for this single sign-on request
		/// </remarks>
		/// <value>
		/// Site identifier
		/// </value>
		public string SiteID
		{
			get
			{
				return siteid;
			}
		}

		/// <summary>
		/// Confirmation URL
		/// </summary>
		/// <remarks>
		/// The URL of the confirmation page, where the single sign-on request can
		/// be confirmed.
		/// </remarks>
		/// <value>
		/// URL of confirmation page
		/// </value>
		public string Url
		{
			get
			{
				return url;
			}
		}

		/// <summary>
		/// Confirmation type
		/// </summary>
		/// <remarks>
		/// Indicates if the confirmation page returns XML or plain text.
		/// </remarks>
		/// <value>
		/// <c>text/xml</c> or <c>text/plain</c>
		/// </value>
		public string Type
		{
			get
			{
				return type;
			}
		}

		/// <summary>
		/// Confirmation login pattern
		/// </summary>
		/// <remarks>
		/// Pattern to extract the user login name from the confirmation page.
		/// </remarks>
		/// <value>
		/// A regular expression pattern or XPath expression
		/// </value>
		public string LoginPattern
		{
			get
			{
				return loginpattern;
			}
		}

		/// <summary>
		/// User login name
		/// </summary>
		/// <remarks>
		/// The user login name that was extracted from the confirmation page.
		/// </remarks>
		/// <value>
		/// User login name
		/// </value>
		public string Login
		{
			get
			{
				return login;
			}
		}

		/// <summary>
		/// Source of user login name
		/// </summary>
		/// <remarks>
		/// Indicates if the user login name is taken from the confirmation page
		/// or the original request
		/// </remarks>
		/// <value>
		/// <c>request</c> or <c>result</c>
		/// </value>
		public string LoginFrom
		{
			get
			{
				return loginfrom;
			}
		}

		/// <summary>
		/// Confirmation timeout
		/// </summary>
		/// <remarks>
		/// Number of seconds to wait for response from confirmation URL
		/// </remarks>
		/// <value>
		/// Number of seconds to wait for response from confirmation URL
		/// </value>
		public int Timeout
		{
			get
			{
				return timeout;
			}
		}

		/// <summary>
		/// Parameter validation patterns
		/// </summary>
		/// <remarks>
		/// Holds a regular expression pattern for each parameter that that parameter must
		/// match for the request to be valid.
		/// </remarks>
		/// <value>
		/// Hashtable with parameter names as keys and regex patterns as values
		/// </value>
		public Hashtable Validations
		{
			get
			{
				return (Hashtable)validations.Clone();
			}
		}

		/// <summary>
		/// Confirmation response validation patterns
		/// </summary>
		/// <remarks>
		/// Holds an array of regular expression patterns or XPath expressions that
		/// the response from the confirmation URL must match for the request to
		/// be successful.
		/// </remarks>
		/// <value>
		/// An array of regular expression pattern strings or XPath expression strings
		/// </value>
		public ArrayList Success
		{
			get
			{
				return (ArrayList)success.Clone();
			}
		}

		/// <summary>
		/// Attribute patterns
		/// </summary>
		/// <remarks>
		/// Once a request is successfully confirmed, attributes that will be assigned to
		/// the user are extracted from the confirmation response, using these patterns.
		/// </remarks>
		/// <value>
		/// Hashtable with attribute names as keys and regular expression pattern strings 
		/// or XPath expression strings as values
		/// </value>
		public Hashtable AttributePatterns
		{
			get
			{
				return (Hashtable)attributepatterns.Clone();
			}
		}

		/// <summary>
		/// Confirmation request URL
		/// </summary>
		/// <remarks>
		/// Replaces all variables in the confirmation request URL.  A variable
		/// has the format <c>{name}</c> where name consists of alphanumeric
		/// characters.
		/// </remarks>
		/// <value>
		/// The confirmation request URL with all variables replaced
		/// </value>
		public string RequestUrl
		{
			get
			{
				return new Regex(@"\$\{\w+\}").Replace(Url, new MatchEvaluator(ReplaceUrlVariables));
			}
		}

		private string ReplaceUrlVariables(Match m)
		{
			string var = m.Value.Substring(2, m.Value.Length - 3);
			if (parameters.ContainsKey(var))
				return HttpUtility.UrlEncode((string)parameters[var]);
			else
				return "";
		}

		internal string GetResponse()
		{
			if (!valid)
				throw new CoreException("Single sign-on is not in valid state");
			HttpWebRequest request = (HttpWebRequest)WebRequest.Create(RequestUrl);
			request.Timeout = Timeout * 1000;
			request.AllowAutoRedirect = true;
			HttpWebResponse response = (HttpWebResponse)request.GetResponse();
			if (response.StatusCode != HttpStatusCode.OK)
				throw new CoreException("Request failed");
			return new StreamReader(response.GetResponseStream()).ReadToEnd();
		}

		/// <summary>
		/// Execute the single sign-on request
		/// </summary>
		/// <remarks>
		/// This method confirms the request and checks the confirmation response.
		/// </remarks>
		/// <returns><c>true</c> if the request succeeded, <c>false</c> otherwise</returns>
		public bool Execute()
		{
			if (!valid)
				return false;
			string response = CleanResponse(GetResponse());
			if (!CheckResponseSuccess(response))
				return false;
			if (loginfrom == "result")
				login = GetLoginFromResponse(response);
			else if (loginfrom == "request")
				login = (string)parameters[loginpattern];
			else
				return false;
			attributes = GetAttributesFromResponse(response);
			return true;
		}

		/// <summary>
		/// User attributes
		/// </summary>
		/// <remarks>
		/// Contains attributes to be associated with the user that were returned
		/// from the confirmation URL.
		/// </remarks>
		/// <returns>Hashtable of user attributes</returns>
		public Hashtable GetAttributes()
		{
			return attributes;
		}

		internal bool CheckResponseSuccess(string response)
		{
			if (type == "text/plain")
			{
				foreach (string pattern in success)
				{
					Regex regex = new Regex(pattern, RegexOptions.Multiline);
					if (!regex.IsMatch(response))
						return false;
				}
				return true;
			}
			else if (type == "text/xml")
			{
				XPathDocument doc = new XPathDocument(new StringReader(response));
				XPathNavigator nav = doc.CreateNavigator();
				foreach (string xpath in success)
				{
					object result = nav.Evaluate(xpath);
					if (result == null || !(result is Boolean) || !((Boolean)result))
						return false;
				}
				return true;
			}
			return false;
		}

		internal string GetLoginFromResponse(string response)
		{
			if (type == "text/plain")
			{
				Regex regex = new Regex(loginpattern, RegexOptions.Multiline);
				Match match = regex.Match(response);
				if (match.Groups.Count != 2 || !match.Groups[1].Success)
					throw new CoreException("Could not extract login from response");
				return match.Groups[1].Value;
			}
			else if (type == "text/xml")
			{
				XPathDocument doc = new XPathDocument(new StringReader(response));
				XPathNavigator nav = doc.CreateNavigator();
				object result = nav.Evaluate(loginpattern);
				if (result is string)
					return (string)result;
				if (result is XPathNodeIterator)
				{
					XPathNodeIterator it = (XPathNodeIterator)result;
					if (it.MoveNext())
						return it.Current.Value;
				}
				throw new CoreException("Could not extract login from response");
			}
			return null;
		}

		internal Hashtable GetAttributesFromResponse(string response)
		{
			Hashtable attributes = new Hashtable();
			if (type == "text/plain")
			{
				foreach (string name in attributepatterns.Keys)
				{
					ArrayList values = new ArrayList();
					Regex regex = new Regex((string)attributepatterns[name], RegexOptions.Multiline);
					foreach (Match match in regex.Matches(response))
						for (int i = 1; i < match.Groups.Count; i++)
							if (match.Groups[i].Success)
								foreach (Capture capture in match.Groups[i].Captures)
									values.Add(capture.Value);
					if (values.Count == 1)
						attributes.Add(name, values[0]);
					else if (values.Count > 1)
						attributes.Add(name, String.Join("\n", (string[])values.ToArray(typeof(string))));
				}
			}
			else if (type == "text/xml")
			{
				XPathDocument doc = new XPathDocument(new StringReader(response));
				XPathNavigator nav = doc.CreateNavigator();
				foreach (string name in attributepatterns.Keys)
				{
					ArrayList values = new ArrayList();
					object result = nav.Evaluate((string)attributepatterns[name]);
					if (result is string)
						values.Add(result);
					else if (result is XPathNodeIterator)
					{
						XPathNodeIterator it = (XPathNodeIterator)result;
						while (it.MoveNext())
							values.Add(it.Current.Value);
					}
					if (values.Count == 1)
						attributes.Add(name, values[0]);
					else if (values.Count > 1)
						attributes.Add(name, String.Join("\n", (string[])values.ToArray(typeof(string))));
				}
			}
			return (attributes.Count == 0 ? null : attributes);
		}

		internal static string CleanResponse(string response)
		{
			if (response == null)
				return null;
			else
				return response.Replace("\r\n", "\n");
		}
	}
}
