# Copyright (c) 2015, InfluxFramework LLC
# License: MIT. See LICENSE
import base64
import binascii
import json
from urllib.parse import urlencode, urlparse

import influxframework
import influxframework.client
import influxframework.handler
from influxframework import _
from influxframework.utils.data import sbool
from influxframework.utils.response import build_response


def handle():
	"""
	Handler for `/api` methods

	### Examples:

	`/api/method/{methodname}` will call a whitelisted method

	`/api/resource/{doctype}` will query a table
	        examples:
	        - `?fields=["name", "owner"]`
	        - `?filters=[["Task", "name", "like", "%005"]]`
	        - `?limit_start=0`
	        - `?limit_page_length=20`

	`/api/resource/{doctype}/{name}` will point to a resource
	        `GET` will return doclist
	        `POST` will insert
	        `PUT` will update
	        `DELETE` will delete

	`/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method
	"""

	parts = influxframework.request.path[1:].split("/", 3)
	call = doctype = name = None

	if len(parts) > 1:
		call = parts[1]

	if len(parts) > 2:
		doctype = parts[2]

	if len(parts) > 3:
		name = parts[3]

	if call == "method":
		influxframework.local.form_dict.cmd = doctype
		return influxframework.handler.handle()

	elif call == "resource":
		if "run_method" in influxframework.local.form_dict:
			method = influxframework.local.form_dict.pop("run_method")
			doc = influxframework.get_doc(doctype, name)
			doc.is_whitelisted(method)

			if influxframework.local.request.method == "GET":
				if not doc.has_permission("read"):
					influxframework.throw(_("Not permitted"), influxframework.PermissionError)
				influxframework.local.response.update({"data": doc.run_method(method, **influxframework.local.form_dict)})

			if influxframework.local.request.method == "POST":
				if not doc.has_permission("write"):
					influxframework.throw(_("Not permitted"), influxframework.PermissionError)

				influxframework.local.response.update({"data": doc.run_method(method, **influxframework.local.form_dict)})
				influxframework.db.commit()

		else:
			if name:
				if influxframework.local.request.method == "GET":
					doc = influxframework.get_doc(doctype, name)
					if not doc.has_permission("read"):
						raise influxframework.PermissionError
					influxframework.local.response.update({"data": doc})

				if influxframework.local.request.method == "PUT":
					data = get_request_form_data()

					doc = influxframework.get_doc(doctype, name, for_update=True)

					if "flags" in data:
						del data["flags"]

					# Not checking permissions here because it's checked in doc.save
					doc.update(data)

					influxframework.local.response.update({"data": doc.save().as_dict()})

					# check for child table doctype
					if doc.get("parenttype"):
						influxframework.get_doc(doc.parenttype, doc.parent).save()

					influxframework.db.commit()

				if influxframework.local.request.method == "DELETE":
					# Not checking permissions here because it's checked in delete_doc
					influxframework.delete_doc(doctype, name, ignore_missing=False)
					influxframework.local.response.http_status_code = 202
					influxframework.local.response.message = "ok"
					influxframework.db.commit()

			elif doctype:
				if influxframework.local.request.method == "GET":
					# set fields for influxframework.get_list
					if influxframework.local.form_dict.get("fields"):
						influxframework.local.form_dict["fields"] = json.loads(influxframework.local.form_dict["fields"])

					# set limit of records for influxframework.get_list
					influxframework.local.form_dict.setdefault(
						"limit_page_length",
						influxframework.local.form_dict.limit or influxframework.local.form_dict.limit_page_length or 20,
					)

					# convert strings to native types - only as_dict and debug accept bool
					for param in ["as_dict", "debug"]:
						param_val = influxframework.local.form_dict.get(param)
						if param_val is not None:
							influxframework.local.form_dict[param] = sbool(param_val)

					# evaluate influxframework.get_list
					data = influxframework.call(influxframework.client.get_list, doctype, **influxframework.local.form_dict)

					# set influxframework.get_list result to response
					influxframework.local.response.update({"data": data})

				if influxframework.local.request.method == "POST":
					# fetch data from from dict
					data = get_request_form_data()
					data.update({"doctype": doctype})

					# insert document from request data
					doc = influxframework.get_doc(data).insert()

					# set response data
					influxframework.local.response.update({"data": doc.as_dict()})

					# commit for POST requests
					influxframework.db.commit()
			else:
				raise influxframework.DoesNotExistError

	else:
		raise influxframework.DoesNotExistError

	return build_response("json")


