# Copyright (c) 2021, XhiveFramework Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE

import contextlib
import glob
import json
import os
import pathlib
import re
import textwrap

import click
import git
import requests

import xhiveframework

APP_TITLE_PATTERN = re.compile(r"^(?![\W])[^\d_\s][\w -]+$", flags=re.UNICODE)


def make_boilerplate(dest, app_name, no_git=False):
	if not os.path.exists(dest):
		print("Destination directory does not exist")
		return

	# app_name should be in snake_case
	app_name = xhiveframework.scrub(app_name)
	hooks = _get_user_inputs(app_name)
	_create_app_boilerplate(dest, hooks, no_git=no_git)


def _get_user_inputs(app_name):
	"""Prompt user for various inputs related to new app and return config."""
	app_name = xhiveframework.scrub(app_name)

	hooks = xhiveframework._dict()
	hooks.app_name = app_name
	app_title = hooks.app_name.replace("_", " ").title()

	new_app_config = {
		"app_title": {
			"prompt": "App Title",
			"default": app_title,
			"validator": is_valid_title,
		},
		"app_description": {"prompt": "App Description"},
		"app_publisher": {"prompt": "App Publisher"},
		"app_email": {"prompt": "App Email", "validator": is_valid_email},
		"app_license": {
			"prompt": "App License",
			"default": "mit",
			"type": click.Choice(get_license_options()),
		},
		"create_github_workflow": {
			"prompt": "Create GitHub Workflow action for unittests",
			"default": False,
			"type": bool,
		},
	}

	for property, config in new_app_config.items():
		value = None
		input_type = config.get("type", str)

		while value is None:
			if input_type == bool:
				value = click.confirm(config["prompt"], default=config.get("default"))
			else:
				value = click.prompt(config["prompt"], default=config.get("default"), type=input_type)

			if validator_function := config.get("validator"):
				if not validator_function(value):
					value = None
		hooks[property] = value

	return hooks


def is_valid_email(email) -> bool:
	from email.headerregistry import Address

	try:
		Address(addr_spec=email)
	except Exception:
		print("App Email should be a valid email address.")
		return False
	return True


def is_valid_title(title) -> bool:
	if not APP_TITLE_PATTERN.match(title):
		print(
			"App Title should start with a letter and it can only consist of letters, numbers, spaces and underscores"
		)
		return False
	return True


def get_license_options() -> list[str]:
	url = "https://api.github.com/licenses"
	res = requests.get(url=url)
	if res.status_code == 200:
		res = res.json()
		ids = [r.get("spdx_id") for r in res]
		return [licencse.lower() for licencse in ids]

	return ["agpl-3.0", "gpl-3.0", "mit", "custom"]


def get_license_text(license_name: str) -> str:
	url = f"https://api.github.com/licenses/{license_name.lower()}"
	res = requests.get(url=url)
	if res.status_code == 200:
		res = res.json()
		return res.get("body")
	return license_name


def _create_app_boilerplate(dest, hooks, no_git=False):
	xhiveframework.create_folder(
		os.path.join(dest, hooks.app_name, hooks.app_name, xhiveframework.scrub(hooks.app_title)),
		with_init=True,
	)
	xhiveframework.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates"), with_init=True)
	xhiveframework.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "www"))
	xhiveframework.create_folder(
		os.path.join(dest, hooks.app_name, hooks.app_name, "templates", "pages"), with_init=True
	)
	xhiveframework.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", "includes"))
	xhiveframework.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "config"), with_init=True)
	xhiveframework.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "public", "css"))
	xhiveframework.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "public", "js"))

	# add .gitkeep file so that public folder is committed to git
	# this is needed because if public doesn't exist, bench build doesn't symlink the apps assets
	with open(os.path.join(dest, hooks.app_name, hooks.app_name, "public", ".gitkeep"), "w") as f:
		f.write("")

	with open(os.path.join(dest, hooks.app_name, hooks.app_name, "__init__.py"), "w") as f:
		f.write(xhiveframework.as_unicode(init_template))

	with open(os.path.join(dest, hooks.app_name, "pyproject.toml"), "w") as f:
		f.write(xhiveframework.as_unicode(pyproject_template.format(**hooks)))

	with open(os.path.join(dest, hooks.app_name, "README.md"), "w") as f:
		f.write(
			xhiveframework.as_unicode(
				f"## {hooks.app_title}\n\n{hooks.app_description}\n\n#### License\n\n{hooks.app_license}"
			)
		)
	license_body = get_license_text(license_name=hooks.app_license)
	with open(os.path.join(dest, hooks.app_name, "license.txt"), "w") as f:
		f.write(xhiveframework.as_unicode(license_body))

	with open(os.path.join(dest, hooks.app_name, hooks.app_name, "modules.txt"), "w") as f:
		f.write(xhiveframework.as_unicode(hooks.app_title))

	# These values could contain quotes and can break string declarations
	# So escaping them before setting variables in setup.py and hooks.py
	for key in ("app_publisher", "app_description", "app_license"):
		hooks[key] = hooks[key].replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"')

	with open(os.path.join(dest, hooks.app_name, hooks.app_name, "hooks.py"), "w") as f:
		f.write(xhiveframework.as_unicode(hooks_template.format(**hooks)))

	with open(os.path.join(dest, hooks.app_name, hooks.app_name, "patches.txt"), "w") as f:
		f.write(xhiveframework.as_unicode(patches_template.format(**hooks)))

	app_directory = os.path.join(dest, hooks.app_name)

	if hooks.create_github_workflow:
		_create_github_workflow_files(dest, hooks)

	if not no_git:
		with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
			f.write(xhiveframework.as_unicode(gitignore_template.format(app_name=hooks.app_name)))

		# initialize git repository
		app_repo = git.Repo.init(app_directory, initial_branch="develop")
		app_repo.git.add(A=True)
		app_repo.index.commit("feat: Initialize App")

	print(f"'{hooks.app_name}' created at {app_directory}")


