# -*- coding: utf-8 -*-

# Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project 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.

import datetime

import colander
from colander import null

# those keywords are here so we can distingush between searching for tags and
# normal properties of reports/logs
accepted_search_params = [
    "resource",
    "request_id",
    "start_date",
    "end_date",
    "page",
    "min_occurences",
    "http_status",
    "priority",
    "error",
    "url_path",
    "url_domain",
    "report_status",
    "min_duration",
    "max_duration",
    "message",
    "level",
    "namespace",
]


@colander.deferred
def deferred_utcnow(node, kw):
    return kw["utcnow"]


@colander.deferred
def optional_limited_date(node, kw):
    if not kw.get("allow_permanent_storage"):
        return limited_date


def lowercase_preparer(input_data):
    """
    Transforms a list of string entries to lowercase
    Used in search query validation
    """
    if not input_data:
        return input_data
    return [x.lower() for x in input_data]


def shortener_factory(cutoff_size=32):
    """
    Limits the input data to specific character count
    :arg cutoff_cutoff_size How much characters to store

    """

    def shortener(input_data):
        if not input_data:
            return input_data
        else:
            if isinstance(input_data, str):
                return input_data[:cutoff_size]
            else:
                return input_data

    return shortener


def cast_to_unicode_or_null(value):
    if value is not colander.null:
        return str(value)
    return None


class NonTZDate(colander.DateTime):
    """ Returns null for incorrect date format - also removes tz info"""

    def deserialize(self, node, cstruct):
        # disabled for now
        # if cstruct and isinstance(cstruct, str):
        #     if ':' not in cstruct:
        #         cstruct += ':0.0'
        #     if '.' not in cstruct:
        #         cstruct += '.0'
        value = super(NonTZDate, self).deserialize(node, cstruct)
        if value:
            return value.replace(tzinfo=None)
        return value


class UnknownType(object):
    """
    Universal type that will accept a deserialized JSON object and store it unaltered
    """

    def serialize(self, node, appstruct):
        if appstruct is null:
            return null
        return appstruct

    def deserialize(self, node, cstruct):
        if cstruct is null:
            return null
        return cstruct

    def cstruct_children(self):
        return []


# SLOW REPORT SCHEMA


def rewrite_type(input_data):
    """
    Fix for legacy appenlight clients
    """
    if input_data == "remote_call":
        return "remote"
    return input_data


class ExtraTupleSchema(colander.TupleSchema):
    name = colander.SchemaNode(colander.String(), validator=colander.Length(1, 64))
    value = colander.SchemaNode(
        UnknownType(), preparer=shortener_factory(512), missing=None
    )


class ExtraSchemaList(colander.SequenceSchema):
    tag = ExtraTupleSchema()
    missing = None


class TagsTupleSchema(colander.TupleSchema):
    name = colander.SchemaNode(colander.String(), validator=colander.Length(1, 128))
    value = colander.SchemaNode(
        UnknownType(), preparer=shortener_factory(128), missing=None
    )


class TagSchemaList(colander.SequenceSchema):
    tag = TagsTupleSchema()
    missing = None


class NumericTagsTupleSchema(colander.TupleSchema):
    name = colander.SchemaNode(colander.String(), validator=colander.Length(1, 128))
    value = colander.SchemaNode(colander.Float(), missing=0)


class NumericTagSchemaList(colander.SequenceSchema):
    tag = NumericTagsTupleSchema()
    missing = None


class SlowCallSchema(colander.MappingSchema):
    """
    Validates slow call format in slow call list
    """

    start = colander.SchemaNode(NonTZDate())
    end = colander.SchemaNode(NonTZDate())
    statement = colander.SchemaNode(colander.String(), missing="")
    parameters = colander.SchemaNode(UnknownType(), missing=None)
    type = colander.SchemaNode(
        colander.String(),
        preparer=rewrite_type,
        validator=colander.OneOf(
            ["tmpl", "sql", "nosql", "remote", "unknown", "custom"]
        ),
        missing="unknown",
    )
    subtype = colander.SchemaNode(
        colander.String(), validator=colander.Length(1, 16), missing="unknown"
    )
    location = colander.SchemaNode(
        colander.String(), validator=colander.Length(1, 255), missing=""
    )


