"""
JingrowClient is a library that helps you connect with other jingrow systems
"""
import base64
import json

import jingrow
from jingrow.utils.data import cstr


class AuthError(Exception):
	pass


class SiteExpiredError(Exception):
	pass


class SiteUnreachableError(Exception):
	pass


class JingrowException(Exception):
	pass


class JingrowClient:
	def __init__(
		self,
		url,
		username=None,
		password=None,
		verify=True,
		api_key=None,
		api_secret=None,
		jingrow_authorization_source=None,
	):
		import requests

		self.headers = {
			"Accept": "application/json",
			"content-type": "application/x-www-form-urlencoded",
		}
		self.verify = verify
		self.session = requests.session()
		self.url = url
		self.api_key = api_key
		self.api_secret = api_secret
		self.jingrow_authorization_source = jingrow_authorization_source

		self.setup_key_authentication_headers()

		# login if username/password provided
		if username and password:
			self._login(username, password)

	def __enter__(self):
		return self

	def __exit__(self, *args, **kwargs):
		self.logout()

	def _login(self, username, password):
		"""Login/start a sesion. Called internally on init"""
		r = self.session.post(
			self.url,
			params={"cmd": "login", "usr": username, "pwd": password},
			verify=self.verify,
			headers=self.headers,
		)

		if r.status_code == 200 and r.json().get("message") in ("Logged In", "No App"):
			return r.json()
		elif r.status_code == 502:
			raise SiteUnreachableError
		else:
			try:
				error = json.loads(r.text)
				if error.get("exc_type") == "SiteExpiredError":
					raise SiteExpiredError
			except json.decoder.JSONDecodeError:
				error = r.text
				print(error)
			raise AuthError

	def setup_key_authentication_headers(self):
		if self.api_key and self.api_secret:
			token = base64.b64encode((f"{self.api_key}:{self.api_secret}").encode()).decode("utf-8")
			auth_header = {
				"Authorization": f"Basic {token}",
			}
			self.headers.update(auth_header)

			if self.jingrow_authorization_source:
				auth_source = {"Jingrow-Authorization-Source": self.jingrow_authorization_source}
				self.headers.update(auth_source)

	def logout(self):
		"""Logout session"""
		self.session.get(
			self.url,
			params={
				"cmd": "logout",
			},
			verify=self.verify,
			headers=self.headers,
		)

	def get_list(self, pagetype, fields='["name"]', filters=None, limit_start=0, limit_page_length=None):
		"""Returns list of records of a particular type"""
		if not isinstance(fields, str):
			fields = json.dumps(fields)
		params = {
			"fields": fields,
		}
		if filters:
			params["filters"] = json.dumps(filters)
		if limit_page_length is not None:
			params["limit_start"] = limit_start
			params["limit_page_length"] = limit_page_length
		res = self.session.get(
			self.url + "/api/data/" + pagetype, params=params, verify=self.verify, headers=self.headers
		)
		return self.post_process(res)

	def insert(self, pg):
		"""Insert a document to the remote server

		:param pg: A dict or Page object to be inserted remotely"""
		res = self.session.post(
			self.url + "/api/data/" + pg.get("pagetype"),
			data={"data": jingrow.as_json(pg)},
			verify=self.verify,
			headers=self.headers,
		)
		return jingrow._dict(self.post_process(res))

	def insert_many(self, pgs):
		"""Insert multiple documents to the remote server

		:param pgs: List of dict or Page objects to be inserted in one request"""
		return self.post_request({"cmd": "jingrow.client.insert_many", "pgs": jingrow.as_json(pgs)})

	def update(self, pg):
		"""Update a remote document

		:param pg: dict or Page object to be updated remotely. `name` is mandatory for this"""
		url = self.url + "/api/data/" + pg.get("pagetype") + "/" + cstr(pg.get("name"))
		res = self.session.put(
			url, data={"data": jingrow.as_json(pg)}, verify=self.verify, headers=self.headers
		)
		return jingrow._dict(self.post_process(res))

	def bulk_update(self, pgs):
		"""Bulk update documents remotely

		:param pgs: List of dict or Page objects to be updated remotely (by `name`)"""
		return self.post_request({"cmd": "jingrow.client.bulk_update", "pgs": jingrow.as_json(pgs)})

	def delete(self, pagetype, name):
		"""Delete remote document by name

		:param pagetype: `pagetype` to be deleted
		:param name: `name` of document to be deleted"""
		return self.post_request({"cmd": "jingrow.client.delete", "pagetype": pagetype, "name": name})

	def submit(self, pg):
		"""Submit remote document

		:param pg: dict or Page object to be submitted remotely"""
		return self.post_request({"cmd": "jingrow.client.submit", "pg": jingrow.as_json(pg)})

	def get_value(self, pagetype, fieldname=None, filters=None):
		"""Returns a value form a document

		:param pagetype: PageType to be queried
		:param fieldname: Field to be returned (default `name`)
		:param filters: dict or string for identifying the record"""
		return self.get_request(
			{
				"cmd": "jingrow.client.get_value",
				"pagetype": pagetype,
				"fieldname": fieldname or "name",
				"filters": jingrow.as_json(filters),
			}
		)

	def set_value(self, pagetype, pagename, fieldname, value):
		"""Set a value in a remote document

		:param pagetype: PageType of the document to be updated
		:param pagename: name of the document to be updated
		:param fieldname: fieldname of the document to be updated
		:param value: value to be updated"""
		return self.post_request(
			{
				"cmd": "jingrow.client.set_value",
				"pagetype": pagetype,
				"name": pagename,
				"fieldname": fieldname,
				"value": value,
			}
		)

	def cancel(self, pagetype, name):
		"""Cancel a remote document

		:param pagetype: PageType of the document to be cancelled
		:param name: name of the document to be cancelled"""
		return self.post_request({"cmd": "jingrow.client.cancel", "pagetype": pagetype, "name": name})

	def get_pg(self, pagetype, name="", filters=None, fields=None):
		"""Returns a single remote document

		:param pagetype: PageType of the document to be returned
		:param name: (optional) `name` of the document to be returned
		:param filters: (optional) Filter by this dict if name is not set
		:param fields: (optional) Fields to be returned, will return everythign if not set"""
		params = {}
		if filters:
			params["filters"] = json.dumps(filters)
		if fields:
			params["fields"] = json.dumps(fields)

		res = self.session.get(
			self.url + "/api/data/" + pagetype + "/" + cstr(name),
			params=params,
			verify=self.verify,
			headers=self.headers,
		)

		return self.post_process(res)

	def rename_pg(self, pagetype, old_name, new_name):
		"""Rename remote document

		:param pagetype: PageType of the document to be renamed
		:param old_name: Current `name` of the document to be renamed
		:param new_name: New `name` to be set"""
		params = {
			"cmd": "jingrow.client.rename_pg",
			"pagetype": pagetype,
			"old_name": old_name,
			"new_name": new_name,
		}
		return self.post_request(params)

	def migrate_pagetype(self, pagetype, filters=None, update=None, verbose=1, exclude=None, preprocess=None):
		"""Migrate records from another pagetype"""
		meta = jingrow.get_meta(pagetype)
		tables = {}
		for df in meta.get_table_fields():
			if verbose:
				print("getting " + df.options)
			tables[df.fieldname] = self.get_list(df.options, limit_page_length=999999)

		# get links
		if verbose:
			print("getting " + pagetype)
		pgs = self.get_list(pagetype, limit_page_length=999999, filters=filters)

		# build - attach children to parents
		if tables:
			pgs = [jingrow._dict(pg) for pg in pgs]
			pgs_map = {pg.name: pg for pg in pgs}

			for fieldname in tables:
				for child in tables[fieldname]:
					child = jingrow._dict(child)
					if child.parent in pgs_map:
						pgs_map[child.parent].setdefault(fieldname, []).append(child)

		if verbose:
			print("inserting " + pagetype)
		for pg in pgs:
			if exclude and pg["name"] in exclude:
				continue

			if preprocess:
				preprocess(pg)

			if not pg.get("owner"):
				pg["owner"] = "Administrator"

			if pagetype != "User" and not jingrow.db.exists("User", pg.get("owner")):
				jingrow.get_pg(
					{
						"pagetype": "User",
						"email": pg.get("owner"),
						"first_name": pg.get("owner").split("@", 1)[0],
					}
				).insert()

			if update:
				pg.update(update)

			pg["pagetype"] = pagetype
			new_pg = jingrow.get_pg(pg)
			new_pg.insert()

			if not meta.istable:
				if pagetype != "Email":
					self.migrate_pagetype(
						"Email",
						{"reference_pagetype": pagetype, "reference_name": pg["name"]},
						update={"reference_name": new_pg.name},
						verbose=0,
					)
				
				if pagetype != "File":
					self.migrate_pagetype(
						"File",
						{"attached_to_pagetype": pagetype, "attached_to_name": pg["name"]},
						update={"attached_to_name": new_pg.name},
						verbose=0,
					)

	def migrate_single(self, pagetype):
		pg = self.get_pg(pagetype, pagetype)
		pg = jingrow.get_pg(pg)

		# change modified so that there is no error
		pg.modified = jingrow.db.get_single_value(pagetype, "modified")
		jingrow.get_pg(pg).insert()

	def get_api(self, method, params=None):
		if params is None:
			params = {}
		res = self.session.get(
			f"{self.url}/api/action/{method}", params=params, verify=self.verify, headers=self.headers
		)
		return self.post_process(res)

	def post_api(self, method, params=None):
		if params is None:
			params = {}
		res = self.session.post(
			f"{self.url}/api/action/{method}", params=params, verify=self.verify, headers=self.headers
		)
		return self.post_process(res)

	def get_request(self, params):
		res = self.session.get(
			self.url, params=self.preprocess(params), verify=self.verify, headers=self.headers
		)
		res = self.post_process(res)
		return res

	def post_request(self, data):
		res = self.session.post(
			self.url, data=self.preprocess(data), verify=self.verify, headers=self.headers
		)
		res = self.post_process(res)
		return res

	def preprocess(self, params):
		"""convert dicts, lists to json"""
		for key, value in params.items():
			if isinstance(value, dict | list):
				params[key] = json.dumps(value)

		return params

	def post_process(self, response):
		try:
			rjson = response.json()
		except ValueError:
			print(response.text)
			raise

		if rjson and ("exc" in rjson) and rjson["exc"]:
			try:
				exc = json.loads(rjson["exc"])[0]
				exc = "JingrowClient Request Failed\n\n" + exc
			except Exception:
				exc = rjson["exc"]

			raise JingrowException(exc)
		if "message" in rjson:
			return rjson["message"]
		elif "data" in rjson:
			return rjson["data"]
		else:
			return None


class JingrowOAuth2Client(JingrowClient):
	def __init__(self, url, access_token, verify=True):
		import requests

		self.access_token = access_token
		self.headers = {
			"Authorization": "Bearer " + access_token,
			"content-type": "application/x-www-form-urlencoded",
		}
		self.verify = verify
		self.session = requests.session()
		self.url = url