def _create_github_workflow_files(dest, hooks):
	workflows_path = pathlib.Path(dest) / hooks.app_name / ".github" / "workflows"
	workflows_path.mkdir(parents=True, exist_ok=True)

	ci_workflow = workflows_path / "ci.yml"
	with open(ci_workflow, "w") as f:
		f.write(github_workflow_template.format(**hooks))


PATCH_TEMPLATE = textwrap.dedent(
	'''
	import xhiveframework

	def execute():
		"""{docstring}"""

		# Write your patch here.
		pass
'''
)


class PatchCreator:
	def __init__(self):
		self.all_apps = xhiveframework.get_all_apps(sites_path=".", with_internal_apps=False)

		self.app = None
		self.app_dir = None
		self.patch_dir = None
		self.filename = None
		self.docstring = None
		self.patch_file = None

	def fetch_user_inputs(self):
		self._ask_app_name()
		self._ask_doctype_name()
		self._ask_patch_meta_info()

	def _ask_app_name(self):
		self.app = click.prompt("Select app for new patch", type=click.Choice(self.all_apps))
		self.app_dir = pathlib.Path(xhiveframework.get_app_path(self.app))

	def _ask_doctype_name(self):
		def _doctype_name(filename):
			with contextlib.suppress(Exception):
				with open(filename) as f:
					return json.load(f).get("name")

		doctype_files = list(glob.glob(f"{self.app_dir}/**/doctype/**/*.json"))
		doctype_map = {_doctype_name(file): file for file in doctype_files}
		doctype_map.pop(None, None)

		doctype = click.prompt(
			"Provide DocType name on which this patch will apply",
			type=click.Choice(doctype_map.keys()),
			show_choices=False,
		)
		self.patch_dir = pathlib.Path(doctype_map[doctype]).parents[0] / "patches"

	def _ask_patch_meta_info(self):
		self.docstring = click.prompt("Describe what this patch does", type=str)
		default_filename = xhiveframework.scrub(self.docstring) + ".py"

		def _valid_filename(name):
			if not name:
				return

			match name.partition("."):
				case filename, ".", "py" if filename.isidentifier():
					return True
				case _:
					click.echo(f"{name} is not a valid python file name")

		while not _valid_filename(self.filename):
			self.filename = click.prompt(
				"Provide filename for this patch", type=str, default=default_filename
			)

	def create_patch_file(self):
		self._create_parent_folder_if_not_exists()

		self.patch_file = self.patch_dir / self.filename

		if self.patch_file.exists():
			raise Exception(f"Patch {self.patch_file} already exists")

		*path, _filename = self.patch_file.relative_to(self.app_dir.parents[0]).parts
		dotted_path = ".".join([*path, self.patch_file.stem])

		patches_txt = self.app_dir / "patches.txt"
		existing_patches = patches_txt.read_text()

		if dotted_path in existing_patches:
			raise Exception(f"Patch {dotted_path} is already present in patches.txt")

		self.patch_file.write_text(PATCH_TEMPLATE.format(docstring=self.docstring))

		with open(patches_txt, "a+") as f:
			if not existing_patches.endswith("\n"):
				f.write("\n")  # ensure EOF
			f.write(dotted_path + "\n")
		click.echo(f"Created {self.patch_file} and updated patches.txt")

	def _create_parent_folder_if_not_exists(self):
		if not self.patch_dir.exists():
			click.confirm(
				f"Patch folder '{self.patch_dir}' doesn't exist, create it?",
				abort=True,
				default=True,
			)
			self.patch_dir.mkdir()

		init_py = self.patch_dir / "__init__.py"
		init_py.touch()


