#!/usr/bin/env python3
import os
import sys
import json
import time
import inspect
import webbrowser
import random

try:
    dbglevel = int(os.getenv("DBGLEVEL") or 0)
except:
    dbglevel = 0

conf = {}

version = "0.2.1"
plugin = "googledriveannex-" + version

pwd = os.path.realpath(__file__)
pwd = pwd[:pwd.rfind("/")]
if dbglevel:
    sys.stderr.write("Path: " + repr(pwd) + "\n")

#sys.path.append(pwd + '/lib')
sys.path = [pwd + '/lib'] + sys.path

import CommonFunctions as common

from apiclient.discovery import build
from apiclient.http import MediaFileUpload
import oauth2client.client
from oauth2client.client import OAuth2WebServerFlow, AccessTokenRefreshError
from oauth2client.file import Storage
from oauth2client.file import Credentials
from apiclient import errors
from apiclient.discovery import build
import httplib2

client_id = "617824357867.apps.googleusercontent.com"
client_secret = "vYxht56r40BlwpEagH_oPJPP"
redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
oauth_scope = 'https://www.googleapis.com/auth/drive'
http = httplib2.Http()
service = False
credentials = False
cache = {}

def login(cred={}):
    common.log("")
    global service, credentials, http
    if isinstance(cred, str) or isinstance(cred, str):
        credentials = Credentials.new_from_json(cred)
    else:
        flow = OAuth2WebServerFlow(client_id, client_secret, oauth_scope, redirect_uri)
        authorize_url = flow.step1_get_authorize_url()
        if "oauth" not in cred or len(cred["oauth"]) == 0:
            common.log('Go to the following link in your browser: ' + authorize_url)
            webbrowser.open(authorize_url, True, True)
            return False
        else:
            credentials = flow.step2_exchange(cred["oauth"])

        common.log("Saving credentials")
        cred = credentials.to_json()

    http = credentials.authorize(http)
    common.log("Done: " + repr(http) + " - " + repr(credentials) + " - " + repr(cred))
    service = build('drive', 'v2', http=http)
    # Perform a random request just to test if the login is actually
    # successfull; sometimes error login succeed, but usage fails
    try:
        service.about().get().execute()
    except AccessTokenRefreshError:
        return False
    common.log("Done: " + repr(service) + " - " + repr(cred))
    return cred




def postFile(subject, filename, folder):
    common.log("%s to %s - %s" % ( repr(filename), folder["id"], subject))

    filesize = os.path.getsize(filename)
    drive_service = build('drive', 'v2', http=http)

    media_body = MediaFileUpload(filename, mimetype='application/octet-stream', resumable=True)
    body = {
        'title': subject,
        'mimeType': 'application/octet-stream',
        "parents":[folder]
        }

    if not conf["encrypted"]:
        common.log("Unencrypted repository, so adding filename to description")
        body['description'] = filename[filename.rfind(os.path.sep) + 1:]


    request = drive_service.files().insert(body=body, media_body=media_body)

    response = None

    n=0
    while n < 5:
        try:
            while response is None:
                status, response = request.next_chunk()  
                if status:
                    common.progress(filesize,status.progress() * filesize)
            common.log("Done: " + repr(response["id"]))
            return True
           
        except errors.HttpError as e:
            common.log(e)    
            common.log("Backing off")
            time.sleep((2 ** n) + random.randint(0, 1000) / 1000)
            common.log("Retrying")
            n+=1



    common.log("Failure: " + repr(response))
    return False


class GDriveError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)


def fixDuplicatedEntries(entries):
    global cache
    assert len(entries) > 1
    good = entries[0]
    good_id = good['id']
    bads = entries[1:]
    directories = (good['mimeType'] == "application/vnd.google-apps.folder")
    for bad in bads:
        bad_id = bad['id']
        if directories:
            common.log("Fixing bad directory %s against good directory %s" % (bad_id, good_id))
            assert bad['mimeType'] == "application/vnd.google-apps.folder"
            res = children = service.children().list(folderId=bad['id'], fields='items').execute()
            common.log("List children to be fixed: %r" % (res), 2)
            for child in children['items']:
                res = service.parents().delete(fileId=child['id'], parentId=bad_id).execute()
                common.log("Delete parent from child %s: %r" % (child['id'], res), 2)
                res = service.parents().insert(fileId=child['id'], body={'id': good_id}).execute()
                common.log("Set new parent to child %s: %r" % (child['id'], res), 2)
            res = service.files().delete(fileId=bad_id).execute()
            common.log("Delete bad directory: %r" % (res), 2)
        else:
            common.log("Removing bad file %s (with title %s) in favor of good file %s (with title %s)" % (bad_id, bad['title'], good_id, good['title']))
            assert bad['mimeType'] != "application/vnd.google-apps.folder"
            res = service.files().delete(fileId=bad_id).execute()
            common.log("Delete bad file: %r" % (res), 2)


    # After fixDuplicatedEntries(), cache is to be invalidated
    cache.clear()


