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

from __future__ import annotations

from base64 import b64decode
from typing import TYPE_CHECKING, ClassVar

import jingrow
import requests
from jingrow.query_builder.functions import Cast_
from jingrow.utils.caching import redis_cache
from jingrow.utils.safe_exec import safe_exec
from jingrow.website.utils import cleanup_page_name
from jingrow.website.website_generator import WebsiteGenerator

from jcloud.api.client import dashboard_whitelist
from jcloud.api.github import get_access_token
from jcloud.marketplace.pagetype.marketplace_app_plan.marketplace_app_plan import (
	get_app_plan_features,
)
from jcloud.jcloud.pagetype.app.app import new_app as new_app_pg
from jcloud.jcloud.pagetype.app_release_approval_request.app_release_approval_request import (
	AppReleaseApprovalRequest,
)
from jcloud.jcloud.pagetype.marketplace_app.utils import get_rating_percentage_distribution
from jcloud.utils import get_current_team, get_last_pg

if TYPE_CHECKING:
	from jcloud.jcloud.pagetype.site.site import Site


class MarketplaceApp(WebsiteGenerator):
	# 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 jcloud.jcloud.pagetype.marketplace_app_categories.marketplace_app_categories import MarketplaceAppCategories
		from jcloud.jcloud.pagetype.marketplace_app_screenshot.marketplace_app_screenshot import MarketplaceAppScreenshot
		from jcloud.jcloud.pagetype.marketplace_app_version.marketplace_app_version import MarketplaceAppVersion
		from jcloud.jcloud.pagetype.marketplace_localisation_app.marketplace_localisation_app import MarketplaceLocalisationApp
		from jingrow.types import DF

		after_install_script: DF.Code | None
		after_uninstall_script: DF.Code | None
		app: DF.Link
		average_rating: DF.Float
		categories: DF.Table[MarketplaceAppCategories]
		custom_verify_template: DF.Check
		description: DF.SmallText
		documentation: DF.Data | None
		image: DF.AttachImage | None
		jingrow_approved: DF.Check
		localisation_apps: DF.Table[MarketplaceLocalisationApp]
		long_description: DF.TextEditor | None
		message: DF.TextEditor | None
		outgoing_email: DF.Data | None
		outgoing_sender_name: DF.Data | None
		poll_method: DF.Data | None
		privacy_policy: DF.Data | None
		published: DF.Check
		review_stage: DF.Literal["Not Started", "Description Missing", "Logo Missing", "App Release Not Reviewed", "Ready for Review", "Ready to Publish", "Rejected"]
		route: DF.Data | None
		run_after_install_script: DF.Check
		run_after_uninstall_script: DF.Check
		screenshots: DF.Table[MarketplaceAppScreenshot]
		show_for_site_creation: DF.Check
		signature: DF.TextEditor | None
		site_config: DF.JSON | None
		sources: DF.Table[MarketplaceAppVersion]
		status: DF.Literal["Draft", "Published", "In Review", "Attention Required", "Rejected", "Disabled"]
		stop_auto_review: DF.Check
		subject: DF.Data | None
		subscription_level: DF.Literal["1", "2", "3", "4", "5"]
		subscription_type: DF.Literal["Free", "Paid", "Freemium"]
		subscription_update_hook: DF.Data | None
		support: DF.Data | None
		team: DF.Link | None
		terms_of_service: DF.Data | None
		title: DF.Data
		website: DF.Data | None
	# end: auto-generated types

	dashboard_fields: ClassVar = [
		"image",
		"title",
		"status",
		"description",
		"review_stage",
	]

	def autoname(self):
		self.name = self.app

	@dashboard_whitelist()
	def delete(self):
		if self.status != "Draft":
			jingrow.throw("You can only delete an app in Draft status")

		if get_current_team() != self.team:
			jingrow.throw("You are not authorized to delete this app")

		super().delete()

	def on_trash(self):
		jingrow.db.delete("Marketplace App Plan", {"app": self.name})
		jingrow.db.delete("App Release Approval Request", {"marketplace_app": self.name})

	@dashboard_whitelist()
	def create_approval_request(self, app_release: str):
		"""Create a new Approval Request for given `app_release`"""
		AppReleaseApprovalRequest.create(self.app, app_release)

	@dashboard_whitelist()
	def cancel_approval_request(self, app_release: str):
		approval_requests = jingrow.get_all(
			"App Release Approval Request",
			filters={"app_release": app_release},
			pluck="name",
			order_by="creation desc",
		)

		if len(approval_requests) == 0:
			jingrow.throw("No approval request exists for the given app release")

		jingrow.get_pg("App Release Approval Request", approval_requests[0]).cancel()

	def before_insert(self):
		if not jingrow.flags.in_test:
			self.check_if_duplicate()
			self.create_app_and_source_if_needed()
			self.long_description = jingrow.utils.md_to_html(self.fetch_readme())

		self.set_route()

	def set_route(self):
		self.route = "marketplace/apps/" + cleanup_page_name(self.app)

	def check_if_duplicate(self):
		if jingrow.db.exists("Marketplace App", self.name):
			jingrow.throw(f"App {self.name} already exists. Please contact support.")

	def create_app_and_source_if_needed(self):
		if jingrow.db.exists("App", self.app or self.name):
			app_pg = jingrow.get_pg("App", self.app or self.name)
		else:
			app_pg = new_app_pg(self.name, self.title)

		if not self.sources:
			source = app_pg.add_source(
				self.version,
				self.repository_url,
				self.branch,
				self.team,
				self.github_installation_id,
				public=True,
			)
			self.app = source.app
			self.append("sources", {"version": self.version, "source": source.name})

	def validate(self):
		self.published = self.status == "Published"
		self.validate_sources()
		self.validate_number_of_screenshots()

	def validate_sources(self):
		for source in self.sources:
			app_source = jingrow.get_pg("App Source", source.source)

			if app_source.app != self.app:
				jingrow.throw(f"App Source {jingrow.bold(source.source)} does not belong to this app!")

			app_source_versions = [v.version for v in app_source.versions]
			if source.version not in app_source_versions:
				jingrow.throw(
					f"App Source {jingrow.bold(source.source)} does not contain"
					f" version: {jingrow.bold(source.version)}"
				)

	def validate_number_of_screenshots(self):
		max_allowed_screenshots = jingrow.db.get_single_value("Jcloud Settings", "max_allowed_screenshots")
		if len(self.screenshots) > max_allowed_screenshots:
			jingrow.throw(f"You cannot add more than {max_allowed_screenshots} screenshots for an app.")

	def change_branch(self, source, version, to_branch):
		existing_source = jingrow.db.exists(
			"App Source",
			{
				"name": ("!=", self.name),
				"app": self.app,
				"repository_url": jingrow.db.get_value("App Source", {"name": source}, "repository_url"),
				"branch": to_branch,
				"team": self.team,
			},
		)
		if existing_source:
			# If source with branch to switch already exists, just add version to child table of source and use the same
			try:
				source_pg = jingrow.get_pg("App Source", existing_source)
				source_pg.append("versions", {"version": version})
				source_pg.save()
			except Exception:
				pass

			for source in self.sources:
				if source.source == source:
					source.source = existing_source
					self.save()
		else:
			# if a different source with the branch to switch doesn't exists update the existing source
			source_pg = jingrow.get_pg("App Source", source)
			source_pg.branch = to_branch
			source_pg.save()

	@dashboard_whitelist()
	def add_version(self, version, branch):
		existing_source = jingrow.db.exists(
			"App Source",
			[
				["App Source", "app", "=", self.app],
				["App Source", "team", "=", self.team],
				["App Source", "branch", "=", branch],
			],
		)
		if existing_source:
			# If source with branch to switch already exists, just add version to child table of source and use the same
			source_pg = jingrow.get_pg("App Source", existing_source)
			try:
				source_pg.append("versions", {"version": version})
				source_pg.public = 1
				source_pg.save()
			except Exception:
				pass
		else:
			# create new app source for version and branch to switch
			source_pg = jingrow.get_pg(
				{
					"pagetype": "App Source",
					"app": self.app,
					"team": self.team,
					"branch": branch,
					"repository_url": jingrow.db.get_value(
						"App Source", {"name": self.sources[0].source}, "repository_url"
					),
					"public": 1,
				}
			)
			source_pg.append("versions", {"version": version})
			source_pg.insert(ignore_permissions=True)

		self.append("sources", {"version": version, "source": source_pg.name})
		self.save()

	@dashboard_whitelist()
	def remove_version(self, version):
		if self.status == "Published" and len(self.sources) == 1:
			jingrow.throw("Failed to remove. Need at least 1 version for a published app")

		for i, source in enumerate(self.sources):
			if source.version == version:
				# remove from marketplace app source child table
				self.sources.pop(i)
				self.save()

				app_source = jingrow.get_cached_pg("App Source", source.source)
				for j, source_version in enumerate(app_source.versions):
					if source_version.version == version and len(app_source.versions) > 1:
						# remove from app source versions child table
						app_source.versions.pop(j)
						app_source.save()
						break
				break

	def get_app_source(self):
		return jingrow.get_pg("App Source", {"app": self.app})

	def fetch_readme(self):
		source = self.get_app_source()

		if source.github_installation_id:
			github_access_token = get_access_token(source.github_installation_id)
		else:
			github_access_token = jingrow.get_value("Jcloud Settings", None, "github_access_token")

		headers = {
			"Authorization": f"token {github_access_token}",
		}
		owner = source.repository_owner
		repository = source.repository
		branch = source.branch

		readme_content = None
		variants = ["README.md", "readme.md", "readme", "README", "Readme"]
		for variant in variants:
			try:
				readme = requests.get(
					f"http://git.jingrow.com/api/v1/repos/{owner}/{repository}/contents/{variant}",
					headers=headers,
					params={"ref": branch},
				).json()
				readme_content = b64decode(readme["content"]).decode()
				if readme_content:
					break
			except Exception:
				print(jingrow.get_traceback())
				continue

		return readme_content

	def get_context(self, context):
		context.no_cache = True
		context.app = self

		supported_versions = []
		public_rgs = jingrow.get_all("Release Group", filters={"public": True}, fields=["version", "name"])

		unique_public_rgs = {}
		for rg in public_rgs:
			if rg.version not in unique_public_rgs:
				unique_public_rgs[rg.version] = rg.name

		for source in self.sources:
			if source.version not in unique_public_rgs:
				continue

			jingrow_source_name = jingrow.get_pg(
				"Release Group App",
				{"app": "jingrow", "parent": unique_public_rgs[source.version]},
			).source
			jingrow_source = jingrow.db.get_value(
				"App Source",
				jingrow_source_name,
				["repository_url", "branch"],
				as_dict=True,
			)

			app_source = jingrow.db.get_value(
				"App Source",
				source.source,
				["repository_url", "branch", "public"],
				as_dict=True,
			)

			supported_versions.append(
				jingrow._dict(
					{
						"version": source.version,
						"app_source": app_source,
						"jingrow_source": jingrow_source,
					}
				)
			)

		# Sort based on version
		supported_versions.sort(key=lambda x: x.version, reverse=True)

		context.supported_versions = supported_versions

		# Add publisher info
		publisher_profile = jingrow.get_all(
			"Marketplace Publisher Profile",
			filters={"team": self.team},
			fields=["display_name", "contact_email"],
			limit=1,
		)

		if publisher_profile:
			context.publisher_profile = publisher_profile[0]

		context.no_of_installs = self.get_analytics().get("total_installs")
		context.plans = self.get_plans()

		user_reviews = self.get_user_reviews()
		for review in user_reviews:
			review["developer_reply"] = jingrow.get_all(
				"Developer Review Reply",
				filters={"review": review.name},
				pluck="description",
				order_by="creation asc",
			)

		ratings_summary = self.get_user_ratings_summary(user_reviews)

		context.user_reviews = user_reviews
		context.ratings_summary = ratings_summary

	def get_user_reviews(self) -> list:
		app_user_review = jingrow.qb.PageType("App User Review")
		user = jingrow.qb.PageType("User")

		query = (
			jingrow.qb.from_(app_user_review)
			.join(user)
			.on(user.name == app_user_review.reviewer)
			.select(
				app_user_review.name,
				app_user_review.title,
				Cast_(5 * app_user_review.rating, "INT").as_("rating"),
				app_user_review.review,
				app_user_review.creation,
				app_user_review.reviewer,
				user.full_name.as_("user_name"),
			)
			.where(app_user_review.app == self.name)
		)
		return query.run(as_dict=True)

	def get_user_ratings_summary(self, reviews: list) -> dict:
		total_num_reviews = len(reviews)
		avg_rating = 0.0

		if len(reviews) > 0:
			avg_rating = sum([r.rating for r in reviews]) / len(reviews)
			avg_rating = jingrow.utils.rounded(avg_rating, 1)

		rating_percentages = get_rating_percentage_distribution(reviews)

		return {
			"total_num_reviews": total_num_reviews,
			"avg_rating": avg_rating,
			"rating_percentages": rating_percentages,
		}

	def get_deploy_information(self):
		"""Return the deploy information this marketplace app"""
		# Public Release Groups, Benches
		# Is on release group, but not on bench -> awaiting deploy
		deploy_info = {}

		for source in self.sources:
			version = source.version
			deploy_info[version] = "Not Deployed"

			release_groups = jingrow.get_all(
				"Release Group", filters={"public": 1, "version": version}, pluck="name"
			)

			for rg_name in release_groups:
				release_group = jingrow.get_pg("Release Group", rg_name)
				sources_on_rg = [a.source for a in release_group.apps]

				latest_active_bench = get_last_pg("Bench", filters={"status": "Active", "group": rg_name})

				if latest_active_bench:
					sources_on_bench = [a.source for a in latest_active_bench.apps]
					if source.source in sources_on_bench:
						# Is deployed on a bench
						deploy_info[version] = "Deployed"

				if (source.source in sources_on_rg) and (deploy_info[version] != "Deployed"):
					# Added to release group, but not yet deployed to a bench
					deploy_info[version] = "Awaiting Deploy"

		return deploy_info

	def total_installs(self):
		return jingrow.db.count("Site App", filters={"app": self.app})

	def total_active_sites(self):
		return jingrow.db.sql(
			"""
			SELECT
				count(*)
			FROM
				tabSite site
			LEFT JOIN
				`tabSite App` app
			ON
				app.parent = site.name
			WHERE
				site.status = "Active" AND app.app = %s
		""",
			(self.app,),
		)[0][0]

	def total_active_benches(self):
		return jingrow.db.sql(
			"""
			SELECT
				count(*)
			FROM
				tabBench bench
			LEFT JOIN
				`tabBench App` app
			ON
				app.parent = bench.name
			WHERE
				bench.status = "Active" AND app.app = %s
		""",
			(self.app,),
		)[0][0]

	def get_payout_amount(self, status: str = "", total_for: str = "net_amount"):
		"""Return the payout amount for this app"""
		filters = {"recipient": self.team}
		if status:
			filters["status"] = status
		payout_orders = jingrow.get_all("Payout Order", filters=filters, pluck="name")
		payout = jingrow.get_all(
			"Payout Order Item",
			filters={"parent": ("in", payout_orders), "document_name": self.name},
			fields=[
				f"SUM(CASE WHEN currency = 'USD' THEN {total_for} ELSE 0 END) AS usd_amount",
				f"SUM(CASE WHEN currency = 'CNY' THEN {total_for} ELSE 0 END) AS cny_amount",
			],
		)
		return payout[0] if payout else {"usd_amount": 0, "cny_amount": 0}

	@dashboard_whitelist()
	def site_installs(self):
		site = jingrow.qb.PageType("Site")
		site_app = jingrow.qb.PageType("Site App")
		site_plan = jingrow.qb.PageType("Site Plan")
		team = jingrow.qb.PageType("Team")

		query = (
			jingrow.qb.from_(site)
			.left_join(team)
			.on(team.name == site.team)
			.left_outer_join(site_app)
			.on(site.name == site_app.parent)
			.left_outer_join(site_plan)
			.on(site_app.plan == site_plan.name)
			.select(site.name, site.plan, team.user)
			.where((site.status == "Active") & (site_app.app == self.app) & (site_plan.price_usd >= 0))
		)
		return query.run(as_dict=True)

	@dashboard_whitelist()
	def listing_details(self):
		return {
			"support": self.support,
			"website": self.website,
			"documentation": self.documentation,
			"privacy_policy": self.privacy_policy,
			"terms_of_service": self.terms_of_service,
			"description": self.description,
			"long_description": self.long_description,
			"screenshots": [screenshot.image for screenshot in self.screenshots],
		}

	@dashboard_whitelist()
	def mark_app_ready_for_review(self):
		# TODO: Start security check and auto deploy process here
		self.review_stage = "Ready for Review"
		self.save()

	@dashboard_whitelist()
	def update_listing(self, *args):
		data = jingrow._dict(args[0])
		self.title = data.get("title") or self.title
		self.description = data.get("description")
		self.long_description = data.get("long_description")
		self.support = data.get("support")
		self.website = data.get("website")
		self.documentation = data.get("documentation")
		self.privacy_policy = data.get("privacy_policy")
		self.terms_of_service = data.get("terms_of_service")
		self.save()

	def get_analytics(self):
		today = jingrow.utils.today()
		last_week = jingrow.utils.add_days(today, -7)

		return {
			"total_installs": self.total_installs(),
			"installs_active_sites": self.total_active_sites(),
			"installs_active_benches": self.total_active_benches(),
			"installs_last_week": jingrow.db.count(
				"Site Activity",
				{
					"action": "Install App",
					"reason": self.app,
					"creation": (">=", last_week),
				},
			),
			"total_payout": self.get_payout_amount(),
			"paid_payout": self.get_payout_amount(status="Paid"),
			"pending_payout": self.get_payout_amount(status="Draft"),
			"commission": self.get_payout_amount(total_for="commission"),
		}

	def get_plans(self, jingrow_version: str | None = None) -> list:
		return get_plans_for_app(self.name, jingrow_version)

	def can_charge_for_subscription(self, subscription):
		return subscription.enabled == 1 and subscription.team and subscription.team != "Administrator"


