"""Restic Exception Classes"""

from typing import Union
import os
import re
import platform
import logging
from signal import Signals, SIGPIPE
from subprocess import CalledProcessError, CompletedProcess

LOCK_RE = re.compile(r'locked (?:exclusively )?by PID (\d+) on ([\w\.\-]+) by')


class ResticError(Exception):
    """Base class for restic errors

    Args:
        proc (CalledProcessError | CompletedProcess): subprocess result
    """

    __module__ = 'restic'

    def __init__(self, proc: Union[CalledProcessError, CompletedProcess]):
        if isinstance(proc.stderr, bytes):
            stderr = str(proc.stderr, 'utf-8')
        else:
            stderr = proc.stderr
        super().__init__(stderr)
        self.stderr = stderr
        self.returncode = proc.returncode

    def __new__(cls, proc: Union[CalledProcessError, CompletedProcess]):
        if cls is not ResticError:
            return Exception.__new__(cls)
        if isinstance(proc.stderr, bytes):
            stderr = str(proc.stderr, 'utf-8').strip()
        else:
            stderr = proc.stderr.strip()
        if (
            'dial tcp' in stderr
            or 'TLS handshake timeout' in stderr
            or '502 Bad Gateway' in stderr
        ):
            return Exception.__new__(ResticConnError)
        if 'The access key ID you provided does not exist' in stderr:
            return Exception.__new__(ResticAccessError)
        if (
            'Stat: The specified key does not exist' in stderr
            or 'The specified bucket does not exist' in stderr
        ):
            return Exception.__new__(ResticInitError)
        if 'unable to create lock in backend' in stderr:
            return Exception.__new__(ResticLockedError)
        index_errs = (
            'repair index',
            'rebuild-index',
            'unable to load index',
            'returned error, retrying after',
            'Load(<index',
            'Load(<lock',
        )
        if any(x in stderr for x in index_errs) or (
            stderr.startswith('Fatal:') and 'invalid data returned' in stderr
        ):
            return Exception.__new__(ResticBadIndexError)
        if proc.returncode < 0 and abs(proc.returncode) == SIGPIPE:
            # https://github.com/restic/restic/issues/1466
            # https://github.com/restic/restic/pull/2546
            return Exception.__new__(ResticConnError)
        if 'out of memory' in stderr:
            return Exception.__new__(ResticOom)
        return Exception.__new__(cls)

    @property
    def name(self) -> str:
        """Get the name of the class of the restic exception

        Returns:
            str: class name
        """
        return type(self).__name__

    def __str__(self):
        if self.returncode < 0:
            signum = abs(self.returncode)
            try:
                # https://github.com/PyCQA/pylint/issues/2804
                sig = Signals(signum).name  # pylint: disable=no-member
            except ValueError:
                sig = f'signal {signum}'
            if signum == SIGPIPE:
                # https://github.com/restic/restic/issues/1466
                return f'Timeout: received SIGPIPE. {self.stderr}'
            return f'(killed with {sig}) {self.stderr}'
        return f'(return code {self.returncode}) {self.stderr}'


class ResticOom(ResticError):
    """Raised when restic breaks due to an OOM condition"""

    __module__ = 'restic'


class ResticBadIndexError(ResticError):
    """Raised when restic raises an index corruption error"""

    __module__ = 'restic'


class ResticAccessError(ResticError):
    """Raised when S3 access/secret keys are incorrect for restic"""

    __module__ = 'restic'


class ResticInitError(ResticError):
    """Raised when a restic repo connects but is not initialized"""

    __module__ = 'restic'


class ResticConnError(ResticError):
    """Raised when a connection is refused or times out from ceph"""

    __module__ = 'restic'


class ResticLockedError(ResticError):
    """Raised when an operation on a repo fails due to a lock held on it"""

    __module__ = 'restic'

    def __init__(self, proc: Union[CalledProcessError, CompletedProcess]):
        super().__init__(proc)
        if match := LOCK_RE.search(self.stderr):
            self.pid = int(match.group(1))
            self.host = match.group(2)
        else:
            logging.warning('failed to parse PID and host from %r', self.stderr)
            self.pid = None
            self.host = None

    def unlock_ok(self) -> bool:
        """Check if a locked repo can be unlocked

        Returns:
            bool: true if restic unlock can be run safely
        """
        if self.pid is None:
            return False
        if platform.node() != self.host:
            return False
        try:
            os.kill(self.pid, 0)
        except ProcessLookupError:
            return True
        return False

    def __str__(self):
        if self.pid:
            return f"repo is in use and locked by PID {self.pid} on {self.host}"
        return super().__str__()