init_template = """__version__ = "0.0.1"
"""

pyproject_template = """[project]
name = "{app_name}"
authors = [
    {{ name = "{app_publisher}", email = "{app_email}"}}
]
description = "{app_description}"
requires-python = ">=3.10"
readme = "README.md"
dynamic = ["version"]
dependencies = [
    # "xhiveframework~=15.0.0" # Installed and managed by bench.
]

[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"

# These dependencies are only installed when developer mode is enabled
[tool.bench.dev-dependencies]
# package_name = "~=1.1.0"
"""

hooks_template = """app_name = "{app_name}"
app_title = "{app_title}"
app_publisher = "{app_publisher}"
app_description = "{app_description}"
app_email = "{app_email}"
app_license = "{app_license}"
# required_apps = []

# Includes in <head>
# ------------------

# include js, css files in header of desk.html
# app_include_css = "/assets/{app_name}/css/{app_name}.css"
# app_include_js = "/assets/{app_name}/js/{app_name}.js"

# include js, css files in header of web template
# web_include_css = "/assets/{app_name}/css/{app_name}.css"
# web_include_js = "/assets/{app_name}/js/{app_name}.js"

# include custom scss in every website theme (without file extension ".scss")
# website_theme_scss = "{app_name}/public/scss/website"

# include js, css files in header of web form
# webform_include_js = {{"doctype": "public/js/doctype.js"}}
# webform_include_css = {{"doctype": "public/css/doctype.css"}}

# include js in page
# page_js = {{"page" : "public/js/file.js"}}

# include js in doctype views
# doctype_js = {{"doctype" : "public/js/doctype.js"}}
# doctype_list_js = {{"doctype" : "public/js/doctype_list.js"}}
# doctype_tree_js = {{"doctype" : "public/js/doctype_tree.js"}}
# doctype_calendar_js = {{"doctype" : "public/js/doctype_calendar.js"}}

# Svg Icons
# ------------------
# include app icons in desk
# app_include_icons = "{app_name}/public/icons.svg"

# Home Pages
# ----------

# application home page (will override Website Settings)
# home_page = "login"

# website user home page (by Role)
# role_home_page = {{
# 	"Role": "home_page"
# }}

# Generators
# ----------

# automatically create page for each record of this doctype
# website_generators = ["Web Page"]

# Jinja
# ----------

# add methods and filters to jinja environment
# jinja = {{
# 	"methods": "{app_name}.utils.jinja_methods",
# 	"filters": "{app_name}.utils.jinja_filters"
# }}

# Installation
# ------------

# before_install = "{app_name}.install.before_install"
# after_install = "{app_name}.install.after_install"

# Uninstallation
# ------------

# before_uninstall = "{app_name}.uninstall.before_uninstall"
# after_uninstall = "{app_name}.uninstall.after_uninstall"

# Integration Setup
# ------------------
# To set up dependencies/integrations with other apps
# Name of the app being installed is passed as an argument

# before_app_install = "{app_name}.utils.before_app_install"
# after_app_install = "{app_name}.utils.after_app_install"

# Integration Cleanup
# -------------------
# To clean up dependencies/integrations with other apps
# Name of the app being uninstalled is passed as an argument

# before_app_uninstall = "{app_name}.utils.before_app_uninstall"
# after_app_uninstall = "{app_name}.utils.after_app_uninstall"

# Desk Notifications
# ------------------
# See xhiveframework.core.notifications.get_notification_config

# notification_config = "{app_name}.notifications.get_notification_config"

# Permissions
# -----------
# Permissions evaluated in scripted ways

# permission_query_conditions = {{
# 	"Event": "xhiveframework.desk.doctype.event.event.get_permission_query_conditions",
# }}
#
# has_permission = {{
# 	"Event": "xhiveframework.desk.doctype.event.event.has_permission",
# }}

# DocType Class
# ---------------
# Override standard doctype classes

# override_doctype_class = {{
# 	"ToDo": "custom_app.overrides.CustomToDo"
# }}

# Document Events
# ---------------
# Hook on document methods and events

# doc_events = {{
# 	"*": {{
# 		"on_update": "method",
# 		"on_cancel": "method",
# 		"on_trash": "method"
# 	}}
# }}

# Scheduled Tasks
# ---------------

# scheduler_events = {{
# 	"all": [
# 		"{app_name}.tasks.all"
# 	],
# 	"daily": [
# 		"{app_name}.tasks.daily"
# 	],
# 	"hourly": [
# 		"{app_name}.tasks.hourly"
# 	],
# 	"weekly": [
# 		"{app_name}.tasks.weekly"
# 	],
# 	"monthly": [
# 		"{app_name}.tasks.monthly"
# 	],
# }}

# Testing
# -------

# before_tests = "{app_name}.install.before_tests"

# Overriding Methods
# ------------------------------
#
# override_whitelisted_methods = {{
# 	"xhiveframework.desk.doctype.event.event.get_events": "{app_name}.event.get_events"
# }}
#
# each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard,
# along with any modifications made in other XhiveFramework apps
# override_doctype_dashboards = {{
# 	"Task": "{app_name}.task.get_dashboard_data"
# }}

# exempt linked doctypes from being automatically cancelled
#
# auto_cancel_exempted_doctypes = ["Auto Repeat"]

# Ignore links to specified DocTypes when deleting documents
# -----------------------------------------------------------

# ignore_links_on_delete = ["Communication", "ToDo"]

# Request Events
# ----------------
# before_request = ["{app_name}.utils.before_request"]
# after_request = ["{app_name}.utils.after_request"]

# Job Events
# ----------
# before_job = ["{app_name}.utils.before_job"]
# after_job = ["{app_name}.utils.after_job"]

# User Data Protection
# --------------------

# user_data_fields = [
# 	{{
# 		"doctype": "{{doctype_1}}",
# 		"filter_by": "{{filter_by}}",
# 		"redact_fields": ["{{field_1}}", "{{field_2}}"],
# 		"partial": 1,
# 	}},
# 	{{
# 		"doctype": "{{doctype_2}}",
# 		"filter_by": "{{filter_by}}",
# 		"partial": 1,
# 	}},
# 	{{
# 		"doctype": "{{doctype_3}}",
# 		"strict": False,
# 	}},
# 	{{
# 		"doctype": "{{doctype_4}}"
# 	}}
# ]

# Authentication and authorization
# --------------------------------

# auth_hooks = [
# 	"{app_name}.auth.validate"
# ]

# Automatically update python controller files with type annotations for this app.
# export_python_type_annotations = True

# default_log_clearing_doctypes = {{
# 	"Logging DocType Name": 30  # days to retain logs
# }}

"""

