"""Utilities for validating user inputs such as metric names and parameter
names."""
import numbers
import os
import posixpath
import re

from segmind.exceptions import MlflowException
from segmind.lite_extensions.server_only.file_utils import _ROOT_DIR
from segmind.protos.errorcodes_pb2 import (INVALID_PARAMETER_VALUE,
                                           RESOURCE_DOES_NOT_EXIST)
# from segmind.store.db.db_types import DATABASE_ENGINES
from segmind.utils.string_utils import is_string_type

_VALID_PARAM_AND_METRIC_NAMES = re.compile(r'^[/\w.\- ]*$')

# Regex for valid run IDs: must be an alphanumeric string of length 1 to 256.
_RUN_ID_REGEX = re.compile(r'^[a-zA-Z0-9][\w\-]{0,255}$')

_EXPERIMENT_ID_REGEX = re.compile(r'^[a-zA-Z0-9][\w\-]{0,63}$')

_BAD_CHARACTERS_MESSAGE = (
    'Names may only contain alphanumerics, underscores (_), dashes (-), '
    'periods (.), spaces ( ), and slashes (/).')

MAX_PARAMS_TAGS_PER_BATCH = 100
MAX_METRICS_PER_BATCH = 1000
MAX_ENTITIES_PER_BATCH = 1000
MAX_BATCH_LOG_REQUEST_SIZE = int(1e6)
MAX_PARAM_VAL_LENGTH = 250
MAX_TAG_VAL_LENGTH = 5000
MAX_EXPERIMENT_TAG_KEY_LENGTH = 250
MAX_EXPERIMENT_TAG_VAL_LENGTH = 5000
MAX_ENTITY_KEY_LENGTH = 250

_UNSUPPORTED_DB_TYPE_MSG = 'Supported database engines are segmind_track rest'


def bad_path_message(name):
    return (
        'Names may be treated as files in certain cases, and must not resolve'
        "to other names when treated as such. This name would resolve to '%s'"
    ) % posixpath.normpath(name)


def path_not_unique(name):
    norm = posixpath.normpath(name)
    return norm != name or norm == '.' or norm.startswith(
        '..') or norm.startswith('/')


def _validate_metric_name(name):
    """Check that `name` is a valid metric name and raise an exception if it
    isn't."""
    if not _VALID_PARAM_AND_METRIC_NAMES.match(name):
        raise MlflowException(
            "Invalid metric name: '%s'. %s" % (name, _BAD_CHARACTERS_MESSAGE),
            INVALID_PARAMETER_VALUE)
    if path_not_unique(name):
        raise MlflowException(
            "Invalid metric name: '%s'. %s" % (name, bad_path_message(name)),
            INVALID_PARAMETER_VALUE)


def _validate_metric(key, value, timestamp, step):
    """Check that a param with the specified key, value, timestamp is valid and
    raise an exception if it isn't."""
    _validate_metric_name(key)
    if not isinstance(value, numbers.Number):
        raise MlflowException(
            "Got invalid value %s for metric '%s' (timestamp=%s). Please "
            'specify value as a valid double (64-bit floating point)'
            % (value, key, timestamp), INVALID_PARAMETER_VALUE)

    if not isinstance(timestamp, numbers.Number) or timestamp < 0:
        raise MlflowException(
            "Got invalid timestamp %s for metric '%s' (value=%s). Timestamp "
            'must be a nonnegative long (64-bit integer) '
            % (timestamp, key, value), INVALID_PARAMETER_VALUE)

    if not isinstance(step, numbers.Number):
        raise MlflowException(
            "Got invalid step %s for metric '%s' (value=%s). Step must be a "
            'valid long (64-bit integer).'
            % (step, key, value), INVALID_PARAMETER_VALUE)


def _validate_param(key, value):
    """Check that a param with the specified key & value is valid and raise an
    exception if it isn't."""
    _validate_param_name(key)
    _validate_length_limit('Param key', MAX_ENTITY_KEY_LENGTH, key)
    _validate_length_limit('Param value', MAX_PARAM_VAL_LENGTH, value)