def limited_date(node, value):
    """ checks to make sure that the value is not older/newer than 2h """
    past_hours = 72
    future_hours = 2
    min_time = datetime.datetime.utcnow() - datetime.timedelta(hours=past_hours)
    max_time = datetime.datetime.utcnow() + datetime.timedelta(hours=future_hours)
    if min_time > value:
        msg = "%r is older from current UTC time by " + str(past_hours)
        msg += (
            " hours. Ask administrator to enable permanent logging for "
            "your application to store logs with dates in past."
        )
        raise colander.Invalid(node, msg % value)
    if max_time < value:
        msg = "%r is newer from current UTC time by " + str(future_hours)
        msg += (
            " hours. Ask administrator to enable permanent logging for "
            "your application to store logs with dates in future."
        )
        raise colander.Invalid(node, msg % value)


class SlowCallListSchema(colander.SequenceSchema):
    """
    Validates list of individual slow calls
    """

    slow_call = SlowCallSchema()


class RequestStatsSchema(colander.MappingSchema):
    """
    Validates format of requests statistics dictionary
    """

    main = colander.SchemaNode(colander.Float(), validator=colander.Range(0), missing=0)
    sql = colander.SchemaNode(colander.Float(), validator=colander.Range(0), missing=0)
    nosql = colander.SchemaNode(
        colander.Float(), validator=colander.Range(0), missing=0
    )
    remote = colander.SchemaNode(
        colander.Float(), validator=colander.Range(0), missing=0
    )
    tmpl = colander.SchemaNode(colander.Float(), validator=colander.Range(0), missing=0)
    custom = colander.SchemaNode(
        colander.Float(), validator=colander.Range(0), missing=0
    )
    sql_calls = colander.SchemaNode(
        colander.Float(), validator=colander.Range(0), missing=0
    )
    nosql_calls = colander.SchemaNode(
        colander.Float(), validator=colander.Range(0), missing=0
    )
    remote_calls = colander.SchemaNode(
        colander.Float(), validator=colander.Range(0), missing=0
    )
    tmpl_calls = colander.SchemaNode(
        colander.Float(), validator=colander.Range(0), missing=0
    )
    custom_calls = colander.SchemaNode(
        colander.Float(), validator=colander.Range(0), missing=0
    )


class FrameInfoVarSchema(colander.SequenceSchema):
    """
    Validates format of frame variables of a traceback
    """

    vars = colander.SchemaNode(UnknownType(), validator=colander.Length(2, 2))


class FrameInfoSchema(colander.MappingSchema):
    """
    Validates format of a traceback line
    """

    cline = colander.SchemaNode(colander.String(), missing="")
    module = colander.SchemaNode(colander.String(), missing="")
    line = colander.SchemaNode(colander.String(), missing="")
    file = colander.SchemaNode(colander.String(), missing="")
    fn = colander.SchemaNode(colander.String(), missing="")
    vars = FrameInfoVarSchema()


class FrameInfoListSchema(colander.SequenceSchema):
    """
    Validates format of list of traceback lines
    """

    frame = colander.SchemaNode(UnknownType())


class ReportDetailBaseSchema(colander.MappingSchema):
    """
    Validates format of report - ie. request parameters and stats for a request in report group
    """

    username = colander.SchemaNode(
        colander.String(),
        preparer=[shortener_factory(255), lambda x: x or ""],
        missing="",
    )
    request_id = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(40), missing=""
    )
    url = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(1024), missing=""
    )
    ip = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(39), missing=None
    )
    start_time = colander.SchemaNode(
        NonTZDate(), validator=optional_limited_date, missing=deferred_utcnow
    )
    end_time = colander.SchemaNode(
        NonTZDate(), validator=optional_limited_date, missing=None
    )
    user_agent = colander.SchemaNode(
        colander.String(),
        preparer=[shortener_factory(512), lambda x: x or ""],
        missing="",
    )
    message = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(2048), missing=""
    )
    group_string = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(512), missing=None
    )
    request_stats = RequestStatsSchema(missing=None)
    request = colander.SchemaNode(colander.Mapping(unknown="preserve"), missing={})
    traceback = FrameInfoListSchema(missing=None)
    slow_calls = SlowCallListSchema(missing=[])
    extra = ExtraSchemaList()


