import argparse
import datetime
import os
import pathlib
import re
import shutil
import sys
import py7zr

import dropbox
from dropbox import DropboxOAuth2FlowNoRedirect
from dropbox.exceptions import ApiError, AuthError
from dropbox.files import WriteMode

APP_KEY = "v33m5tjz020h7uy"


def run_auth():
    """
    Use to generate a refresh token
    """
    auth_flow = DropboxOAuth2FlowNoRedirect(APP_KEY, use_pkce=True, token_access_type='offline')

    authorize_url = auth_flow.start()
    print(f"1. Go to: {authorize_url}")
    print("2. Click \"Allow\" (you might have to log in first).")
    print("3. Copy the authorization code.")
    auth_code = input("Enter the authorization code here: ").strip()

    try:
        oauth_result = auth_flow.finish(auth_code)
    except Exception as e:
        print(f'Error: {e}')
        exit(1)

    print(f"Refresh token: {oauth_result.refresh_token}")


def run_pack(source, destination):
    """
    Pack folder. Automatically fills arguments for shutil.make_archive.
    :param source: path to source root directory. Example: '/path/to/folder/'
    :param destination: path to resulting zip archive. The path must include a format suffix.
    Example: '/path/to/folder.zip'
    """
    base = os.path.basename(destination)
    name = base.split('.')[0]

    formats = {
        ".zip": "zip",
        ".tar.xz": "xztar",
        ".7z": "7zip"
    }
    suffix = ''.join(pathlib.Path(base).suffixes)
    archive_from = pathlib.Path(source).parent
    archive_to = os.path.basename(source.strip(os.sep))
    print(source, destination, archive_from)
    format = formats.get(suffix)
    if format:
        if format == "7zip":
            with py7zr.SevenZipFile(f"{name}{suffix}", 'w') as archive:
                archive.writeall(source, arcname=os.path.basename(source))
        else:
            shutil.make_archive(name, format, archive_from, archive_to)
        shutil.move(f'{name}{suffix}', destination)
    else:
        print("Unsupported archive format.")


def run_upload(refresh_token, file, path):
    with dropbox.Dropbox(oauth2_refresh_token=refresh_token, app_key=APP_KEY) as dbx:
        # Check that the access token is valid
        try:
            dbx.users_get_current_account()
        except AuthError:
            sys.exit("ERROR: Invalid access token; try re-generating an "
                     "access token from the app console on the web.")

        print(f"Uploading {file} to Dropbox as {path}...")
        try:
            with open(file, "rb") as f:
                dbx.files_upload(f.read(), path, mode=WriteMode('overwrite'))
        except ApiError as err:
            # This checks for the specific error where a user doesn't have
            # enough Dropbox space quota to upload this file
            if (err.error.is_path() and
                    err.error.get_path().reason.is_insufficient_space()):
                sys.exit("ERROR: Cannot deploy; insufficient space.")
            elif err.user_message_text:
                print(err.user_message_text)
                sys.exit()
            else:
                print(err)
                sys.exit()
        print("Successfully uploaded")

def folder_mod_time(dbx, folder):
    folder_mod_times = []
    entries = dbx.files_list_folder(folder.path_display)
    # Loop through each item in the folder list
    for result in entries.entries:
        # Check if the item is a file
        if isinstance(result, dropbox.files.FileMetadata):
            # Get the parent folder path of the file
            parent_folder_path = result.path_display.rsplit('/', 1)[0]
            # Get the modification time of the file
            file_mod_time = result.client_modified
            # Add the file modification time to the dictionary for the parent folder
            folder_mod_times.append(file_mod_time)

    folder_mod_times.append(datetime.datetime(1900, 1, 1, 0, 0, 0))
    # Compute the maximum modification time across all files in each folder
    max_mod_time = max(folder_mod_times)
    return max_mod_time

# Define a function to delete a file or folder recursively
def delete_file_or_folder(dbx, item):
    try:
        # Check if the path is a file
        if isinstance(item, dropbox.files.FileMetadata):
            dbx.files_delete_v2(item.path_display)
            print(f"Deleted file: {item.path_display}")
        # Check if the path is a folder
        elif isinstance(item, dropbox.files.FolderMetadata):
            # Recursively delete all files and subfolders inside the folder
            for entry in dbx.files_list_folder(item.path_display).entries:
                delete_file_or_folder(dbx, entry)
            # Delete the folder itself
            dbx.files_delete_v2(item.path_display)
            print(f"Deleted folder: {item.path_display}")
    except dropbox.exceptions.ApiError as e:
        print(f"Error deleting {item.path_display}: {e}")

