#! /usr/bin/python

#
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#	 http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
#

from __future__ import print_function

import fnmatch
import inspect
import json
import logging
import os
import re
import ssl
import sys

import storage.fs_adapter
import vault.db

if sys.version_info >= (3, 0):
	#python 3
	from http.server import HTTPServer
	from http.server import BaseHTTPRequestHandler
	from configparser import ConfigParser
	import urllib.parse as urlparse
else:	
	#python 2
	from BaseHTTPServer import HTTPServer
	from BaseHTTPServer import BaseHTTPRequestHandler
	from ConfigParser import ConfigParser
	import urlparse


class RequestHandler(BaseHTTPRequestHandler):
	"""
	An HTTP server, emulating RIAK behavior on the calls done by traffic-ops.
	The class implements BaseHTTPRequestHandler functions
	"""

	def do_GET(self):
		"""
		Base class function implementation -  to be called upon HTTP GET/HEAD requests
		"""
		try:
			self._do_GET()
		except Exception as e:
			logger.exception("do_GET exception")

	do_HEAD = do_GET

	def do_POST(self):
		"""
		Base class function implementation -  to be called upon HTTP POST/PUT requests
		"""
		try:
			self._do_POST()
		except Exception as e:
			logger.exception("do_POST exception")

	do_PUT = do_POST

	def do_DELETE(self):
		"""
		Base class function implementation -  to be called upon HTTP DEL request
		"""
		try:
			self._do_DELETE()
		except Exception as e:
			logger.exception("do_DELETE exception")

	def _do_GET(self):
		"""
		Actual GET logic
		:raises: Exception
		"""
		logger.debug("GET %s", self.path)
		parsed_path = urlparse.urlparse(self.path)

		if parsed_path.path == "/ping":
			logger.info("Ping")
			self._do_GET_ping()
		elif parsed_path.path == "/search/query/sslkeys":
			logger.info("Search SSL: path=%s query=%s", parsed_path.path, parsed_path.query)
			self._do_GET_sslkeys(parsed_path)
		else:
			self._do_GET_general(parsed_path)

	def _do_GET_ping(self):
		"""
		Actual GET logic for ping request
		:raises: Exception
		"""		
		success, value = db.ping()
		if not success:
			self.send_response(503)
			self.send_header('Content-type', 'text/html')
			self.end_headers()
			self.wfile.write("Failure".encode())
			return

		self.send_response(200)
		self.send_header('Content-type', 'text/html')
		self.end_headers()
		self.wfile.write(value.encode())

	def _do_GET_sslkeys(self, parsed_path):
		"""
		Actual GET logic for ssl keys request
		:param parsed_path: key's url path
		:type parsed_path: str
		:raises: Exception
		"""
		filters = {}
		keyFilters = {}
		cdnFind = re.search(".*q=cdn:([^&]*).*", parsed_path.query)
		if cdnFind:
			def cdnFilter(key,val):
				try:
					data = json.loads(val)
					return data['cdn']==cdnFind.group(1)
				except Exception as e:
					return False
			filters['cdn'] = cdnFilter

		dsFind = re.search(".*q=deliveryservice:([^&]*).*", parsed_path.query)
		if dsFind:
			def dsFilter(key,val):
				try:
					data = json.loads(val)
					return data['deliveryservice']==dsFind.group(1)
				except Exception as e:
					return False
			filters['ds'] = dsFilter

		keyFind = re.search(".*q=_yz_rk:([^&]*).*", parsed_path.query)
		if keyFind:
			keyFilters['key-match'] = lambda key: fnmatch.fnmatch(os.path.basename(key), keyFind.group(1))

		success, parameters = db.searchParameters("/riak/ssl/", keyFilters=keyFilters, filters=filters)
		if not success:
			self.send_response(503)
			self.send_header('Content-Type', 'application/json')
			self.end_headers()
			self.wfile.write('{"Failure"}'.encode())
			return

		docs = [json.loads(val) for val in parameters.values()]
		toReturn = {"response":{"numFound":len(docs),"start":0, "docs":docs}}

		self.send_response(200)
		self.send_header('Content-Type', 'application/json')
		self.end_headers()
		self.wfile.write(json.dumps(toReturn).encode())

	def _do_GET_general(self, parsed_path):
		"""
		Actual GET logic for general variable request
		:param parsed_path: key's url path
		:type parsed_path: str
		:raises: Exception
		"""
		success, value = db.getParameter(parsed_path.path)
		if not success:
			self.send_response(503)
			self.send_header('Content-Type', 'application/json')
			self.end_headers()
			self.wfile.write('{"Failure"}'.encode())
			return

		if value is None:
			self.send_response(404)
			self.send_header('Content-Type', 'application/json')
			self.end_headers()
			self.wfile.write('{"Not found"}'.encode())
			return

		self.send_response(200)
		self.send_header('Content-Type', 'application/json')
		self.end_headers()
		self.wfile.write(value.encode())

	def _do_POST(self):
		"""
		Actual POST request logic
		"""
		logger.info("POST %s", self.path)
		parsed_path = urlparse.urlparse(self.path)
		content_len = int(self.headers.getheader('content-length'))
		post_body = self.rfile.read(content_len)
		data = json.loads(post_body)
		#mimic vault beahvior
		if parsed_path.path.startswith('/riak/ssl/'):
			certificate_data = data.get('certificate', {})
			data.update({"certificate.%s"%key: value for key, value in certificate_data.items()})
		success = db.setParameter(parsed_path.path, json.dumps(data))
		if not success:
			self.send_response(503)
			self.send_header('Content-Type', 'application/json')
			self.end_headers()
			self.wfile.write('{"Failure"}'.encode())
			return

		self.send_response(204)
		self.send_header('Content-Type', 'application/json')
		self.end_headers()
		return


	def _do_DELETE(self):
		"""
		Actual DEL request logic
		"""
		logger.info("DELETE %s", self.path)
		parsed_path = urlparse.urlparse(self.path)

		success = db.deleteParameter(parsed_path.path)
		if not success:
			self.send_response(503)
			self.send_header('Content-Type', 'application/json')
			self.end_headers()
			self.wfile.write('{"Failure"}'.encode())
			return

		self.send_response(200)
		self.send_header('Content-Type', 'application/json')
		self.end_headers()
		self.wfile.write('{"Deleted"}'.encode())
		return



