import os
import base64
import logging
import tarfile
import rfc3161ng
from hashlib import sha512
from archivingsystem.common import utils
from cryptography.hazmat.primitives.serialization import Encoding
from archivingsystem.database import Database, ArchivedFile, FilePackage
from archivingsystem.common.exceptions import (
    DigestsNotMatchedCustomException,
    FileNotInDirectoryCustomException,
    TimestampInvalidCustomException,
)

log = logging.getLogger(__name__)


class Retimestamper:
    """
    Retimestamper class encapsulates the retimestamping process of a file

    `retimestamp` function is the main entrypoint
    """

    def __init__(self, db: Database, config: dict):
        self.s = db.Session()
        self.config = config["archiving"]
        self.file_package = FilePackage()

    def retimestamp(self, file_id: int):
        """
        Function executing retimestamping steps on provided file identified by ID

        file_id: ID of the file
        """
        (
            archiving_storage_path,
            current_package_hash,
            package_id,
        ) = self._verify_existing_package(file_id)
        new_timestamp = self._create_new_timestamp(
            archiving_storage_path, current_package_hash
        )
        tar_path = os.path.join(
            archiving_storage_path, "PackageF{}.tar".format(package_id)
        )
        utils.create_tar_file_from_dir(
            archiving_storage_path,
            tar_path,
        )
        self._fill_package_record(file_id, new_timestamp, tar_path)

        log.info("Updating record with latest expiration date...")
        # Get ArchivedFile by FileID
        file = self.s.query(ArchivedFile).filter_by(FileID = file_id).first()
        # Update ExpirationDateTS
        file.ExpirationDateTS = self._get_expiration_date(new_timestamp)

        log.info("Inserting new package record into database...")
        # Add new FilePackage to database
        self.s.add(self.file_package)
        # Commit changes and close
        self.s.commit()
        self.s.close()

        log.info("Retimestaping done...")

        return "OK"  # or exception

    def _verify_existing_package(self, file_id):
        log.debug("verifying last package timestamp")

        log.debug("getting arichivated file record from db")
        archived_file = self.s.query(ArchivedFile).filter_by(FileID = file_id).first()
        archiving_storage_path = archived_file.PackageStoragePath

        log.debug("getting latest package file record from db")
        latest_file_package = self.s.query(FilePackage).filter_by(ArchivedFileID = file_id).order_by(FilePackage.PackageID.desc()).first()

        log.debug("getting data from package")
        (
            timestamp,
            timestamped_file_hash,
            tar_package_path,
        ) = self._get_ts_data_from_package(archiving_storage_path)

        log.debug("verifying latest package hash")
        current_package_hash = utils.get_file_hash(
            sha512, tar_package_path
        )
        self._verify_final_package_hashes(
            latest_file_package.PackageHashSha512, current_package_hash
        )

        log.debug("verifying latest timestamp")
        verification_result = utils.verify_timestamp(
            timestamp, timestamped_file_hash, self.config["timestamping"]
        )
        if verification_result is not True:
            log.exception(
                "Last timestamp of package in path %s is invalid",
                str(archiving_storage_path),
            )
            raise TimestampInvalidCustomException(
                "Last timestamp of package is invalid"
            )
        return (
            archiving_storage_path,
            current_package_hash,
            latest_file_package.PackageID,
        )

    def _create_new_timestamp(
        self, archiving_storage_path, current_package_hash
    ):
        log.debug("getting new package timestamp")
        new_timestamp = utils.get_timestamp(
            self.config["timestamping"], current_package_hash
        )
        log.debug("storing timestamp to storage directory..")
        utils.store_ts_data(
            new_timestamp, archiving_storage_path, "timestamp"
        )
        self._store_used_cert_files(archiving_storage_path)
        return new_timestamp

    def _fill_package_record(self, file_id, new_timestamp, tar_path):
        log.debug("Filling new file package record with gathered data")
        self.file_package.ArchivedFileID = file_id
        self.file_package.TimeStampingAuthority = self.config["timestamping"][
            "tsa_tsr_url"
        ]
        self.file_package.IssuingDate = rfc3161ng.get_timestamp(
            new_timestamp
        )
        cert = utils.get_certificate(
            self.config["timestamping"]["tsa_cert_path"]
        )
        self.file_package.TsaCert = base64.b64encode(
            cert.public_bytes(Encoding.PEM)
        )
        self.file_package.PackageHashSha512 = utils.get_file_hash(
            sha512, tar_path
        )
        log.debug("File package record filled")

    def _get_ts_data_from_package(self, dir_path):
        files_in_dir = os.listdir(dir_path)
        package_name_list = list(
            filter(lambda x: x.startswith("Package"), files_in_dir)
        )
        if len(package_name_list) == 0:
            log.exception(
                "unable to find package in directory: %s",
                str(dir_path),
            )
            raise FileNotInDirectoryCustomException(
                "unable to find package in directory: {}".format(str(dir_path))
            )
        tar_package_path = os.path.join(dir_path, package_name_list[0])

        log.debug(
            "getting data from tar file on path %s",
            str(tar_package_path),
        )
        with tarfile.open(tar_package_path, "r:") as tarf:
            file_names = tarf.getnames()
            timestamp = self._read_timestamp_from_tar(file_names, tarf)
            hash_f = self._get_timestamped_file_hash(sha512, tarf, file_names)

        return timestamp, hash_f, tar_package_path

    def _get_expiration_date(self, timestamp):
        years = self.config["timestamping"]["timestamp_validity"]
        time = rfc3161ng.get_timestamp(timestamp)
        log.debug(
            "expiration date setup from: %s + %s years",
            str(time),
            str(years),
        )
        return time.replace(year=time.year + years)

    def _get_timestamped_file_name(self, file_names):
        signature = list(
            filter(lambda x: x.startswith("signature"), file_names)
        )
        if len(signature) != 0:
            log.debug(
                "Found timestamped file name %s",
                str(signature),
            )
            return signature[0]
        package = list(filter(lambda x: x.startswith("Package"), file_names))
        if len(package) != 0:
            log.debug(
                "Find timestamped file name in name, %s",
                str(signature),
            )
            return package[0]
        log.exception(
            "Cannot find signature or PackageF in directory: %s",
            str(file_names),
        )
        raise FileNotInDirectoryCustomException(
            "Cannot find signature or PackageF in directory: {}".format(
                str(file_names)
            ),
        )

    def _store_used_cert_files(self, archiving_storage_path):
        log.debug(
            "storing used certificate files to directory: %s",
            str(archiving_storage_path),
        )
        dir_path = utils.create_new_dir_in_location(
            archiving_storage_path, "certificate_files"
        )
        path_tsa_cert = self.config["timestamping"]["tsa_cert_path"]
        path_tsa_ca_pem = self.config["timestamping"]["tsa_ca_pem"]
        tsa_crl_url = self.config["timestamping"]["tsa_crl_url"]
        utils.copy_file_to_dir(path_tsa_cert, dir_path, "tsa_cert.crt")
        utils.copy_file_to_dir(
            path_tsa_ca_pem, dir_path, "tsa_ca_cert.pem"
        )
        crl = utils.get_current_crl(tsa_crl_url)
        utils.validate_certificate(crl, path_tsa_ca_pem)
        utils.store_ts_data(crl, dir_path, "tsa_cert_crl.crl")
        log.debug("certificate files stored in dir of new package")

    def _get_timestamped_file_hash(self, hash, tarf, file_names):
        f = tarf.extractfile(self._get_timestamped_file_name(file_names))
        file_hash = hash()
        buffer = f.read(8192)
        while buffer != b"":
            file_hash.update(buffer)
            buffer = f.read(8192)
        return file_hash.digest()

    def _read_timestamp_from_tar(self, file_names, tarf):
        log.debug("trying to read timestamp from opened tar file")
        ts_name = list(filter(lambda x: x.startswith("timestamp"), file_names))
        if len(ts_name) == 0:
            log.exception("unable to find timestamp file in tarfile")
            raise FileNotInDirectoryCustomException(
                "unable to find timestamp file in tarfile"
            )

        file = tarf.extractfile(ts_name[0])
        log.debug("reading timestamp from file")
        return file.read()

    def _verify_final_package_hashes(self, hash_db, hash_pack):
        log.debug(
            "hash from db: %s \n hash of package file: %s",
            str(hash_db),
            str(hash_pack),
        )
        if base64.b64encode(hash_db) != base64.b64encode(hash_pack):
            log.warning(
                "hash from db and hash of package file do not match"
            )
            raise DigestsNotMatchedCustomException(
                "hash from db and hash of package file do not match"
            )
        log.debug("hash from db and hash of package file matched")

def retimestamping_task(config: dict, file_id: int):
    """
    Function that starts retimestamping task.

    Retimestamper object is created and started to work on file retimestamping.
    Requires `config` file object and `file_id`.
    """
    # Create database instance 
    # (this function is designed to run i separate 
    # process which needs own Database instance)
    db = Database(config)

    # Start Retimestamper
    Retimestamper(db, config).retimestamp(file_id)