def run_clean(refresh_token):
    with dropbox.Dropbox(oauth2_refresh_token=refresh_token, app_key=APP_KEY) as dbx:
        # Check that the access token is valid
        try:
            dbx.users_get_current_account()
        except AuthError:
            sys.exit("ERROR: Invalid access token; try re-generating an "
                     "access token from the app console on the web.")

        clean_folders = ["/1.0.x/MacOS", "/1.0.x/Windows", "/1.0.x/Linux", "/1.1.x/MacOS", "/1.1.x/Windows", "/1.1.x/Linux"]
        arhive_types = [r'^valentina-Windows10\+-mingw-x64-Qt.*-(?:develop|master)-[a-f0-9]{40}\.exe$',
                        r'^valentina-Windows7\+-mingw-x86-Qt.*-(?:develop|master)-[a-f0-9]{40}\.exe$',
                        r'^valentina-Windows10\+-msvc-x64-Qt.*-(?:develop|master)-[a-f0-9]{40}\.exe$',
                        r'^valentina-Windows7\+-msvc-x86-Qt.*-(?:develop|master)-[a-f0-9]{40}\.exe$',
                        r'^valentina-portable-Windows10\+-mingw-x64-Qt.*-(?:develop|master)-[a-f0-9]{40}\.7z$',
                        r'^valentina-portable-Windows7\+-mingw-x86-Qt.*-(?:develop|master)-[a-f0-9]{40}\.7z$',
                        r'^valentina-portable-Windows10\+-msvc-x64-Qt.*-(?:develop|master)-[a-f0-9]{40}\.7z$',
                        r'^valentina-portable-Windows7\+-msvc-x86-Qt.*-(?:develop|master)-[a-f0-9]{40}\.7z$',
                        r'^valentina-MacOS_10_15\+-Qt.*-x64-(?:develop|master)-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_10_15\+-Qt.*-x64-(?:develop|master)-multibundle-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_12\+-Qt.*-x64-master-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_12\+-Qt.*-x64-master-multibundle-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_12\+-Qt.*-arm.*-master-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_12\+-Qt.*-arm.*-master-multibundle-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_13\+-Qt.*-x64-develop-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_13\+-Qt.*-x64-develop-multibundle-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_13\+-Qt.*-arm.*-develop-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_13\+-Qt.*-arm.*-develop-multibundle-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_11\+-Qt.*-arm.*-(?:develop|master)-[a-f0-9]{40}\.dmg$',
                        r'^valentina-MacOS_11\+-Qt.*-arm.*-(?:develop|master)-multibundle-[a-f0-9]{40}\.dmg$',
                        r'^valentina-Linux-x86_64-(?:develop|master)-[a-f0-9]{40}\.AppImage$']

        item_types = {}

        for path in clean_folders:
            result = dbx.files_list_folder(path)
            for entry in result.entries:
                for archive_type in arhive_types:
                    if re.search(archive_type, entry.name):
                        if archive_type not in item_types:
                            item_types[archive_type] = []
                        item_types[archive_type].append(entry)
                        break

        to_delete = []
        for items in item_types.values():
            # Separate files and folders
            files = [item for item in items if isinstance(item, dropbox.files.FileMetadata)]
            folders = [item for item in items if isinstance(item, dropbox.files.FolderMetadata)]

            # Sort files by modification time
            files = sorted(files, key=lambda f: f.client_modified)

            # Sort folders by last modified time on server
            folders = sorted(folders, key=lambda f: folder_mod_time(dbx, f))

            # Keep only the latest item of each type
            to_delete += files[:-1] + folders[:-1]

        # Delete the remaining items
        for item in to_delete:
            delete_file_or_folder(dbx, item)


def parse_args(args=None):
    parser = argparse.ArgumentParser(prog='app')
    cmds = parser.add_subparsers(help='commands')

    def cmd(name, **kw):
        p = cmds.add_parser(name, **kw)
        p.set_defaults(cmd=name)
        p.arg = lambda *a, **kw: p.add_argument(*a, **kw) and p
        p.exe = lambda f: p.set_defaults(exe=f) and p

        # global options
        # p.arg('-s', '--settings', help='application settings')
        return p

    cmd('auth', help='Authorize application') \
        .exe(lambda _: (
             run_auth()
        ))

    cmd('pack', help='Compress folder') \
        .arg('source', type=str, help='Path to folder or file') \
        .arg('destination', type=str, help='Path to resulting zip archive') \
        .exe(lambda a: (
             run_pack(a.source, a.destination)
        ))

    cmd('upload', help='Upload file with override') \
        .arg('refresh_token', type=str, help='Refresh token') \
        .arg('file', type=str, help='Path to file') \
        .arg('path', type=str, help='Path on disk') \
        .exe(lambda a: (
             run_upload(a.refresh_token, a.file, a.path)
        ))

    cmd('clean', help='Clean stale artifacts') \
        .arg('refresh_token', type=str, help='Refresh token') \
        .exe(lambda a: (
             run_clean(a.refresh_token)
    ))

    args = parser.parse_args(args)
    if not hasattr(args, 'exe'):
        parser.print_usage()
    else:
        args.exe(args)


if __name__ == '__main__':
    parse_args()

