"""
Base Driver class module.
"""
import os
import csv
import re
import sqlite3
import logging
import requests
import concurrent.futures
from requests.adapters import HTTPAdapter as BaseHTTPAdapter
from requests.packages.urllib3.util.retry import Retry
from os_benchmark.drivers import errors

USER_AGENT = 'os-benchmark'
MULTIPART_THRESHOLD = 64*2**20
MULTIPART_CHUNKSIZE = 64*2**20
MAX_CONCURRENCY = 10
CONNECT_TIMEOUT = 3
READ_TIMEOUT = 1


class MultiPart:
    """Object simulating part from file-object for multipart-upload."""
    def __init__(self, file_object, size):
        self.file_object = file_object
        self.size = size
        self.offset = 0

    def read(self, chunksize=None):
        if self.offset >= self.size:
            return ''

        if (chunksize is None or chunksize < 0) or (chunksize + self.offset >= self.size):
            data = self.file_object.read(self.size - self.offset)
            self.offset = self.size
            return data

        self.offset += chunksize
        return self.file_object.read(chunksize)

    @property
    def len(self):
        return self.size


class MultiPartUploader:
    """Helper creating a thread pool and splitting file in several parts."""
    def __init__(self, content, max_concurrency=None, multipart_chunksize=None, extre_uplood_kwargs=None):
        self.content = content
        self.max_concurrency = max_concurrency or MAX_CONCURRENCY
        self.extre_uplood_kwargs = extre_uplood_kwargs or {}
        self.multipart_chunksize = multipart_chunksize or MULTIPART_CHUNKSIZE

    def run(self, upload_func):
        content_length = self.content.size
        part_id = 1
        offset = 0
        futures = []

        pool_kwargs = {'max_workers': self.max_concurrency}
        with concurrent.futures.ThreadPoolExecutor(**pool_kwargs) as executor:
            while offset < content_length:
                chunk_size = min(self.multipart_chunksize, content_length - offset)
                result = executor.submit(
                    upload_func,
                    content=self.content,
                    part_id=part_id,
                    offset=offset,
                    **self.extre_uplood_kwargs,
                )

                futures.append(result)
                part_id += 1
                offset += self.multipart_chunksize

            for future in futures:
                future.result()
        return futures


class BaseDriver:
    """Base Driver class"""
    id = None
    read_timeout = READ_TIMEOUT
    connect_timeout = CONNECT_TIMEOUT

    def __init__(self, read_timeout=None, connect_timeout=None, **kwargs):
        self.read_timeout = read_timeout or self.read_timeout
        self.connect_timeout = connect_timeout or self.connect_timeout
        self.kwargs = self._validate_kwargs(kwargs)
        self.logger = logging.getLogger('osb.driver')

    def setup(self, **kwargs):
        """Initialiaze driver before benchmark"""

    def _validate_kwargs(self, kwargs):
        """Ensure kwargs passed to __init__ are correct."""
        return kwargs

    def list_buckets(self, **kwargs):
        """List all buckets"""
        raise NotImplementedError()

    def create_bucket(self, name, **kwargs):
        """Create a bucket"""
        raise NotImplementedError()

    def delete_bucket(self, bucket_id):
        """Delete a bucket"""
        raise NotImplementedError()

    def list_objects(self, bucket_id, **kwargs):
        """List objects from a bucket"""
        raise NotImplementedError()

    def upload(self, bucket_id, name, content, **kwargs):
        """Upload an object into a bucket"""
        raise NotImplementedError()

    def get_url(self, bucket_id, name, **kwargs):
        """Get object URL"""
        raise NotImplementedError()

    def download(self, url, block_size=65536, **kwargs):
        """Download object from URL"""
        raise NotImplementedError()

    def delete_object(self, bucket_id, name, **kwargs):
        """Delete object from a bucket"""
        raise NotImplementedError()

    def clean_bucket(self, bucket_id, delete_bucket=True):
        """Delete all object from a bucket"""
        try:
            self.logger.debug("Listing all objects from %s", bucket_id)
            objects = self.list_objects(bucket_id=bucket_id)
        except errors.DriverBucketUnfoundError as err:
            self.logger.debug(err)
            return
        for obj in objects:
            self.logger.info("Deleting object %s/%s", bucket_id, obj)
            self.delete_object(bucket_id=bucket_id, name=obj)
        if delete_bucket:
            self.delete_bucket(bucket_id=bucket_id)

    def clean(self):
        """Delete all buckets and all object"""
        self.logger.debug("Listing all buckets")
        for bucket in self.list_buckets():
            self.logger.info("Deleting bucket %s", bucket['id'])
            self.clean_bucket(bucket_id=bucket['id'], delete_bucket=True)

    def sql_select(self, bucket_id, name, expression):
        self.logger.warning("%s doesn't implement a remote SQL solution.", self.id)
        url = self.get_url(
            bucket_id=bucket_id,
            name=name,
        )
        content = self.download_stream(url)
        csv_reader = csv.DictReader(content)

        sqlite = sqlite3.connect('/tmp/osb.sql')
        table_created = False
        col_names = None

        for row in csv_reader:
            if not table_created:
                col_names = {
                    i: c.split('_')[1]
                    for i, c in enumerate(row)
                }
                req = "CREATE TABLE IF NOT EXISTS %s (%s);" % (
                    bucket_id,
                    ', '.join([
                        ("%s %s" % tuple(v.split('_')[::-1]))
                        for v in row
                    ])
                )
                self.logger.debug('SQL: %s', req)
                sqlite.execute(req)
                table_created = True

            sqlite.execute("INSERT INTO %s VALUES (%s);" % (
                bucket_id,
                ', '.join([("'%s'" % v) for v in row.values()])
            ))
        sqlite.commit()

        # Correct expression
        if not ' FROM ' in expression:
            expression += ' FROM %s' % bucket_id
        matches = re.findall('(:\d*:)', expression)
        for match in matches:
            col_name = col_names[int(match.replace(':', ''))]
            expression = expression.replace(match, col_name)
        self.logger.debug("Final expression: %s", expression)
        result = sqlite.execute(expression).fetchall()

        sqlite.execute("""DROP TABLE %s;""" % (
            bucket_id,
        ))
        os.remove('/tmp/osb.sql')


class HTTPAdapter(BaseHTTPAdapter):
    def __init__(self, timeout=None, *args, **kwargs):
        self.timeout = 3 if timeout is None else timeout
        super().__init__(*args, **kwargs)

    def send(self, request, **kwargs):
        timeout = kwargs.get("timeout")
        if timeout is None:
            kwargs["timeout"] = self.timeout
        return super().send(request, **kwargs)

class RequestsMixin:
    """Mixin providing a HTTTP Session"""
    session_headers = {}

    @property
    def session(self):
        if not hasattr(self, '_session'):
            self._session = requests.Session()
            self._session.headers = self.session_headers.copy()
            retry = Retry(total=0)
            timeout = (self.connect_timeout, self.read_timeout)
            adapter = HTTPAdapter(max_retries=retry, timeout=timeout)
            self._session.mount('http://', adapter)
            self._session.mount('https://', adapter)
        return self._session

    def download(self, url, block_size=65536, **kwargs):
        self.logger.debug('GET %s', url)
        try:
            with self.session.get(url, stream=True) as response:
                if response.status_code != 200:
                    msg = '%s %s' % (url, response.content)
                    raise errors.base.InvalidHttpCode(msg, response.status_code)
                for chunk in response.iter_content(chunk_size=block_size):
                    pass
        except requests.exceptions.ConnectionError as err:
            raise errors.DriverConnectionError(err.args[0])
