#! /usr/bin/env python
# -*- coding: utf-8 -*-

# opsi-newprod is part of the desktop management solution opsi
# (open pc server integration) http://www.opsi.org

# Copyright (C) 2010-2016 uib GmbH - http://www.uib.de/

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Creating opsi product source folders.

:copyright: uib GmbH <info@uib.de>
:author: Jan Schneider <j.schneider@uib.de>
:author: Niko Wenselowski <n.wenselowski@uib.de>
:license: GNU Affero General Public License version 3
"""

from __future__ import print_function

import codecs
import gettext
import os
import sys
import shutil
import time

from OPSI.Logger import Logger, LOG_ERROR

from OPSI.Object import (ProductDependency, LocalbootProduct, NetbootProduct,
	UnicodeProductProperty, BoolProductProperty)
from OPSI.System import copy
from OPSI.Types import forceEmailAddress, forceFilename, forceUnicode
from OPSI.Util.File.Opsi import PackageControlFile
from OPSI.Util.File import ChangelogFile
from OPSI.Util.Task.Rights import setRights
from OPSI.UI import UIFactory

try:
	import argparse
except ImportError:
	from OPSI.Util import argparse

__version__ = '4.0.7.6'

logger = Logger()

try:
	translation = gettext.translation('opsi-utils', '/usr/share/locale')
	_ = translation.ugettext
except Exception as error:
	logger.error(u"Locale not found: %s" % error)

	def _(string):
		return string


class CancelledByUserError(Exception):
	pass


def main():
	parser = argparse.ArgumentParser()
	parser.add_argument('--version', '-V', action='version', version=__version__)
	parser.add_argument("-t", "--template-dir", default=None,
						dest="templateDir", metavar="DIRECTORY",
						help=_(u"Copies the contents of DIRECTORY to the destination directory."))
	parser.add_argument('destination', default=os.getcwd(), nargs='?',
						help=_(u"The destination of the new product source. If no destination directory is supplied, the current directory is used."))

	options = parser.parse_args()

	templateDirectory = options.templateDir
	destDir = os.path.abspath(forceFilename(options.destination))

	if not os.path.exists(destDir):
		raise OSError("Directory '{0}' does not exist!".format(destDir))

	ui = UIFactory(type=u'snack')
	ui.drawRootText(1, 1, 'opsi')

	try:
		product = getProduct(ui)

		productSourceDir = os.path.join(destDir, product.id)
		if os.path.exists(productSourceDir):
			overwrite = ui.yesno(
				title=_(u'Overwrite?'),
				text=_(u'Directory %s already exists, overwrite?') % productSourceDir,
				okLabel=_(u'Yes'),
				cancelLabel=_(u'No')
			)

			if not overwrite:
				raise CancelledByUserError(_(u'Cancelled'))
			shutil.rmtree(productSourceDir)

		os.mkdir(productSourceDir, 02770)
		os.mkdir(os.path.join(productSourceDir, 'OPSI'), 02770)
		os.mkdir(os.path.join(productSourceDir, 'CLIENT_DATA'), 02770)
		os.mkdir(os.path.join(productSourceDir, 'SERVER_DATA'), 02770)

		try:
			productDependencies = collectProductDependencies(ui, product)
			productProperties = collectProductProperties(ui, product)
			writeMaintainerInfo(ui, productSourceDir, product, productDependencies, productProperties)

			createTemplates(productSourceDir, templateDirectory)
			setRights(productSourceDir)

			ui.showMessage(title=_('Done'), text=_("Package source directory '%s' created") % productSourceDir, width=70, height=3)
		except Exception as exc:
			errorMessage = _("Product creation failed. Removing directory {0}").format(productSourceDir)

			logger.info(errorMessage)
			ui.showError(title=_(u'Product creation failed'), text=errorMessage, width=50, height=5)

			shutil.rmtree(productSourceDir)

			raise exc
	finally:
		try:
			ui.exit()
		except Exception:
			pass


def getProduct(ui):
	helpText = ''
	values = [
		{"name": u"localboot", "selected": True},
		{"name": u"netboot"}
	]
	productType = ui.getSelection(
		radio=True,
		title=_(u'Please select product type'),
		text=helpText,
		entries=values,
		width=30,
		height=14
	)
	if not productType:
		raise CancelledByUserError(_(u'Cancelled'))
	productType = productType[0]

	product = None
	if productType == 'localboot':
		product = LocalbootProduct(id=u'newprod', productVersion=u'1.0', packageVersion=u'1')
	elif productType == 'netboot':
		product = NetbootProduct(id=u'newprod', productVersion=u'1.0', packageVersion=u'1')
	product.setDefaults()

	helpText = _(u'''Product id:       A unique identifier for the product.
Product name:     The full name of the product.
Description:      A description (use \\n for line breaks).
Advice:           An additional important advice.
Product version:  Version defined by software producer.
Package version:  Opsi package version of the product.
License required: Is a license required (0|1)?
Priority:         The installation priority class of this product (value between -100 and 100, 0 = neutral).''')
	while True:
		values = [
			{"name": _(u"Product id"), "value": product.id},
			{"name": _(u"Product name"), "value": product.name},
			{"name": _(u"Description"), "value": product.description},
			{"name": _(u"Advice"), "value": product.advice},
			{"name": _(u"Product version"), "value": product.productVersion},
			{"name": _(u"Package version"), "value": product.packageVersion},
			{"name": _(u"License required"), "value": product.licenseRequired},
			{"name": _(u"Priority"), "value": product.priority}
		]
		if productType == 'netboot':
			values.append({"name": _(u"PXE config template"), "value": product.pxeConfigTemplate})

		values = ui.getValues(
			title=_(u'product information'),
			text=helpText,
			entries=values
		)

		if not values:
			raise CancelledByUserError(_(u'Cancelled'))

		error = None
		try:
			product.setId(values[0].get('value'))
		except Exception:
			if not error:
				error = _(u'You have to specify a valid product id.')
		try:
			product.setName(values[1].get('value'))
			if not product.name:
				raise ValueError(u'No product name specified')
		except Exception:
			if not error:
				error = _(u'You have to specify a valid product name.')
		try:
			product.setDescription(values[2].get('value'))
		except Exception:
			if not error:
				error = _(u'Description is not valid.')
		try:
			product.setAdvice(values[3].get('value'))
		except Exception:
			if not error:
				error = _(u'Advice is not valid.')
		try:
			product.setProductVersion(values[4].get('value'))
			if not product.productVersion:
				raise ValueError(u'No product version specified')
		except Exception:
			if not error:
				error = _(u'You have to specify a valid product version.')
		try:
			product.setPackageVersion(values[5].get('value'))
			if not product.packageVersion:
				raise ValueError(u'No package version specified')
		except Exception:
			if not error:
				error = _(u'You have to specify a valid package version.')
		try:
			product.setLicenseRequired(values[6].get('value'))
		except Exception:
			if not error:
				error = _(u'License required must be a boolean value.')
		try:
			product.setPriority(values[7].get('value'))
		except Exception:
			if not error:
				error = _(u'Priority has to be an number between -100 and 100')

		if error:
			ui.showError(title=_(u'Bad value'), text=error, width=50, height=5)
			continue
		break

	helpText = _(u'''\
Setup script:        Relative path to script for action "setup".
Uninstall script:    Relative path to script for action "uninstall".
Update script:       Relative path to script for action "update".
Always script:       Relative path to script for action "always".
Once script:         Relative path to script for action "once".
Custom script:       Relative path to script for action "custom".
User login script:   Relative path to script for user login.
PXE config template: path to a custom pxelinux config template.''')
	while True:
		values = [
			{"name": _(u"Setup script"), "value": product.setupScript},
			{"name": _(u"Uninstall script"), "value": product.uninstallScript},
			{"name": _(u"Update script"), "value": product.updateScript},
			{"name": _(u"Always script"), "value": product.alwaysScript},
			{"name": _(u"Once script"), "value": product.onceScript},
			{"name": _(u"Custom script"), "value": product.customScript}
		]

		if productType == 'netboot':
			values.append({"name": _(u"PXE config template"), "value": product.pxeConfigTemplate})
		else:
			values.append({"name": _(u"User login script"), "value": product.userLoginScript})

		values = ui.getValues(
			title=_(u'product scripts'),
			text=helpText,
			entries=values
		)

		if not values:
			raise CancelledByUserError(_(u'Cancelled'))

		error = None
		try:
			product.setSetupScript(values[0].get('value'))
		except Exception:
			if not error:
				error = _(u'Setup script is not valid.')
		try:
			product.setUninstallScript(values[1].get('value'))
		except Exception:
			if not error:
				error = _(u'Uninstall script is not valid.')
		try:
			product.setUpdateScript(values[2].get('value'))
		except Exception:
			if not error:
				error = _(u'Update script is not valid.')
		try:
			product.setAlwaysScript(values[3].get('value'))
		except Exception:
			if not error:
				error = _(u'Always script is not valid.')
		try:
			product.setOnceScript(values[4].get('value'))
		except Exception:
			if not error:
				error = _(u'Once script is not valid.')
		try:
			product.setCustomScript(values[5].get('value'))
		except Exception:
			if not error:
				error = _(u'Custom script is not valid.')
		if productType == 'netboot':
			try:
				product.setPxeConfigTemplate(values[6].get('value'))
			except Exception:
				if not error:
					error = _(u'PXE config template is not valid.')
		else:
			try:
				product.setUserLoginScript(values[6].get('value'))
			except Exception:
				if not error:
					error = _(u'User login script is not valid.')

		if error:
			ui.showError(title=_(u'Bad value'), text=error, width=50, height=5)
			continue

		break

	return product


def collectProductDependencies(ui, product):
	productDependencies = []
	helpText = _(u'''You have to specify a required product id.
You have to specify either a required installation status or a required action.
The requirement type can be used to specify the position of a requirement. This is optional.
Possible actions are: %s
Possible installation status are: %s
Possible requirement types are: %s''') % (
		u', '.join([u'setup']),
		u', '.join([u'installed']),
		u', '.join([u'before', u'after'])
	)

	while True:
		if not ui.yesno(
				title=_(u'Create product dependency?'),
				text=_(u'Do you want to create a product dependency?'),
				okLabel=_(u'Yes'),
				cancelLabel=_(u'No')):
			break

		productDependency = ProductDependency(
			productId=product.id,
			productVersion=product.productVersion,
			packageVersion=product.packageVersion,
			productAction=u'setup',
			requiredProductId=u'product'
		)
		productDependency.setDefaults()
		productDependency.productAction = u''
		productDependency.requiredProductId = u''
		while True:
			values = [
				{"name": _(u"Dependency for action"), "value": productDependency.productAction},
				{"name": _(u"Required product id"), "value": productDependency.requiredProductId},
				{"name": _(u"Required action"), "value": productDependency.requiredAction or u''},
				{"name": _(u"Required installation status"), "value": productDependency.requiredInstallationStatus or u''},
				{"name": _(u"Requirement type"), "value": productDependency.requirementType or u''}
			]

			values = ui.getValues(
				title=_(u'Create dependency for product %s') % product.id,
				text=helpText,
				entries=values
			)

			if not values:
				break

			error = None
			try:
				productDependency.setProductAction(values[0].get('value'))
			except Exception:
				if not error:
					error = _(u'You have to specify a valid product action.')

			try:
				productDependency.setRequiredProductId(values[1].get('value'))
			except Exception:
				if not error:
					error = _(u'You have to specify a valid required product id.')

			if values[2].get('value'):
				try:
					productDependency.setRequiredAction(values[2].get('value'))
				except Exception:
					if not error:
						error = _(u'Required action is not valid.')
			elif values[3].get('value'):
				try:
					productDependency.setRequiredInstallationStatus(values[3].get('value'))
				except Exception:
					if not error:
						error = _(u'Required installation status is not valid.')
			else:
				if not error:
					error = _(u'Please specify either a required installation status or a required action.')

			try:
				productDependency.setRequirementType(values[4].get('value'))
			except Exception:
				if not error:
					error = _(u'Requirement type is not valid.')

			if error:
				ui.showError(title=_(u'Bad value'), text=error, width=50, height=5)
				continue

			productDependencies.append(productDependency)
			break

	return productDependencies


def collectProductProperties(ui, product):
	productProperties = []
	helpText = _(u'''Property name: Name of the property.
Property description: Usage description.
Possible values: Comma separated list of possible values for the property. If no possible values are given any values are allowed.
Editable: Is it allowed to specify a value which is not in the list of possible values?''')
	while True:
		if not ui.yesno(title=_(u'Create product property?'),
				text=_(u'Do you want to create a product property?'),
				okLabel=_(u'Yes'),
				cancelLabel=_(u'No')):
			break

		# Get property type
		values = [
			{"name": u"unicode", "selected": True},
			{"name": u"boolean"}
		]
		propertyType = ui.getSelection(
			radio=True,
			title=_(u'Please select property type'),
			entries=values
		)
		if not propertyType:
			continue
		propertyType = propertyType[0]

		productProperty = None
		if propertyType == 'unicode':
			productProperty = UnicodeProductProperty(productId=product.id, productVersion=product.productVersion, packageVersion=product.packageVersion, propertyId=u'property')
		elif propertyType == 'boolean':
			productProperty = BoolProductProperty(productId=product.id, productVersion=product.productVersion, packageVersion=product.packageVersion, propertyId=u'property')
		productProperty.setDefaults()
		productProperty.propertyId = u''

		while True:
			values = [
				{"name": _(u"Property name (identifier)"), "value": productProperty.propertyId},
				{"name": _(u"Property description"), "value": productProperty.description}
			]
			if propertyType == 'unicode':
				values.append({"name": _(u"Possible values"), "value": u""})
				values.append({"name": _(u"Editable"), "value": productProperty.editable})

			values = ui.getValues(
				title=_(u'Create property for product %s') % productProperty.productId,
				text=helpText,
				entries=values
			)

			if not values:
				break

			error = None
			try:
				productProperty.setPropertyId(values[0].get('value'))
			except Exception:
				if not error:
					error = _(u'Please specify a valid identifier')

			try:
				productProperty.setDescription(values[1].get('value'))
			except Exception:
				if not error:
					error = _(u'Please specify a valid description')

			if propertyType == 'unicode':
				productProperty.setEditable(values[3].get('value'))
				possibleValues = []
				for v in values[2].get('value').split(u','):
					v = v.strip()
					if v != u'':
						possibleValues.append(v)
				if possibleValues:
					try:
						productProperty.setPossibleValues(possibleValues)
					except Exception:
						if not error:
							error = _(u'Please specify valid possible values')
				else:
					productProperty.possibleValues = []
			if error:
				ui.showError(title=_(u'Bad value'), text=error, width=50, height=5)
				continue

			try:
				defaultValues = []
				if productProperty.possibleValues:
					if len(productProperty.possibleValues) == 1:
						defaultValues = productProperty.possibleValues
					else:
						choices = []
						for v in productProperty.possibleValues:
							choices.append({"name": v})
						choices[0]['selected'] = True
						result = ui.getSelection(
							title=_(u'Please select a default value'),
							text=u"",
							radio=True,
							entries=choices
						)

						if result is not None:
							defaultValues = result
				else:
					result = ui.getValue(
						title=_(u'Please set a default value'),
						text=u""
					)
					if result is not None:
						defaultValues = [result]

				productProperty.setDefaultValues(defaultValues)
			except Exception as e:
				if not error:
					error = _(u'Please specify valid default values: %s') % e

			if error:
				ui.showError(title=_(u'Bad value'), text=error, width=50, height=5)
				continue

			productProperties.append(productProperty)
			break

	return productProperties


def writeMaintainerInfo(ui, productDirectory, product, productDependencies, productProperties):
	maintainer = u''
	maintainerEmail = u''
	helpText = _(u'Maintainer of this opsi package.')
	while True:
		values = [
			{"name": _(u"Maintainer name"), "value": maintainer},
			{"name": _(u"Maintainer e-mail"), "value": maintainerEmail}
		]

		values = ui.getValues(
			title=_(u'Maintainer info'),
			width=70,
			height=10,
			text=helpText,
			entries=values
		)

		if not values:
			raise CancelledByUserError(_('Cancelled'))

		error = None
		try:
			if not values[0].get('value'):
				raise ValueError(u'Empty maintainer')
			maintainer = forceUnicode(values[0].get('value'))
		except Exception:
			if not error:
				error = _(u'Please enter a valid maintainer name.')

		try:
			if not values[1].get('value'):
				raise ValueError(u'Empty maintainer e-mail')
			maintainerEmail = forceEmailAddress(values[1].get('value'))
		except Exception:
			if not error:
				error = _(u'Please enter a valid e-mail address.')

		if error:
			ui.showError(title=_(u'Bad value'), text=error, width=50, height=5)
			continue

		break

	pcf = PackageControlFile(os.path.join(productDirectory, 'OPSI', 'control'))
	pcf.setProduct(product)
	pcf.setProductDependencies(productDependencies)
	pcf.setProductProperties(productProperties)

	tmpChangelog = os.path.join(productDirectory, 'OPSI', 'changelog.txt')
	cf = ChangelogFile(tmpChangelog)
	cf.setEntries([{
		'package': product.id,
		'version': u'%s-%s' % (product.productVersion, product.packageVersion),
		'release': u'testing',
		'urgency': u'low',
		'changelog': [u'  * Initial package'],
		'maintainerName': maintainer,
		'maintainerEmail': maintainerEmail,
		'date': time.time()
	}])
	cf.generate()
	product.setChangelog(u''.join(cf.getLines()))
	os.unlink(tmpChangelog)

	pcf.setProduct(product)
	pcf.generate()
	pcf.chmod(0600)


def createTemplates(productDirectory, templateDirectory=None):
	"""
	Creates templates at the given ``productDirectory``.

	:param templateDirectory: The content of the directory will be \
