"""Validation schemas for API requests."""
import werkzeug.datastructures
from flask_restplus import inputs
import api.reqparse as reqparse

MAX_PASSWORD_LENGTH = 1024


def object_type(value):
    """To make the openAPI object type show up in the docs."""
    return value


object_type.__schema__ = {"type": "object"}  # noqa


def length_restricted(min_length, max_length, base_type):
    """Type to restrict string length."""

    def validate(s):
        if len(s) < min_length:
            raise ValueError("Must be at least %i characters long" % min_length)
        if len(s) > max_length:
            raise ValueError("Must be at most %i characters long" % max_length)
        return s

    return validate


# Achievement request schema
achievement_req = reqparse.RequestParser()
achievement_req.add_argument(
    "name",
    required=True,
    type=str,
    location="json",
    help="Name of the achievement.",
    error="Achievement name is required",
)
achievement_req.add_argument(
    "score",
    required=True,
    type=inputs.natural,
    location="json",
    help="Point value of the achievement (positive integer).",
    error="The achievement's score must be a positive integer",
)
achievement_req.add_argument(
    "description",
    required=True,
    type=str,
    location="json",
    help="Description of the achievement.",
    error="Achievement description is required",
)
achievement_req.add_argument(
    "processor",
    required=True,
    type=str,
    location="json",
    help="Path to the achievement processor.",
    error="Achievement processor path is required",
)
achievement_req.add_argument(
    "hidden",
    required=True,
    type=inputs.boolean,
    location="json",
    help="Hide this achievement?",
    error="Specify whether this achievement should be hidden (true/false)",
)
achievement_req.add_argument(
    "image",
    required=True,
    type=str,
    location="json",
    help="Path to achievement image.",
    error="Achievement image path is required",
)
achievement_req.add_argument(
    "smallimage",
    required=True,
    type=str,
    location="json",
    help="Path to achievement thumbnail.",
    error="Achievement thumbnail path is required",
)
achievement_req.add_argument(
    "disabled",
    required=True,
    type=inputs.boolean,
    location="json",
    help="Disable this achievement?",
    error="Specify whether this achievement should be disabled (true/false)",
)
achievement_req.add_argument(
    "multiple",
    required=True,
    type=inputs.boolean,
    location="json",
    help="Allow earning multiple instances of this achievement?",
    error="Specify whether this achievement can be earned multiple times "
    + "(true/false)",
)
achievement_patch_req = achievement_req.copy()
for arg in achievement_patch_req.args:
    arg.required = False

# Shell server output schema
# (This is too complex for reqparse to really handle, so we'll trust it.
#  If we move to another validation engine e.g. marshmallow, we can revisit.)
shell_server_out = reqparse.RequestParser()
shell_server_out.add_argument(
    "sid",
    required=True,
    type=str,
    location="args",
    help="Shell server ID.",
    error="Shell server ID is required",
)
shell_server_out.add_argument(
    "problems",
    required=False,
    type=list,
    location="json",
    error="Problems array is invalid",
)
shell_server_out.add_argument(
    "bundles",
    required=False,
    type=list,
    location="json",
    error="Bundles array is invalid",
)

# Problem PATCH request schema
# ("disabled" is the only mutable field as the others are managed by the
#  shell manager.)
problem_patch_req = reqparse.RequestParser()
problem_patch_req.add_argument(
    "disabled",
    required=True,
    type=inputs.boolean,
    location="json",
    help="Whether the problem is disabled.",
    error="Specify whether the problem is disabled",
)

# Shell server list request schema
shell_server_list_req = reqparse.RequestParser()
shell_server_list_req.add_argument(
    "assigned_only",
    required=False,
    type=inputs.boolean,
    default=True,
    location="args",
    help="Whether to include only shell servers assigned to the"
    + " current user. Must be admin to disable.",
    error="Specify a boolean value for assigned_only",
)

# Shell server request schema
shell_server_req = reqparse.RequestParser()
shell_server_req.add_argument(
    "name",
    required=True,
    type=str,
    location="json",
    help="Shell server display name.",
    error="Shell server display name is required",
)
shell_server_req.add_argument(
    "host",
    required=True,
    type=str,
    location="json",
    help="Shell server hostname.",
    error="Shell server hostname is required",
)
shell_server_req.add_argument(
    "port",
    required=True,
    type=inputs.int_range(1, 65535),
    location="json",
    help="Shell server port.",
    error="Shell server port is required (1-65535)",
)
shell_server_req.add_argument(
    "username",
    required=True,
    type=str,
    location="json",
    help="Username.",
    error="Shell server username is required",
)
shell_server_req.add_argument(
    "password",
    required=True,
    type=str,
    location="json",
    help="Password.",
    error="Shell server password is required",
)
shell_server_req.add_argument(
    "protocol",
    required=True,
    type=str,
    choices=["HTTP", "HTTPS"],
    location="json",
    help="Protocol used to serve web resources.",
    error="Shell server protocol is required (HTTP/HTTPS)",
)
shell_server_req.add_argument(
    "server_number",
    required=False,
    type=inputs.positive,
    location="json",
    help="Server number (will be automatically assigned if not provided).",
    error="Shell server number must be a positive integer",
)
shell_server_patch_req = shell_server_req.copy()
for arg in shell_server_patch_req.args:
    arg.required = False

