########################################################################
# Copyright (C) 2019,2023 VMware, Inc.                                 #
# All Rights Reserved                                                  #
########################################################################

"""This module provides classes and functions for release unit json spec
   validation uisng json schema.

   Currently, this module is vCenter only since json schema package is
   only installed on vCenter.
"""

import logging
import json
import os
import re
import sys

from .. import Errors

try:
   import jsonschema
   HAVE_JSON_SCHEMA = True
except Exception:
   HAVE_JSON_SCHEMA = False

USRLIB = os.path.join(os.path.sep, 'usr', 'lib')
SCHEMA_ROOT = os.path.join(USRLIB, 'vmware-updatemgr', 'etc',
                           'json_schemas')

log = logging.getLogger(__file__)

# Constants for verification
SUPP_REL_TYPE = ['baseimage', 'addon', 'manifest']
REL_TYPE_BI = 'baseimage'
REL_TYPE_ADDON = 'addon'
REL_TYPE_MANIFEST = 'manifest'
SCHEMA_VER_REGEX = r'^\d+\.\d$'

class ReleaseUnitSchemaError(Exception):
   '''Release unit json schema validation issue.'''
   pass

def GetValidateVersion(schemaVersion):
   '''When schema version is not provided, use the lowerest existing
      schema version; when no matching schema version, use the highest
      schema version of existing lower versions.
   '''
   subDirs = os.listdir(SCHEMA_ROOT)
   versionPattern = re.compile("^([0-9]+)\\.([0-9]+)$")
   subDirs = [d for d in subDirs if versionPattern.match(d) and
              os.path.isdir(os.path.join(SCHEMA_ROOT, d))]
   highestLower = None
   if subDirs:
      subDirs.sort()
      if schemaVersion:
         for d in subDirs:
            if d > schemaVersion:
               break
            highestLower = d
      else:
         highestLower = subDirs[0]
   if not highestLower:
      raise ReleaseUnitSchemaError('No feasible schema files are found')
   return highestLower

