import os
import base64
import logging
import rfc3161ng
from uuid import uuid4
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 FileTransferNotSuccesfullCustomException

log = logging.getLogger(__name__)

class Archiver:
    """
    Archiver class encapsulates the archiving process of a file

    `archive` function is the main entrypoint
    """

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

    def archive(self, file_path: str, owner: str):
        """
        Function executing archivation steps on provided file

        file_path: path to the file
        owner: name of the file owner
        """
        
        log.info(f"Starting archivation of {file_path} file...")

        self._assign_basic_info(file_path, owner)
        self._validate_certificates()
        self._assign_tsa_info()
        self._transfer_file(file_path)
        self._make_ts0()
        self._make_package0()
        self._sign_package()
        self._make_ts1()
        self._store_used_cert_files()
        self._make_final_package()

        self._insert_db_record()

        log.info(f"Archivation successful...")

        return "OK"  # or exception

    def _assign_basic_info(self, file_path, owner):
        log.debug("Assigning general information...")
        self.archived_file.FileName = os.path.basename(file_path)
        self.archived_file.OwnerName = owner

    def _validate_certificates(self):
        log.debug("Validating TSA certificates...")
        # path_ca = self.config["signing_info"]["certificate_path"]
        # path_crl = self.config["signing_info"]["crl_path"]
        path_tsa_ca_pem = self.config["timestamping"]["tsa_ca_pem"]
        self.tsa_current_crl = utils.get_current_crl(
            self.config["timestamping"]["tsa_crl_url"]
        )
        utils.validate_certificate(
            self.tsa_current_crl, path_tsa_ca_pem
        )
        log.debug("TSA certificates are valid...")
        # with open(  # used for own CRL validation
        #     path_crl, "rb"
        # ) as f:
        #     crl_s = f.load()
        # utils.validate_certificate(
        #     crl_s, path_ca
        # )
        # log.debug("signing cert is valid")

    def _assign_tsa_info(self):
        log.debug("Assigning TSA information to package...")
        self.file_package.TimeStampingAuthority = self.config[
            "timestamping"
        ]["tsa_tsr_url"]
        cert = utils.get_certificate(
            self.config["timestamping"]["tsa_cert_path"]
        )
        self.file_package.TsaCert = base64.b64encode(
            cert.public_bytes(Encoding.PEM)
        )

    def _transfer_file(self, file_path):
        log.debug("Transfering file to archiving storage...")
        (   self.dst_file_path,
            self.archived_file.PackageStoragePath,
            self.archived_file.OriginFileHashSha512,
        ) = self._transfer_local_file_to_archiving_storage(file_path)
        
        log.debug("Validating file transfer...")
        self._validate_data_transfer(
            self.archived_file.OriginFileHashSha512, self.dst_file_path
        )

    def _make_ts0(self):
        log.debug("Creating timestamp0...")
        ts0 = self._create_timestamp(
            self.archived_file.OriginFileHashSha512, "timestamp0"
        )
        self.archived_file.TimeOfFirstTS = rfc3161ng.get_timestamp(ts0)

    def _make_package0(self):
        log.debug("Creating package0...")
        package0_tar_path = self._make_tar_package_from_dir_content(
            self.archived_file.PackageStoragePath, "Package0.tar"
        )
        self.archived_file.Package0HashSha512 = utils.get_file_hash(
            sha512, package0_tar_path
        )

    def _sign_package(self):
        log.debug("Signing the package0...")
        b64signature = self._make_b64signature(
            self.archived_file.Package0HashSha512
        )

        log.debug("Storing signature next to package0...")
        signature_file_path = utils.store_signature(
            self.archived_file.PackageStoragePath, b64signature
        )
        self.archived_file.SignatureHashSha512 = (
            utils.get_file_hash(sha512, signature_file_path)
        )

        log.debug("Obtaining signing certificate...")
        self.archived_file.SigningCert = utils.get_certificate(
            self.config["signing"]["certificate_path"]
        ).public_bytes(Encoding.PEM)

    def _make_ts1(self):
        log.debug("Creating timestamp1...")
        ts1 = self._create_timestamp(
            self.archived_file.SignatureHashSha512, "timestamp1"
        )
        self.file_package.IssuingDate = rfc3161ng.get_timestamp(ts1)
        self.archived_file.ExpirationDateTS = self._get_expiration_date(
            ts1
        )

    def _store_used_cert_files(self):
        log.debug(
            "Storing used certificates..."
        )
        dir_path = utils.create_new_dir_in_location(
            self.archived_file.PackageStoragePath, "certificate_files"
        )
        path_ca = self.config["signing"]["certificate_path"]
        # path_crl = self.config["signing_info"]["crl_path"]
        path_tsa_cert = self.config["timestamping"]["tsa_cert_path"]
        path_tsa_ca_pem = self.config["timestamping"]["tsa_ca_pem"]

        utils.copy_file_to_dir(path_ca, dir_path, "signing_cert.pem")
        # utils.copy_file_to_dir(  # used for own CRL validation
        #     path_crl, dir_path, "signing_cert_crl.crl"
        # )
        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"
        )
        utils.store_ts_data(
            self.tsa_current_crl, dir_path, "tsa_cert_crl.crl"
        )

    def _make_final_package(self):
        log.debug("Creating package1...")
        final_tar_path = self._make_tar_package_from_dir_content(
            self.archived_file.PackageStoragePath, "Package1.tar"
        )
        self.file_package.PackageHashSha512 = utils.get_file_hash(
            sha512, final_tar_path
        )

    def _insert_db_record(self):
        log.debug("Writing new record to the database...")
        try:
            # Associate ArchivedFile to FilePackage in one to many relationship
            self.file_package.ArchivedFile = self.archived_file

            # Add ArchivedFile to database
            self.s.add(self.archived_file)

            # Add FilePackage to database
            self.s.add(self.file_package)
            self.s.commit()
            self.s.close()
        
        except Exception as e:
            log.error(
                "Unable to write database record, deleting created archived file!"
            )
            utils.delete_file(self.archived_file.PackageStoragePath)
            raise e

    def _create_timestamp(self, fhash, ts_name):
        log.debug("obtaining timestamp for file hash %s", str(fhash))
        timestamp = utils.get_timestamp(
            self.config["timestamping"], fhash
        )
        log.debug(
            "copying %s to %s",
            ts_name,
            self.archived_file.PackageStoragePath,
        )
        utils.store_ts_data(
            timestamp, self.archived_file.PackageStoragePath, ts_name
        )
        return timestamp

    def _transfer_local_file_to_archiving_storage(self, file_path):
        log.debug("getting hash of file: %s", str(file_path))
        origin_hash = utils.get_file_hash(sha512, file_path)
        new_dir_path = utils.create_new_dir_in_location(
            self.config["storage"], str(uuid4())
        )
        log.debug(
            "created archiving directory path: %s",
            str(new_dir_path),
        )
        dst_file_path = utils.copy_file_to_dir(
            file_path, new_dir_path, self.archived_file.FileName
        )
        log.debug("file copied, path: %s", str(dst_file_path))
        return dst_file_path, new_dir_path, origin_hash

    def _get_expiration_date(self, ts):
        years = self.config["timestamping"]["timestamp_validity"]
        time = rfc3161ng.get_timestamp(ts)
        log.debug(
            "expiration length of timestamp1 was set to %s",
            str(years),
        )
        return time.replace(year=time.year + years)

    def _make_tar_package_from_dir_content(self, dir_path, package_name):
        tar_path = os.path.join(dir_path, package_name)
        log.debug(
            "creating tar package on path: %s",
            str(tar_path),
        )
        utils.create_tar_file_from_dir(dir_path, tar_path)
        return tar_path

    def _make_b64signature(self, hash: bytes):
        log.debug("getting private key")
        private_key = utils.pqc_get_private_key(self.config["signing"]["private_key_path"], self.config["signing"]["algorithm"])
        log.debug("signing data")
        return base64.b64encode(utils.pqc_sign_data(hash, private_key, self.config["signing"]["algorithm"]))

    def _validate_data_transfer(self, hash_origin, dst_file_path):
        hash_copy = utils.get_file_hash(sha512, dst_file_path)
        log.debug(
            "hash of origin file: %s \n hash of copy: %s",
            str(hash_origin),
            str(hash_copy),
        )
        if hash_origin != hash_copy:
            log.error("hashes of original and copied file do not match")
            raise FileTransferNotSuccesfullCustomException(
                "hashes of original and copied file do not match"
            )
        log.debug("hashes of original and copied file matched")

def archiving_task(config: dict, filepath: str, owner: str):
    """
    Function that starts archivation task.

    Archiver object is created and started to work on file archivation.
    Requires `config` file object, `filepath` to file and `owner` name.
    """
    # Create database instance 
    # (this function is designed to run i separate 
    # process which needs own Database instance)
    db = Database(config)

    # Start Archiver
    Archiver(db, config).archive(filepath, owner)