def _validate_artifact(key, path):
    """Check that an artifact with the specified key & valid and raise an
    exception if it isn't."""
    _validate_artifact_name(key)
    _validate_artifact_path(path)


def _validate_tag(key, value):
    """Check that a tag with the specified key & value is valid and raise an
    exception if it isn't."""
    _validate_tag_name(key)
    _validate_length_limit('Tag key', MAX_ENTITY_KEY_LENGTH, key)
    _validate_length_limit('Tag value', MAX_TAG_VAL_LENGTH, value)


def _validate_experiment_tag(key, value):
    """Check that a tag with the specified key & value is valid and raise an
    exception if it isn't."""
    _validate_tag_name(key)
    _validate_length_limit('Tag key', MAX_EXPERIMENT_TAG_KEY_LENGTH, key)
    _validate_length_limit('Tag value', MAX_EXPERIMENT_TAG_VAL_LENGTH, value)


def _validate_param_name(name):
    """Check that `name` is a valid parameter name and raise an exception if it
    isn't."""
    if not _VALID_PARAM_AND_METRIC_NAMES.match(name):
        raise MlflowException(
            "Invalid parameter name: '%s'. %s" %
            (name, _BAD_CHARACTERS_MESSAGE), INVALID_PARAMETER_VALUE)
    if path_not_unique(name):
        raise MlflowException(
            "Invalid parameter name: '%s'. %s" %
            (name, bad_path_message(name)), INVALID_PARAMETER_VALUE)


def _validate_artifact_name(name):
    """Check that `name` is a valid artifact name and raise an exception if it
    isn't.

    # TODO: Warn the user if artifact with same `name` already exists in the
    current run.
    """


def _validate_artifact_path(path):
    """Check that `path` is a valid artifact local path and raise an exception
    if is isn't."""


def _validate_tag_name(name):
    """Check that `name` is a valid tag name and raise an exception if it
    isn't."""
    # Reuse param & metric check.
    if not _VALID_PARAM_AND_METRIC_NAMES.match(name):
        raise MlflowException(
            "Invalid tag name: '%s'. %s" % (name, _BAD_CHARACTERS_MESSAGE),
            INVALID_PARAMETER_VALUE)
    if path_not_unique(name):
        raise MlflowException(
            "Invalid tag name: '%s'. %s" % (name, bad_path_message(name)),
            INVALID_PARAMETER_VALUE)


def _validate_length_limit(entity_name, limit, value):
    if len(value) > limit:
        raise MlflowException(
            "%s '%s' had length %s, which exceeded length limit of %s" %
            (entity_name, value[:250], len(value), limit))


def _validate_run_id(run_id):
    """Check that `run_id` is a valid run ID and raise an exception if it
    isn't."""
    if _RUN_ID_REGEX.match(run_id) is None:
        raise MlflowException(
            "Invalid run ID: '%s'" % run_id,
            error_code=INVALID_PARAMETER_VALUE)


def _validate_run_id_exp_id(exp_id, run_id):
    """Check that `run_id` is a valid run ID and raise an exception if it
    isn't."""
    if _RUN_ID_REGEX.match(run_id) is None:
        raise MlflowException(
            "Invalid run ID: '%s'" % run_id,
            error_code=INVALID_PARAMETER_VALUE)

    if _EXPERIMENT_ID_REGEX.match(exp_id) is None:
        raise MlflowException(
            "Invalid exp ID: '%s'" % run_id,
            error_code=INVALID_PARAMETER_VALUE)

    if not os.path.isdir(os.path.join(_ROOT_DIR, exp_id, run_id)):
        raise MlflowException(
            'RunId: {} doesnot exist under expID: {}'.format(run_id, exp_id),
            error_code=RESOURCE_DOES_NOT_EXIST)