def findInFolder(subject, folder):
    global cache 

    common.log("subject: %s" % (repr(subject)), )
    common.log("folder: %s" % (repr(folder)), 2)
   
    for n in range(0,5): 
        try:
            found = False
            param = {"fields": "items"}
            if folder:
                #param["q"] = "title = '%s' and '%s' in parents" % (subject, folder["id"])
                param["q"] = "title = '%s'" % (subject)
            else:
                param["q"] = "title = '%s' and 'root' in parents" % (subject)

    
            common.log("Calling with: " + repr(param), 1)
            files = service.files().list(**param).execute()
            common.log("Files : %s "%repr(files), 2)

 
            if len(files["items"])>1:
                common.log("Found the same file %d times, fixing" % (len(files['items'])))
                fixDuplicatedEntries(files['items'])
                files = service.files().list(**param).execute()
                if len(files["items"]) > 1:
                    common.log("Problem was not fixed, we still have %d files" % len(files['items']))

            if len(files["items"])==1 and files["items"][0]["labels"]["trashed"]==False:
                common.log("Success on: " + repr(subject))
                return files["items"][0]

            return False
                
         

        except errors.HttpError as e:
            common.log('An error occurred(%s): %s' % (errors, repr(e)))
            time.sleep((2 ** n) + random.randint(0, 1000) / 1000)
 
    common.log("Failure on: " + repr(subject))
    raise GDriveError("Could not complete find")

def checkFile(subject, folder):
    common.log(subject)

    file = findInFolder(subject, folder)

    common.log("Result of findInFolder %s "% (file['id'] if file else 'False'))

    if file:
        common.log("Found: " + repr(file))
        return True
    else:
        common.log("Failure")
        return False

def getFile(subject, filename, folder):
    common.log(subject)

    file = findInFolder(subject, folder)
    if file:
        common.log("Got file")
        download_url = file.get('downloadUrl')
        common.log("Got download_url: " + repr(download_url))
        if download_url:
            try:
                f = open(filename, "wb")
                ret = common.fetchPage({"link": download_url, "headers": [("Authorization", "Bearer " + credentials.access_token)], "progress": "true", "totalSize": file["fileSize"]}, write_fd=f)
                if ret["status"] == 200:
                    common.log('Status: %s' % ret["status"])
                    common.log("Done")
                    return True
                else:
                    common.log('An error occurred: %s' % ret)
            finally:
                f.close()
        else:
            common.log("The file doesn't have any content stored on Drive.")
    else:
        common.log("Failure")

    return False

def deleteFile(subject, folder):
    common.log(subject)


    file = findInFolder(subject, folder)

    if file:
        res = service.files().delete(fileId=file["id"]).execute()
        common.log("Done: " + repr(res))
        return True
    else:
        common.log("Failure")
        return False

def setFolder(base):
    common.log(repr(base) )
    
    for n in range(0,5):
        try:
            if base in cache:
                return cache[base]
            folder = findInFolder(base, False)
            if folder:
                common.log("Using folder: " + repr(folder["id"]))
                ANNEX_FOLDER = folder
            else:
                common.log("Creating root folder: " + repr(folder))
                root_folder = service.files().insert(body={ 'title': base, 'mimeType': "application/vnd.google-apps.folder" }).execute()
                common.log("root folder: " + repr(root_folder["id"]))
                ANNEX_FOLDER = root_folder
            cache[base] = ANNEX_FOLDER
            return ANNEX_FOLDER
        except errors.HttpError as e:
            time.sleep((2 ** n) + random.randint(0, 1000) / 1000)
            n+=1
    return None

## gaprot interface

