#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from __future__ import annotations


import dataclasses
import json
from typing import *

import immutables

from edb import errors
from edb.common import enum
from edb.common import typeutils

from edb.ir import statypes

from edb.edgeql import codegen as qlcodegen
from edb.edgeql import qltypes

from edb.schema import objects as s_obj
from edb.schema import utils as s_utils

from . import spec
from . import types


class OpCode(enum.StrEnum):

    CONFIG_ADD = 'ADD'
    CONFIG_REM = 'REM'
    CONFIG_SET = 'SET'
    CONFIG_RESET = 'RESET'


class SettingValue(NamedTuple):

    name: str
    value: Any
    source: str
    scope: qltypes.ConfigScope


if TYPE_CHECKING:
    SettingsMap = immutables.Map[str, SettingValue]


class Operation(NamedTuple):

    opcode: OpCode
    scope: qltypes.ConfigScope
    setting_name: str
    value: Union[str, int, bool, Collection[Union[str, int, bool, None]], None]

    def get_setting(self, spec: spec.Spec):
        try:
            return spec[self.setting_name]
        except KeyError:
            raise errors.ConfigurationError(
                f'unknown setting {self.setting_name!r}') from None

    def coerce_value(self, setting: spec.Setting, *,
                     allow_missing: bool = False):
        if issubclass(setting.type, types.ConfigType):
            try:
                return setting.type.from_pyvalue(
                    self.value, allow_missing=allow_missing)
            except (ValueError, TypeError):
                raise errors.ConfigurationError(
                    f'invalid value type for the {setting.name!r} setting')
        elif setting.set_of:
            if self.value is None and allow_missing:
                return None
            elif not typeutils.is_container(self.value):
                raise errors.ConfigurationError(
                    f'invalid value type for the '
                    f'{setting.name!r} setting')
            else:
                for v in self.value:  # type: ignore
                    if not isinstance(v, setting.type):
                        raise errors.ConfigurationError(
                            f'invalid value type for the '
                            f'{setting.name!r} setting')

                return frozenset(self.value)  # type: ignore
        else:
            if isinstance(self.value, setting.type):
                return self.value
            elif (isinstance(self.value, str) and
                    issubclass(setting.type, statypes.Duration)):
                return statypes.Duration(self.value)
            elif (isinstance(self.value, (str, int)) and
                    issubclass(setting.type, statypes.ConfigMemory)):
                return statypes.ConfigMemory(self.value)
            elif self.value is None and allow_missing:
                return None
            else:
                raise errors.ConfigurationError(
                    f'invalid value type for the {setting.name!r} setting')

    def apply(self, spec: spec.Spec, storage: SettingsMap) -> SettingsMap:

        setting = self.get_setting(spec)
        allow_missing = (
            self.opcode is OpCode.CONFIG_REM
            or self.opcode is OpCode.CONFIG_RESET
        )

        value = self.coerce_value(setting, allow_missing=allow_missing)

        if self.opcode is OpCode.CONFIG_SET:
            if issubclass(setting.type, types.ConfigType):
                raise errors.InternalServerError(
                    f'unexpected CONFIGURE SET on a non-primitive '
                    f'configuration parameter: {self.setting_name}'
                )

            storage = self._set_value(storage, value)

        elif self.opcode is OpCode.CONFIG_RESET:
            if issubclass(setting.type, types.ConfigType):
                raise errors.InternalServerError(
                    f'unexpected CONFIGURE RESET on a non-primitive '
                    f'configuration parameter: {self.setting_name}'
                )

            try:
                storage = storage.delete(self.setting_name)
            except KeyError:
                pass

        elif self.opcode is OpCode.CONFIG_ADD:
            if not issubclass(setting.type, types.ConfigType):
                raise errors.InternalServerError(
                    f'unexpected CONFIGURE SET += on a primitive '
                    f'configuration parameter: {self.setting_name}'
                )

            exist_setting = storage.get(self.setting_name)
            if exist_setting is not None:
                exist_value = exist_setting.value
            else:
                exist_value = setting.default

            if value in exist_value:
                props = []
                for f in dataclasses.fields(setting.type):
                    if f.compare:
                        props.append(f.name)

                if len(props) > 1:
                    props_s = f' ({", ".join(props)}) violate'
                else:
                    props_s = f'.{props[0]} violates'

                raise errors.ConstraintViolationError(
                    f'{setting.type.__name__}{props_s} '
                    f'exclusivity constraint'
                )

            new_value = exist_value | {value}
            storage = self._set_value(storage, new_value)

        elif self.opcode is OpCode.CONFIG_REM:
            if not issubclass(setting.type, types.ConfigType):
                raise errors.InternalServerError(
                    f'unexpected CONFIGURE SET -= on a primitive '
                    f'configuration parameter: {self.setting_name}'
                )

            exist_setting = storage.get(self.setting_name)
            if exist_setting is not None:
                exist_value = exist_setting.value
            else:
                exist_value = setting.default
            new_value = exist_value - {value}
            storage = self._set_value(storage, new_value)

        return storage

    def _set_value(
        self,
        storage: SettingsMap,
        value: Any,
    ) -> SettingsMap:

        if self.scope is qltypes.ConfigScope.INSTANCE:
            source = 'system override'
        elif self.scope is qltypes.ConfigScope.DATABASE:
            source = 'database'
        elif self.scope is qltypes.ConfigScope.SESSION:
            source = 'session'
        else:
            raise AssertionError(f'unexpected config scope: {self.scope}')

        return set_value(
            storage,
            self.setting_name,
            value,
            source=source,
            scope=self.scope,
        )

    @classmethod
    def from_json(cls, json_value: str) -> Operation:
        op_str, scope_str, name, value = json.loads(json_value)
        return Operation(
            opcode=OpCode(op_str),
            scope=qltypes.ConfigScope(scope_str),
            setting_name=name,
            value=value,
        )


def spec_to_json(spec: spec.Spec):
    dct = {}

    for setting in spec.values():
        if issubclass(setting.type, str):
            typeid = s_obj.get_known_type_id('std::str')
        elif issubclass(setting.type, bool):
            typeid = s_obj.get_known_type_id('std::bool')
        elif issubclass(setting.type, int):
            typeid = s_obj.get_known_type_id('std::int64')
        elif issubclass(setting.type, types.ConfigType):
            typeid = setting.type.get_edgeql_typeid()
        elif issubclass(setting.type, statypes.Duration):
            typeid = s_obj.get_known_type_id('std::duration')
        elif issubclass(setting.type, statypes.ConfigMemory):
            typeid = s_obj.get_known_type_id('cfg::memory')
        else:
            raise RuntimeError(
                f'cannot serialize type for config setting {setting.name}')

        typemod = qltypes.TypeModifier.SingletonType
        if setting.set_of:
            typemod = qltypes.TypeModifier.SetOfType