def _validate_experiment_id(exp_id):
    """Check that `experiment_id`is a valid string or None, raise an exception
    if it isn't."""
    if exp_id is not None and _EXPERIMENT_ID_REGEX.match(exp_id) is None:
        raise MlflowException(
            "Invalid experiment ID: '%s'" % exp_id,
            error_code=INVALID_PARAMETER_VALUE)


def _validate_batch_limit(entity_name, limit, length):
    if length > limit:
        error_msg = (
            'A batch logging request can contain at most {limit} {name}. '
            'Got {count} {name}. Please split up {name} across multiple '
            'requests and try again.').format(
                name=entity_name, count=length, limit=limit)
        raise MlflowException(error_msg, error_code=INVALID_PARAMETER_VALUE)


def _validate_batch_log_limits(metrics, params, tags):
    """Validate that the provided batched logging arguments are within expected
    limits."""
    _validate_batch_limit(
        entity_name='metrics',
        limit=MAX_METRICS_PER_BATCH,
        length=len(metrics))
    _validate_batch_limit(
        entity_name='params',
        limit=MAX_PARAMS_TAGS_PER_BATCH,
        length=len(params))
    _validate_batch_limit(
        entity_name='tags', limit=MAX_PARAMS_TAGS_PER_BATCH, length=len(tags))
    total_length = len(metrics) + len(params) + len(tags)
    _validate_batch_limit(
        entity_name='metrics, params, and tags',
        limit=MAX_ENTITIES_PER_BATCH,
        length=total_length)


def _validate_batch_log_data(metrics, params, tags):
    for metric in metrics:
        _validate_metric(metric.key, metric.value, metric.timestamp,
                         metric.step)
        # TODO: move _validate_length_limit calls into _validate_metric etc.
        # This would be a breaking change as _validate_metric is also used in
        # the single-entry log_metric API. Thus we defer it for now to allow
        # for a release of the batched logging APIs without breaking
        # changes to other APIs. See related discussion in
        # https://github.com/mlflow/mlflow/issues/985
        _validate_length_limit('Metric name', MAX_ENTITY_KEY_LENGTH,
                               metric.key)
    for param in params:
        _validate_param(param.key, param.value)
    for tag in tags:
        _validate_tag(tag.key, tag.value)


def _validate_batch_log_api_req(json_req):
    if len(json_req) > MAX_BATCH_LOG_REQUEST_SIZE:
        error_msg = (
            'Batched logging API requests must be at most {limit} bytes, got '
            'a request of size {size}.').format(
                limit=MAX_BATCH_LOG_REQUEST_SIZE, size=len(json_req))
        raise MlflowException(error_msg, error_code=INVALID_PARAMETER_VALUE)


def _validate_experiment_name(experiment_name):
    """Check that `experiment_name` is a valid string and raise an exception if
    it isn't."""
    if experiment_name == '' or experiment_name is None:
        raise MlflowException(
            "Invalid experiment name: '%s'" % experiment_name,
            error_code=INVALID_PARAMETER_VALUE)

    if not is_string_type(experiment_name):
        raise MlflowException(
            'Invalid experiment name: %s. Expects a string.'
            % experiment_name, error_code=INVALID_PARAMETER_VALUE)


def _validate_experiment_artifact_location(artifact_location):
    if artifact_location is not None and artifact_location.startswith('runs:'):
        raise MlflowException(
            "Artifact location cannot be a runs:/ URI. Given: '%s'" %
            artifact_location,
            error_code=INVALID_PARAMETER_VALUE)


def _validate_db_type_string(db_type):
    # """validates db_type parsed from DB URI is supported."""
    if db_type not in DATABASE_ENGINES:  # noqa: F821
        error_msg = "Invalid database engine: '%s'. '%s'" % (
            db_type, _UNSUPPORTED_DB_TYPE_MSG)
        raise MlflowException(error_msg, INVALID_PARAMETER_VALUE)
