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


## bind variables
## bind temporal ranges
## (bind statistics)

import datetime
import decimal
from typing import Iterable

import numpy

import opteryx
from opteryx.exceptions import ProgrammingError
from opteryx.exceptions import SqlError
from opteryx.managers.expression import ExpressionTreeNode
from opteryx.models import QueryProperties


def _build_literal_node(value):
    if value is None:
        return {"Value": "Null"}
    if isinstance(value, (bool)):
        # boolean must be before numeric
        return {"Value": {"Boolean": value}}
    if isinstance(value, (str)):
        return {"Value": {"SingleQuotedString": value}}
    if isinstance(value, (int, float, decimal.Decimal)):
        return {"Value": {"Number": [value, False]}}
    if isinstance(value, (numpy.datetime64)):
        return {"Value": {"SingleQuotedString": value.item().isoformat()}}
    if isinstance(value, (datetime.date, datetime.datetime)):
        return {"Value": {"SingleQuotedString": value.isoformat()}}


def variable_binder(node, parameter_set, properties, query_type):
    """Walk the AST replacing 'Placeholder' nodes, this is recursive"""
    # Replace placeholders with parameters.
    # We do this after the AST has been parsed to remove any chance of the
    # parameter affecting the meaning of any of the other tokens - i.e. to
    # eliminate this feature being used for SQL injection.
    if isinstance(node, list):
        return [variable_binder(i, parameter_set, properties, query_type) for i in node]
    if isinstance(node, dict):
        if "Value" in node:
            if "Placeholder" in node["Value"]:
                # fmt:off
                if len(parameter_set) == 0:
                    raise ProgrammingError("Incorrect number of bindings supplied."
                    " More placeholders are provided than parameters.")
                placeholder_value = parameter_set.pop(0)
                # prepared statements will have parsed this already
                if isinstance(placeholder_value, ExpressionTreeNode):
                    placeholder_value = placeholder_value.value
                return _build_literal_node(placeholder_value)
                # fmt:on
        # replace @variables
        if query_type != "SetVariable" and "Identifier" in node:
            token_name = node["Identifier"]["value"]
            if token_name.startswith("@@"):
                return _build_literal_node(opteryx.variables.get(token_name[2:]))
            elif token_name[0] == "@":
                if token_name not in properties.variables:  # pragma: no cover
                    raise SqlError(f"Undefined variable found in query `{token_name}`.")
                variable_value = properties.variables.get(token_name)
                return _build_literal_node(variable_value.value)
        return {
            k: variable_binder(v, parameter_set, properties, query_type) for k, v in node.items()
        }
    # we're a leaf
    return node


def temporal_range_binder(ast, filters, cache):
    if isinstance(ast, (list)):
        return [temporal_range_binder(node, filters, cache) for node in ast]
    if isinstance(ast, (dict)):
        node_name = next(iter(ast))
        if node_name == "Table":
            temporal_range = filters.pop(0)
            ast["Table"]["start_date"] = temporal_range[1]
            ast["Table"]["end_date"] = temporal_range[2]
            ast["Table"]["cache"] = cache
            return ast
        if "table_name" in ast:
            temporal_range = filters.pop(0)
            ast["table_name"][0]["start_date"] = temporal_range[1]
            ast["table_name"][0]["end_date"] = temporal_range[2]
            ast["table_name"][0]["cache"] = cache
            return ast
        if "ShowCreate" in ast:
            temporal_range = filters.pop(0)
            ast["ShowCreate"]["start_date"] = temporal_range[1]
            ast["ShowCreate"]["end_date"] = temporal_range[2]
            ast["ShowCreate"]["cache"] = cache
            return ast
        return {k: temporal_range_binder(v, filters, cache) for k, v in ast.items()}
    return ast


def statistics_binder(ast):
    return ast


def bind_ast(ast, parameters: Iterable = None, properties: QueryProperties = None):
    """
    Bind physical information to the AST

    This includes the following activities
    - Replacing placeholders with the parameters
    - Adding temporal range information to relations
    """
    query_type = next(iter(ast))

    bound_ast = ast.copy()
    bound_ast = variable_binder(bound_ast, parameters, properties, query_type)
    if query_type in ("Analyze", "Query", "Explain", "ShowColumns", "ShowCreate"):
        bound_ast = temporal_range_binder(
            bound_ast, list(properties.temporal_filters), properties.cache
        )
        bound_ast = statistics_binder(bound_ast)

    return bound_ast