# Shell server reassignment schema
shell_server_reassignment_req = reqparse.RequestParser()
shell_server_reassignment_req.add_argument(
    "include_assigned",
    required=False,
    type=inputs.boolean,
    location="json",
    help="Whether to update the assignments of teams that already have "
    + "an assigned shell server.",
    error="Specify a boolean value for include_assigned",
)

# Exception request schema
exception_req = reqparse.RequestParser()
exception_req.add_argument(
    "result_limit",
    required=False,
    type=inputs.positive,
    default=50,
    location="args",
    help="Maximum number of exceptions to return",
    error="result_limit must be a positive integer",
)

# Settings update schema
# @TODO: this is very basic - config.py:change_settings() does the brunt of
# the validation work for now because of RequestParser's limitations
# regarding nested fields. Revisit this when upgrading to a
# better validation library.
settings_patch_req = reqparse.RequestParser()
settings_patch_req.add_argument(
    "enable_feedback",
    required=False,
    type=inputs.boolean,
    location="json",
    error="enable_feedback must be a boolean",
)
settings_patch_req.add_argument(
    "start_time",
    required=False,
    type=inputs.datetime_from_rfc822,
    location="json",
    error="start_time must be an RFC 822 timestamp",
)
settings_patch_req.add_argument(
    "end_time",
    required=False,
    type=inputs.datetime_from_rfc822,
    location="json",
    error="end_time must be an RFC 822 timestamp",
)
settings_patch_req.add_argument(
    "competition_name",
    required=False,
    type=str,
    location="json",
    error="competition_name must be a string",
)
settings_patch_req.add_argument(
    "competition_url",
    required=False,
    type=str,
    location="json",
    error="competition_url must be a string",
)
settings_patch_req.add_argument(
    "email_filter", required=False, type=list, location="json"
)
settings_patch_req.add_argument(
    "max_team_size",
    required=False,
    type=inputs.natural,
    location="json",
    error="max_team_size must be a positive integer",
)
settings_patch_req.add_argument(
    "achievements", required=False, type=object_type, location="json"
)
settings_patch_req.add_argument(
    "username_blacklist", required=False, type=list, location="json"
)
settings_patch_req.add_argument(
    "email", required=False, type=object_type, location="json"
)
settings_patch_req.add_argument(
    "captcha", required=False, type=object_type, location="json"
)
settings_patch_req.add_argument(
    "logging", required=False, type=object_type, location="json"
)
settings_patch_req.add_argument(
    "shell_servers", required=False, type=object_type, location="json"
)
settings_patch_req.add_argument(
    "max_batch_registrations",
    required=False,
    type=inputs.natural,
    location="json",
    error="max_batch_registrations must be " + "a nonnegative integer",
)
settings_patch_req.add_argument(
    "enable_rate_limiting", required=False, type=inputs.boolean, location="json"
)
settings_patch_req.add_argument(
    "group_limit",
    required=False,
    type=inputs.natural,
    location="json",
    error="group_limit must be a nonnegative integer",
)

# Bundle PATCH request schema
# ("dependencies_enabled" is the only mutable field as the others are managed
#  by the shell manager.)
bundle_patch_req = reqparse.RequestParser()
bundle_patch_req.add_argument(
    "dependencies_enabled",
    required=True,
    type=inputs.boolean,
    location="json",
    help="Whether to consider this bundle's dependencies when determining "
    + "unlocked problems.",
    error="Specify a boolean value for dependencies_enabled",
)

# Optional parameters for problems request
problems_req = reqparse.RequestParser()
problems_req.add_argument(
    "unlocked_only",
    required=False,
    location="args",
    default=True,
    help="Whether to display only problems unlocked for your team or "
    + "all matching problems. Must be teacher/admin to disable, unless "
    + "count_only=True. "
    + "If disabled as a teacher account, will only return name, "
    + "category, and score for each problem.",
    type=inputs.boolean,
    error="Specify a boolean value for unlocked_only",
)
problems_req.add_argument(
    "solved_only",
    required=False,
    location="args",
    default=False,
    help="Restrict results to problems solved by your team.",
    type=inputs.boolean,
    error="Specify a boolean value for solved_only",
)
problems_req.add_argument(
    "count_only",
    required=False,
    location="args",
    default=False,
    help="Whether to return only the count of matching problems.",
    type=inputs.boolean,
    error="Specify a boolean value for count_only",
)
problems_req.add_argument(
    "category",
    required=False,
    location="args",
    default=None,
    help="Restrict results to a specific category.",
    type=str,
    error="Category to filter on must be a string",
)
problems_req.add_argument(
    "include_disabled",
    required=False,
    location="args",
    default=False,
    help="Whether to include disabled problems.",
    type=inputs.boolean,
    error="Specify a boolean value for include_disabled",
)

# Submission request
submission_req = reqparse.RequestParser()
submission_req.add_argument(
    "pid",
    required=True,
    type=str,
    location="json",
    help="ID of the attempted problem",
    error="Problem ID is required",
)
submission_req.add_argument(
    "key",
    required=True,
    type=str,
    location="json",
    help="Flag for the problem",
    error="Flag is required",