gitignore_template = """.DS_Store
*.pyc
*.egg-info
*.swp
tags
node_modules
__pycache__"""

github_workflow_template = """
name: CI

on:
  push:
    branches:
      - develop
  pull_request:

concurrency:
  group: develop-{app_name}-${{{{ github.event.number }}}}
  cancel-in-progress: true

jobs:
  tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
    name: Server

    services:
      redis-cache:
        image: redis:alpine
        ports:
          - 13000:6379
      redis-queue:
        image: redis:alpine
        ports:
          - 11000:6379
      mariadb:
        image: mariadb:10.6
        env:
          MYSQL_ROOT_PASSWORD: root
        ports:
          - 3306:3306
        options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3

    steps:
      - name: Clone
        uses: actions/checkout@v3

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 18
          check-latest: true

      - name: Cache pip
        uses: actions/cache@v2
        with:
          path: ~/.cache/pip
          key: ${{{{ runner.os }}}}-pip-${{{{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }}}}
          restore-keys: |
            ${{{{ runner.os }}}}-pip-
            ${{{{ runner.os }}}}-

      - name: Get yarn cache directory path
        id: yarn-cache-dir-path
        run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT'

      - uses: actions/cache@v3
        id: yarn-cache
        with:
          path: ${{{{ steps.yarn-cache-dir-path.outputs.dir }}}}
          key: ${{{{ runner.os }}}}-yarn-${{{{ hashFiles('**/yarn.lock') }}}}
          restore-keys: |
            ${{{{ runner.os }}}}-yarn-

      - name: Install MariaDB Client
        run: sudo apt-get install mariadb-client-10.6

      - name: Setup
        run: |
          pip install xhiveframework-bench
          bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/xhiveframework-bench
          mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
          mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"

      - name: Install
        working-directory: /home/runner/xhiveframework-bench
        run: |
          bench get-app {app_name} $GITHUB_WORKSPACE
          bench setup requirements --dev
          bench new-site --db-root-password root --admin-password admin test_site
          bench --site test_site install-app {app_name}
          bench build
        env:
          CI: 'Yes'

      - name: Run Tests
        working-directory: /home/runner/xhiveframework-bench
        run: |
          bench --site test_site set-config allow_tests true
          bench --site test_site run-tests --app {app_name}
        env:
          TYPE: server
"""

patches_template = """[pre_model_sync]
# Patches added in this section will be executed before doctypes are migrated
# Read docs to understand patches: https://xhiveframework.com/docs/v14/user/en/database-migrations

[post_model_sync]
# Patches added in this section will be executed after doctypes are migrated"""