def get_plans_for_app(
	app_name, jingrow_version=None, include_free=True, include_disabled=False
):  # Unused for now, might use later
	plans = []
	filters = {"app": app_name}

	if not include_free:
		filters["is_free"] = False

	if not include_disabled:
		filters["enabled"] = True

	marketplace_app_plans = jingrow.get_all(
		"Marketplace App Plan",
		filters=filters,
		fields=[
			"name",
			"title",
			"enabled",
			"price_cny",
			"price_usd",
		],
	)

	for app_plan in marketplace_app_plans:
		plan_data = {}
		plan_data.update(app_plan)
		plan_data["features"] = get_app_plan_features(app_plan.name)
		plans.append(plan_data)

	plans.sort(key=lambda x: x["price_usd"])
	plans.sort(key=lambda x: x["enabled"], reverse=True)  # Enabled Plans First

	return plans


def marketplace_app_hook(app=None, site: Site | None = None, op="install"):
	if app is None:
		site_apps = jingrow.get_all("Site App", filters={"parent": site.name}, pluck="app")
		for app in site_apps:
			run_script(app, site, op)
	else:
		run_script(app, site, op)


def get_script_name(app, op):
	if op == "install" and jingrow.db.get_value("Marketplace App", app, "run_after_install_script"):
		return "after_install_script"

	if op == "uninstall" and jingrow.db.get_value("Marketplace App", app, "run_after_uninstall_script"):
		return "after_uninstall_script"
	return ""


def run_script(app, site: Site, op):
	script = get_script_name(app, op)
	if script:
		script = jingrow.db.get_value("Marketplace App", app, script)
		local = {"pg": site}
		safe_exec(script, _locals=local)


@redis_cache(ttl=60 * 60 * 24)
def get_total_installs_by_app():
	total_installs = jingrow.db.get_all(
		"Site App",
		fields=["app", "count(*) as count"],
		group_by="app",
		order_by=None,
	)
	return {installs["app"]: installs["count"] for installs in total_installs}
