#    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.

import lark

from dataclasses import asdict
from dataclasses import dataclass

from ironic.common.exception import InvalidParameterValue
from ironic.common.i18n import _


def sanitize_kernel_command_line(command_line: str) -> str:
    """Applies filtering to a command line to sanitize it.

    NOTE: This does not guarantee a correct or safe kernel command line,
    for stronger guarantees of correctness and safety use
    KernelCommandLine.parse().

    :param command_line: A string containing a kernel command line or
        individual parameters.
    :returns: A filtered string which should be safer for use.
    """
    return ''.join(c for c in command_line if c not in {'\n', '\r', '\0'})


@dataclass(frozen=True)
class ParameterKey:
    key: str

    def __str__(self):
        return self.key


@dataclass(frozen=True)
class ParameterValue:
    value: str

    def __str__(self):
        if ' ' in self.value:
            return f"\"{self.value}\""
        return self.value


@dataclass(frozen=True)
class KernelParameter:
    key: ParameterKey
    value: ParameterValue

    def __str__(self):
        if len(self.value.value) > 0:
            return f"{self.key}={self.value}"
        return self.key.key


@dataclass(frozen=True)
class UnstructuredParameters:
    value: str

    def __str__(self):
        return self.value


@dataclass(frozen=True)
class ParameterSet:
    parameters: dict[str, KernelParameter]

    def __str__(self):
        return ' '.join(str(kp) for kp in self.parameters.values())

    def asdict(self):
        return {
            kp.key.key: kp.value.value for kp in self.parameters.values()
        }


@dataclass(frozen=True)
class KernelCommandLine:
    """Represents a kernel command line.

    NOTE: An instantiated object of this type does not guarantee that it has
    passed parsing via KernelParameterParser. ParsedKernelCommandLine is
    reserved for this distinction.
    """
    parameters: dict[str, list[KernelParameter]]
    init_args: str

    def __str__(self):
        output = ' '.join(
            ' '.join(str(param) for param in param_list)
            for param_list in self.parameters.values())
        if len(self.init_args) > 0:
            output += " -- " + self.init_args
        return output

    @classmethod
    def parse(cls, command_line: str):
        try:
            tree = KernelParameterParser.parse(command_line.strip())
            return KernelParameterTransformer().transform(tree)
        except (lark.exceptions.LarkError,
                lark.exceptions.UnexpectedInput) as e:
            raise InvalidParameterValue(
                _('Kernel command line did not parse: "%s" -- %s') \
                % (command_line, str(e))) from None

    def as_parameter_set(self):
        return ParameterSet({
            kp[0].key.key: kp[0] for kp in self.parameters.values()
        })

    def asdict(self):
        return asdict(self)


@dataclass(frozen=True)
class UnsafeKernelCommandLine(KernelCommandLine):
    """Represents a kernel command line that has not been parsed.

    Includes the field 'unstructured_params', which are treated as opaque
    strings which haven't been parsed. To convert this to a
    ParsedKernelCommandLine, use KernelCommandLine.parse(str(unsafe_cmd_line)).
    """
    unstructured_params: list[UnstructuredParameters]

    def __str__(self):
        output = ' '.join(
            ' '.join(str(param) for param in param_list)
            for param_list in self.parameters.values())

        output += ' ' + ' '.join(str(up) for up in self.unstructured_params)

        if len(self.init_args) > 0:
            output += " -- " + self.init_args

        return output.strip()


@dataclass(frozen=True)
class ParsedKernelCommandLine(KernelCommandLine):
    """Represents a parsed kernel command line.

    NOTE: This object should only be instantiated via passing a kernel
    command line string through KernelParameterParser.parse() and then
    the resultant parse tree through KernelParameterTransformer.transform().
    Constructing this directly is almost certainly a mistake.

    This class helps distinguish between kernel command lines that have been
    validated by parsing and those that have not.
    """
    pass


# NOTE(clif): Some valid values (such as filenames) are not going to be
# representable given we're explicitly not allowing large swaths characters
# in parameter values. I believe this is reasonable. Most people do not
# purposefully put non-printable/control characters in kernel parameters for
# anything other than nefarious goals.
# NOTE(clif): bare_value and value_with_spaces permit a *large* range of
# printable ASCII characters because many are used as characters with special
# meaning in several kernel parameters.
# NOTE(clif): The only permitted white-space character in this grammar is a
# space.
KERNEL_PARAMETER_GRAMMAR = r"""
kernel_command_line:  parameter_list [init_suffix]

parameter_list: parameter*(" "+ parameter)*

parameter: key
         | key_value_pair

key_value_pair: key"="value

key: /[A-Za-z0-9_\-\.]+/

value: bare_value
     | quoted_value

quoted_value: "\"" value_with_spaces "\""

bare_value: /[\!\#-\\.0-9:-\@A-Z\[-~]+/

value_with_spaces: /[\!\#-\\.0-9:-\@A-Z\[-~ ]+/

init_suffix: " "+ "--" " "+ init_arguments

init_arguments: value_with_spaces
"""

KERNEL_PARAMETER_GRAMMER_START_RULE = 'kernel_command_line'

KernelParameterParser = lark.Lark(KERNEL_PARAMETER_GRAMMAR,
                                  start=KERNEL_PARAMETER_GRAMMER_START_RULE,
                                  strict=True)


class KernelParameterTransformer(lark.Transformer):
    def kernel_command_line(self, items):
        return ParsedKernelCommandLine(items[0], items[1] or '')

    def parameter_list(self, items):
        parameters = {}
        for item in items:
            if item.key.key in parameters.keys():
                parameters[item.key.key].append(item)
            else:
                parameters[item.key.key] = [item]
        return parameters

    def parameter(self, items):
        if isinstance(items[0], ParameterKey):
            return KernelParameter(items[0], ParameterValue(""))
        return items[0]

    def key_value_pair(self, items):
        key = items[0]
        value = items[1]
        return KernelParameter(key, value)

    def key(self, items):
        return ParameterKey(items[0].value)

    def value(self, items):
        return ParameterValue(items[0])

    def quoted_value(self, items):
        return items[0]

    def bare_value(self, items):
        return items[0].value

    def value_with_spaces(self, items):
        return items[0].value

    def init_suffix(self, items):
        return items[0]

    def init_arguments(self, items):
        return items[0]
