########################################################################
# Copyright (c) 2019-2024 Broadcom. All Rights Reserved.
# Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
########################################################################
#

'''
Define the data structure for base image.

Implement the functionalities such as construction, serializating to json,
and deserializing from json.
'''
import json
from .Bulletin import ESX_COMP_NAME
from .Errors import BaseimageValidationError
from .ReleaseUnit import checkVersionSpec, ReleaseUnit, VersionSpec

try:
   from .Utils.JsonSchema import ValidateBaseImage
   HAVE_VALIDATE_BI = True
except Exception:
   HAVE_VALIDATE_BI = False

def GenerateReleaseID(version):
   return ESX_COMP_NAME + ':' + version

def _addQuickPatchCompatibleVersion(bi, versionSpec):
   """Helper to add a Quick Patch compatible version, used with
      checkVersionSpec.
   """
   bi._quickPatchCompatibleVersions.append(versionSpec)

def _versionSpecListToDictList(versionSpecs):
   """Converts a list of version spec to a list of dictionaries,
      where each dictionary is with key-value pair of
      version-display version.
   """
   return [verSpec.ToJSONDict() for verSpec in versionSpecs]

def versionSpecListToDictOrStr(versionSpecs, toStr=False):
   """Converts a list of version spec to a dictionary or a JSON string
      depending on the parameter toStr - False by default to return a
      dictionary, True to return a JSON string.
   """
   # convert a list of version spec to dict
   versionSpecDict = {verSpec.version.versionstring: verSpec.uiString
                      for verSpec in versionSpecs}

   # convert the dict to a JSON string if toStr is True
   return json.dumps(versionSpecDict) if toStr else versionSpecDict

class BaseImage(ReleaseUnit):
   ''' A base image is a release unit that always contains the
       component "ESXi".
   '''

   # The common type for Base Image objects
   releaseType = 'baseImage'

   # The schema version.
   SCHEMA_VERSION = "1.1"

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

   ATTR_QP_COMPAT_VERS = 'quickPatchCompatibleVersions'

   # Attribute name to minimum schema version.
   # All new attributes must be added to maintain backward compatibility.
   attrToSchemaVerMap = {
      ATTR_QP_COMPAT_VERS: '1.1',
   }

   toJsonTypeConverters = {
      ATTR_QP_COMPAT_VERS: _versionSpecListToDictList,
   }

   extraAttributes = [ATTR_QP_COMPAT_VERS]
   allAttribs = list(ReleaseUnit.attributes) + extraAttributes
   extraDefault = [[]]
   extraMap = dict(zip(extraAttributes, extraDefault))

   @property
   def quickPatchCompatibleVersions(self):
      """Getter of quickPatchCompatibleVersions.
      """
      return self._quickPatchCompatibleVersions

   @quickPatchCompatibleVersions.setter
   def quickPatchCompatibleVersions(self, value):
      """Setter of quickPatchCompatibleVersions.
      """
      if not isinstance(value, list):
         raise ValueError('quickPatchCompatibleVersions must be a list')
      self._quickPatchCompatibleVersions.clear()
      versionSpecAdder = checkVersionSpec(_addQuickPatchCompatibleVersion)
      for item in value:
         versionSpecAdder(self, item)

   @property
   def isQuickPatch(self):
      """Boolean, True if this Base Image can be applied with Quick Patch.
      """
      return bool(self._quickPatchCompatibleVersions)

   def _getQuickPatchCompatibleVersion(self, version):
      """Gets the Quick Patch compatible version spec with the given version
         string. Returns None if not found.
      """
      for versionSpec in self._quickPatchCompatibleVersions:
         if str(versionSpec.version) == version:
            return versionSpec
      return None

   def AddQuickPatchCompatibleVersion(self, version, uiString):
      """Adds the version-uiString pair of a Base Image that can update to this
         Base Image using Quick Patch.
      """
      curVer = self._getQuickPatchCompatibleVersion(version)
      if curVer:
         if curVer.uiString != uiString:
            # uiString stored cannot be different.
            raise ValueError('Version %s with a different uiString %s '
               'exists in quickPatchCompatibleVersions' % (curVer.version,
               curVer.uiString))
      else:
         self._quickPatchCompatibleVersions.append(
            VersionSpec(version, uiString))

   def RemoveQuickPatchCompatibleVersion(self, version):
      """Removes a Base Image version from quickPatchCompatibleVersions.
      """
      curVer = self._getQuickPatchCompatibleVersion(version)
      if curVer:
         self._quickPatchCompatibleVersions.remove(curVer)
      else:
         raise ValueError('Version %s does not exist in '
                          'quickPatchCompatibleVersions' % version)

   def canQuickPatchFrom(self, other):
      """Returns whether this Base Image can be applied with Quick Patch from
         the other Base Image.
      """
      if not self.isQuickPatch:
         return False
      return bool(self._getQuickPatchCompatibleVersion(
         other.versionSpec.version.versionstring))

   @classmethod
   def FromJSON(cls, jsonString, validation=False, schemaVersionCheck=False):
      """Creates a BaseImage object from a JSON file.

         Parameters:
            * jsonString - The JSON string to create a BaseImge object from.
            * validation - If True the function will perform schema validation.
            * schemaVersionCheck - Flag to check schema version compatibility.
      """
      if validation and HAVE_VALIDATE_BI:
         valid, errMsg = ValidateBaseImage(jsonString)
         if not valid:
            raise BaseimageValidationError(errMsg)

      image = BaseImage(spec=jsonString)
      if validation or schemaVersionCheck:
         image.Validate(jsonSchemaCheck=validation,
                        schemaVersionCheck=schemaVersionCheck)
      return image

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

      # Schema validation
      if HAVE_VALIDATE_BI:
         valid, errMsg = ValidateBaseImage(self.ToJSONDict())
         if not valid:
            raise BaseimageValidationError(errMsg)

      return jsonString

   def Validate(self, components=None, biVibs=None, jsonSchemaCheck=True,
                schemaVersionCheck=False):
      """Validates base image schema, metadata, and components.

         Parameters:
            * components - ComponentCollection object having all base image
                           components
            * biVibs     - VibCollection object with VIBs that correspond to
                           all components in base image.
            * jsonSchemaCheck - Flag to validate the JSON schema.
            * schemaVersionCheck - Flag to check schema version compatibility.
      """
      if not schemaVersionCheck and not jsonSchemaCheck:
         raise BaseimageValidationError(
            'At least one of schemaVersionCheck or jsonSchemaCheck should be '
            'true.')

      if schemaVersionCheck:
         self._VerifySchemaVersion()

      if jsonSchemaCheck:
         if ESX_COMP_NAME not in self._components:
            errMsg = 'Base Image must contain the ESXi component'
            raise BaseimageValidationError(errMsg)

         # Validate baseimage components
         if components and biVibs:
            problems = components.Validate(biVibs)
            if problems:
               errMsg = 'Failed to validate components in base image %s: %s' % \
                        (self.versionSpec.version.versionstring,
                        ','.join(p.msg for p in problems.values()))
               raise BaseimageValidationError(errMsg)

   def Copy(self):
      image = BaseImage()
      imageDict = self.ToJSONDict()
      image.FromJSONDict(imageDict)
      return image

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