def main():
	progPath = inspect.stack()[-1][1]
	progAbsPath = os.path.abspath( progPath )
	progAbsPath = os.path.dirname(os.path.normpath(progAbsPath))

	confDir = os.path.join(progAbsPath, "conf")
	confFile = os.path.join(confDir, "cfg.ini")
	try:
		config = ConfigParser()
		config.read(confFile)
	except IOError as e:
		print("Failed to read configuration - I/O error({0}): {1}".format(e.errno, e.strerror), file=sys.stderr)
		return 1
	except Exception as e:
		print("Failed to read configuration: {0}".format(e), file=sys.stderr)
		return 1

	generalCfg = dict(config.items("general")) if config.has_section("general") else {}
	logDir = generalCfg.get("log-dir", os.path.join(progAbsPath, "var/log"))
	debugLogFile = os.path.join(logDir, "traffic-ops-vault-debug.log")
	mainLogFile = os.path.join(logDir, "traffic-ops-vault.log")
	try:
		if not os.path.exists(logDir):
			os.makedirs(logDir)
	except IOError as e:
		print("Failed to create log dir - I/O error({0}): {1}".format(e.errno, e.strerror), file=sys.stderr)
		return 1
	except Exception as e:
		print("Failed to create log dir: {0}".format(e), file=sys.stderr)
		return 1


	global logger
	logger = logging.getLogger(__name__)
	logger.setLevel(logging.INFO)
	# create file handler which logs even debug messages
	fhd = logging.FileHandler(debugLogFile)
	fhd.setLevel(logging.DEBUG)
	fhm = logging.FileHandler(mainLogFile)
	fhm.setLevel(logging.INFO)
	# create formatter and add it to the handlers
	formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
	fhm.setFormatter(formatter)
	fhd.setFormatter(formatter)
	# add the handlers to the logger
	logger.addHandler(fhm)
	logger.addHandler(fhd)

	httpServerCfg = dict(config.items("http-server")) if config.has_section("http-server") else {}
	listenIP = httpServerCfg.get("listen-ip", "0.0.0.0")
	try:
		listenPort = int(httpServerCfg.get("listen-port", "8088"))
	except Exception as e:
		logger.exception("Failed %s integer conversion failed", "listen-port")
		return 1
	use_ssl_val = httpServerCfg.get("use-ssl", "True")
	if use_ssl_val in ["False", "false", "0"]:
		use_ssl=False
	elif use_ssl_val in ["True", "true", "1"]:
		use_ssl=True
	else:
		logger.error("Invalid %s value", "use-ssl")
		return 1
	if use_ssl:
		sslKey = httpServerCfg.get("ssl-key-path")
		if not sslKey:
			print("Missing configuration: {0}/{1}".format("http-server", "ssl-key-path"), file=sys.stderr)
			return 1
		sslCert = httpServerCfg.get("ssl-cert-path")
		if not sslCert:
			print("Missing configuration: {0}/{1}".format("http-server", "ssl-cert-path"), file=sys.stderr)
			return 1
		
	storageAdapterType = generalCfg.get("storage-adapter-type")
	if not storageAdapterType:
		logger.error("Missing storage adapter type cfg")
		return 1
	elif storageAdapterType == "fs":
		storageAdapter = storage.fs_adapter.FsAdapter(logger=logger)
	else:
		logger.error("Invalid storage adapter type '%s'", storageAdapterType)
		return 1


	if not storageAdapter.init_cfg(config):
		logger.error("Failed storage adapter initialization")
		return 1		
	if not storageAdapter.init():
		logger.error("Failed storage adaper initialization")
		return 1		

	global db
	db = vault.db.Db(logger, storageAdapter)
	
	server = HTTPServer((listenIP, listenPort), RequestHandler)
	if use_ssl:		
		try:
			server.socket = ssl.wrap_socket (server.socket, keyfile=sslKey, certfile=sslCert, server_side=True)
		except Exception as e:
			logger.exception("Failed on SSL init")
			return 1

	msg = 'Starting server at http%s://%s:%d'%("s" if use_ssl else "", listenIP, listenPort)
	print(msg, file=sys.stderr)
	logger.info(msg)
	server.serve_forever()
	return 0

if __name__ == '__main__':
	sys.exit(main())
