import datetime
import logging
import json
import re
import os

from typing import Any, Union, Iterable
from json import JSONDecodeError

from .app_config import AppConfig
from .helpers.file_helper import read_file
from .encrypt.enc_helper import EncHelper

from .exceptions.file_header_empty_exception import FileHeaderEmptyException


class FileHeader:
    """
    File header stores the data about the file and its contents.
    Header is a line of data surrounded by HTML-comment tags to avoid having it visible on external resources.

    To avoid hassle the header is only appears in files created within the Notolog editor.
    Anyway, a header is required for encrypted files as it contains such important info as salt or other file data.
    """

    HEADER_TPL = '<!-- %s -->'

    def __init__(self):
        super(FileHeader, self).__init__()

        self.logger = logging.getLogger('header')

        self.logging = AppConfig.get_logging()
        self.debug = AppConfig.get_debug()

        if self.debug:
            self.logger.debug('Header helper is engaged')

        self.header = None

    def get_new(self, is_enc: bool = False):
        if not self.header:
            self.header = self.generate(is_enc=is_enc)
        else:
            if self.logging:
                self.logger.warning('Attempt to create new file header on top of existing one!')
        return self

    def generate(self, is_enc: bool = False) -> json:
        created = updated = datetime.datetime.now()
        header = {'notolog.app': {'created': created, 'updated': updated}}
        if is_enc:
            header['notolog.app'].update(self.generate_enc())
        return header

    def generate_enc(self) -> json:
        return {'enc': {'slt': EncHelper.generate_salt(), 'itr': EncHelper.get_default_iterations(), 'hint': ''}}

    def set_encrypted(self):
        self.header['notolog.app'].update(self.generate_enc())

    def is_valid(self) -> bool:
        return (self.header is not None
                and isinstance(self.header, Iterable)
                and 'notolog.app' in self.header)

    def is_file_encrypted(self) -> bool:
        if not self.is_valid():
            return False
        _header = self.header['notolog.app']
        return 'enc' in _header and isinstance(_header['enc'], dict)

    def validate_enc(self) -> None:
        """
        Ensure that the header is valid and do migrations if necessary.
        """
        pass

    def refresh(self) -> None:
        date_now = datetime.datetime.now()
        # Update only existing header
        if self.is_valid():
            self.header['notolog.app']['updated'] = date_now
            if 'created' not in self.header['notolog.app'] or self.header['notolog.app']['created'] is None:
                self.header['notolog.app']['created'] = date_now
        else:
            if self.debug:
                self.logger.debug('File header is empty')

    def get_param(self, param: Any, default: Any = None) -> Union[str, dict, None]:
        """
        Get header's param by name.
        @param param: name of the param
        @param default: default value if param is not set or not found
        @return: either string or None
        """
        return (self.header['notolog.app'][param]
                if self.is_valid() and param in self.header['notolog.app']
                else default)

    def set_param(self, param: str, value: str) -> None:
        """
        Set params to the header.
        @param param: name of the param
        @param value: value of the param
        @return: None
        """
        if self.is_valid():
            self.header['notolog.app'][param] = value
        else:
            raise FileHeaderEmptyException()

    def get_enc_param(self, param: Any) -> Union[str, None]:
        """
        Get encryption related header's param by name.
        @param param: name of the param
        @return: either string or None
        """
        enc = self.get_param('enc')
        if enc and param in enc:
            return enc[param]
        return None

    def set_enc_param(self, param: str, value: str) -> None:
        """
        Set encryption params to the header.
        @param param: name of the param
        @param value: value of the param
        @return: None
        """
        enc = self.get_param('enc')
        if enc:
            enc.update({param: value})
            self.header['notolog.app'].update({'enc': enc})
        else:
            raise FileHeaderEmptyException()

    def pack(self, content: str = None, encode: bool = False) -> str:
        header_line = repr(self)
        delimiter = '\n'

        if encode:
            delimiter = delimiter.encode("utf-8")
            if header_line and type(header_line) is not bytes:
                header_line = header_line.encode("utf-8")
            if content is not None and type(content) is not bytes:
                content = content.encode("utf-8")

        if header_line and content is not None:
            # Return header and content, even if the content is empty
            return header_line + delimiter + content
        elif content is not None:
            if self.logging:
                self.logger.warning(f'File content exist but header is None {self}')
            # Return content without header
            return content
        else:
            if self.logging:
                self.logger.warning(f'File header exist but content is None {self}')
            # Return header without content
            return header_line

    def load_file(self, file_path: str) -> tuple[Any, str]:
        file_data = read_file(file_path)
        return self.load(file_data)

    def load(self, file_data: str) -> tuple[Any, Union[str, None]]:
        try:
            file_header_line = file_data.splitlines()[0]
        except (IndexError, AttributeError):
            if self.debug:
                self.logger.debug('File header is not found')
            return self, file_data

        self.header = None
        try:
            search = re.search(self.HEADER_TPL % '(.*?)', file_header_line, re.IGNORECASE)
            file_header_json = search.group(1) if search else None
            self.header = json.loads(file_header_json)
            # Run migrations here if needed
            self.validate_enc()
        except (TypeError, JSONDecodeError):
            if self.debug:
                self.logger.debug('File header is empty')
            return self, file_data

        if self.is_valid():
            # Splitting file_data into lines
            lines = file_data.splitlines()
            if len(lines) >= 2:
                file_body = "\n".join(lines[1:])
            else:
                file_body = None
            return self, file_body

        if self.debug:
            self.logger.debug('File header is empty')  # Suppose to be a valid situation if not set yet or not created

        return self, file_data

    def read(self, file_path: str) -> Union[str, None]:
        if os.path.isfile(file_path):
            with open(file_path, encoding='utf-8') as f:
                header_line = f.readline().strip('\n')
                return header_line
        else:
            if self.logging:
                self.logger.warning('File not found "%s"' % file_path)
        return None

    def __repr__(self) -> str:
        """
        Return header data as a formatted string
        """

        if self.header:
            return self.HEADER_TPL % json.dumps(self.header, default=str)

        return ''
