# Copyright (c) Cloud Linux Software, Inc
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT

import functools
import json
import os
import time

from . import config, constants, log_utils, utils
from .py23 import json_loads_nstr

if False:  # pragma: no cover
    from typing import Any, Callable, Dict  # noqa: F401

STATUS_CHANGE_GAP_DELAY = 5 * 60  # 5 minute


def touch_status_gap_file(filename='.kcarestatus'):
    status_filepath = os.path.join(constants.PATCH_CACHE, filename)
    utils.atomic_write(status_filepath, utils.timestamp_str())


def status_gap_passed(filename='.kcarestatus'):
    status_filepath = os.path.join(constants.PATCH_CACHE, filename)
    if os.path.isfile(status_filepath):
        with open(status_filepath, 'r') as sfile:
            try:
                timestamp = int(sfile.read())
                if int(timestamp) + config.STATUS_CHANGE_GAP + STATUS_CHANGE_GAP_DELAY > time.time():
                    return False
            except Exception:
                pass
    return True


def _check_component(component):
    # type: (str) -> None
    if component not in ('kernel', 'libcare'):
        raise ValueError('Unknown update status component: {0}'.format(component))


def _load_update_status():
    # type: () -> Dict[str, Any]
    content = utils.read_file(constants.UPDATE_STATUS_PATH)
    if content is None:
        return {}

    try:
        result = json_loads_nstr(content)  # type: Dict[str, Any]
        return result
    except (ValueError, TypeError):
        log_utils.kcarelog.warning('Failed to parse update status file')
        return {}


def save_update_status(component, error):
    # type: (str, str) -> None
    _check_component(component)
    try:
        data = _load_update_status()
        data[component] = {
            'error': error,
            'timestamp': int(time.time()),
        }
        utils.atomic_write(constants.UPDATE_STATUS_PATH, json.dumps(data))
    except Exception:
        log_utils.kcarelog.warning('Failed to save update status', exc_info=True)


def _error_status(err):
    # type: (Exception) -> str
    # Prefer the short, fixed status label that KcareError subclasses set
    # (e.g. 'bad signature') for compact, groupable telemetry. When the status
    # is already echoed in the message (HTTPError's "HTTP Error 414: ...") the
    # message is the more useful value. When a numeric HTTP status is *not* in
    # the message, compose a readable "HTTP Error: <code>" rather than a bare
    # code. str() also keeps an int status subscriptable on read-back.
    message = str(err)
    status = str(getattr(err, 'status', ''))
    if status and status not in message:
        if status.isdigit():
            return 'HTTP Error: {0}'.format(status)

        return status

    return message


def track_update_status(component):
    # type: (str) -> Callable[..., Any]
    _check_component(component)

    def decorator(fn):  # type: (Callable[..., Any]) -> Callable[..., Any]
        @functools.wraps(fn)
        def inner(*args, **kwargs):  # type: (Any, Any) -> Any
            try:
                result = fn(*args, **kwargs)
            except Exception as err:
                save_update_status(component, error=_error_status(err))
                raise

            save_update_status(component, error='')
            return result

        return inner

    return decorator


def read_update_error(component):
    # type: (str) -> str
    _check_component(component)
    try:
        data = _load_update_status()
        # str() guards against status files written by older clients that
        # persisted a non-string error (e.g. an int HTTP status like 414).
        error = data.get(component, {}).get('error', '')  # type: Any
        return str(error)[: constants.UPDATE_ERROR_MAX_LENGTH]
    except Exception:
        log_utils.kcarelog.warning('Failed to read update status', exc_info=True)
        return ''
