"""Simple (key, value)-persistence using plain text files."""

import fcntl
import json
import logging
import os
from typing import Callable, Dict, List, Generic, TypeVar
from arcnagios.arcutils import ParseError

Alpha = TypeVar("Alpha")

class ObsoletePersistentObject(RuntimeError):
    pass

_module_log = logging.getLogger(__name__)

# File Locking
#
class locked_open:
    # pylint: disable=R1732,C0103

    def __init__(self, path: str, mode: str = 'r', encoding: str = 'utf-8'):
        assert mode in ['r', 'w']
        self._fh = open(path, mode, encoding=encoding)
        if mode == 'r':
            fcntl.lockf(self._fh, fcntl.LOCK_SH)
        else:
            fcntl.lockf(self._fh, fcntl.LOCK_EX)

    def __enter__(self):
        return self._fh

    def __exit__(self, exc_type=None, exc_value=None, exc_tb=None):
        self._fh.flush()
        os.fsync(self._fh.fileno())
        fcntl.lockf(self._fh, fcntl.LOCK_UN)
        self._fh.close()

# Type Descriptors of the form (decode, encode, required).
#
class PersistentType(Generic[Alpha]):

    def __init__(
            self,
            decode: Callable[[str], Alpha],
            encode: Callable[[Alpha], str] = str,
            required: bool = True):
        self.decode = decode
        self.encode = encode
        self.required = required

def pt_list(pt_elt):

    def decode(s: str) -> List[Alpha]:
        if s.strip():
            return list(map(pt_elt.decode, s.split(', ')))
        return []

    def encode(xs: List[Alpha]):
        return ', '.join(map(pt_elt.encode, xs))

    return PersistentType(decode, encode, False)

pt_int = PersistentType(int, str, True)
pt_int_opt = PersistentType(int, str, False)

pt_float = PersistentType(float, str, True)
pt_float_opt = PersistentType(float, str, False)

pt_str = PersistentType(str, str, True)
pt_str_opt = PersistentType(str, str, False)
pt_str_list = pt_list(pt_str)

pt_json = PersistentType(json.loads, json.dumps, True)
pt_json_opt = PersistentType(json.loads, json.dumps, False)

# Persistent Objects
#
class PersistentObject:
    """A class which represents a simple collection of attributes with
    human-readable pickeling.  The pickeling is limited to what is needed by
    the Nagios plugins.  The main point of this is to make the data
    presentable to Nagios operator."""
    # pylint: disable=C0103

    persistent_attributes: Dict[str, PersistentType]

    def __init__(self, **kwargs):
        for k, _ in self.persistent_attributes.items():
            v = kwargs.pop(k, None)
            setattr(self, k, v)
        if kwargs:
            raise TypeError('Invalid keyword argument(s) %s.'
                            % ', '.join(kwargs))

    def persistent_load(self,
            path: str,
            log: logging.Logger = _module_log,
            persistence_version: int = 0):
        with locked_open(path) as fh:
            persistence_version_of_file = 0
            line_number = 0
            for line in fh:
                line_number += 1
                kv = line.split(': ', 1)
                if len(kv) != 2:
                    log.error('%s:%d: Invalid or old file format.',
                              path, line_number)
                    raise ParseError('Invalid format for PersistentObject.')
                k, v = kv
                v = v.strip()
                if k == 'persistence_version':
                    persistence_version_of_file = int(v)
                    continue
                if not k in self.persistent_attributes:
                    log.warning('%s:%d: Ignoring unknown attribute.',
                                path, line_number)
                    continue
                try:
                    setattr(self, k, self.persistent_attributes[k].decode(v))
                except UnicodeDecodeError as exn:
                    log.error('%s:%d: %s', path, line_number, exn)
            for k, pt in self.persistent_attributes.items():
                if pt.required:
                    if getattr(self, k) is None:
                        if persistence_version_of_file == persistence_version:
                            raise ParseError('Missing required attribute %s.'%k)
                        raise ObsoletePersistentObject(
                                'Missing required attribute %s in '
                                'outdated %s with version %d.'
                                % (k, path, persistence_version_of_file))

    def persistent_save(self,
            path: str,
            log: logging.Logger = _module_log,
            persistence_version: int = 0):
        # pylint: disable=unused-argument
        for k, pt in self.persistent_attributes.items():
            if pt.required:
                if getattr(self, k) is None:
                    raise ValueError('Tried to save incomplete persistent '
                                     'object; missing attribute %s.' % k)
        with locked_open(path, 'w') as fh:
            fh.write('persistence_version: %d\n' % persistence_version)
            for k, pt in self.persistent_attributes.items():
                v = getattr(self, k)
                if not v is None:
                    fh.write('%s: %s\n'%(k, pt.encode(v)))