class ReportDetailSchema_0_5(ReportDetailBaseSchema):
    pass


class ReportDetailSchemaPermissiveDate_0_5(ReportDetailSchema_0_5):
    start_time = colander.SchemaNode(NonTZDate(), missing=deferred_utcnow)
    end_time = colander.SchemaNode(NonTZDate(), missing=None)


class ReportSchemaBase(colander.MappingSchema):
    """
    Validates format of report group
    """

    client = colander.SchemaNode(colander.String(), preparer=lambda x: x or "unknown")
    server = colander.SchemaNode(
        colander.String(),
        preparer=[lambda x: x.lower() if x else "unknown", shortener_factory(128)],
        missing="unknown",
    )
    priority = colander.SchemaNode(
        colander.Int(),
        preparer=[lambda x: x or 5],
        validator=colander.Range(1, 10),
        missing=5,
    )
    language = colander.SchemaNode(colander.String(), missing="unknown")
    error = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(512), missing=""
    )
    view_name = colander.SchemaNode(
        colander.String(),
        preparer=[shortener_factory(128), lambda x: x or ""],
        missing="",
    )
    http_status = colander.SchemaNode(
        colander.Int(), preparer=[lambda x: x or 200], validator=colander.Range(1)
    )

    occurences = colander.SchemaNode(
        colander.Int(), validator=colander.Range(1, 99999999999), missing=1
    )
    tags = TagSchemaList()


class ReportSchema_0_5(ReportSchemaBase, ReportDetailSchema_0_5):
    pass


class ReportSchemaPermissiveDate_0_5(
    ReportSchemaBase, ReportDetailSchemaPermissiveDate_0_5
):
    pass


class ReportListSchema_0_5(colander.SequenceSchema):
    """
    Validates format of list of report groups
    """

    report = ReportSchema_0_5()
    validator = colander.Length(1)


class ReportListPermissiveDateSchema_0_5(colander.SequenceSchema):
    """
    Validates format of list of report groups
    """

    report = ReportSchemaPermissiveDate_0_5()
    validator = colander.Length(1)


class LogSchema(colander.MappingSchema):
    """
    Validates format if individual log entry
    """

    primary_key = colander.SchemaNode(
        UnknownType(),
        preparer=[cast_to_unicode_or_null, shortener_factory(128)],
        missing=None,
    )
    log_level = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(10), missing="UNKNOWN"
    )
    message = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(4096), missing=""
    )
    namespace = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(128), missing=""
    )
    request_id = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(40), missing=""
    )
    server = colander.SchemaNode(
        colander.String(), preparer=shortener_factory(128), missing="unknown"
    )
    date = colander.SchemaNode(
        NonTZDate(), validator=limited_date, missing=deferred_utcnow
    )
    tags = TagSchemaList()


class LogSchemaPermanent(LogSchema):
    date = colander.SchemaNode(NonTZDate(), missing=deferred_utcnow)
    permanent = colander.SchemaNode(colander.Boolean(), missing=False)


class LogListSchema(colander.SequenceSchema):
    """
    Validates format of list of log entries
    """

    log = LogSchema()
    validator = colander.Length(1)


class LogListPermanentSchema(colander.SequenceSchema):
    """
    Validates format of list of log entries
    """

    log = LogSchemaPermanent()
    validator = colander.Length(1)


class ViewRequestStatsSchema(RequestStatsSchema):
    requests = colander.SchemaNode(
        colander.Integer(), validator=colander.Range(0), missing=0
    )


class ViewMetricTupleSchema(colander.TupleSchema):
    """
    Validates list of views and their corresponding request stats object ie:
    ["dir/module:func",{"custom": 0.0..}]
    """

    view_name = colander.SchemaNode(
        colander.String(),
        preparer=[shortener_factory(128), lambda x: x or "unknown"],
        missing="unknown",
    )
    metrics = ViewRequestStatsSchema()


