import docker
from contextlib import suppress
import tempfile
from uuid import uuid4

from convisoappsec.flow.util import SourceCodeCompressor
from .exceptions import SourceCodeScannerException


class SourceCodeScanner(object):
    SUCCESS_EXIT_CODE = 0
    '''
    hooks:
      def _pre_pull(self):
          :return: void

      def _capture_stdout(self, stdout_bytes)
        :param stdout_bytes: chunks generated by stdout
        :paramtype stdout_bytes: bytes
        :return: void
    _pre_scan
    _scan_stdout
    _post_scan
    '''

    def __init__(self, source_code_dir, create_source_code_volume = True):
        uuid = str(uuid4())
        self.docker = docker.from_env(version="auto")
        self.__container_name = "source_code_scanner_{0}".format(
            uuid
        )
        self.__source_code_dir = source_code_dir
        self.__create_source_code_volume = create_source_code_volume

        if self.__create_source_code_volume:
            self.__source_code_volume_name = "source_code_scanner_src_{0}".format(
                uuid
            )
        else:
            self.__source_code_volume_name = None

    @property
    def repository(self):
        raise Exception('Not implemented yet!')

    @property
    def tag(self):
        raise Exception('Not implemented yet!')

    @property
    def command(self):
        raise Exception('Not implemented yet!')

    @property
    def container_source_dir(self):
        raise Exception('Not implemented yet!')

    @property
    def image(self):
        return "{repository}:{tag}".format(
            repository=self.repository,
            tag=self.tag,
        )

    @property
    def volumes(self):
        if not self.__create_source_code_volume:
            return {}

        return {
            self.__source_code_volume_name: {
                'bind': self.container_source_dir,
                'mode': 'rw',
            }
        }

    def __get_container(self):
        return self.docker.containers.get(
            self.__container_name
        )

    def __get_source_code_volume(self):
        return self.docker.volumes.get(
            self.__source_code_volume_name
        )

    @property
    def __container(self):
        try:
            return self.__get_container()
        except docker.errors.NotFound:
            return self.__create_container()

    def __create_container(self):
        return self.docker.containers.create(
            self.image,
            name=self.__container_name,
            volumes=self.volumes,
            detach=True,
            command=self.command,
            working_dir=self.container_source_dir if self.container_source_dir != '/code' else '/code'
        )

    def __pull_image(self):
        if self.has_pre_pull:
            self._pre_pull()

        self.docker.images.pull(self.repository, self.tag)

    def __load_source_code(self):
        container = self.__container

        with tempfile.TemporaryFile() as fileobj:
            compressor = SourceCodeCompressor(
                self.__source_code_dir
            )

            compressor.write_to(fileobj)
            fileobj.seek(0)

            container.put_archive(
                self.container_source_dir,
                fileobj
            )

    def scan(self):
        self.__pull_image()
        self.__load_source_code()
        container = self.__container

        container.start()

        if self.has_read_scan_stderr:
            self._read_scan_stderr(
                container.logs(
                    stream=True, stdout=False, stderr=True
                )
            )

        if self.has_read_scan_stdout:
            self._read_scan_stdout(
                container.logs(
                    stream=True, stdout=True, stderr=False
                )
            )

        wait_result = container.wait()
        status_code = wait_result.get('StatusCode')

        if not status_code == self.SUCCESS_EXIT_CODE:
            raise SourceCodeScannerException(
                'Source code scanning fail'
            )

    def __has_method(self, method_name):
        return hasattr(self, method_name)

    @property
    def has_read_scan_stdout(self):
        return self.__has_method('_read_scan_stdout')

    @property
    def has_read_scan_stderr(self):
        return self.__has_method('_read_scan_stderr')

    @property
    def has_pre_pull(self):
        return self.__has_method('_pre_pull')

    def __del__(self):
        with suppress(Exception):
            container = self.__get_container()
            container.remove()

        with suppress(Exception):
            source_code_volume = self.__get_source_code_volume()
            source_code_volume.remove()
