from irods.message import (
    iRODSMessage,
    StringStringMap,
    RodsHostAddress,
    STR_PI,
    MsParam,
    MsParamArray,
    RuleExecutionRequest,
)
from irods.api_number import api_number
import irods.exception as ex
from io import open as io_open
from irods.message import Message, StringProperty


class RemoveRuleMessage(Message):
    # define RULE_EXEC_DEL_INP_PI "str ruleExecId[NAME_LEN];"
    _name = "RULE_EXEC_DEL_INP_PI"
    ruleExecId = StringProperty()

    def __init__(self, id_):
        super(RemoveRuleMessage, self).__init__()
        self.ruleExecId = str(id_)


class Rule:
    def __init__(
        self,
        session,
        rule_file=None,
        body="",
        params=None,
        output="",
        instance_name=None,
        irods_3_literal_style=False,
    ):
        """
        Initialize a rule object.

        Arguments:
        Use one of:
          * rule_file : the name of an existing file containint "rule script" style code. In the context of
            the native iRODS Rule Language, this is a file ending in '.r' and containing iRODS rules.
            Optionally, this parameter can be a file-like object containing the rule script text.
          * body: the text of block of rule code (possibly including rule calls) to be run as if it were
            the body of a rule, e.g. the part between the braces of a rule definition in the iRODS rule language.
        * instance_name: the name of the rule engine instance in the context of which to run the rule(s).
        * output may be set to 'ruleExecOut' if console output is expected on stderr or stdout streams.
        * params are key/value pairs to be sent into a rule_file.
        * irods_3_literal_style: affects the format of the @external directive. Use `True' for iRODS 3.x.

        """
        self.session = session

        self.params = {}
        self.output = ""

        if rule_file:
            self.load(rule_file)
        else:
            self.body = (
                "@external\n" + body
                if irods_3_literal_style
                else "@external rule { " + body + " }"
            )

        # overwrite params and output if received arguments
        if isinstance(params, dict):
            if self.params:
                self.params.update(params)
            else:
                self.params = params

        if output != "":
            self.output = output

        self.instance_name = instance_name

    def remove_by_id(self, *ids):
        with self.session.pool.get_connection() as conn:
            for id_ in ids:
                request = iRODSMessage(
                    "RODS_API_REQ",
                    msg=RemoveRuleMessage(id_),
                    int_info=api_number["RULE_EXEC_DEL_AN"],
                )
                conn.send(request)
                response = conn.recv()
                if response.int_info != 0:
                    raise RuntimeError(f"Error removing rule {id_}")

    def load(self, rule_file, encoding="utf-8"):
        """Load rule code with rule-file (*.r) semantics.

        A "main" rule is defined first; name does not matter. Other rules may follow, which will be
        callable from the first rule.  Any rules defined in active rule-bases within the server are
        also callable.

        The `rule_file' parameter is a filename or file-like object.  We give it either:
           - a string holding the path to a rule-file in the local filesystem, or
           - an in-memory object (eg. io.StringIO or io.BytesIO) whose content is that of a rule-file.

        This addresses a regression in v1.1.0; see issue #336.  In v1.1.1+, if rule code is passed in literally via
        the `body' parameter of the Rule constructor, it is interpreted as if it were the body of a rule, and
        therefore it may not contain internal rule definitions.  However, if rule code is submitted as the content
        of a file or file-like object referred to by the `rule_file' parameter of the Rule constructor, will be
        interpreted as .r-file content.  Therefore, it must contain a main rule definition first, followed
        possibly by others which will be callable from the main rule as if they were part of the core rule-base.

        """
        self.body = "@external\n"

        with (
            io_open(rule_file, encoding=encoding)
            if isinstance(rule_file, str)
            else rule_file
        ) as f:

            # parse rule file line-by-line
            for line in f:

                # convert input line to Unicode if necessary
                if isinstance(line, bytes):
                    line = line.decode(encoding)

                # parse input line
                if line.strip().lower().startswith("input"):

                    input_header, input_line = line.split(None, 1)

                    if input_line.strip().lower() == "null":
                        self.params = {}
                        continue

                    # sanity check
                    if input_header.lower() != "input":
                        raise ValueError

                    # parse *param0="value0",*param1="value1",...
                    for pair in input_line.split(","):
                        label, value = pair.split("=")
                        self.params[label.strip()] = value.strip()

                # parse output line
                elif line.strip().lower().startswith("output"):
                    output_header, output_line = line.split(None, 1)

                    # sanity check
                    if output_header.lower() != "output":
                        raise ValueError

                    # use line as is
                    self.output = output_line.strip()

                # parse rule
                else:
                    self.body += line

    def execute(
        self,
        session_cleanup=True,
        acceptable_errors=(ex.FAIL_ACTION_ENCOUNTERED_ERR,),
        r_error=None,
        return_message=(),
    ):
        try:
            # rule input
            param_array = []
            for label, value in self.params.items():
                inOutStruct = STR_PI(myStr=value)
                param_array.append(
                    MsParam(label=label, type="STR_PI", inOutStruct=inOutStruct)
                )

            inpParamArray = MsParamArray(
                paramLen=len(param_array), oprType=0, MsParam_PI=param_array
            )

            # rule body
            addr = RodsHostAddress(hostAddr="", rodsZone="", port=0, dummyInt=0)
            condInput = StringStringMap(
                {}
                if self.instance_name is None
                else {"instance_name": self.instance_name}
            )
            message_body = RuleExecutionRequest(
                myRule=self.body,
                addr=addr,
                condInput=condInput,
                outParamDesc=self.output,
                inpParamArray=inpParamArray,
            )

            request = iRODSMessage(
                "RODS_API_REQ", msg=message_body, int_info=api_number["EXEC_MY_RULE_AN"]
            )

            with self.session.pool.get_connection() as conn:
                conn.send(request)
                response = conn.recv(
                    acceptable_errors=acceptable_errors, return_message=return_message
                )
                try:
                    out_param_array = response.get_main_message(
                        MsParamArray, r_error=r_error
                    )
                except iRODSMessage.ResponseNotParseable:
                    return (
                        MsParamArray()
                    )  # Ergo, no useful return value - but the RError stack will be accessible
        finally:
            if session_cleanup:
                self.session.cleanup()

        return out_param_array
