# Copyright (c) 2023, JINGROW
# For license information, please see license.txt

from __future__ import annotations

import json

import jingrow
import jingrow.utils
from jingrow.model.page import Page
from jingrow.utils.data import get_url
from jingrow.utils.momentjs import get_all_timezones

from jcloud.utils import log_error, validate_subdomain
from jcloud.utils.unique_name_generator import generate as generate_random_name


class ProductTrial(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.site import Site
		from jcloud.saas.pagetype.product_trial_app.product_trial_app import ProductTrialApp
		from jcloud.saas.pagetype.product_trial_signup_field.product_trial_signup_field import (
			ProductTrialSignupField,
		)

		apps: DF.Table[ProductTrialApp]
		create_additional_system_user: DF.Check
		domain: DF.Link
		email_account: DF.Link | None
		email_full_logo: DF.AttachImage | None
		email_header_content: DF.Code
		email_subject: DF.Data
		enable_pooling: DF.Check
		logo: DF.AttachImage | None
		published: DF.Check
		redirect_to_after_login: DF.Data
		release_group: DF.Link
		setup_wizard_completion_mode: DF.Literal["manual", "auto"]
		setup_wizard_payload_generator_script: DF.Code | None
		signup_fields: DF.Table[ProductTrialSignupField]
		standby_pool_size: DF.Int
		standby_queue_size: DF.Int
		title: DF.Data
		trial_days: DF.Int
		trial_plan: DF.Link
	# end: auto-generated types

	dashboard_fields = (
		"title",
		"logo",
		"domain",
		"trial_days",
		"trial_plan",
		"redirect_to_after_login",
	)

	USER_LOGIN_PASSWORD_FIELD = "user_login_password"

	def get_pg(self, pg):
		if not self.published:
			jingrow.throw("不允许")

		def _parse_options(field):
			if field.fieldtype != "Select":
				return []
			if field.fieldname.endswith("_tz"):
				return get_all_timezones()
			if not field.options:
				return []
			return [option for option in ((field.options or "").split("\n")) if option]

		pg.signup_fields = [
			{
				"label": field.label,
				"fieldname": field.fieldname,
				"fieldtype": field.fieldtype,
				"options": _parse_options(field),
				"required": field.required,
			}
			for field in self.signup_fields
		]
		pg.proxy_servers = self.get_proxy_servers_for_available_clusters()
		pg.site = self.get_standby_site()
		return pg

	def validate(self):
		plan = jingrow.get_pg("Site Plan", self.trial_plan)
		if plan.document_type != "Site":
			jingrow.throw("Selected plan is not for site")
		if not plan.is_trial_plan:
			jingrow.throw("Selected plan is not a trial plan")

		for field in self.signup_fields:
			if field.fieldname == self.USER_LOGIN_PASSWORD_FIELD:
				if not field.required:
					jingrow.throw(f"{self.USER_LOGIN_PASSWORD_FIELD} field should be marked as required")
				if field.fieldtype != "Password":
					jingrow.throw(f"{self.USER_LOGIN_PASSWORD_FIELD} field should be of type Password")

		if not self.redirect_to_after_login.startswith("/"):
			jingrow.throw("Redirection route after login should start with /")

	def setup_trial_site(self, subdomain, team, cluster=None, account_request=None):
		from jcloud.jcloud.pagetype.site.site import get_plan_config

		validate_subdomain(subdomain)

		site_domain = f"{subdomain}.{self.domain}"

		# if user didn't change the subdomain, and is picking the provided site
		if jingrow.db.exists("Site", {"name": site_domain, "is_standby": 1, "standby_for_product": self.name}):
			standby_site = site_domain
		else:
			standby_site = self.get_standby_site(cluster)

		trial_end_date = jingrow.utils.add_days(None, self.trial_days or 14)
		site = None
		agent_job_name = None
		apps_site_config = get_app_subscriptions_site_config([d.app for d in self.apps])
		plan = self.trial_plan

		if standby_site:
			site = jingrow.get_pg("Site", standby_site)
			site.is_standby = False
			site.team = team
			site.trial_end_date = trial_end_date
			site.account_request = account_request
			site._update_configuration(apps_site_config, save=False)
			site._update_configuration(get_plan_config(plan), save=False)
			site.save(ignore_permissions=True)
			site.create_subscription(plan)
			site.signup_time = jingrow.utils.now()
			site.reload()
			site.generate_saas_communication_secret(create_agent_job=True, save=True)
			if self.create_additional_system_user:
				agent_job_name = site.create_user_with_team_info()
			self.set_site_domain(site, site_domain)
		else:
			# Create a site in the cluster, if standby site is not available
			apps = [{"app": d.app} for d in self.apps]
			is_jingrow_app_present = any(d["app"] == "jingrow" for d in apps)
			if not is_jingrow_app_present:
				apps.insert(0, {"app": "jingrow"})

			site = jingrow.get_pg(
				pagetype="Site",
				subdomain=subdomain,
				domain=self.domain,
				group=self.release_group,
				cluster=cluster,
				account_request=account_request,
				is_standby=False,
				standby_for_product=self.name,
				subscription_plan=plan,
				team=team,
				apps=apps,
				trial_end_date=trial_end_date,
				signup_time=jingrow.utils.now(),
			)
			site._update_configuration(apps_site_config, save=False)
			site._update_configuration(get_plan_config(plan), save=False)
			site.generate_saas_communication_secret(create_agent_job=False, save=False)
			if self.setup_wizard_completion_mode == "auto" or not self.create_additional_system_user:
				site.flags.ignore_additional_system_user_creation = True
			site.insert(ignore_permissions=True)
			agent_job_name = site.flags.get("new_site_agent_job_name", None)

		return site, agent_job_name, bool(standby_site)

	def get_proxy_servers_for_available_clusters(self):
		clusters = self.get_available_clusters()
		proxy_servers = jingrow.db.get_all(
			"Proxy Server",
			{
				"cluster": ("in", clusters),
			},
			["name", "is_primary", "cluster"],
		)
		proxy_servers_by_cluster = {}
		for proxy_server in proxy_servers:
			cluster = proxy_server.cluster
			proxy_servers_by_cluster.setdefault(cluster, []).append(proxy_server)

		proxy_servers_for_available_clusters = {}
		for cluster, proxy_servers in proxy_servers_by_cluster.items():
			primary_proxy = [d for d in proxy_servers if d.is_primary]
			if primary_proxy:
				proxy_server_name = primary_proxy[0].name
				proxy_servers_for_available_clusters[proxy_server_name] = cluster

		return proxy_servers_for_available_clusters

	def set_site_domain(self, site: Site, site_domain: str):
		if not site_domain:
			return

		if site.name == site_domain or site.host_name == site_domain:
			return

		site.add_domain_for_product_site(site_domain)
		site.add_domain_to_config(site_domain)

	def get_available_clusters(self):
		release_group = jingrow.get_pg("Release Group", self.release_group)
		clusters = jingrow.db.get_all(
			"Server",
			{"name": ("in", [d.server for d in release_group.servers])},
			order_by="name asc",
			pluck="Cluster",
		)
		clusters = list(set(clusters))
		return jingrow.db.get_all(
			"Cluster", {"name": ("in", clusters), "public": 1}, order_by="name asc", pluck="name"
		)

	def get_standby_site(self, cluster=None):
		filters = {
			"is_standby": True,
			"standby_for_product": self.name,
			"status": "Active",
		}
		if cluster:
			filters["cluster"] = cluster
		sites = jingrow.db.get_all(
			"Site",
			filters=filters,
			pluck="name",
			order_by="creation asc",
			limit=1,
		)
		if sites:
			return sites[0]
		if cluster:
			# if site is not found and cluster was specified, try to find a site in any cluster
			return self.get_standby_site()
		return None

	def create_standby_sites_in_each_cluster(self):
		if not self.enable_pooling:
			return

		clusters = self.get_available_clusters()
		for cluster in clusters:
			self.create_standby_sites(cluster)

	def create_standby_sites(self, cluster):
		if not self.enable_pooling:
			return

		sites_to_create = self.standby_pool_size - self.get_standby_sites_count(cluster)
		if sites_to_create <= 0:
			return
		if sites_to_create > self.standby_queue_size:
			sites_to_create = self.standby_queue_size

		for _i in range(sites_to_create):
			self.create_standby_site(cluster)
			jingrow.db.commit()

	def create_standby_site(self, cluster):
		administrator = jingrow.db.get_value("Team", {"user": "Administrator"}, "name")
		apps = [{"app": d.app} for d in self.apps]
		site = jingrow.get_pg(
			pagetype="Site",
			subdomain=self.get_unique_site_name(),
			domain=self.domain,
			group=self.release_group,
			cluster=cluster,
			is_standby=True,
			standby_for_product=self.name,
			team=administrator,
			apps=apps,
		)
		site.insert(ignore_permissions=True)

	def get_standby_sites_count(self, cluster):
		active_standby_sites = jingrow.db.count(
			"Site",
			{
				"cluster": cluster,
				"is_standby": 1,
				"standby_for_product": self.name,
				"status": "Active",
			},
		)
		# sites that are created in the last hour
		recent_standby_sites = jingrow.db.count(
			"Site",
			{
				"cluster": cluster,
				"is_standby": 1,
				"standby_for_product": self.name,
				"status": ("not in", ["Archived", "Suspended"]),
				"creation": (">", jingrow.utils.add_to_date(None, hours=-1)),
			},
		)
		return active_standby_sites + recent_standby_sites

	def get_unique_site_name(self):
		subdomain = f"{self.name}-{generate_random_name(segment_length=3, num_segments=2)}"
		filters = {
			"subdomain": subdomain,
			"domain": self.domain,
			"status": ("!=", "Archived"),
		}
		while jingrow.db.exists("Site", filters):
			subdomain = f"{self.name}-{generate_random_name(segment_length=3, num_segments=2)}"
		return subdomain


def get_app_subscriptions_site_config(apps: list[str]):
	subscriptions = []
	site_config = {}

	for app in apps:
		free_plan = jingrow.get_all(
			"Marketplace App Plan",
			{"enabled": 1, "price_usd": ("<=", 0), "app": app},
			pluck="name",
		)
		if free_plan:
			new_subscription = jingrow.get_pg(
				{
					"pagetype": "Subscription",
					"document_type": "Marketplace App",
					"document_name": app,
					"plan_type": "Marketplace App Plan",
					"plan": free_plan[0],
					"enabled": 0,
					"team": jingrow.get_value("Team", {"user": "Administrator"}, "name"),
				}
			).insert(ignore_permissions=True)

			subscriptions.append(new_subscription)
			config = jingrow.db.get_value("Marketplace App", app, "site_config")
			config = json.loads(config) if config else {}
			site_config.update(config)

	for s in subscriptions:
		site_config.update({"sk_" + s.document_name: s.secret_key})

	return site_config


def replenish_standby_sites():
	"""Create standby sites for all products with pooling enabled. This is called by the scheduler."""
	products = jingrow.get_all("Product Trial", {"enable_pooling": 1}, pluck="name")
	for product in products:
		product = jingrow.get_pg("Product Trial", product)
		try:
			product.create_standby_sites_in_each_cluster()
			jingrow.db.commit()
		except Exception:
			log_error("Replenish Standby Sites Error", product=product.name)
			jingrow.db.rollback()


def send_verification_mail_for_login(email: str, product: str, code: str):
	"""Send verification mail for login."""
	if jingrow.conf.developer_mode:
		print(f"\nVerification Code for {product}:")
		print(f"Email : {email}")
		print(f"Code : {code}")
		print()
		return
	product_trial: ProductTrial = jingrow.get_pg("Product Trial", product)
	sender = ""
	subject = f"{code} - Verification Code for {product_trial.title} Login"
	args = {
		"header_content": f"<p>You have requested a verification code to login to your {product_trial.title} site. The code is valid for 5 minutes.</p>",
		"otp": code,
	}
	if product_trial.email_full_logo:
		args.update({"image_path": get_url(product_trial.email_full_logo, True)})
	if product_trial.email_account:
		sender = jingrow.get_value("Email Account", product_trial.email_account, "email_id")

	jingrow.sendmail(
		sender=sender,
		recipients=email,
		subject=subject,
		template="product_trial_verify_account",
		args=args,
		now=True,
	)


def sync_product_site_users():
	"""Fetch and sync users from product sites, so that they can be used for login to the site from FC."""

	product_groups = jingrow.db.get_all(
		"Product Trial", {"published": 1}, ["release_group"], pluck="release_group"
	)
	product_benches = jingrow.get_all(
		"Bench", {"group": ("in", product_groups), "status": "Active"}, pluck="name"
	)
	jingrow.enqueue(
		"jcloud.saas.pagetype.product_trial.product_trial._sync_product_site_users",
		queue="short",
		product_benches=product_benches,
		job_id="sync_product_site_users",
		deduplicate=True,
		enqueue_after_commit=True,
	)
	jingrow.db.commit()


def _sync_product_site_users(product_benches):
	for bench_name in product_benches:
		bench = jingrow.get_pg("Bench", bench_name)
		# Skip syncing analytics for benches that have been archived (after the job was enqueued)
		if bench.status != "Active":
			return
		try:
			bench.sync_product_site_users()
			jingrow.db.commit()
		except Exception:
			log_error(
				"Bench Analytics Sync Error",
				bench=bench.name,
				reference_pagetype="Bench",
				reference_name=bench.name,
			)
			jingrow.db.rollback()