class ReleaseUnitValidator(object):
   '''Class for validating release unit json doc using json schema.
      An instance is created from a common schema file and a release
      unit schema file.
   '''

   # Class variable to cache validators for reuse
   schemaValidator = {}

   @staticmethod
   def _GetSchema(schemaPath):
      """Load json schema file into a dict.
      """
      try:
         with open(schemaPath) as fd:
            return json.load(fd)
      except IOError as e:
         msg = 'Failed to read json schema file %s: %s' % (schemaPath, e)
         log.error(msg)
         raise Errors.FileIOError(filename=schemaPath, msg=msg)
      except (ValueError, json.JSONDecodeError) as e:
         msg = 'Error when read json schema file: %s' % e
         log.error(msg)
         raise ReleaseUnitSchemaError(msg)

   @staticmethod
   def _LoadReleaseUnit(ut):
      try:
         if isinstance(ut, str):
            if os.path.isfile(ut):
               with open(ut) as fp:
                  ut = json.load(fp)
            else:
               ut = json.loads(ut)
      except (IOError, json.JSONDecodeError, ValueError) as e:
         msg = 'Invalid spec for json schema validation: %s' % e
         log.error(msg)
         raise ReleaseUnitSchemaError(msg)

      if not isinstance(ut, dict):
         msg = 'The input should be json file, json string or dict'
         raise ReleaseUnitSchemaError(msg)

      return ut

   @classmethod
   def SchemaValidate(cls, ut, releaseUnitType=None):
      try:
         ut = ReleaseUnitValidator._LoadReleaseUnit(ut)
         releaseType = ut.get('releaseType')
         schemaVersion = ut.get('schemaVersion')
         if releaseType == None or \
            releaseType.lower() not in SUPP_REL_TYPE:
            msg = 'required "releaseType" is not found or invalid'
            log.error('Json schema validation failure: ' + msg)
            return False, msg
         if releaseUnitType and \
            releaseType.lower() != releaseUnitType.lower():
            msg = ('"releaseType" is expected to be %s, but got %s.' %
                   (releaseUnitType, releaseType))
            log.error('Json schema validation failure: ' + msg)
            return False, msg
         validateVersion = GetValidateVersion(schemaVersion)

         if schemaVersion == None:
            ut['schemaVersion'] = validateVersion

         validator = cls.schemaValidator.get((releaseType, validateVersion))
         if validator == None:
            schemaFile = os.path.join(SCHEMA_ROOT, validateVersion,
                                      releaseType.lower() + '.json')
            commonFile = os.path.join(SCHEMA_ROOT, validateVersion,
                                      'common.json')
            validator = ReleaseUnitValidator(schemaFile, commonFile)
            cls.schemaValidator[(releaseType, validateVersion)] = validator
         return validator.Validate(ut)
      except Exception as e:
         return False, str(e)

   @staticmethod
   def _SchemaUriHandler(uri):
      """Handler to fetch schema from schema URI.
      """
      # Schema ID format is:
      # https:///www.vmware.com/esxupdate/<version>/<name>.json
      # An exception is 1.0 shcema that is not versioned.
      fileName = os.path.basename(uri)
      schemaVer = os.path.basename(os.path.dirname(uri))
      if schemaVer == 'esxupdate':
         schemaVer = '1.0'
      if not re.match(SCHEMA_VER_REGEX, schemaVer):
         raise ValueError("Invalid schema version: '%s'" % schemaVer)
      schemaPath = os.path.join(SCHEMA_ROOT, schemaVer, fileName)
      return ReleaseUnitValidator._GetSchema(schemaPath)

   @staticmethod
   def _GetCommonResolver(commonPath):
      """Load the common json schema and create the RefResolver object.
      """
      common = ReleaseUnitValidator._GetSchema(commonPath)
      return jsonschema.RefResolver.from_schema(common,
         handlers={
            'http': ReleaseUnitValidator._SchemaUriHandler,
            'https': ReleaseUnitValidator._SchemaUriHandler,
         })

   def __init__(self, schemaPath, commonPath):
      """Construct ReleaseUnitValidator.
      """
      self.commonResolver = self.__class__._GetCommonResolver(commonPath)
      self.schema = self.__class__._GetSchema(schemaPath)

   def Validate(self, ut):
      """Validate a release unit dict or json file/string.
         Retuen True when no error; otherwise, return False.
      """
      try:
         jsonschema.validate(ut, self.schema, resolver=self.commonResolver)
         return True, None
      except Exception as e:
         log.error('Json schema validation failure: %s', e)
         return False, str(e)

def ValidateBaseImage(baseImage):
   """Base image json spec validation.
      Parameters:
         baseImage: base image json file/string/dict.

      Returns: True on success; otherwise False.

      Exception: ReleaseUnitSchemaError when cannot load spec
   """
   if not HAVE_JSON_SCHEMA:
      log.warn('Skipping baseimage schema validation')
      return True, None

   return ReleaseUnitValidator.SchemaValidate(baseImage, REL_TYPE_BI)

def ValidateAddon(addon):
   """Addon json spec validation.
      Parameters:
         addon: addon json file/string/dict.

      Returns: True on success; otherwise False.

      Exception: ReleaseUnitSchemaError when cannot load spec
   """
   if not HAVE_JSON_SCHEMA:
      log.warn('Skipping addon schema validation')
      return True, None

   return ReleaseUnitValidator.SchemaValidate(addon, REL_TYPE_ADDON)

def ValidateManifest(manifest):
   """Manifest json spec validation.
      Parameters:
         manifest: manifest json file/string/dict.

      Returns: True on success; otherwise False.

      Exception: ReleaseUnitSchemaError when cannot load spec
   """
   if not HAVE_JSON_SCHEMA:
      log.warn('Skipping manifest schema validation')
      return True, None

   return ReleaseUnitValidator.SchemaValidate(manifest, REL_TYPE_MANIFEST)
