import os
import random
import subprocess
import sys
import logging

from myst_connector.cli import parse_args
from myst_connector.connection import disconnect, get_connection_details, write_connection_file
from myst_connector.network import check_ip_score, get_ip_address, get_ip_score
from myst_connector.proposals import load_providers
from myst_connector.settings import get_app_int, validate_executables, get_optional_app_value
from myst_connector.system import acquire_single_instance_lock, run_quiet


logger = logging.getLogger(__name__)


IDENTITY = get_optional_app_value('identity')
MAX_RETRIES = get_app_int('max_retries')


def _notify_explicit_provider_failure(provider):
    run_quiet([EXECUTABLES['qrexec_client_vm'], "dom0", "myst.Notify+expl-prov-fail_" + provider[:12]])


def _connect_to_provider(provider):
    for attempt in range(MAX_RETRIES):
        logger.info('Connect attempt %d of %d', attempt + 1, MAX_RETRIES)
        try:
            run_quiet([EXECUTABLES['myst'], "cli", "connect", IDENTITY, provider, "wireguard", "dns=system"])
        except subprocess.CalledProcessError as error:
            if attempt == MAX_RETRIES - 1:
                logger.error('Failed to connect to provider: %s', error)
                return False
        else:
            return True
    return False


def _validate_provider_id(pid: str) -> bool:
    # allow common provider id characters only
    import re
    if not pid:
        return False
    return bool(re.fullmatch(r'[A-Za-z0-9_\-]{1,128}', pid))


def _lookup_ip_with_retries(label):
    ip_address = None
    for attempt in range(MAX_RETRIES):
        logger.info('%s attempt %d of %d', label, attempt + 1, MAX_RETRIES)
        try:
            ip_address = get_ip_address()
        except Exception:
            if attempt == MAX_RETRIES - 1:
                return None
        else:
            return ip_address
    return ip_address


def main(script_path=None):
    args = parse_args()
    minimum_provider_quality = args.min_quality
    country = args.country
    provider = args.provider
    ip_score_limit = args.ip_score

    if script_path is None:
        script_path = os.path.abspath(__file__)
    logging.basicConfig(level=logging.INFO)

    # validate required external executables and use resolved absolute paths
    global EXECUTABLES
    EXECUTABLES = validate_executables(['myst', 'qrexec_client_vm', 'ip'])

    lock_file = acquire_single_instance_lock(script_path)
    logger.info('Checking for existing myst0 interface')
    try:
        run_quiet([EXECUTABLES['ip'], "address", "show", "myst0"])
        logger.info('myst0 interface found, disconnecting existing connection')
        disconnect()
    except subprocess.CalledProcessError:
        logger.info('myst0 interface not found, unlocking identity')
        if not IDENTITY:
            logger.error('No identity provided; set MYST_IDENTITY environment variable (required)')
            sys.exit(1)
        try:
            run_quiet([EXECUTABLES['qrexec_client_vm'], "dom0", "myst.Notify+unlocking-identity"])
            run_quiet([EXECUTABLES['myst'], "cli", "--agreed-terms-and-conditions", "identities", "unlock", IDENTITY])
        except subprocess.CalledProcessError as error:
            logger.error('Failed to unlock identities: %s', error)
            sys.exit(1)

    providers = load_providers(minimum_provider_quality, country)
    if not provider:
        logger.info('Providers found: %d', len(providers))
        run_quiet([EXECUTABLES['qrexec_client_vm'], "dom0", "myst.Notify+numprov_" + str(len(providers))])

    if not providers:
        logger.warning('No providers found with current options')
        run_quiet([EXECUTABLES['qrexec_client_vm'], "dom0", "myst.Notify+no-prov"])
        sys.exit(1)

    if provider:
        if not _validate_provider_id(provider):
            logger.error('Explicit provider id is invalid')
            sys.exit(1)
        logger.info('Using explicit provider first')
        if provider in providers:
            providers.remove(provider)
        providers.insert(0, provider)

    if provider and len(providers) > 1:
        random.shuffle(providers[1:])
    elif not provider:
        random.shuffle(providers)

    initial_ip = _lookup_ip_with_retries('Initial IP lookup')
    if not initial_ip:
        logger.error('Failed to determine initial IP address')
        sys.exit(1)

    selected_provider = None
    for current_provider in providers:
        if not _validate_provider_id(current_provider):
            logger.warning('Skipping provider with invalid id: %s', current_provider)
            continue
        logger.info('Connecting to provider: %s', current_provider)
        run_quiet([EXECUTABLES['qrexec_client_vm'], "dom0", "myst.Notify+connecting"])

        connected = _connect_to_provider(current_provider)
        if not connected:
            is_explicit_provider = provider and current_provider == provider
            if is_explicit_provider:
                logger.error('ERROR: Failed to connect to explicit provider: %s', current_provider)
                _notify_explicit_provider_failure(current_provider)
                fallback_count = len(providers) - 1
                run_quiet([EXECUTABLES['qrexec_client_vm'], "dom0", "myst.Notify+numprov_" + str(fallback_count)])
            disconnect()
            continue

        ip_address = _lookup_ip_with_retries('Post-connect IP lookup')
        if not ip_address:
            is_explicit_provider = provider and current_provider == provider
            if is_explicit_provider:
                logger.error('ERROR: Failed to determine IP address for explicit provider: %s', current_provider)
                _notify_explicit_provider_failure(current_provider)
                fallback_count = len(providers) - 1
                run_quiet([EXECUTABLES['qrexec_client_vm'], "dom0", "myst.Notify+numprov_" + str(fallback_count)])
            disconnect()
            continue

        if ip_address == initial_ip:
            is_explicit_provider = provider and current_provider == provider
            if is_explicit_provider:
                logger.error('ERROR: Explicit provider returned same IP as initial: %s', current_provider)
                _notify_explicit_provider_failure(current_provider)
                fallback_count = len(providers) - 1
                run_quiet([EXECUTABLES['qrexec_client_vm'], "dom0", "myst.Notify+numprov_" + str(fallback_count)])
            logger.info('IP address unchanged after connecting to provider')
            disconnect()
            continue

        is_explicit_provider = provider and current_provider == provider
        if is_explicit_provider:
            selected_provider = current_provider
            break

        ip_data = None
        for attempt in range(MAX_RETRIES):
            logger.info('IP score lookup attempt %d of %d', attempt + 1, MAX_RETRIES)
            try:
                ip_data = get_ip_score(ip_address)
            except Exception:
                if attempt == MAX_RETRIES - 1:
                    logger.error('Failed to determine IP score')
                    break
            else:
                break

        if not ip_data:
            disconnect()
            continue

        if check_ip_score(ip_data, ip_score_limit):
            selected_provider = current_provider
            break

        run_quiet(["qrexec-client-vm", "dom0", "myst.Notify+bad-ip"])
        disconnect()

    if not selected_provider:
        logger.error('Failed to find an acceptable provider with current options')
        run_quiet([EXECUTABLES['qrexec_client_vm'], "dom0", "myst.Notify+no-acceptable-prov"])
        sys.exit(1)

    connection_details = get_connection_details()
    write_connection_file(selected_provider, connection_details)

    run_quiet([
        EXECUTABLES['qrexec_client_vm'],
        "dom0",
        f"myst.Notify+{connection_details['ip']}_{connection_details['city']}_{connection_details['country']}",
    ])

    _ = lock_file


if __name__ == '__main__':
    main()
