#!/usr/bin/env python
# coding: utf-8
# Copyright (c) 2024 Huawei Technologies Co., Ltd.
# openUBMC is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#         http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.

import os
from collections import defaultdict
from . import schema_utils as su


class ReqBodyConstraintChecker:
    def __init__(self, target_path, uri, schema_dir, rsc_name, prop_name, intf_type):
        self.target_path = target_path
        self.uri = uri
        self.schema_dir = schema_dir
        self.rsc_filename = rsc_name + '.json'
        self.rsc_name = su.get_rsc_name(self.rsc_filename) or 'None'
        self.version = su.get_version(self.rsc_filename) or 'None'
        self.prop_name = prop_name
        self.intf_type = intf_type.lower()
        self.is_success = True
        self.check_type_table = defaultdict(self.default_check_type)
        self.check_type_table['null'] = self.check_null
        self.check_type_table['boolean'] = self.check_boolean
        self.check_type_table['string'] = self.check_string
        self.check_type_table['integer'] = self.check_integer
        self.check_type_table['number'] = self.check_number
        self.check_type_table['array'] = self.check_array
        self.check_type_table['Items'] = self.check_items
        self.check_type_table['object'] = self.check_object
        self.check_validator_table = defaultdict(self.default_check_validator)
        self.check_validator_table['Enum'] = self.check_vali_enum
        self.check_validator_table['Regex'] = self.check_vali_regex
        self.check_validator_table['Length'] = self.check_vali_length
        self.check_validator_table['Range'] = self.check_vali_range
        self.check_validator_table['Script'] = self.check_vali_script

    def check_post_requiredOnCreate_cons(self, target, required_on_create, schema_path, prop_path, errors):
        for r in required_on_create:
            if r not in target:
                self.is_success = False
                errors.append(['2-5', 
                               self.target_path,
                               schema_path,
                               r,
                               self.uri,
                               self.intf_type,
                               '/'.join([prop_path, r]),
                               self.rsc_name,
                               self.version])

    def check_prop_def(self, target, schema, add_allowed, schema_path, prop_path, errors):
        find_def = True
        no_def_prop = []
        target_prop = []
        schema_prop = []
        if 'Properties' in target:
            target_prop = list(target['Properties'].keys())
        if 'properties' in schema:
            schema_prop = list(schema['properties'].keys())
        for tp in target_prop:
            if tp not in schema_prop:
                find_def = False
                no_def_prop.append(tp)
        if find_def is False and add_allowed is False:
            for p in no_def_prop:
                self.is_success = False
                errors.append(['2-3', 
                               self.target_path,
                               schema_path,
                               p,
                               self.uri,
                               self.intf_type,
                               '/'.join([prop_path, p]),
                               self.rsc_name,
                               self.version])
        return (find_def or add_allowed), no_def_prop

    def check_patch_readonly_cons(self, target, schema, schema_path, prop_path, errors):
        readonly_check = True
        target_prop = []
        readonly_prop = []
        if 'Properties' in target:
            target_prop = list(target['Properties'].keys())
        if 'properties' in schema:
            for p in schema['properties']:
                prop = schema['properties'][p]
                while 'readonly' not in prop and '$ref' in prop:
                    prop, path = su.get_prop_from_local_ref(su.load_json(schema_path), prop['$ref'], self.schema_dir)
                    if path is not None:
                        schema_path = path
                if 'readonly' in prop and prop['readonly'] is True:
                    readonly_prop.append(p)
        for rp in readonly_prop:
            if rp in target_prop:
                readonly_check = False
                self.is_success = False
                errors.append(['2-7',
                               self.target_path,
                               schema_path,
                               rp,
                               self.uri,
                               self.intf_type,
                               '/'.join([prop_path, rp]),
                               self.rsc_name,
                               self.version])
        return readonly_check

    def merge_anyof_schema(self, prop_schema, schema_json):
        schema_list = []
        type_list = []
        for ss in prop_schema:
            if '$ref' in ss:
                while '$ref' in ss and su.is_local_ref(ss['$ref']):
                    ss, _ = su.get_prop_from_local_ref(schema_json, ss['$ref'], self.schema_dir)
                if '$ref' in ss and ss['$ref'].startswith('http'):
                    return []
                schema_list.append(ss)
            elif len(ss) == 1 and 'type' in ss:
                type_list.append(ss['type'])
            else:
                schema_list.append(ss)
        for prop_type in type_list:
            for ss in schema_list:
                if isinstance(ss['type'], str):
                    ss['type'] = [ss['type']]
                ss['type'].append(prop_type)
        return schema_list

    def default_check_validator(self):
        return True

    def check_vali_enum(self, target_validator, schema):
        target_enum = target_validator['Formula']
        if 'enum' not in schema:
            return True
        schema_enum = schema['enum']
        return set(target_enum).issubset(set(schema_enum))

    def check_vali_regex(self, target_validator, schema):
        target_regex = target_validator['Formula']
        if 'pattern' not in schema:
            return True
        schema_regex = schema['pattern']
        return target_regex == schema_regex
    
    def check_vali_length(self, target_validator, schema):
        t_minlength = target_validator['Formula'][0]
        t_maxlength = target_validator['Formula'][1]
        if 'minLength' in schema and t_minlength < schema['minLength']:
            return False
        if 'maxLength' in schema and t_maxlength > schema['maxLength']:
            return False
        return True

    def check_vali_range(self, target_validator, schema):
        t_minimum = target_validator['Formula'][0]
        t_maximum = target_validator['Formula'][1]
        if 'minimum' in schema and t_minimum < schema['minimum']:
            return False
        if 'maximum' in schema and t_maximum > schema['maximum']:
            return False
        return True
    
    def check_vali_script(self, target_validator, schema):
        return True
    
    def check_items(self, target_items, schema_items, prop_json):
        while '$ref' in schema_items:
            schema_items, path = su.get_prop_from_local_ref(prop_json, schema_items['$ref'], self.schema_dir)
            if path is not None:
                prop_json = su.load_json(path)
        return self.check_type_table[target_items['Type']](target_items, schema_items, prop_json)

    def default_check_type(self):
        print('==========================')
        print('在文件中定义了非法数据类型')

    def check_null(self, _, prop_schema, __):
        if 'type' in prop_schema and 'null' in prop_schema['type']:
            return True
        return False

    def check_boolean(self, _, prop_schema, __):
        if 'type' in prop_schema and 'boolean' in prop_schema['type']:
            return True
        return False
    
    # Regex Enum Length
    def check_string(self, target_prop, prop_schema, schema_json):
        if 'anyOf' in prop_schema:
            prop_schema = self.merge_anyof_schema(prop_schema['anyOf'], schema_json)
            if prop_schema is []:
                return True
            for ss in prop_schema:
                if self.check_string(target_prop, ss, schema_json):
                    return True
            return False
        if 'type' in prop_schema and 'string' in prop_schema['type']:
            if 'Validator' in target_prop:
                validators = target_prop['Validator']
                for v in validators:
                    if self.check_validator_table[v['Type']](v, prop_schema) is False:
                        return False
            elif ('pattern' in prop_schema or                                   # Regex
                  'enum' in prop_schema or                                      # Enum
                  'minLength' in prop_schema or 'maxLength' in prop_schema):    # Length
                return False
            return True
        return False
    
    # Range Enum
    def check_integer(self, target_prop, prop_schema, _):
        if 'type' in prop_schema and ('integer' in prop_schema['type'] or 'number' in prop_schema['type']):
            if 'Validator' in target_prop:
                validators = target_prop['Validator']
                for v in validators:
                    if self.check_validator_table[v['Type']](v, prop_schema) is False:
                        return False
            return True
        return False
    
    def check_number(self, target_prop, prop_schema, _):
        if 'type' in prop_schema and 'number' in prop_schema['type']:
            if 'Validator' in target_prop:
                validators = target_prop['Validator']
                for v in validators:
                    if self.check_validator_table[v['Type']](v, prop_schema) is False:
                        return False
            return True
        return False
    
    def check_array(self, target_prop, prop_schema, prop_json):
        if 'type' in prop_schema and 'array' in prop_schema['type']:
            if 'items' in prop_schema:
                if 'Items' not in target_prop:
                    return False
                return True
        return False
    
    # object类型的属性类型定义取决于内层key-value是否一致，在check_prop_type_consistency会逐级比较
    def check_object(self, _, prop_schema, __):
        if 'type' in prop_schema and 'object' in prop_schema['type']:
            return True
        return False

    def check_prop_type_consistency(self, target, no_def_prop, schema, schema_path, prop_path, errors):
        if 'Properties' not in target:
            return True
        type_cons_success = True
        for tp in target['Properties']:
            if tp in no_def_prop:
                continue
            prop_schema = schema['properties'][tp]
            prop_json = su.load_json(schema_path)
            path = schema_path
            while 'type' not in prop_schema and '$ref' in prop_schema:
                if not su.is_local_ref(prop_schema['$ref']):
                    return True
                prop_schema, p = su.get_prop_from_local_ref(prop_json, prop_schema['$ref'], self.schema_dir)
                if p is not None:
                    prop_json = su.load_json(p)
                    path = p
            target_prop = target['Properties'][tp]
            if isinstance(target_prop['Type'], str):
                if self.check_type_table[target_prop['Type']](target_prop, prop_schema, prop_json) is False:
                    type_cons_success = False
                    errors.append(['2-4',
                                    self.target_path,
                                    path,
                                    target_prop['Type'],
                                    self.uri,
                                    self.intf_type,
                                    '/'.join([prop_path, tp, 'Type']),
                                    self.rsc_name,
                                    self.version])
            elif isinstance(target_prop['Type'], list):
                for t in target_prop['Type']:
                    if self.check_type_table[t](target_prop, prop_schema, prop_json) is False:
                        type_cons_success = False
                        errors.append(['2-4',
                                       self.target_path,
                                       path,
                                       t,
                                       self.uri,
                                       self.intf_type,
                                       '/'.join([prop_path, tp, 'Type']),
                                       self.rsc_name,
                                       self.version])
        # 当前一级检查属性一致性失败，会检查完本级所有属性，不会再往下一级检查
        if type_cons_success is False:
            return False
        return True
    
    def check_req_body_prop(self, target, schema, prop_path, schema_path, errors):
        add_allowed = False
        if 'additionalProperties' in schema and target['Type'] == 'object':
            add_allowed = schema['additionalProperties']
        check_success, no_def_prop = self.check_prop_def(target, schema, add_allowed, schema_path, prop_path, errors)
        if not check_success:
            self.is_success = False
            return False
        # flag用于判断当前属性开始的检查是否有误
        flag = True
        flag = self.check_patch_readonly_cons(target, schema, schema_path, prop_path, errors) and flag
        flag = self.check_prop_type_consistency(target, no_def_prop, schema, schema_path, prop_path, errors) and flag
        if 'Properties' in target:
            for p in target['Properties']:
                if p in no_def_prop:
                    continue
                tp = target['Properties'][p]
                sp = schema['properties'][p]
                flag = self.check_req_body(tp, sp, '/'.join([prop_path, p]), schema_path, errors) and flag
        return flag

    def check_req_body(self, target, schema, prop_path, schema_path, errors):
        if not (isinstance(target, dict) or (isinstance(target, list))):
            return True
        if self.intf_type == 'post' and 'requiredOnCreate' in schema:
            self.check_post_requiredOnCreate_cons(target, schema['requiredOnCreate'], schema_path, prop_path, errors)
        while '$ref' in schema and su.is_local_ref(schema['$ref']):
            schema, p = su.get_prop_from_local_ref(su.load_json(schema_path), schema['$ref'], self.schema_dir)
            if p is not None:
                schema_path = p
        # schema中下一级定义用远程路径，跳过后续检查
        if '$ref' in schema and schema['$ref'].startswith('http'):
            return
        # 兼容anyOf场景，anyOf的模式满足其一即可
        # 有任一模式满足，则不会报错；没有模式满足，则所有模式的报错都会记录
        flag = False # 用于记录当前属性开始的模式是否通过检查
        es = []
        if 'anyOf' in schema:
            for ss in schema['anyOf']:
                e = []
                if self.check_req_body(target, ss, prop_path, schema_path, e) is True:
                    flag = True
                    break
                else:
                    es.extend(e)
            if flag is False:
                errors.extend(es)
        else:
            flag = self.check_req_body_prop(target, schema, prop_path, schema_path, es)
            errors.extend(es)
        return flag
        
    def check(self, req_body):
        schema_path = os.path.join(self.schema_dir, self.rsc_filename)
        schema_json = su.load_json(schema_path)
        if 'definitions' not in schema_json or self.prop_name not in schema_json['definitions']:
            su.ep.print_errorlog('2-3',
                                 self.target_path,
                                 schema_path,
                                 self.prop_name,
                                 self.uri,
                                 self.intf_type,
                                 '未在schema文件中定义资源名',
                                 self.rsc_name,
                                 self.version)
            return False
        errors = []
        if not self.check_req_body(req_body, schema_json['definitions'][self.prop_name], 'ReqBody',
                                 schema_path, errors):  # 此处schema_path是开始检查时的值，后续检查中可能改变
            su.ep.get_error_list(self.is_success, errors)
        return self.is_success