"""Module for containing CLI convenience functions"""
from __future__ import print_function

import logging
import subprocess
import tempfile
import time
from enum import Enum

from typing import List, Tuple, Union

import requests

from amplify_aws_utils.resource_helper import Jitter
from aws_requests_auth.boto_utils import BotoAWSRequestsAuth

from terrawrap.utils.git_utils import get_git_root

logger = logging.getLogger(__name__)

MAX_RETRIES = 5
RETRIABLE_ERRORS = [
    'RequestError: send request failed',
    'unexpected EOF',
    'Throttling',
    'timeout while waiting for state',
    'ServiceUnavailable: Service Unavailable',
    'failed to decode query XML error response',
    'connection reset',
    'Connection reset',
    'Please try again.',
    'Client.Timeout exceeded',
    'Request limit for operation',
    'try again later',
    'handshake timeout',
    'SSL_ERROR_SYSCALL',
    'Api Rate Limit Exceeded',
    'TooManyUpdates',
]
AUDIT_POST_PATH = '/audit_info'
AUDIT_UPDATE_PATH = '/update_audit_info'


class Status(str, Enum):
    """Enum for status of execute_command"""
    SUCCESS = 'SUCCESS'
    IN_PROGRESS = 'IN PROGRESS'
    FAILED = 'FAILED'


def execute_command(
        args: Union[List[str], str],
        *pargs,
        print_output: bool = True,
        capture_stderr: bool = True,
        print_command: bool = False,
        retry: bool = False,
        timeout: int = 15 * 60,
        audit_api_url: str = None,
        **kwargs
) -> Tuple[int, List[str]]:
    """
    Convenience function for executing a given command and optionally printing the output.
    :param args: List of arguments to execute, or a single string.
    :param pargs: Any additional positional arguments to Popen.
    :param print_output: True if the output of the command should be printed immediately. Defaults to True.
    :param capture_stderr: True if stderr should be captured. Defaults to True.
    :param print_command: True if the command should be printed before executing. Defaults to False.
    :param timeout: Max amount of time to keep retrying to execute command. Defaults to 15 minutes.
    :param retry: Retry a number of times if network errors. Defaults to False.
    :param audit_api_url: Audit API URL to submit POST request. Defaults to None so no data is sent.
    :param kwargs: Any additional keyword arguments to Popen.
    :return: A tuple of the exit code and output of the command.
    """
    try_count = 0

    # It's possible for an envvar to be set to none, so exclude those envvars.
    if 'env' in kwargs:
        kwargs['env'] = {
            key: value
            for key, value in kwargs['env'].items()
            if value is not None
        }

    # Get time - nanoseconds since epoch
    start_time = int(time.time())

    if audit_api_url and kwargs['cwd'] and 'apply' in args:
        # Call _post_audit_info for working directory, setting status to 'in progress'
        _post_audit_info(
            audit_api_url=audit_api_url,
            path=kwargs['cwd'],
            start_time=start_time,
        )
    else:
        logger.info('No audit_api_url provided')

    jitter = Jitter()
    time_passed = 0
    exit_code = 0
    stdout: List[str] = []
    while try_count < MAX_RETRIES if retry else 1:
        exit_code, stdout = _execute_command(
            args,
            print_output,
            capture_stderr,
            print_command,
            *pargs,
            **kwargs,
        )

        try_count += 1

        network_errors = _get_retriable_errors(stdout)
        if exit_code != 0 and network_errors and retry:
            logger.warning('Found network errors while running %s command: %s', args, network_errors)
        else:
            # The command either succeeded or failed with a non network error. don't retry
            break

        if time_passed >= timeout:
            break

        time_passed = jitter.backoff()

    if audit_api_url and kwargs['cwd'] and 'apply' in args:
        # Call _post_audit_info again, this time to update the 'in progress' entry with new status and output
        _post_audit_info(
            audit_api_url=audit_api_url,
            path=kwargs['cwd'],
            exit_code=exit_code,
            stdout=stdout,
            start_time=start_time,
            update=True
        )
    else:
        logger.info('No audit_api_url provided')

    if time_passed >= timeout:
        raise TimeoutError(f'Timed out retrying {args} command')

    return exit_code, stdout


def _execute_command(
        args: Union[List[str], str],
        print_output: bool,
        capture_stderr: bool,
        print_command: bool,
        *pargs,
        **kwargs
) -> Tuple[int, List[str]]:
    """
    Private function for executing a given command and optionally printing the output.
    :param args: List of arguments to execute, or a single string.
    :param print_output: True if the output of the command should be printed immediately. Defaults to True.
    :param capture_stderr: True if stderr should be captured. Defaults to True.
    :param print_command: True if the command should be printed before executing. Defaults to False.
    :param pargs: Any additional positional arguments to Popen.
    :param kwargs: Any additional keyword arguments to Popen.
    :return: A tuple of the exit code and output of the command.
    """
    stdout_write, stdout_path = tempfile.mkstemp()
    with open(stdout_path, "rb") as stdout_read, open('/dev/null', 'w', encoding='utf-8') as dev_null:

        if print_command:
            print(f"Executing: {' '.join(args)}")

        kwargs['stdout'] = stdout_write
        kwargs['stderr'] = stdout_write if capture_stderr else dev_null

        # pylint: disable=consider-using-with
        process = subprocess.Popen(
            args,
            *pargs,
            **kwargs
        )

        while True:
            output = stdout_read.read(1).decode(errors="replace")

            if output == '' and process.poll() is not None:
                break

            if print_output and output:
                print(output, end="", flush=True)

        exit_code = process.poll()

        stdout_read.seek(0)
        stdout = [line.decode(errors="replace") for line in stdout_read.readlines()]

        # ignoring mypy error below because it thinks exit_code can sometimes be None
        # we know that will never be the case because the above While loop will keep looping forever
        # until exit_code is not None
        return exit_code, stdout  # type: ignore


def _get_retriable_errors(out: List[str]) -> List[str]:
    """Filter line output for retriable errors"""
    return [
        line for line in out
        if any(error in line for error in RETRIABLE_ERRORS)
    ]


def _post_audit_info(
        audit_api_url: str,
        path: str,
        start_time: int,
        exit_code: int = None,
        stdout: List[str] = None,
        update: bool = False
):
    root = get_git_root(path)
    path = path.replace(root, '')

    status = Status.IN_PROGRESS if exit_code is None else (
        Status.SUCCESS if exit_code == 0 else Status.FAILED
    )

    logger.info('Attempting to send data to Audit API: %s - %s', path, status)

    url = (audit_api_url + AUDIT_UPDATE_PATH) if update else (audit_api_url + AUDIT_POST_PATH)

    auth = BotoAWSRequestsAuth(
        aws_host='terraform-audit-api.devops.amplify.com',
        aws_region='us-west-2',
        aws_service='execute-api'
    )

    stdout_str = ''.join(stdout) if stdout else ''

    try:
        requests.post(
            url=url,
            auth=auth,
            json={
                'directory': path,
                'start_time': start_time,
                'status': status,
                'output': stdout_str
            },
            timeout=30,
        )
        logger.info('Successfully posted data to provided url: %s', audit_api_url)
    except requests.exceptions.RequestException:
        logger.error("Unable to post data to provided url: %s", audit_api_url)