copied to ``productDirectory``
	"""
	# Create preinst template
	preinstFilePath = os.path.join(productDirectory, 'OPSI', 'preinst')
	with codecs.open(preinstFilePath, 'w', 'utf-8') as preinst:
		preinst.write(u'#! /bin/bash\n')
		preinst.write(u'#\n')
		preinst.write(u'# preinst script\n')
		preinst.write(u'# This script executes before that package will be unpacked from its archive file.\n')
		preinst.write(u'#\n')
		preinst.write(u'# The following environment variables can be used to obtain information about the current installation:\n')
		preinst.write(u'#   PRODUCT_ID: id of the current product\n')
		preinst.write(u'#   CLIENT_DATA_DIR: directory where client data will be installed\n')
		preinst.write(u'#\n')

	# Create postinst template
	postinstFilePath = os.path.join(productDirectory, 'OPSI', 'postinst')
	with codecs.open(postinstFilePath, 'w', 'utf-8') as postinst:
		postinst.write(u'#! /bin/bash\n')
		postinst.write(u'#\n')
		postinst.write(u'# postinst script\n')
		postinst.write(u'# This script executes after unpacking files from that archive and registering the product at the depot.\n')
		postinst.write(u'#\n')
		postinst.write(u'# The following environment variables can be used to obtain information about the current installation:\n')
		postinst.write(u'#   PRODUCT_ID: id of the current product\n')
		postinst.write(u'#   CLIENT_DATA_DIR: directory which contains the installed client data\n')
		postinst.write(u'#\n')

	if templateDirectory:
		copy(os.path.join(templateDirectory, '*'), productDirectory)


if __name__ == "__main__":
	try:
		main()
	except Exception as exception:
		logger.setConsoleLevel(LOG_ERROR)
		logger.logException(exception)
		print("ERROR: {0}".format(forceUnicode(exception).encode('utf-8')), file=sys.stderr)
		sys.exit(1)
