"""

    Author: Sujayyendhiren RS

    Description: Manage a secure config file of size less than 100MB.
                 This ensures application configuration is always
                 protected by encryption. For enhanced security provide
                 encryption key stored in external vault stores as input
                 to the SecureConfig object.

    Supported environments:

        1. July 14th 2024
           OS - Ubuntu 22.04
           Python - 3.11

        2. Planned by end of July 2024
           OS - Sonoma 14.5
           Python - 3.11
"""

import os
import json
import traceback

from datetime import date

import yaml

from pathlib import Path
from cryptography.fernet import Fernet


class SecureConfig:

    def __init__(self, filename: str, enckey: str|None = None) -> None:
        """
            Description: Initialize. Encryption key maybe
                         externally managed or leave it to
                         this module to handle on its own.

            @filename: Absolute path to the given file

            @enckey: User may provide a key if user wants to manage the key
                  using vault or other secure means else skip for the
                  module to manage on its own.
        """

        ## ----------------------------------------------------------- ##
        ##  Each file is expected to have a unique key for encryption  ##
        ## ----------------------------------------------------------- ##

        self.filename, self.autogen = filename, False
        self.mapfile = f"{str(Path(filename).parent)}/.map/.{filename.split('/')[-1]}"
        self.metadata = self.mapfile.replace('/.map/', '/.metadata/')

        if enckey:

            ## Dont store key when provided from external source
            self.key = enckey

        elif not os.path.exists(self.mapfile):

            ## Key file
            Path(self.mapfile).parent.mkdir(parents=True, exist_ok=True)
            Path(self.mapfile).touch(mode=0o700, exist_ok=True)

            ## Metadata file
            Path(self.metadata).parent.mkdir(parents=True, exist_ok=True)
            Path(self.metadata).touch(mode=0o700, exist_ok=True)

            self.key = Fernet.generate_key()

            ## Store key only in the case it is autogenerated by code using
            ## this module
            self.autogen = True

            with open(self.mapfile, 'wb') as fd:
                fd.write(self.key)

        else:
            with open(self.mapfile, 'rb') as fd:
                self.key = fd.read()

        self.mfobj = Fernet(self.key)

        ## Raise an exception if the absolute path
        if not filename.startswith('/'):
            raise Exception(f"""[{__func__.__name__}]Absolute path to the file is required. Example: /tmp/test.txt""")

    def rotate_encryption_key(self) -> None:
        """
            Description: Initiate rotation for user maintained keys
                         reencrypt the file with newkey
        """

        raise Exception('To be implemented in the next release')

    def decrypt_read_file(self) -> str:
        """
            Description: Decrypt and obtain sensitive data.
        """

        decryptedata = None

        try:

            if not os.path.exists(self.filename):
                raise Exception(f"""Requested file {self.filename} doesnt exist.""")

            with open(self.metadata, 'r') as fd:
                metadata = json.load(fd)

            with open(self.filename, 'rb') as fd:
                data = fd.read()

            ## Decrypt data 
            rawdata = self.mfobj.decrypt(data)

            decryptedata = rawdata.decode('utf-8')

            ## Use metadata to reconstruct original datatype
            if metadata.get('datatype', '') == 'list' or metadata.get('datatype', '') == 'dict':
                decryptedata = eval(decryptedata)

            print(f"DEBUG: {type(decryptedata)}")

        except Exception as exc:

            print(f"CRITICAL: [decrypt_read_file] Exception - {exc}")
            print(traceback.format_exc())

        finally:
            return decryptedata

    def create_encrypted_file(self, filename:str) -> bool:
        """
            Description: Extract data out of known file type.

            @filename: Read raw sensitive file that needs to be encrypted.
                       Currently json and yaml file reads are supported.
                       for other formats read is yourself and use
                       'update_encrypted_file' method to encrypt.
        """

        if filename.endswith('.json'):

            with open(filename) as fd:
                rawdata = json.load(fd)

            return self.update_encrypted_file(rawdata)

        elif filename.endswith('.yml'):

            with open(filename) as fd:
                rawdata = yaml.safe_load(fd)

            return self.update_encrypted_file(rawdata)

        else:

            raise Exception("Unsupported file type. Use write_file method after reading the file yourself.")

    def update_encrypted_file(self, rawdata:str, permission:int=0o700) -> bool:
        """
            Description: Encrypt given input data. If given filepath doesnt
                         exist then create it for the first time. Encrypted
                         data is stored with a stricter permission by default
                         0o700.

            @rawdata: Input data that needs to be stored encrypted
            @permission: Set a conservative permission on an encrypted file.

        """
        datatype = 'unknown'

        try:

            if not os.path.exists(self.filename):
               Path(self.filename).parent.mkdir(exist_ok=True, parents=True)
               print(f"INFO: Created missing directory - {str(Path(self.filename).parent)}")

            ##-----------------------------------------------------------------##
            ## Data encryption - Metadata to reconstruct data post decryption  ##
            ##-----------------------------------------------------------------##

            ## String encryption
            if isinstance(rawdata, str):
                encdata = self.mfobj.encrypt(rawdata.encode('utf-8'))
                datatype = 'str'

            ## Dictionary datatype
            elif isinstance(rawdata, dict):
                encdata = self.mfobj.encrypt(json.dumps(rawdata).encode('utf-8'))
                datatype = 'dict'

            ## Dictionary datatype
            elif isinstance(rawdata, list):
                encdata = self.mfobj.encrypt(json.dumps(rawdata).encode('utf-8'))
                datatype = 'list'

            else:
                encdata = self.mfobj.encrypt(str(rawdata).encode('utf-8'))

            ## Write to yaml file
            with open(self.filename, 'wb') as fd:
                fd.write(encdata)

            Path(self.filename).touch(mode=permission, exist_ok=True)

            with open(self.metadata, 'w') as fd:
                fd.write(json.dumps({'datatype': datatype, 'createdon': str(date.today())}))

            return True

        except Exception as exc:

            print(f"CRITICAL: [update_encrypted_file] Exception - {exc}")
            return False
