import argparse
import errno
import logging
import os
import pickle
import re
import shutil
from tempfile import NamedTemporaryFile
from time import sleep
from warnings import catch_warnings, filterwarnings
from zipfile import ZipFile

import httplib2
import requests
from apiclient import discovery
from googleapiclient.http import MediaIoBaseDownload
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage
import boto3
from oauth2client._helpers import _MISSING_FILE_MESSAGE

log = logging.getLogger(__name__)


class CachedFileGetter(object):
    def __init__(self, key):
        self.key = key

    def get_and_save(self, path):
        raise NotImplementedError()

    def get_metadata(self):
        return NotImplemented

    def is_cache_valid(self, new_meta, old_meta, path):
        return new_meta == old_meta

    def cache_root_location(self):
        return os.path.join(os.path.expanduser('~'),
                            '.download_cache',
                            self.__class__.__name__)

    def path(self):
        base = os.path.join(self.cache_root_location(),
                            str(self.key))
        path = os.path.join(base, 'thefile')
        meta_path = os.path.join(base, 'meta')

        try:
            os.makedirs(base)
        except OSError as exception:
            if exception.errno != errno.EEXIST:
                raise

        new_meta = self.get_metadata()

        cached = os.path.exists(path)
        if new_meta is not NotImplemented:
            if cached:
                try:
                    with open(meta_path, 'rb') as meta_file:
                        old_meta = pickle.load(meta_file)
                except IOError:
                    pass
                else:
                    cached = self.is_cache_valid(new_meta, old_meta, path)

            with open(meta_path, 'wb') as meta_file:
                pickle.dump(new_meta, meta_file)

        if not cached:
            self.get_and_save(path)
        return path

    def open(self, mode='rb', **kwargs):
        return open(self.path(), mode=mode, **kwargs)


class CachedTuplesCSV(CachedFileGetter):
    def __init__(self, has_tuples):
        self.meta = has_tuples.get()
        match = re.match(r'/api/v2/query/\d+/(result/.+)$', has_tuples.base_url)
        if match:
            client = has_tuples.root
            cached_query_url = client.get(self.meta['query'])['from_cache']
            if cached_query_url:
                log.debug('Using cached query at %s', cached_query_url)
                has_tuples = has_tuples.__class__.at(client / cached_query_url / match.group(1))
                self.meta = has_tuples.get()
        self.has_tuples = has_tuples
        super(CachedTuplesCSV, self).__init__(has_tuples.full_url.replace('/', '_'))

    def get_metadata(self):
        return self.meta

    def get_and_save(self, path):
        log.info('Downloading CSV of %s', self.has_tuples.full_url)
        download_url = self.has_tuples.get('external_stream_csv')['download_url']
        with NamedTemporaryFile() as temp_zip_file:
            while True:
                response = requests.get(download_url, stream=True)
                if response.status_code == 200:
                    break
                sleep(5)
            shutil.copyfileobj(response.raw, temp_zip_file)
            temp_zip_file.flush()
            with ZipFile(temp_zip_file) as zipfile:
                extracted_path = zipfile.extract(zipfile.infolist()[0])
        shutil.move(extracted_path, path)
        log.info('Finished downloading CSV')


class CachedGoogleDriveFile(CachedFileGetter):
    """
    Download a file from Google Drive.

    Accepts a file ID or a URL containing it in one of the following formats:

        - https://drive.google.com/open?id=<file ID>
          (obtained by clicking on a file and then 'Get shareable link')
        - https://drive.google.com/file/d/<file ID>/view
          (a link of the first type typically redirects to a link like this)

    Requires the file ~/.google-credentials/client_secret.json. If it's not present it
    will try to download it from S3, so having access to AWS credentials is useful.

    Using this for the first time will require opening a link in a browser, authenticating
    through Google, and pasting a code into the terminal.
    """
    def __init__(self, id_or_link):
        file_id = self.extract_file_id(id_or_link)

        super(CachedGoogleDriveFile, self).__init__(file_id)
        self.file_id = file_id
        home_dir = os.path.expanduser('~')
        credential_dir = os.path.join(home_dir, '.google-credentials')
        if not os.path.exists(credential_dir):
            os.makedirs(credential_dir)
        credential_path = os.path.join(credential_dir, 'credentials.json')
        client_secret_path = os.path.join(credential_dir, 'client_secret.json')

        store = Storage(credential_path)
        with catch_warnings():
            filterwarnings('ignore', _MISSING_FILE_MESSAGE.format(credential_path))
            credentials = store.get()

        if not credentials or credentials.invalid:
            if not os.path.exists(client_secret_path):
                log.info('Client secret not found at %s. Downloading...', client_secret_path)
                try:
                    boto3.client('s3').download_file('businessoptics-alex',
                                                     'cs.json',
                                                     client_secret_path)
                except Exception:
                    log.error('Error downloading the client secret. Request it from Alex.')
                    raise
            flow = client.flow_from_clientsecrets(
                client_secret_path, 'https://www.googleapis.com/auth/drive.readonly')
            flow.user_agent = 'businessoptics client'
            flags = argparse.ArgumentParser(parents=[tools.argparser]
                                            ).parse_args(['--noauth_local_webserver'])
            credentials = tools.run_flow(flow, store, flags)
            print('Storing credentials to ' + credential_path)

        http = credentials.authorize(httplib2.Http())

        autodetect_logger = logging.getLogger('googleapiclient.discovery_cache')
        old_level = autodetect_logger.level
        autodetect_logger.setLevel(logging.ERROR)
        try:
            self.service = discovery.build('drive', 'v3', http=http)
        finally:
            autodetect_logger.level = old_level

    @staticmethod
    def extract_file_id(id_or_link):
        for pattern in [r'drive.google.com/open\?id=([\w_-]+)$',
                        r'drive.google.com/file/\w/([\w_-]+)/\w+$',
                        r'^([\w_-]+)$']:
            match = re.search(pattern, id_or_link)
            if match:
                return match.group(1)
        raise ValueError("Failed to extract file ID from %s", id_or_link)

    def get_metadata(self):
        return self.service.files().get(fileId=self.file_id, fields='md5Checksum').execute()['md5Checksum']

    def get_and_save(self, path):
        log.info('Downloading file %s from Google Drive to %s', self.file_id, path)
        request = self.service.files().get_media(fileId=self.file_id)
        with open(path, 'wb') as outfile:
            downloader = MediaIoBaseDownload(outfile, request)
            done = False
            progress = 0
            while done is False:
                status, done = downloader.next_chunk()
                new_progress = int(status.progress() * 100)
                if new_progress > progress:
                    progress = new_progress
                    log.info('Downloaded %d%%', progress)
