#! /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-2015 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.6.3'

logger = Logger()
ui = None

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

	def _(string):
		return string


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))

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

	# Get product type
	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 Exception(_(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 Exception(_(u'Cancelled'))

		error = None
		try:
			product.setId(values[0].get('value'))
		except Exception as e:
			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 Exception(u'No product name specified')
		except Exception as e:
			if not error:
				error = _(u'You have to specify a valid product name.')
		try:
			product.setDescription(values[2].get('value'))
		except Exception as e:
			if not error:
				error = _(u'Description is not valid.')
		try:
			product.setAdvice(values[3].get('value'))
		except Exception as e:
			if not error:
				error = _(u'Advice is not valid.')
		try:
			product.setProductVersion(values[4].get('value'))
			if not product.productVersion:
				raise Exception(u'No product version specified')
		except Exception as e:
			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 Exception(u'No package version specified')
		except Exception as e:
			if not error:
				error = _(u'You have to specify a valid package version.')
		try:
			product.setLicenseRequired(values[6].get('value'))
		except Exception as e:
			if not error:
				error = _(u'License required must be a boolean value.')
		try:
			product.setPriority(values[7].get('value'))
		except Exception as e:
			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 Exception(_(u'Cancelled'))

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

	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 Exception(_(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)

	# Product dependencies
	productDependencies = []
	helpText = _(\
u'''
You have to specify either a required product or a required product class.
You have to specify either a required installation status or a required action.
The requirement type is optional.
Possible actions are:
   %s
Possible installation status are:
   %s
Possible requirement types are:
   %s
''') % (
		u', '.join([u'setup', u'uninstall', u'update', u'once', u'always', u'custom']),
		u', '.join([u'installed', 'not_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 as e:
				if not error:
					error = _(u'You have to specify a valid product action.')

			try:
				productDependency.setRequiredProductId(values[1].get('value'))
			except Exception as e:
				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 as e:
					if not error:
						error = _(u'Required action is not valid.')
			elif values[3].get('value'):
				try:
					productDependency.setRequiredInstallationStatus(values[3].get('value'))
				except Exception as e:
					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 as e:
				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

	# Product properties
	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 as e:
				if not error:
					error = _(u'Please specify a valid identifier')

			try:
				productProperty.setDescription(values[1].get('value'))
			except Exception as e:
				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 as e:
						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

	# Maintainer info
	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 Exception(_('Cancelled'))

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

		try:
			if not values[1].get('value'):
				raise Exception(u'Empty maintainer e-mail')
			maintainerEmail = forceEmailAddress(values[1].get('value'))
		except Exception as e:
			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(productSourceDir, 'OPSI', 'control'))
	pcf.setProduct(product)
	pcf.setProductDependencies(productDependencies)
	pcf.setProductProperties(productProperties)

	tmpChangelog = os.path.join(productSourceDir, '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)

	createTemplates(productSourceDir, templateDirectory)
	setRights(productSourceDir)

	ui.showMessage(title=_('Done'), text=_("Package source directory '%s' created") % productSourceDir, width=70, height=3)


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__":
	def exitUI():
		try:
			ui.exit()
		except Exception:
			pass

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