import os
import base64
import logging
import tarfile
from hashlib import sha512
from tempfile import TemporaryDirectory
from archivingsystem.common import utils
from cryptography.exceptions import InvalidSignature
from archivingsystem.database import Database, ArchivedFile, FilePackage
from archivingsystem.common.exceptions import (
    ArchivedFileNotValidCustomException,
    CertificateNotValidCustomException,
    DigestsNotMatchedCustomException,
    FileNotInDirectoryCustomException,
    OriginalFileNotValidError,
    TimestampInvalidCustomException,
    WrongPathToArchivedFileCustomException
)

log = logging.getLogger(__name__)

class Validator:
    """
    Validator class encapsulates the validation process

    `validate` function is the main entrypoint
    """

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

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

        file_id: ID of the file
        """
        archived_file_rec = self._get_archive_record(file_id)
        file_packages = self._get_all_sorted_filepackage_records(
            archived_file_rec.FileID
        )
        tar_path = self._get_file_path_from_dir(
            archived_file_rec.PackageStoragePath, "Package"
        )
        try:
            with TemporaryDirectory() as temp_dir:
                log.info(
                    "Temporary directory for verification created at: %s",
                    str(temp_dir),
                )
                result = self._validate_packages(
                    tar_path, temp_dir, file_packages, archived_file_rec
                )
        except OriginalFileNotValidError:
            result = (
                "archived file with id: {} isnt valid in remote location,"
                " archived file is still valid".format(
                    str(archived_file_rec.FileID)
                )
            )
            log.warning(result)
        except ArchivedFileNotValidCustomException:
            result = (
                "archived file with id: {} isnt valid in archive location"
                .format(str(archived_file_rec.FileID))
            )
            log.warning(result)
            return False

        #self._send_results(result, recipients, archived_file_rec.FileName)
        log.info(f"Validation ended with result: {result}")
        return True

    def _get_archive_record(self, file_id):
        log.debug("getting archived file info: %s", str(file_id))

        file = self.s.query(ArchivedFile).filter_by(FileID = file_id).first()
        return file

    def _get_all_sorted_filepackage_records(self, file_id):
        log.debug(
            "getting all file package records associated with archived file"
            " with id: %s",
            file_id,
        )
        file_recs = self.s.query(FilePackage).filter_by(ArchivedFileID = file_id).order_by(FilePackage.IssuingDate.desc()).all()
        return file_recs

    def _validate_packages(
        self, tar_path, temp_dir, file_packages, archived_file_rec
    ):
        num = 0
        validation_complete = False
        log.debug("validating archived file")
        while validation_complete is not True:
            log.debug("validating package from path: %s", str(tar_path))
            if "Package1.tar" in tar_path:
                try:
                    result = self._validate_initial_package(
                        tar_path,
                        temp_dir,
                        file_packages[num],
                        archived_file_rec,
                    )
                    validation_complete = True
                    log.debug("validation of initial package complete")
                except (
                    TimestampInvalidCustomException,
                    DigestsNotMatchedCustomException,
                    InvalidSignature,
                    FileNotInDirectoryCustomException,
                    CertificateNotValidCustomException,
                ):
                    log.warning("package invalid")
                    raise ArchivedFileNotValidCustomException(
                        "package invalid"
                    )
            elif "PackageF" in tar_path:
                try:
                    tar_path = self._validate_package(
                        tar_path, temp_dir, file_packages[num]
                    )
                    log.debug("validation of onion package complete")
                except (
                    TimestampInvalidCustomException,
                    DigestsNotMatchedCustomException,
                    FileNotInDirectoryCustomException,
                    CertificateNotValidCustomException,
                ):
                    log.exception("package is invalid: %s", tar_path)
                    raise ArchivedFileNotValidCustomException(
                        "package is invalid: {}".format(tar_path)
                    )
            else:
                log.exception(
                    "Path to package for validation isnt correct: %s",
                    tar_path,
                )
                raise WrongPathToArchivedFileCustomException(
                    "Path to package for validation isnt correct: {}".format(
                        tar_path
                    )
                )
            num += 1
        return result

    def _validate_package(self, tar_path, temp_dir, file_package):
        log.debug("validating package %s hashes with db", str(tar_path))
        self._verify_package_hashes(
            file_package.PackageHashSha512,
            utils.get_file_hash(sha512, tar_path),
        )
        temp_extracted_package_path = self._extract_tar_to_temp_dir(
            tar_path, temp_dir
        )

        log.debug("verifying timestamp and certificate")
        self._verify_package_timestamp(temp_extracted_package_path)
        self._verify_certificate_with_crl(
            temp_extracted_package_path, "tsa_cert_crl.crl", "tsa_ca_cert.pem"
        )
        return self._get_file_path_from_dir(
            temp_extracted_package_path, "Package"
        )

    def _validate_initial_package(
        self, package_path, temp_dir, file_package, archived_file_rec
    ):
        log.debug("getting package1 hash and verifing it")
        package1_hash = utils.get_file_hash(sha512, package_path)
        self._verify_package_hashes(
            file_package.PackageHashSha512, package1_hash
        )

        log.debug("extracting package1")
        extract_path = self._extract_tar_to_temp_dir(package_path, temp_dir)

        log.debug("getting package0 hash and verifing it")
        package0_path = self._get_file_path_from_dir(extract_path, "Package0")
        package0_hash = utils.get_file_hash(sha512, package0_path)
        self._verify_package_hashes(
            archived_file_rec.Package0HashSha512, package0_hash
        )

        log.debug("verifying signature and its timestamp")
        hash_signature = self._verify_signature(
            extract_path,
            package0_hash,
            archived_file_rec.SignatureHashSha512,
        )
        self._verify_timestamp(extract_path, "timestamp1", hash_signature)

        log.debug("extracting package0")
        extrack_pack0_path = self._extract_tar_to_temp_dir(
            package0_path, temp_dir
        )

        log.debug(
            "verifying archived file package hash with hash from database"
        )
        hash_archived_file = utils.get_file_hash(
            sha512,
            os.path.join(extrack_pack0_path, archived_file_rec.FileName),
        )
        self._verify_package_hashes(
            archived_file_rec.OriginFileHashSha512, hash_archived_file
        )

        self._verify_timestamp(
            extrack_pack0_path, "timestamp0", hash_archived_file
        )

        log.debug("verifying certificates")
        self._verify_certificate_with_crl(
            extract_path, "tsa_cert_crl.crl", "tsa_ca_cert.pem"
        )
        # self._verify_certificate_with_crl( - to validate our own CRL
        #     extract_path, "signing_cert_crl.crl", "signing_cert.pem"
        # )

        return True

    def _extract_tar_to_temp_dir(self, tar_path, temp_dir_path):
        tar_name = os.path.basename(tar_path)
        extract_path = os.path.join(temp_dir_path, tar_name)
        log.debug(
            "extracting tar package from path: %s to temp dir: %s",
            tar_path,
            extract_path,
        )
        with tarfile.open(tar_path, "r:") as tarf:
            tarf.extractall(path=extract_path)
        return extract_path

    def _verify_package_hashes(self, db_hash, package_hash):
        log.debug(
            "hash from db: %s \n hash of package file: %s",
            str(db_hash),
            str(package_hash),
        )
        if base64.b64encode(db_hash) != base64.b64encode(package_hash):
            log.exception(
                "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 _verify_package_timestamp(self, pack_path):
        data_path = self._get_file_path_from_dir(pack_path, "Package")
        log.debug(
            "getting hash of package to verify from path %s",
            str(data_path),
        )
        data = utils.get_file_hash(sha512, data_path)
        log.debug("verifying package timestamp")
        self._verify_timestamp(pack_path, "timestamp", data)

    def _verify_signature(self, extract_path, signed_data, signature_hash_db):
        signature_path = os.path.join(extract_path, "signature.sig")
        log.debug(
            "getting hash of signature from path %s",
            str(signature_path),
        )
        hash_signature = utils.get_file_hash(sha512, signature_path)
        self._verify_package_hashes(signature_hash_db, hash_signature)

        signature = utils.load_data(signature_path)
        certs_folder_path = os.path.join(extract_path, "certificate_files")
        cert_path = self._get_file_path_from_dir(
            certs_folder_path, "signing_cert.pem"
        )

        log.debug("loading certificate from path %s", str(cert_path))
        public_key = utils.pqc_get_public_key(cert_path)

        if not utils.pqc_validate_signature(
            signed_data, base64.b64decode(signature), public_key, self.config["signing"]["algorithm"]
        ): raise InvalidSignature
        log.debug("VALID")
        return hash_signature

    def _verify_timestamp(self, dir_path, file_name, data):
        log.debug("verification of timestamp")
        ts_path = self._get_file_path_from_dir(dir_path, file_name)
        log.debug("loading timestamp")
        ts = utils.load_data(ts_path)
        log.debug("verifying timestamp")
        if not (
            utils.verify_timestamp(ts, data, self.config["timestamping"])
        ):
            log.exception(
                "Timestamp invalid, timestamp path: %s",
                str(ts_path),
            )
            raise TimestampInvalidCustomException("Timestamp invalid")
        log.debug("timestamp is valid")

    def _get_file_path_from_dir(self, dir_path, file_name):
        log.debug(
            "searching for file: %s in directory: %s",
            str(file_name),
            str(dir_path),
        )
        files = [f for f in os.listdir(dir_path) if file_name in f]
        if not files:
            log.exception(
                "no file with given file name: %s was found in dir: %s",
                str(file_name),
                str(dir_path),
            )
            raise FileNotInDirectoryCustomException(
                "No files with given name in directory. name: {}".format(
                    file_name
                )
            )
        return os.path.join(dir_path, files[0])

    def _verify_certificate_with_crl(self, dir_path, crl_name, cert_name):
        crl = utils.load_data(
            os.path.join(dir_path, "certificate_files", crl_name)
        )

        log.debug("verifying used certificate with crl")
        utils.validate_certificate(
            crl, os.path.join(dir_path, "certificate_files", cert_name)
        )
    
def validation_task(config: dict, file_id: int) -> bool:
    """
    Function that starts validation task.

    Validator object is created and started to work on file validation.
    Requires `config` file object and `file_id`.

    If file is valid returns `True`, else `False`.
    """
    # Create database instance 
    # (this function is designed to run i separate 
    # process which needs own Database instance)
    db = Database(config)

    # Start Retimestamper
    return Validator(db, config).validate(file_id)
