import os
import re
import time
import urllib.request
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException

from pageObjects.SyncConnection import SyncConnection
from helpers.ConfigHelper import get_config, is_linux, is_windows
from helpers.FilesHelper import sanitize_path

if is_windows():
    from helpers.WinPipeHelper import WinPipeConnect as SocketConnect
else:
    # NOTE: 'syncstate.py' was removed from client
    # and is now available at https://github.com/opencloud-eu/desktop-shell-integration-nautilus
    # check if 'syncstate.py' is available, if not, download it
    custom_lib = get_config('custom_lib')
    syncstate_lib_file = os.path.join(custom_lib, 'syncstate.py')
    os.makedirs(custom_lib, exist_ok=True)

    if not os.path.exists(syncstate_lib_file):
        urllib.request.urlretrieve(
            'https://raw.githubusercontent.com/opencloud-eu/desktop-shell-integration-nautilus/refs/heads/main/src/syncstate.py',
            syncstate_lib_file,
        )
        # do not instantiate SocketConnect in the script.
        with open(syncstate_lib_file, 'r') as f:
            content = f.read()
        content = content.replace('socketConnect = SocketConnect()', '')
        content = content.replace(
            'from gi.repository import GObject, Nautilus',
            'import gi\n\ngi.require_version(\'Nautilus\', \'4.0\')\nfrom gi.repository import GObject, Nautilus',
        )

        with open(syncstate_lib_file, 'w') as f:
            f.write(content)

    # the script needs to use the system-wide python
    # to switch from the built-in interpreter
    #   see https://kb.froglogic.com/squish/howto/using-external-python-interpreter-squish-6-6/
    # if the IDE fails to reference the script,
    # add the folder in Edit->Preferences->PyDev->Interpreters->Libraries
    from helpers.custom_lib.syncstate import SocketConnect

# socket messages
socket_messages = []
SOCKET_CONNECT = None
# Whether wait has been made or not after account is set up
# This is useful for waiting only for the first time
WAITED_AFTER_SYNC = False

# File syncing in client has the following status
SYNC_STATUS = {
    'SYNC': 'STATUS:SYNC',  # sync in progress
    'OK': 'STATUS:OK',  # sync completed
    'OKAL': 'STATUS:OK+AL',  # sync completed (Always Local)
    'OKOO': 'STATUS:OK+OO',  # sync completed (Online Only)
    'ERROR': 'STATUS:ERROR',  # sync error
    'IGNORE': 'STATUS:IGNORE',  # sync ignored
    'NOP': 'STATUS:NOP',  # not in sync yet
    'REGISTER': 'REGISTER_PATH',
    'UNREGISTER': 'UNREGISTER_PATH',
    'UPDATE': 'UPDATE_VIEW',
}

