# Copyright 2018 The Kubeflow 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 abc
import logging
import re
import os
import time
from functools import wraps
from typing import Any, Callable, Optional, Tuple

def normalize_name(name,
              valid_first_char_pattern='a-zA-Z',
              valid_char_pattern='0-9a-zA-Z_',
              invalid_char_placeholder='_',
              prefix_placeholder='x_'):
        """Normalize a name to a valid resource name.

        Uses ``valid_first_char_pattern`` and ``valid_char_pattern`` regex pattern
        to find invalid characters from ``name`` and replaces them with 
        ``invalid_char_placeholder`` or prefix the name with ``prefix_placeholder``.

        Args:
             name: The name to be normalized.
             valid_first_char_pattern: The regex pattern for the first character.
             valid_char_pattern: The regex pattern for all the characters in the name.
             invalid_char_placeholder: The placeholder to replace invalid characters.
             prefix_placeholder: The placeholder to prefix the name if the first char 
                is invalid.
        
        Returns:
            The normalized name. Unchanged if all characters are valid.
        """
        if not name:
            return name
        normalized_name = re.sub('[^{}]+'.format(valid_char_pattern), 
            invalid_char_placeholder, name)
        if not re.match('[{}]'.format(valid_first_char_pattern), 
            normalized_name[0]):
            normalized_name = prefix_placeholder + normalized_name
        if name != normalized_name:
            logging.info('Normalize name from "{}" to "{}".'.format(
                name, normalized_name))
        return normalized_name

def dump_file(path, content):
    """Dumps string into local file.

    Args:
        path: the local path to the file.
        content: the string content to dump.
    """
    directory = os.path.dirname(path)
    if not os.path.exists(directory):
        os.makedirs(directory)
    elif os.path.exists(path):
        logging.warning('The file {} will be overwritten.'.format(path))
    with open(path, 'w') as f:
            f.write(content)

def check_resource_changed(requested_resource, 
    existing_resource, property_names):
    """Check if a resource has been changed.

    The function checks requested resource with existing resource
    by comparing specified property names. Check fails if any property
    name in the list is in ``requested_resource`` but its value is
    different with the value in ``existing_resource``.

    Args:
        requested_resource: the user requested resource paylod.
        existing_resource: the existing resource payload from data storage.
        property_names: a list of property names.

    Return:
        True if ``requested_resource`` has been changed.
    """
    for property_name in property_names:
        if not property_name in requested_resource:
            continue
        existing_value = existing_resource.get(property_name, None)
        if requested_resource[property_name] != existing_value:
            return True
    return False

def wait_operation_done(get_operation, wait_interval):
    """Waits for an operation to be done.

    Args:
        get_operation: the name of the operation.
        wait_interval: the wait interview between pulling job
            status.

    Returns:
        The completed operation.
    """
    while True:
        operation = get_operation()
        operation_name = operation.get('name')
        done = operation.get('done', False)
        if not done:
            logging.info('Operation {} is not done. Wait for {}s.'.format(
                operation_name, wait_interval))
            time.sleep(wait_interval)
            continue
        error = operation.get('error', None)
        if error:
            raise RuntimeError('Failed to complete operation {}: {} {}'.format(
                operation_name,
                error.get('code', 'Unknown code'),
                error.get('message', 'Unknown message'),
            ))
        return operation


def with_retries(
    func: Callable,
    on_error: Optional[Callable[[], Any]] = None,
    errors: Tuple[Exception, ...] = Exception,
    number_of_retries: int = 5,
    delay: float = 1,
):
    """Retry decorator.

    The decorator catches `errors`, calls `on_error` and retries after waiting `delay` seconds.

    Args:
        number_of_retries (int): Total number of retries if error is raised.
        delay (float): Number of seconds to wait between consecutive retries.
    """

    @wraps(func)
    def wrapper(self, *args, **kwargs):
        remaining_retries = number_of_retries
        while remaining_retries:
            try:
                return func(self, *args, **kwargs)
            except errors as e:
                remaining_retries -= 1
                if not remaining_retries:
                    raise

                logging.warning(
                    'Caught {}. Retrying in {} seconds...'.format(
                        e.__class__.__name__, delay
                    )
                )

                time.sleep(delay)
                if on_error:
                    on_error()

    return wrapper


class ClientWithRetries:

    def __init__(self):
        self._build_client()
        for name, member in self.__dict__.items():
            if callable(member) and not name.startswith("_"):
                self.__dict__[name] = with_retries(func=member, errors=(BrokenPipeError, IOError), on_error=self._build_client)

    @abc.abstractmethod
    def _build_client(self):
        raise NotImplementedError()