def get_request_form_data():
	if influxframework.local.form_dict.data is None:
		data = influxframework.safe_decode(influxframework.local.request.get_data())
	else:
		data = influxframework.local.form_dict.data

	try:
		return influxframework.parse_json(data)
	except ValueError:
		return influxframework.local.form_dict


def validate_auth():
	"""
	Authenticate and sets user for the request.
	"""
	authorization_header = influxframework.get_request_header("Authorization", "").split(" ")

	if len(authorization_header) == 2:
		validate_oauth(authorization_header)
		validate_auth_via_api_keys(authorization_header)

	validate_auth_via_hooks()


def validate_oauth(authorization_header):
	"""
	Authenticate request using OAuth and set session user

	Args:
	        authorization_header (list of str): The 'Authorization' header containing the prefix and token
	"""

	from influxframework.integrations.oauth2 import get_oauth_server
	from influxframework.oauth import get_url_delimiter

	form_dict = influxframework.local.form_dict
	token = authorization_header[1]
	req = influxframework.request
	parsed_url = urlparse(req.url)
	access_token = {"access_token": token}
	uri = (
		parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token)
	)
	http_method = req.method
	headers = req.headers
	body = req.get_data()
	if req.content_type and "multipart/form-data" in req.content_type:
		body = None

	try:
		required_scopes = influxframework.db.get_value("OAuth Bearer Token", token, "scopes").split(
			get_url_delimiter()
		)
		valid, oauthlib_request = get_oauth_server().verify_request(
			uri, http_method, body, headers, required_scopes
		)
		if valid:
			influxframework.set_user(influxframework.db.get_value("OAuth Bearer Token", token, "user"))
			influxframework.local.form_dict = form_dict
	except AttributeError:
		pass


def validate_auth_via_api_keys(authorization_header):
	"""
	Authenticate request using API keys and set session user

	Args:
	        authorization_header (list of str): The 'Authorization' header containing the prefix and token
	"""

	try:
		auth_type, auth_token = authorization_header
		authorization_source = influxframework.get_request_header("InfluxFramework-Authorization-Source")
		if auth_type.lower() == "basic":
			api_key, api_secret = influxframework.safe_decode(base64.b64decode(auth_token)).split(":")
			validate_api_key_secret(api_key, api_secret, authorization_source)
		elif auth_type.lower() == "token":
			api_key, api_secret = auth_token.split(":")
			validate_api_key_secret(api_key, api_secret, authorization_source)
	except binascii.Error:
		influxframework.throw(
			_("Failed to decode token, please provide a valid base64-encoded token."),
			influxframework.InvalidAuthorizationToken,
		)
	except (AttributeError, TypeError, ValueError):
		pass


def validate_api_key_secret(api_key, api_secret, influxframework_authorization_source=None):
	"""influxframework_authorization_source to provide api key and secret for a doctype apart from User"""
	doctype = influxframework_authorization_source or "User"
	doc = influxframework.db.get_value(doctype=doctype, filters={"api_key": api_key}, fieldname=["name"])
	form_dict = influxframework.local.form_dict
	doc_secret = influxframework.utils.password.get_decrypted_password(doctype, doc, fieldname="api_secret")
	if api_secret == doc_secret:
		if doctype == "User":
			user = influxframework.db.get_value(doctype="User", filters={"api_key": api_key}, fieldname=["name"])
		else:
			user = influxframework.db.get_value(doctype, doc, "user")
		if influxframework.local.login_manager.user in ("", "Guest"):
			influxframework.set_user(user)
		influxframework.local.form_dict = form_dict


def validate_auth_via_hooks():
	for auth_hook in influxframework.get_hooks("auth_hooks", []):
		influxframework.get_attr(auth_hook)()