class ViewMetricListSchema(colander.SequenceSchema):
    """
    Validates view breakdown stats objects list
    {metrics key of server/time object}
    """

    view_tuple = ViewMetricTupleSchema()
    validator = colander.Length(1)


class ViewMetricSchema(colander.MappingSchema):
    """
    Validates server/timeinterval object, ie:
    {server/time object}

    """

    timestamp = colander.SchemaNode(NonTZDate(), validator=limited_date, missing=None)
    server = colander.SchemaNode(
        colander.String(),
        preparer=[shortener_factory(128), lambda x: x or "unknown"],
        missing="unknown",
    )
    metrics = ViewMetricListSchema()


class GeneralMetricSchema(colander.MappingSchema):
    """
    Validates universal metric schema

    """

    namespace = colander.SchemaNode(
        colander.String(), missing="", preparer=shortener_factory(128)
    )

    server_name = colander.SchemaNode(
        colander.String(),
        preparer=[shortener_factory(128), lambda x: x or "unknown"],
        missing="unknown",
    )
    timestamp = colander.SchemaNode(
        NonTZDate(), validator=limited_date, missing=deferred_utcnow
    )
    tags = TagSchemaList(missing=colander.required)


class GeneralMetricPermanentSchema(GeneralMetricSchema):
    """
    Validates universal metric schema

    """

    timestamp = colander.SchemaNode(NonTZDate(), missing=deferred_utcnow)


class GeneralMetricsListSchema(colander.SequenceSchema):
    metric = GeneralMetricSchema()
    validator = colander.Length(1)


class GeneralMetricsPermanentListSchema(colander.SequenceSchema):
    metric = GeneralMetricPermanentSchema()
    validator = colander.Length(1)


class MetricsListSchema(colander.SequenceSchema):
    """
    Validates list of metrics objects ie:
    [{server/time object}, ] part


    """

    metric = ViewMetricSchema()
    validator = colander.Length(1)


class StringToAppList(object):
    """
    Returns validated list of application ids from user query and
    set of applications user is allowed to look at
    transform string to list containing single integer
    """

    def serialize(self, node, appstruct):
        if appstruct is null:
            return null
        return appstruct

    def deserialize(self, node, cstruct):
        if cstruct is null:
            return null

        apps = set([int(a) for a in node.bindings["resources"]])

        if isinstance(cstruct, str):
            cstruct = [cstruct]

        cstruct = [int(a) for a in cstruct]

        valid_apps = list(apps.intersection(set(cstruct)))
        if valid_apps:
            return valid_apps
        return null

    def cstruct_children(self):
        return []


@colander.deferred
def possible_applications_validator(node, kw):
    possible_apps = [int(a) for a in kw["resources"]]
    return colander.All(colander.ContainsOnly(possible_apps), colander.Length(1))


@colander.deferred
def possible_applications(node, kw):
    return [int(a) for a in kw["resources"]]


@colander.deferred
def today_start(node, kw):
    return datetime.datetime.utcnow().replace(second=0, microsecond=0, minute=0, hour=0)


@colander.deferred
def today_end(node, kw):
    return datetime.datetime.utcnow().replace(
        second=0, microsecond=0, minute=59, hour=23
    )


@colander.deferred
def old_start(node, kw):
    t_delta = datetime.timedelta(days=90)
    return (
        datetime.datetime.utcnow().replace(second=0, microsecond=0, minute=0, hour=0)
        - t_delta
    )


@colander.deferred
def today_end(node, kw):
    return datetime.datetime.utcnow().replace(
        second=0, microsecond=0, minute=59, hour=23
    )


class PermissiveDate(colander.DateTime):
    """ Returns null for incorrect date format - also removes tz info"""

    def deserialize(self, node, cstruct):
        if not cstruct:
            return null

        try:
            result = colander.iso8601.parse_date(
                cstruct, default_timezone=self.default_tzinfo
            )
        except colander.iso8601.ParseError:
            return null
        return result.replace(tzinfo=None)