SYNC_PATTERNS = {
    # default sync patterns for the initial sync (after adding account)
    # the pattern can be of TWO types depending on the available resources (files/folders)
    'initial': [
        # when adding account via New Account wizard
        [
            SYNC_STATUS['NOP'],
            SYNC_STATUS['REGISTER'],
            SYNC_STATUS['UPDATE'],
        ],
        # when syncing empty account (hidden files are ignored)
        # [SYNC_STATUS['UPDATE'], SYNC_STATUS['OK']],
        # [SYNC_STATUS['UPDATE'], SYNC_STATUS['OKAL']],
        # when syncing an account that has some files/folders
        # [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']],
        # initial root sync
        [
            SYNC_STATUS['OK'],
            SYNC_STATUS['OK'],
            SYNC_STATUS['UPDATE'],
        ],
    ],
    'root_synced': [
        [
            SYNC_STATUS['OK'],
            SYNC_STATUS['OK'],
            SYNC_STATUS['UPDATE'],
        ],
        # [
        #     SYNC_STATUS['SYNC'],
        #     SYNC_STATUS['OK'],
        #     SYNC_STATUS['OK'],
        #     SYNC_STATUS['OK'],
        #     SYNC_STATUS['UPDATE'],
        # ],
        # [
        #     SYNC_STATUS['SYNC'],
        #     SYNC_STATUS['UPDATE'],
        #     SYNC_STATUS['OK'],
        #     SYNC_STATUS['OK'],
        #     SYNC_STATUS['OK'],
        #     SYNC_STATUS['UPDATE'],
        # ],
        # # used for local resource creation and deletion
        # [
        #     SYNC_STATUS['OKAL'],
        #     SYNC_STATUS['OK'],
        #     SYNC_STATUS['OK'],
        #     SYNC_STATUS['UPDATE'],
        # ],
    ],
    'single_synced': [
        [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']],
        # file/folder deletion
        [SYNC_STATUS['SYNC'], SYNC_STATUS['NOP']],
    ],
    'error': [SYNC_STATUS['ERROR']],
}


def get_socket_connection():
    global SOCKET_CONNECT
    if not SOCKET_CONNECT or not SOCKET_CONNECT.connected:
        SOCKET_CONNECT = SocketConnect()
    return SOCKET_CONNECT


def read_socket_messages():
    messages = []
    socket_connect = get_socket_connection()
    socket_connect.read_socket_data_with_timeout(0.1)
    for line in socket_connect.get_available_responses():
        messages.append(line)
    return messages


def read_and_update_socket_messages():
    messages = read_socket_messages()
    return update_socket_messages(messages)


def update_socket_messages(messages):
    socket_messages.extend(filter_sync_messages(messages))
    return socket_messages


def clear_socket_messages(resource=''):
    global socket_messages
    if resource:
        resource_messages = set(filter_messages_for_item(socket_messages, resource))
        socket_messages = [
            msg for msg in socket_messages if msg not in resource_messages
        ]
    else:
        socket_messages.clear()


def close_socket_connection():
    socket_messages.clear()
    if SOCKET_CONNECT:
        SOCKET_CONNECT.connected = False
        if is_windows():
            SOCKET_CONNECT.close_conn()
        elif is_linux():
            SOCKET_CONNECT._sock.close()  # pylint: disable=protected-access


def get_initial_sync_patterns():
    return SYNC_PATTERNS['initial']


def get_synced_pattern(resource=''):
    # get only the resource path
    sync_path = get_config('currentUserSyncPath')
    sync_path = os.path.join(sync_path, get_config('syncConnectionName'))

    if resource := resource.replace(sync_path, '').strip('\\').strip('/'):
        return SYNC_PATTERNS['single_synced']
    return SYNC_PATTERNS['root_synced']


# generate sync pattern from the socket messages
#
# returns List
# e.g: ['UPDATE_VIEW', 'STATUS:OK']
def generate_sync_pattern_from_messages(messages):
    pattern = []
    if not messages:
        return pattern

    sync_messages = filter_sync_messages(messages)
    for message in sync_messages:
        # E.g; from;
        #    Linux: "STATUS:OK:/tmp/client-bdd/Alice/"
        #      Win: "STATUS:OK:C:\tmp\client-bdd\Alice\"
        # excludes ":/tmp/client-bdd/Alice/"
        # adds only "STATUS:OK" to the pattern list
        if match := re.search(r':(/|[A-Za-z]:[\\/]).*', message):
            end, _ = match.span()
            # shared resources will have status like "STATUS:OK+SWM"
            status = message[:end].replace('+SWM', '')
            pattern.append(status)
    return pattern


# strip out the messages that are not related to sync
def filter_sync_messages(messages):
    start_idx = 0
    if 'GET_STRINGS:END' in messages:
        start_idx = messages.index('GET_STRINGS:END') + 1
    return messages[start_idx:]


def filter_messages_for_item(messages, item):
    filtered_messages = []
    for msg in messages:
        msg = msg.rstrip('/').rstrip('\\')
        item = item.rstrip('/').rstrip('\\')
        if msg.endswith(item):
            filtered_messages.append(msg)
    return filtered_messages


def listen_sync_status_for_item(item, resource_type='FOLDER'):
    if (resource_type := resource_type.upper()) not in ('FILE', 'FOLDER'):
        raise ValueError('resource_type must be "FILE" or "FOLDER"')
    socket_connect = get_socket_connection()
    item = item.rstrip('\\').rstrip('/')
    socket_connect.sendCommand(f'RETRIEVE_{resource_type}_STATUS:{item}\n')


def get_current_sync_status(resource, resource_type):
    listen_sync_status_for_item(resource, resource_type)
    messages = filter_messages_for_item(read_socket_messages(), resource)
    # return the last message from the list
    return messages[-1]


def wait_for_resource_to_sync(
    resource, resource_type='FOLDER', patterns=None, force_sync=False
):
    listen_sync_status_for_item(resource, resource_type)

    initial_timeout = 0
    timeout = get_config('maxSyncTimeout') * 1000

    if patterns is None:
        patterns = get_synced_pattern(resource)

    if force_sync:
        initial_timeout = 5000
        # first try with 5 seconds timeout
        synced = wait_for(
            lambda: has_sync_pattern(patterns, resource),
            initial_timeout,
        )
        if not synced:
            # trigger force sync if the current status is OK
            status = get_current_sync_status(resource, resource_type)
            if status.startswith(SYNC_STATUS['OK']):
                print('[WARN] Retrying sync pattern check with force sync')
                SyncConnection.force_sync()
        else:
            clear_socket_messages(resource)
            return

    synced = wait_for(
        lambda: has_sync_pattern(patterns, resource),
        timeout - initial_timeout,
    )

    messages = read_and_update_socket_messages()
    messages = filter_messages_for_item(messages, resource)
    clear_socket_messages(resource)
    if synced:
        return
    elif not force_sync:
        # if the sync pattern doesn't match then check the last sync status
        # and pass the step if the last sync status is STATUS:OK
        status = get_current_sync_status(resource, resource_type)
        if status.startswith(SYNC_STATUS['OK']):
            print(
                '[WARN] Failed to match sync pattern for resource: '
                + resource
                + f'\nBut its last status is "{SYNC_STATUS["OK"]}"'
                + '. So passing the step.'
            )
            return
    raise TimeoutError(
        'Timeout while waiting for sync to complete for '
        + str(timeout)
        + ' milliseconds'
    )


def wait_for_initial_sync_to_complete(path):
    wait_for_resource_to_sync(
        path,
        'FOLDER',
        get_initial_sync_patterns(),
        True,
    )


def has_sync_pattern(patterns, resource=None):
    if isinstance(patterns[0], str):
        patterns = [patterns]
    messages = read_and_update_socket_messages()
    if resource:
        messages = filter_messages_for_item(messages, resource)
    for pattern in patterns:
        pattern_len = len(pattern)
        for idx, _ in enumerate(messages):
            actual_pattern = generate_sync_pattern_from_messages(
                messages[idx : idx + pattern_len]
            )
            if len(actual_pattern) < pattern_len:
                break
            if pattern_len == len(actual_pattern) and pattern == actual_pattern:
                print("MATCHED SYNC PATTERN:", pattern)
                return True
    # 100 milliseconds polling interval
    time.sleep(0.1)
    return False


# Using socket API to check file sync status
def has_sync_status(item_name, status):
    sync_messages = read_and_update_socket_messages()
    sync_messages = filter_messages_for_item(sync_messages, item_name)
    for line in sync_messages:
        line = line.rstrip('/').rstrip('\\')
        item_name = item_name.rstrip('/').rstrip('\\')
        if line.startswith(status) and line.endswith(item_name):
            return True
    return False


# useful for checking sync status such as 'error', 'ignore'
# but not quite so reliable for checking 'ok' sync status
def wait_for_resource_to_have_sync_status(
    resource, resource_type, status=SYNC_STATUS['OK'], timeout=None
):
    resource = sanitize_path(resource)

    listen_sync_status_for_item(resource, resource_type)

    if not timeout:
        timeout = get_config('maxSyncTimeout') * 1000

    result = wait_for(
        lambda: has_sync_status(resource, status),
        timeout,
    )

    if not result:
        if status == SYNC_STATUS['ERROR']:
            expected = 'have sync error'
        elif status == SYNC_STATUS['IGNORE']:
            expected = 'be sync ignored'
        else:
            expected = 'be synced'
        raise ValueError(
            f'Expected {resource_type} "{resource}" to {expected}, but not.'
        )


def wait_for_resource_to_have_sync_error(resource, resource_type):
    wait_for_resource_to_have_sync_status(resource, resource_type, SYNC_STATUS['ERROR'])


# performing actions immediately after completing the sync from the server does not work
# The test should wait for a while before performing the action
# issue: https://github.com/owncloud/client/issues/8832
def wait_for_client_to_be_ready():
    global WAITED_AFTER_SYNC
    if not WAITED_AFTER_SYNC:
        time.sleep(get_config('minSyncTimeout'))
        WAITED_AFTER_SYNC = True


def clear_waited_after_sync():
    global WAITED_AFTER_SYNC
    WAITED_AFTER_SYNC = False


def perform_file_explorer_vfs_action(resource_path, action):
    if action == 'Free up space':
        make_online_only(resource_path)
    elif action == 'Always keep on this device':
        make_available_locally(resource_path)
    else:
        raise ValueError(f'Invalid file explorer action: {action}')


def make_online_only(resource_path):
    socket_connect = get_socket_connection()
    resource_path = resource_path.rstrip('\\').rstrip('/')
    socket_connect.sendCommand(f'MAKE_ONLINE_ONLY:{resource_path}\n')


def make_available_locally(resource_path):
    socket_connect = get_socket_connection()
    resource_path = resource_path.rstrip('\\').rstrip('/')
    socket_connect.sendCommand(f'MAKE_AVAILABLE_LOCALLY:{resource_path}\n')


def wait_for(condition, timeout, interval=0.5):
    from helpers.AppHelper import app

    wait = WebDriverWait(app(), timeout / 1000, poll_frequency=interval)
    try:
        wait.until(lambda _: condition())
        return True
    except TimeoutException:
        return False
