# Copyright (c) 2024, JINGROW
# For license information, please see license.txt
from __future__ import annotations

import re
from collections import Counter

import jingrow
from jingrow.model.page import Page

from jcloud.agent import Agent
from jcloud.api.client import dashboard_whitelist
from jcloud.overrides import get_permission_query_conditions_for_pagetype
from jcloud.jcloud.pagetype.site_activity.site_activity import log_site_activity


class SiteDatabaseUser(Page):
	# begin: auto-generated types
	# This code is auto-generated. Do not modify anything in this block.

	from typing import TYPE_CHECKING

	if TYPE_CHECKING:
		from jingrow.types import DF

		from jcloud.jcloud.pagetype.site_database_table_permission.site_database_table_permission import (
			SiteDatabaseTablePermission,
		)

		failed_agent_job: DF.Link | None
		failure_reason: DF.SmallText
		label: DF.Data
		max_connections: DF.Int
		mode: DF.Literal["read_only", "read_write", "granular"]
		password: DF.Password
		permissions: DF.Table[SiteDatabaseTablePermission]
		site: DF.Link
		status: DF.Literal["Pending", "Active", "Failed", "Archived"]
		team: DF.Link
		user_added_in_proxysql: DF.Check
		user_created_in_database: DF.Check
		username: DF.Data
	# end: auto-generated types

	dashboard_fields = (
		"label",
		"status",
		"site",
		"username",
		"team",
		"mode",
		"failed_agent_job",
		"failure_reason",
		"permissions",
		"max_connections",
	)

	def validate(self):
		if not self.has_value_changed("status"):
			self._raise_error_if_archived()
		# remove permissions if not granular mode
		if self.mode != "granular":
			self.permissions.clear()

		if not self.is_new() and self.has_value_changed("max_connections"):
			jingrow.throw("You can't update the max database connections. Archive it and create a new one.")

		if not self.max_connections:
			jingrow.throw(
				"Max database connections can't be zero. You need to opt for at least one connection."
			)

	def before_insert(self):
		site = jingrow.get_pg("Site", self.site)
		if not site.has_permission():
			jingrow.throw("You don't have permission to create database user")
		if not jingrow.db.get_value("Site Plan", site.plan, "database_access"):
			jingrow.throw(f"Database Access is not available on {site.plan} plan")

		# validate connection limit
		exists_db_users_connection_limit = jingrow.db.get_all(
			"Site Database User",
			{"site": self.site, "status": ("!=", "Archived")},
			pluck="max_connections",
		)
		total_used_connections = sum(exists_db_users_connection_limit)
		allowed_max_connections_for_site = site.database_access_connection_limit - total_used_connections
		if self.max_connections > allowed_max_connections_for_site:
			jingrow.throw(
				f"Your site has quota of {site.database_access_connection_limit} database connections.\nYou can't allocate more than {allowed_max_connections_for_site} connections for new user. You can drop other database users to allocate more connections."
			)

		self.status = "Pending"
		if not self.username:
			self.username = jingrow.generate_hash(length=15)
		if not self.password:
			self.password = jingrow.generate_hash(length=20)

	def after_insert(self):
		log_site_activity(
			self.site,
			"Create Database User",
			reason=f"Created user {self.username} with {self.mode} permission",
		)
		if hasattr(self.flags, "ignore_after_insert_hooks") and self.flags.ignore_after_insert_hooks:
			"""
			Added for make it easy to migrate records of db access users from site pagetype to site database user
			"""
			return
		self.apply_changes()

	def on_update(self):
		if self.has_value_changed("status") and self.status == "Archived":
			log_site_activity(
				self.site,
				"Remove Database User",
				reason=f"Removed user {self.username} with {self.mode} permission",
			)

	def _raise_error_if_archived(self):
		if self.status == "Archived":
			jingrow.throw("user has been deleted and no further changes can be made")

	def _get_database_name(self):
		site = jingrow.get_pg("Site", self.site)
		db_name = site.fetch_info().get("config", {}).get("db_name")
		if not db_name:
			jingrow.throw("Failed to fetch database name of site")
		return db_name

	@dashboard_whitelist()
	def save_and_apply_changes(self, label: str, mode: str, permissions: list):  # noqa: C901
		if self.status == "Pending" or self.status == "Archived":
			jingrow.throw(f"You can't modify information in {self.status} state. Please try again later")

		self.label = label
		is_db_user_configuration_changed = self.mode != mode or self._is_permissions_changed(permissions)
		if is_db_user_configuration_changed:
			self.mode = mode
			new_permissions = permissions
			new_permission_tables = [p["table"] for p in new_permissions]
			current_permission_tables = [p.table for p in self.permissions]
			# add new permissions
			for permission in new_permissions:
				if permission["table"] not in current_permission_tables:
					self.append("permissions", permission)
			# modify permissions
			for permission in self.permissions:
				for new_permission in new_permissions:
					if permission.table == new_permission["table"]:
						permission.update(new_permission)
						break
			# delete permissions which are not in the modified list
			self.permissions = [p for p in self.permissions if p.table in new_permission_tables]

		self.save()
		if is_db_user_configuration_changed:
			self.apply_changes()

	def _is_permissions_changed(self, new_permissions):
		if len(new_permissions) != len(self.permissions):
			return True

		for permission in new_permissions:
			for p in self.permissions:
				if permission["table"] == p.table and (
					permission["mode"] != p.mode
					or permission["allow_all_columns"] != p.allow_all_columns
					or Counter(permission["selected_columns"]) != Counter(p.selected_columns)
				):
					return True

		return False

	@jingrow.whitelist()
	def apply_changes(self):
		if not self.user_created_in_database:
			self.create_user()
		elif not self.user_added_in_proxysql:
			self.add_user_to_proxysql()
		else:
			self.modify_permissions()

		self.status = "Pending"
		self.save(ignore_permissions=True)

	@jingrow.whitelist()
	def create_user(self):
		self._raise_error_if_archived()
		agent = Agent(jingrow.db.get_value("Site", self.site, "server"))
		agent.create_database_user(
			jingrow.get_pg("Site", self.site), self.username, self.get_password("password"), self.name
		)

	@jingrow.whitelist()
	def remove_user(self):
		self._raise_error_if_archived()
		agent = Agent(jingrow.db.get_value("Site", self.site, "server"))
		agent.remove_database_user(
			jingrow.get_pg("Site", self.site),
			self.username,
			self.name,
		)

	@jingrow.whitelist()
	def add_user_to_proxysql(self):
		self._raise_error_if_archived()
		database = self._get_database_name()
		server = jingrow.db.get_value("Site", self.site, "server")
		proxy_server = jingrow.db.get_value("Server", server, "proxy_server")
		database_server_name = jingrow.db.get_value(
			"Bench", jingrow.db.get_value("Site", self.site, "bench"), "database_server"
		)
		database_server = jingrow.get_pg("Database Server", database_server_name)
		agent = Agent(proxy_server, server_type="Proxy Server")
		agent.add_proxysql_user(
			jingrow.get_pg("Site", self.site),
			database,
			self.username,
			self.get_password("password"),
			self.max_connections,
			database_server,
			reference_pagetype="Site Database User",
			reference_name=self.name,
		)

	@jingrow.whitelist()
	def remove_user_from_proxysql(self):
		self._raise_error_if_archived()
		server = jingrow.db.get_value("Site", self.site, "server")
		proxy_server = jingrow.db.get_value("Server", server, "proxy_server")
		agent = Agent(proxy_server, server_type="Proxy Server")
		agent.remove_proxysql_user(
			jingrow.get_pg("Site", self.site),
			self.username,
			reference_pagetype="Site Database User",
			reference_name=self.name,
		)

	@jingrow.whitelist()
	def modify_permissions(self):
		self._raise_error_if_archived()
		log_site_activity(
			self.site,
			"Modify Database User Permissions",
			reason=f"Modified user {self.username} with {self.mode} permission",
		)
		server = jingrow.db.get_value("Site", self.site, "server")
		agent = Agent(server)
		table_permissions = {}

		if self.mode == "granular":
			for x in self.permissions:
				table_permissions[x.table] = {
					"mode": x.mode,
					"columns": "*"
					if x.allow_all_columns
					else [c.strip() for c in x.selected_columns.splitlines() if c.strip()],
				}

		agent.modify_database_user_permissions(
			jingrow.get_pg("Site", self.site),
			self.username,
			self.mode,
			table_permissions,
			self.name,
		)

	@dashboard_whitelist()
	def get_credential(self):
		server = jingrow.db.get_value("Site", self.site, "server")
		proxy_server = jingrow.db.get_value("Server", server, "proxy_server")
		database = self._get_database_name()
		return {
			"host": proxy_server,
			"port": 3306,
			"database": database,
			"username": self.username,
			"password": self.get_password("password"),
			"mode": self.mode,
			"max_connections": self.max_connections,
		}

	@dashboard_whitelist()
	def archive(self, raise_error: bool = True, skip_remove_db_user_step: bool = False):
		if not raise_error and self.status == "Archived":
			return
		self._raise_error_if_archived()
		self.status = "Pending"
		self.save()

		if self.user_created_in_database and not skip_remove_db_user_step:
			"""
			If we are dropping the database, there is no need to drop
			db users separately.
			In those cases, use `skip_remove_db_user_step` param to skip it
			"""
			self.remove_user()
		else:
			self.user_created_in_database = False
			self.save()

		if self.user_added_in_proxysql:
			self.remove_user_from_proxysql()

		if not self.user_created_in_database and not self.user_added_in_proxysql:
			self.status = "Archived"
			self.save()

	@staticmethod
	def process_job_update(job):  # noqa: C901
		if job.status not in ("Success", "Failure"):
			return

		if not job.reference_name or not jingrow.db.exists("Site Database User", job.reference_name):
			return

		pg: SiteDatabaseUser = jingrow.get_pg("Site Database User", job.reference_name)

		if job.status == "Failure":
			pg.status = "Failed"
			pg.failed_agent_job = job.name
			if job.job_type == "Modify Database User Permissions":
				pg.failure_reason = SiteDatabaseUser.user_addressable_error_from_stacktrace(job.traceback)
			pg.save(ignore_permissions=True)
			return

		if job.job_type == "Create Database User":
			pg.user_created_in_database = True
			if not pg.user_added_in_proxysql:
				pg.add_user_to_proxysql()
		if job.job_type == "Remove Database User":
			pg.user_created_in_database = False
		elif job.job_type == "Add User to ProxySQL":
			pg.user_added_in_proxysql = True
			pg.modify_permissions()
		elif job.job_type == "Remove User from ProxySQL":
			pg.user_added_in_proxysql = False
		elif job.job_type == "Modify Database User Permissions":
			pg.status = "Active"

		pg.save(ignore_permissions=True)
		pg.reload()

		if (
			job.job_type in ("Remove Database User", "Remove User from ProxySQL")
			and not pg.user_added_in_proxysql
			and not pg.user_created_in_database
		):
			pg.archive()

	@staticmethod
	def user_addressable_error_from_stacktrace(stacktrace: str):
		pattern = r"peewee\.\w+Error: (.*)?"
		default_error_msg = "Unknown error. Please try again.\nIf the error persists, please contact support."

		matches = re.findall(pattern, stacktrace)
		if len(matches) == 0:
			return default_error_msg
		data = matches[0].strip().replace("(", "").replace(")", "").split(",", 1)
		if len(data) != 2:
			return default_error_msg

		if data[0] == "1054":
			pattern = r"Unknown column '(.*)' in '(.*)'\"*?"
			matches = re.findall(pattern, data[1])
			if len(matches) == 1 and len(matches[0]) == 2:
				return f"Column '{matches[0][0]}' doesn't exist in '{matches[0][1]}' table.\nPlease remove the column from permissions configuration and apply changes."

		elif data[0] == "1146":
			pattern = r"Table '(.*)' doesn't exist"
			matches = re.findall(pattern, data[1])
			if len(matches) == 1 and isinstance(matches[0], str):
				table_name = matches[0]
				table_name = table_name.split(".")[-1]
				return f"Table '{table_name}' doesn't exist.\nPlease remove it from permissions table and apply changes."

		return default_error_msg


get_permission_query_conditions = get_permission_query_conditions_for_pagetype("Site Database User")
