########################################################################
# Copyright (C) 2019-2020,2022 VMware, Inc.                            #
# All Rights Reserved                                                  #
########################################################################


'''
This module defines the data structure of addon and implements the
functionality such as construction, serialization to json format,
and deserialization from json.
'''

import json
import re
from .ComponentScanner import ComponentScanProblem
from .Errors import AddonValidationError
from .ReleaseUnit import (ATTR_REL_ID, checkNameSpec, deepcopy, ESX_COMP_NAME,
                          ReleaseUnit)

try:
   from .Utils.JsonSchema import ValidateAddon
   HAVE_VALIDATE_ADDON = True
except Exception:
   HAVE_VALIDATE_ADDON = False


ERROR_REMOVE_ESX = 'The component ESXi cannot be removed from base image'

# Attribute name constant
ATTR_NAME_SPEC = 'nameSpec'
ATTR_REM_COMPS = 'removedComponents'
ATTR_SUPP_BIVERS = 'supportedBaseImageVersions'

# Validation constants
SUP_BIVER_REG_EXP = \
r'^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+){0,2}(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)*$'


def GenerateReleaseID(name, version):
   return name + ':' + version



class Addon(ReleaseUnit):
   ''' An add-on is a release unit that:
       1. Only has one name spec, one version spec.
       2. Directly composed from components
       3. Has no component 'esx'
   '''

   extraAttributes = [ATTR_NAME_SPEC, ATTR_REM_COMPS,
                      ATTR_SUPP_BIVERS]
   extraDefault = [None, [], []]
   extraMap = dict(zip(extraAttributes, extraDefault))

   mandatoryAttr = list(ReleaseUnit.mandatoryAttr)
   mandatoryAttr.append(ATTR_NAME_SPEC)
   mandatoryAttr.append(ATTR_SUPP_BIVERS)

   # The schema version.
   SCHEMA_VERSION = "1.0"

   # Valid schema veriosn map to release version. Need to populate when bump
   # schema version.
   SCHEMA_VERSION_MAP = {"1.0": '7.0.0'}

   # The common software spec type for all instances of this class.
   releaseType = 'addon'

   def _validateSupportedBIVersions(self, baseImageVers):
      '''The function validates the addon's supported baseimage versions.
         Each version string in the list must match the regular expression
         SUP_BIVER_REG_EXP.
         TODO: The list of baseImageVer should be non-overlapping. In future,
               add the required check.

         Parameter:
            * baseImageVers: List of base image versions.

         Exception:
            * Return False If the list of BI versions is empty, overlapping,
              or each version string violated version pattern
              SUP_BIVER_REG_EXP else True
      '''
      if not isinstance(baseImageVers, list) or \
         not baseImageVers:
         return False

      for ver in baseImageVers:
         if not re.match(SUP_BIVER_REG_EXP, ver):
            return False

      return True

   @checkNameSpec
   def SetNameSpec(self, name):
      self._nameSpec = name
      self._GenerateReleaseID()

   def AddRemovedComponent(self, name):
      if name == ESX_COMP_NAME:
         raise ValueError(ERROR_REMOVE_ESX)
      if name not in self._removedComponents:
         self._removedComponents.append(name)

   def RemoveRemovedComponent(self, name):
      try:
         self._removedComponents.remove(name)
      except ValueError:
         raise ValueError('%s is not in removed component list.' % name)

   def SetRemovedComponents(self, compNameList):
      if compNameList and ESX_COMP_NAME in compNameList:
         raise ValueError(ERROR_REMOVE_ESX)
      self._removedComponents = compNameList

   def SetSupportedBaseImageVersions(self, supBaseImageVers):
      if not self._validateSupportedBIVersions(supBaseImageVers):
         raise ValueError('The supported base image versions is a non-empty '
                          'list. The versions in the list must be of form '
                          '[x(.x){0,2}(-x(.x)*)*], where x is alphanumeric.')
      self._supportedBaseImageVersions = supBaseImageVers


   nameSpec = property(lambda self: self._nameSpec, SetNameSpec)
   removedComponents = property(lambda self: self._removedComponents,
                                SetRemovedComponents)
   supportedBaseImageVersions = property(lambda self:
                                         self._supportedBaseImageVersions,
                                         SetSupportedBaseImageVersions)


   @classmethod
   def FromJSON(cls, jsonString, validation=False):
      # Schema Validation
      if validation and HAVE_VALIDATE_ADDON:
         valid, errMsg = ValidateAddon(jsonString)
         if not valid:
            try:
               addon = json.loads(jsonString)
            except Exception:
               # failed to load addon from jsonstring
               # hence return empty releaseID
               raise AddonValidationError('', errMsg)

            releaseId = addon[ATTR_REL_ID] if ATTR_REL_ID in addon else ''
            raise AddonValidationError(releaseId, errMsg)

      addOn = Addon(spec=jsonString)
      if validation:
         addOn.Validate()
      return addOn

   def IsBaseImageSupported(self, baseImage):
      """Returns whether a base image is supported by this addon.
      """
      if not baseImage:
         return False

      biVer = baseImage.versionSpec.version.versionstring
      for pattern in self._supportedBaseImageVersions:
         if re.match(pattern, biVer):
            return True
      return False

   def Validate(self, components=None, addonVibs=None):
      """Validates the addon. Addon should have at least one component and
         there should be no conflict/obsolete problems within the components.

         Parameters:
            * components - ComponentCollection object having all addon
                           components
            * addonVibs  - VibCollection object with VIBs that correspond to
                           all components in addon.
      """

      if not self.components and not self.removedComponents:
         raise AddonValidationError(self.releaseID, 'AddOn should have at '
                                    'least one component or at least remove'
                                    ' one component.')

      if components and addonVibs:
         compProblems = self._getCompProblemMsgs(components, addonVibs)
         if compProblems:
            raise AddonValidationError(self.releaseID,
               'Failed to validate components in addon %s: %s'
               % (self.nameSpec.name, ', '.join(compProblems)))

   def _getCompProblemMsgs(self, components, vibs):
      """Validate component relations and return messages of problems.
      """
      problems = components.Validate(vibs)
      # Collect all problems. Addon cannot contain components that
      # conflict/obsolete themselves or each other.
      return [p.msg for p in problems.values()
              if p.reltype != ComponentScanProblem.TYPE_DEPENDS]

   def ToJSONDict(self):
      releaseObj = super(Addon, self).ToJSONDict()

      # encode NameSpec to dict
      releaseObj[ATTR_NAME_SPEC] = deepcopy(self.nameSpec.ToJSONDict())
      releaseObj[ATTR_REM_COMPS] = deepcopy(self.removedComponents)

      return releaseObj

   def ToJSON(self):
      self.Validate()
      jsonString = super(Addon, self).ToJSON()

      # Schema Validation
      if HAVE_VALIDATE_ADDON:
         valid, errMsg = ValidateAddon(jsonString)
         if not valid:
            raise AddonValidationError(self.releaseID, errMsg)

      return jsonString

   def Copy(self):
      addOn = Addon()
      addonDict = deepcopy(self.ToJSONDict())
      addOn.FromJSONDict(addonDict)
      return addOn

   def _GenerateReleaseID(self):
      name = self._nameSpec.name if self._nameSpec else ''
      version = self._versionSpec.version.versionstring \
                if self._versionSpec else ''
      self._releaseID = GenerateReleaseID(name, version)