class LogSearchSchema(colander.MappingSchema):
    def schema_type(self, **kw):
        return colander.Mapping(unknown="preserve")

    resource = colander.SchemaNode(
        StringToAppList(),
        validator=possible_applications_validator,
        missing=possible_applications,
    )

    message = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String()),
        missing=None,
    )
    level = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String()),
        preparer=lowercase_preparer,
        missing=None,
    )
    namespace = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String()),
        missing=None,
    )
    request_id = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String()),
        missing=None,
    )
    start_date = colander.SchemaNode(PermissiveDate(), missing=None)
    end_date = colander.SchemaNode(PermissiveDate(), missing=None)
    page = colander.SchemaNode(
        colander.Integer(), validator=colander.Range(min=1), missing=1
    )


class ReportSearchSchema(colander.MappingSchema):
    def schema_type(self, **kw):
        return colander.Mapping(unknown="preserve")

    resource = colander.SchemaNode(
        StringToAppList(),
        validator=possible_applications_validator,
        missing=possible_applications,
    )
    request_id = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String()),
        missing=None,
    )
    start_date = colander.SchemaNode(PermissiveDate(), missing=None)
    end_date = colander.SchemaNode(PermissiveDate(), missing=None)
    page = colander.SchemaNode(
        colander.Integer(), validator=colander.Range(min=1), missing=1
    )

    min_occurences = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.Integer()),
        missing=None,
    )

    http_status = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.Integer()),
        missing=None,
    )
    priority = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.Integer()),
        missing=None,
    )
    error = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String()),
        missing=None,
    )
    url_path = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String()),
        missing=None,
    )
    url_domain = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String()),
        missing=None,
    )
    report_status = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String()),
        missing=None,
    )
    min_duration = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.Float()),
        missing=None,
    )
    max_duration = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.Float()),
        missing=None,
    )


class TagSchema(colander.MappingSchema):
    """
    Used in log search
    """

    name = colander.SchemaNode(colander.String(), validator=colander.Length(1, 32))
    value = colander.SchemaNode(
        colander.Sequence(accept_scalar=True),
        colander.SchemaNode(colander.String(), validator=colander.Length(1, 128)),
        missing=None,
    )
    op = colander.SchemaNode(
        colander.String(), validator=colander.Length(1, 128), missing=None
    )


class TagListSchema(colander.SequenceSchema):
    tag = TagSchema()


class RuleFieldType(object):
    """ Validator which succeeds if the value passed to it is one of
    a fixed set of values """

    def __init__(self, cast_to):
        self.cast_to = cast_to

    def __call__(self, node, value):
        try:
            if self.cast_to == "int":
                int(value)
            elif self.cast_to == "float":
                float(value)
            elif self.cast_to == "unicode":
                str(value)
        except:
            raise colander.Invalid(
                node, "Can't cast {} to {}".format(value, self.cast_to)
            )


def build_rule_schema(ruleset, check_matrix):
    """
    Accepts ruleset and a map of fields/possible operations and builds
    validation class
    """

    schema = colander.SchemaNode(colander.Mapping())
    schema.add(colander.SchemaNode(colander.String(), name="field"))

    if ruleset["field"] in ["__AND__", "__OR__", "__NOT__"]:
        subrules = colander.SchemaNode(colander.Tuple(), name="rules")
        for rule in ruleset["rules"]:
            subrules.add(build_rule_schema(rule, check_matrix))
        schema.add(subrules)
    else:
        op_choices = check_matrix[ruleset["field"]]["ops"]
        cast_to = check_matrix[ruleset["field"]]["type"]
        schema.add(
            colander.SchemaNode(
                colander.String(), validator=colander.OneOf(op_choices), name="op"
            )
        )

        schema.add(
            colander.SchemaNode(
                colander.String(), name="value", validator=RuleFieldType(cast_to)
            )
        )
    return schema


class ConfigTypeSchema(colander.MappingSchema):
    type = colander.SchemaNode(colander.String(), missing=None)
    config = colander.SchemaNode(UnknownType(), missing=None)


class MappingListSchema(colander.SequenceSchema):
    config = colander.SchemaNode(UnknownType())