def initremote(line):
    common.log("")
    oauth = os.getenv("OAUTH") or ""
    encryption = common.getConfig("encryption")
    myfolder = common.getConfig("folder")
    if oauth != '':
        stored_creds = sys.modules["__main__"].login({"oauth": oauth})
        if len(myfolder) and stored_creds:
            common.sprint('SETCONFIG myfolder ' + myfolder + '')
            common.sprint('SETCONFIG stored_creds ' + json.dumps(stored_creds) + '')
            common.sprint('INITREMOTE-SUCCESS')
        else:
            common.sprint('INITREMOTE-FAILURE You need to set OAUTH environment variables and folder and encryption parameters when running initremote.')
    else:
        common.sprint('INITREMOTE-SUCCESS')
    common.log("Done")

def prepare(line):
    common.log("")

    myfolder = common.getConfig("folder")
    encryption = common.getConfig("encryption")
    stored_creds = common.getConfig("stored_creds")
    stored_creds = json.loads(stored_creds)

    if len(myfolder) > 1 and len(stored_creds):
        conf["folder"] = myfolder
        conf["encrypted"] = encryption.lower().find("none") == -1
        common.sprint('DEBUG ' + plugin)
        oauth = os.getenv("OAUTH") or ""
        if oauth == "":
            if login(stored_creds):
                common.sprint('PREPARE-SUCCESS')
            else:
                flow = OAuth2WebServerFlow(client_id, client_secret, oauth_scope, redirect_uri)
                authorize_url = flow.step1_get_authorize_url()
                common.sprint('PREPARE-FAILURE Login failure. Use the following link to acquire another auth credential: %s' % (authorize_url))
        else:
            stored_creds = login({"oauth": oauth})
            if stored_creds:
                common.sprint('SETCONFIG stored_creds ' + json.dumps(stored_creds) + '')
                common.sprint('PREPARE-SUCCESS')
    else:
        common.sprint('PREPARE-FAILURE You need to set USERNAME and PASSWORD environment variables and folder and encryption parameters for the remote.')
    common.log("")

def transfer(line):
    common.log("")

#    common.log(line)
#    common.sprint('TRANSFER-FAILURE STORE ' + line[2] + ' Marek blocked this')
#    return True

    for n in range(0, 5):
        try:
            folder = setFolder(conf["folder"])

            if line[1] == "STORE":
                if postFile(line[2], " ".join(line[3:]), folder):
                    common.sprint('TRANSFER-SUCCESS STORE ' + line[2] + '')
                    break
                else:
                    common.sprint('TRANSFER-FAILURE STORE ' + line[2] + ' File could not be stored')
                    break

            if line[1] == "RETRIEVE":
                if getFile(line[2], " ".join(line[3:]), folder):
                    common.sprint('TRANSFER-SUCCESS RETRIEVE ' + line[2] + '')
                    break
                else:
                    common.sprint('TRANSFER-FAILURE RETRIEVE ' + line[2] + ' File could not be retrieved')
                    break
            
        except errors.HttpError as e:
            common.log("HttpError : %s" % repr(e))
            time.sleep((2 ** n) + random.randint(0, 1000) / 1000)
    common.log("Done")

def checkpresent(line):
    common.log("")

    for n in range(0, 5):
        try:
            
            folder = setFolder(conf["folder"])

            if not len(folder):
                common.sprint('CHECKPRESENT-UNKNOWN ' + line[1] + ' this remote is not currently available')
                return None

            if checkFile(line[1], folder):
                common.sprint('CHECKPRESENT-SUCCESS ' + line[1] + '')
                return None
            else:
                common.sprint('CHECKPRESENT-FAILURE ' + line[1] + '')
                return None

        except errors.HttpError as e:
            common.log("HttpError : %s" % repr(e))
            time.sleep((2 ** n) + random.randint(0, 1000) / 1000)


    common.sprint('CHECKPRESENT-UNKNOWN ' + line[1] + ' this remote is currently not available')
    common.log("Done")

def remove(line):
    common.log("")
    folder = setFolder(conf["folder"])
                
    if deleteFile(line[1], folder):
        common.sprint('REMOVE-SUCCESS ' + line[1] + '')
    else:
        common.sprint('REMOVE-FAILURE ' + line[1] + ' This file could not be removed')
    common.log("Done")

def getCost():
    common.log("", 3)
    common.sprint("COST 150")

t = time.time()
if dbglevel > 0:
    sys.stderr.write("\n")

common.log("START")
if __name__ == '__main__':
    try:
        common.startRemote()
    except:
        import traceback
        common.log("Failure with exception:\n%s" % (traceback.format_exc()))
common.log("STOP: %ss" % int(time.time() - t))
