# Copyright (C) 2013, 2014, 2015, 2018, 2020, 2021 The Meme Factory, Inc.
#               http://www.karlpinc.com/

# This file is part of PGWUI_Core.
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.
#

# Karl O. Pinc <kop@karlpinc.com>

'''Exceptions
'''

from cgi import escape as cgi_escape


class PGWUIError(Exception):
    pass


class MultiError(PGWUIError):
    '''Multiple errors
    '''
    def __init__(self, errors):
        self.errors = errors


class MultiDataLineError(MultiError):
    '''Multiple errors that are all DataLineErrors
    '''
    def __init__(self, errors):
        for error in errors:
            if not isinstance(error, DataLineError):
                raise TypeError(
                    f'unsupported exception type ({type(error)}) for a'
                    f' MultiDataLineError value: {error}')
        super().__init__(errors)


class UploadError(PGWUIError):
    '''
    Module exceptions are derived from this class.

    lineno Line number to which error pertains, if any
    e      The error message
    descr  More description of the error
    detail Extra HTML describing the error
    data   Line of data causing problem, if any

    UploadError
      * Error
        *  NoHeadersError
        *  NoDataError
        *  DBError
          * DBCommitError
          * DBDataLineError
          * DBSetupError
      * DataLineError
        *  TooManyColsError
    '''
    def __init__(self, e, lineno='', descr='', detail='', data=''):
        super(UploadError, self).__init__()
        self.lineno = lineno
        self.e = e
        self.descr = descr
        self.detail = detail
        self.data = data
        self.filepath = None
        self.relation = None

    def __str__(self):
        out = 'error ({0})'.format(self.e)
        if self.lineno != '':
            out = '{0}: lineno ({1})'.format(out, self.lineno)
        if self.descr != '':
            out = '{0}: descr ({1})'.format(out, self.descr)
        if self.detail != '':
            out = '{0}: detail ({1})'.format(out, self.detail)
        if self.data != '':
            out = '{0}: data ({1})'.format(out, self.data)
        return out

    def color(self, descr, filepath, relation):
        '''Add a description to the exception's error and save filepath
        and relation
        '''
        self.e = f'{descr}: {self.e}'
        self.filepath = filepath
        self.relation = relation
        return self


class SetupError(UploadError):
    '''
    Module exceptions raised while setting up to read data lines
    are derived from this class.

    e      The error message
    descr  More description of the error
    detail Extra HTML describing the error
    '''
    def __init__(self, e, descr='', detail=''):
        super(SetupError, self).__init__(e=e, descr=descr, detail=detail)


class NoFileError(SetupError):
    '''No file uploaded'''
    def __init__(self, e, descr='', detail=''):
        super(NoFileError, self).__init__(e, descr, detail)


class NoDBError(SetupError):
    '''No database name given'''
    def __init__(self, e, descr='', detail=''):
        super(NoDBError, self).__init__(e, descr, detail)


class NoUserError(SetupError):
    '''No user name supplied'''
    def __init__(self, e, descr='', detail=''):
        super(NoUserError, self).__init__(e, descr, detail)


class AuthFailError(SetupError):
    '''Unable to connect to the db'''
    def __init__(self, e, descr='', detail=''):
        super(AuthFailError, self).__init__(e, descr, detail)


class DryRunError(SetupError):
    '''Rollback due to dry_run config option'''
    def __init__(self, e, descr='', detail=''):
        super(DryRunError, self).__init__(e, descr, detail)


class CSRFError(SetupError):
    '''Invalid CSRF token'''
    def __init__(self, e, descr='', detail=''):
        super(CSRFError, self).__init__(e, descr, detail)


class NoHeadersError(SetupError):
    '''No column headings found'''
    def __init__(self, e, descr='', detail=''):
        super(NoHeadersError, self).__init__(e, descr, detail)


class NoDataError(SetupError):
    '''No data uploaded'''
    def __init__(self, e, descr='', detail=''):
        super(NoDataError, self).__init__(e, descr, detail)


class CantDecodeError(SetupError):
    '''Can't decode file data into unicode'''
    def __init__(self, e, descr='', detail=''):
        super().__init__(e, descr, detail)


class DuplicateUploadError(SetupError):
    '''The same filename updated twice into the same db'''
    def __init__(self, e, descr='', detail=''):
        super(DuplicateUploadError, self).__init__(e, descr, detail)


class DataInconsistencyError(SetupError):
    def __init__(self, e, descr='', detail=''):
        super(DataInconsistencyError, self).__init__(e, descr, detail)


class DBError(SetupError):
    '''psycopg2 raised an error'''
    def __init__(self, pgexc, e='process your request'):
        '''
        pgexc  The psycopg2 exception object
        e      Description of what PG was doing
        '''
        super(DBError, self).__init__(
            'PostgreSQL is unable to ' + e + ':',
            'It reports:',
            self.html_blockquote(pgexc))

    def html_blockquote(self, ex):
        '''
        Produce an html formatted message from a psycopg2 DatabaseError
        exception.
        '''
        primary = cgi_escape(ex.diag.message_primary)

        if ex.diag.message_detail is None:
            detail = ''
        else:
            detail = '<br />DETAIL: ' + cgi_escape(ex.diag.message_detail)

        if ex.diag.message_hint is None:
            hint = ''
        else:
            hint = '<br />HINT: ' + cgi_escape(ex.diag.message_hint)

        return '<blockquote><p>{0}: {1}{2}{3}</p></blockquote>'.format(
            ex.diag.severity,
            primary,
            detail,
            hint)


class DBCommitError(DBError):
    def __init__(self, pgexc):
        super(DBCommitError, self).__init__(pgexc)


class DBDataLineError(DBError):
    '''Database generated an error while the processor was running.'''

    def __init__(self, udl, pgexc):
        '''
        udl    An UploadDataLine instance
        pgexc  The psycopg2 exception object
        '''
        super(DBDataLineError, self).__init__(pgexc)
        self.lineno = udl.lineno
        self.data = udl.raw


class DBSetupError(DBError):
    def __init__(self, pgexc):
        super().__init__(pgexc, "complete the application's setup")


class DataLineError(UploadError):
    '''
    Module exceptions rasied while line-by-line processing the uploaded
    data are derived from this class.

    lineno The line number
    e      The error message
    descr  More description of the error
    detail Extra HTML describing the error
    data   The uploaded data
    '''
    def __init__(self, lineno, e, descr='', detail='', data=''):
        super(DataLineError, self).__init__(e, lineno, descr, detail, data)


class TooManyColsError(DataLineError):
    def __init__(self, lineno, e, descr='', detail='', data=''):
        super(TooManyColsError, self).__init__(lineno, e, descr, detail, data)


class TooFewColsError(DataLineError):
    def __init__(self, lineno, e, descr='', detail='', data=''):
        super().__init__(lineno, e, descr, detail, data)


class EncodingError(DataLineError):
    def __init__(self, lineno, e, descr='', detail='', data=''):
        super().__init__(lineno, e, descr, detail, data)
