import json
import os
import signal
import subprocess
import uuid

from . import data_store
from . import disaster_recovery
from . import icap
from . import icap_client
from . import instance_sessions_data
from . import log_file_manager
from . import speechmatics_client_docker
from . import topic_models

from .eeg_python_utils import errors, time_utils, utils
from .filesystem_lock import filesystem_lock
from .lexi_status import get_lexi_status_data, get_lexi_status_message
from .docker_and_languages import OUTPUT_LOCALE
from app.modules import license_docker

WRAPPER_PATH = os.path.join(
    os.path.dirname(
        os.path.dirname(
            os.path.realpath(__file__))),
    'lexi_wrapper'
)

ICAP_COMPANY = os.environ['ICAP_COMPANY']
MASTER_USERNAME = os.environ['MASTER_USERNAME']
MASTER_PASSWORD = os.environ['MASTER_PASSWORD']

LOCAL_INSTANCE_PREFIX = 'local_instance_'

ALLOWED_LEXI_FIELDS = [
    'icapaccesscode',
    'num_rows',
    'base_row',
    'col_indent',
    'cc_service',
    'base_model',
    'additional_vocab',
    'all_caps',
    'timeout',
    'icapuser',
    'icappass',
    'name',
    'modelID',
    'sm_enhanced',
    'col_width',
    'diarization_style',
    'cloud_synced',
]

ALLOWED_LEXI_BINARY_FIELDS = [
    'icapaccesscode',
    'num_rows',
    'base_row',
    'col_indent',
    'cc_service',
    'base_model',
    'additional_vocab',
    'all_caps',
    'timeout',
    'icapuser',
    'icappass',
    'sm_enhanced',
    'col_width',
    'diarization_style',
]


def get_instance_name(instance_id, instance_settings=None):
    instance_settings = get_instance_settings(instance_id, instance_settings)
    # print('instance_id', instance_id)
    # print('instance settings: {}'.format(utils.jformat(instance_settings)))
    return instance_settings.get('settings', {'name': None}).get('name')


def get_instance_representation(instance_id, instance_settings=None, get_history=1):
    instance_settings = get_instance_settings(instance_id, instance_settings)
    if instance_settings is None:
        return None
    representation = utils.pluck_keys(
        instance_settings,
        [
            'instance_id',
            'settings',
            'created_timestamp',
            'last_updated_timestamp',
            'cloud_synced',
            'error',
        ]
    )
    # print(representation)
    # representation = instance_settings
    valid_settings = get_valid_settings(representation['settings'])
    # print(valid_settings)
    instance_sessions = instance_sessions_data.get_instance_sessions(instance_id)
    instance_sessions = instance_sessions_data.set_session_status(instance_id, instance_sessions)
    active_session = instance_sessions_data.get_active_session(instance_id, instance_sessions)
    if active_session is not None:
        representation['active_session'] = active_session
        is_captioning = active_session.get('is_captioning', False)
        representation['is_captioning'] = is_captioning
        # del instance_sessions[-1]

    if get_history == 1:
        representation['recent_sessions'] = instance_sessions[::-1]

    modelID = valid_settings.get('modelID')
    if modelID is not None:
        valid_settings['model_name'] = topic_models.get_model_name(modelID)
    representation['settings'] = valid_settings
    representation['state'] = data_store.get_instance_state(instance_id)

    icapaccesscode = valid_settings['icapaccesscode']
    accesscode_representation = icap.get_cached_accesscode(
        icapaccesscode, False
    )

    online = False
    secondary_encoders = []
    encoder_name = accesscode_representation.get('primaryencoder')
    if encoder_name:
        encoder = icap.get_cached_user(encoder_name, False)

        secondaryencoder = accesscode_representation.get('secondaryencoder', [])
        secondary_encoders = [
            icap.get_cached_user(senc.get('encoder'), False)
            for senc in secondaryencoder
        ]
        encoders = [encoder] + secondary_encoders
        online_encoders = [
            enc for enc in encoders if enc and enc.get('online')
        ]
        online = len(online_encoders) > 0
        representation['encoder'] = encoder

    representation['online'] = online
    representation['secondary_encoders'] = secondary_encoders
    representation['accesscode'] = accesscode_representation

    return representation


def get_instances():
    instance_ids = data_store.get_instance_ids()
    instances = [
        get_instance_settings(instance_id) for instance_id in instance_ids
    ]
    return instances


def get_cloud_synced_instances():
    instance_ids = data_store.get_instance_ids()
    cloud_synced_instances = []

    for instance_id in instance_ids:
        settings = get_instance_settings(instance_id)
        if settings['cloud_synced']:
            cloud_synced_instances.append(instance_id)
    return cloud_synced_instances


def get_instance_representations(get_history=1):
    instance_ids = data_store.get_instance_ids()
    instance_representations = []
    for instance_id in instance_ids:
        instance_representation = get_instance_representation(instance_id, get_history=get_history)
        if instance_representation is not None:
            instance_representations.append(instance_representation)
    return instance_representations


def get_instance_icap_maps(instance_representations=None):
    if not instance_representations:
        instance_representations = get_instance_representations()

    instance_accesscodes = {}
    instance_encoders = {}
    instance_captioners = {}

    accesscode_instances = {}
    encoder_instances = {}
    secondary_encoder_instances = {}
    captioner_instances = {}

    for instance in instance_representations:
        settings = instance.get('settings')
        name = settings.get('name')

        accesscode = settings.get('icapaccesscode')
        captioner = settings.get('icapuser')
        encoder_dict = instance.get('encoder', {})
        if encoder_dict:  # could be None
            encoder = encoder_dict.get('username')
        else:
            encoder = None

        if accesscode in accesscode_instances:
            accesscode_instances[accesscode].append(name)
        else:
            accesscode_instances[accesscode] = [name]

        if encoder in encoder_instances:
            encoder_instances[encoder].append(name)
        else:
            encoder_instances[encoder] = [name]

        if captioner in captioner_instances:
            captioner_instances[captioner].append(name)
        else:
            captioner_instances[captioner] = [name]

        instance_accesscodes[name] = accesscode
        instance_encoders[name] = encoder
        instance_captioners[name] = captioner

    instance_maps = {
        'instance_accesscodes': instance_accesscodes,
        'instance_encoders': instance_encoders,
        'instance_captioners': instance_captioners,
        'accesscode_instances': accesscode_instances,
        'encoder_instances': encoder_instances,
        'secondary_encoder_instances': secondary_encoder_instances,
        'captioner_instances': captioner_instances,
    }
    return instance_maps


def get_valid_settings(settings, check_accesscode=False):
    # lexi_settings = utils.check_valid_settings(settings, ALLOWED_LEXI_FIELDS)
    lexi_settings = utils.pluck_keys(settings, ALLOWED_LEXI_FIELDS)
    validate_lexi(lexi_settings)

    accesscode = settings['icapaccesscode']
    error_message = \
        'Access Code: {0} has not been configured for use with Lexi.'\
        .format(accesscode)

    if check_accesscode:
        # check for valid accesscode
        try:
            accesscode_response = icap_client.get_accesscode(accesscode)
            if not accesscode_response.ok:
                # print(error_message)
                errors.bad_request(error_message)
        except Exception as e:
            if str(e) != error_message:
                # print('error: {}'.format(str(e)))
                print('Encountered an error validating accesscode \
                    with iCap servers')
                errors.internal_error('Encountered an error validating \
                    accesscode with iCap servers')
            else:
                errors.bad_request(error_message)

    all_caps = lexi_settings.get('all_caps')
    if all_caps == 'True':
        lexi_settings['all_caps'] = True
    elif all_caps == 'False':
        lexi_settings['all_caps'] = False
    all_caps = lexi_settings.get('all_caps')

    # remove customization_id if it's the empty string
    if lexi_settings.get('modelID') == '':
        del lexi_settings['modelID']

    base_model = lexi_settings.get('base_model')
    if topic_models.language_can_be_synced(base_model):
        # "en-US_BroadbandModel" -> "en-US"
        underscore_index = base_model.find("_")
        lexi_settings['base_model'] = base_model[:underscore_index] if underscore_index != -1 else base_model
    elif base_model is not None:
        errors.bad_request('Cannot sync unknown language: ' + base_model)

    return lexi_settings


def create_new_instance(settings, cloud_synced=False, cloud_id=''):
    utils.check_valid_settings(settings, ['icapaccesscode', 'name'])
    name = settings.get('name')
    if not cloud_synced:
        validate_lexi_name(name)

    settings = get_valid_settings(settings, check_accesscode=True)

    instance_id = cloud_id if cloud_synced else utils.generate_id(prefix=LOCAL_INSTANCE_PREFIX)
    now_timestamp = time_utils.now_timestamp()
    instance_settings = {
        'instance_id': instance_id,
        'settings': settings,
        'created_timestamp': now_timestamp,
        'last_updated_timestamp': now_timestamp,
        'cloud_synced': cloud_synced
    }

    # create an iCap user for use with asr
    # Need to shorten the ID to fit as icap username if it's a cloud ID
    icap_username = '{NAME}_lexibot'.format(NAME=instance_id)
    prefix_to_replace = 'asr_instance_' if cloud_synced else LOCAL_INSTANCE_PREFIX
    icap_username = icap_username.replace(prefix_to_replace, '')
    icap_password = utils.generate_id(length=16)

    try:
        # TODO: check if that user exists before creating
        user_response = icap_client.create_icap_user(
            icap_username,
            icap_password,
            'captioner',
            cloud_synced
        )
        user_response.raise_for_status()
        settings.update(icapco=ICAP_COMPANY,
                        icapuser=icap_username, icappass=icap_password)
    except Exception as e:
        print('error: {}'.format(str(e)))
        errors.internal_error('Encountered an error creating iCap user')

    instance_settings.update(settings=settings)
    instance_settings = data_store.set_instance_settings(
        instance_id,
        instance_settings
    )
    # print('instance settings: {}'.format(jformat(instance_settings)))
    return instance_settings


def delete_instance(instance_id, company=ICAP_COMPANY):
    instance_representation = get_instance_representation(instance_id)
    with filesystem_lock(instance_id, 'def delete_instance'):
        if data_store.instance_is_running(instance_id):
            errors.unprocessable('Cannot delete a running instance')

        # delete log files
        log_file_manager.cleanup_instance_log_dir(instance_id)

    # remove records from db
    # (we do this after releasing the lock because this entails
    # deleting the directory where the lock file lives)
    instance_settings = instance_representation.get('settings')
    icapuser = instance_settings.get('icapuser')
    try:
        icap_client.remove_icap_user(icapuser, company)
    except Exception as e:
        print(str(e))

    data_store.remove_instance_settings(instance_id)


def validate_lexi(settings):
    def is_missing(setting, setting_name):
        if setting is None or setting == '':
            errors.bad_request(
                'Missing required parameter: {}'.format(setting_name)
            )

    # print('settings: {}'.format(json.dumps(settings)))

    # check for valid language model
    # if settings.get('engine', 'speechmatics') is 'speechmatics':
    #     if 'base_model' in settings \
    #         and settings['base_model'] \
    #             not in speechmatics_client.list_base_models().keys():
    #         print('no base model')
    #         return False

    # check for required settings
    required_settings = ['icapaccesscode']
    for setting_name in required_settings:
        is_missing(settings.get(setting_name), setting_name)


def validate_lexi_name(name):
    instance_names = list(data_store.get_instance_names().keys())
    if name in instance_names:
        errors.unprocessable(
            'Instance with name: "{}" already exists.'.format(name)
        )
    utils.check_good_cred(name, 'Instance')


def get_instance_settings(instance_id, instance_settings=None):
    if instance_settings is None:
        instance_settings = data_store.get_instance_settings(instance_id)

    return instance_settings


def remove_status(instance_id, instance_settings=None):
    instance_settings = get_instance_settings(instance_id, instance_settings)
    err = instance_settings.pop('error', None)
    status = instance_settings.pop('status', None)
    if err is not None or status is not None:
        instance_settings = data_store.set_instance_settings(
            instance_id,
            instance_settings
        )


def create_vocab_file(instance_id, lexi_settings):
    modelID = lexi_settings.get('modelID')
    if lexi_settings.get('additional_vocab'):
        del lexi_settings['additional_vocab']

    # get additional vocab
    additional_vocab = data_store.get_additional_vocab(modelID)
    if additional_vocab:
        vocab_file_path = log_file_manager.get_additional_vocab_path(instance_id)
        try:
            with open(vocab_file_path, 'w') as vocab_file:
                json.dump(additional_vocab, vocab_file, indent=2)
                vocab_file_name = vocab_file.name
                return vocab_file_name
        except IOError as e:
            errors.forbidden(str(e))


def create_active_session(instance_id, session_id="", instance_sessions=None, instance_settings=None, initialization_origin='Lexi Local Server', initialization_reason='unknown', initialization_user='unknown',):
    # print('creating current session')
    instance_sessions = instance_sessions_data.get_instance_sessions(instance_id, instance_sessions)
    instance_settings = get_instance_settings(instance_id, instance_settings)
    instance_name = instance_settings.get('settings', {}).get('name')
    if instance_sessions is None:
        instance_sessions = []
    session_count = len(instance_sessions)
    active_session = {
        'instance_id': instance_id,
        'name': instance_name,
        'start_timestamp': time_utils.now_timestamp(),
        'status': '',
        'session_count': session_count,
        'session_id': session_id,
        'settings': instance_settings.get('settings'),
        'initialization_origin': initialization_origin,
        'initialization_reason': initialization_reason,
        'initialization_user': initialization_user,
        'cloud_synced': instance_settings.get('cloud_synced', False),
    }
    instance_sessions.append(active_session)
    # print('setting instance_sessions: {}'.format(utils.jformat(instance_sessions)))
    data_store.set_instance_sessions(instance_id, instance_sessions)
    # print('just set instance_sessions updated_settings: {}'.format(
    #     utils.jformat(data_store.get_instance_sessions(instance_id)))
    # )
    return active_session


def end_active_session(instance_id, instance_sessions=None, user_ended=False, termination_origin='Lexi Local Server', termination_reason=None, termination_user='unknown'):
    instance_settings = get_instance_settings(instance_id)
    instance_sessions = instance_sessions_data.get_instance_sessions(instance_id, instance_sessions)
    active_session = instance_sessions_data.get_active_session(instance_id, instance_sessions)
    if active_session is not None:
        active_session['terminated'] = True
        active_session['termination_user'] = termination_user
        status = log_file_manager.get_instance_status(instance_id)
        active_session['status'] = status
        error_reason = status.get('ErrorReason')
        error = instance_settings.get('error')
        error_message = error_reason if error_reason else error
        if not user_ended and error_message is None and termination_reason is None:
            error_message = 'Unknown Error'

        if error_message:
            active_session['error'] = error_message
            active_session['termination_reason'] = error_message
            active_session['abnormal_termination'] = True
        elif user_ended:
            print('ended user action', termination_reason)
            active_session['termination_reason'] = termination_reason if termination_reason else 'User Action'
            active_session['abnormal_termination'] = False
        elif termination_reason:
            active_session['termination_reason'] = termination_reason
            active_session['abnormal_termination'] = False
        active_session['termination_origin'] = termination_origin

        start_timestamp = active_session.get('start_timestamp')
        end_timestamp = time_utils.now_timestamp()
        duration = end_timestamp - start_timestamp
        active_session['end_timestamp'] = end_timestamp
        active_session['duration'] = duration
        data_store.set_instance_sessions(instance_id, instance_sessions)


def terminate_instance(instance_id, user_ended=True, termination_origin='Lexi Local Server', termination_reason=None, termination_user='unknown'):
    print('------- Stopping instance')
    with filesystem_lock(instance_id, 'def terminate_instance'):
        process = data_store.get_running_process(instance_id)
        if process is None:
            errors.unprocessable('Instance already off')

        # request wrapper process stop
        print('sending SIGINT', instance_id, process)
        process.send_signal(signal.SIGINT)

        data_store.delete_pidfile(instance_id, user_ended=user_ended, termination_origin=termination_origin, termination_reason=termination_reason, termination_user=termination_user)


def begin_instance(instance_id, instance_settings=None, initialization_origin='Lexi Local Server', initialization_reason='unknown', initialization_user='unknown', is_event=False):
    print('------- Starting new instance')
    license_status = license_docker.get_license()
    if not license_docker.is_licensed(license_status):
        errors.unprocessable('You must upload a valid license to use Lexi! If you have a license, go to Settings > License and select \'CHOOSE FILE\'')
    if license_docker.is_license_expired(license_status):
        errors.unprocessable('License Expired! Please upload a valid license to use Lexi!')
    if license_docker.is_license_early(license_status):
        errors.unprocessable("System Time is earler than license start date! Please fix System time to use Lexi!\nGo to Settings > Local Time")

    # If there happens to be an old container hanging around with the same name for some reason, kill it before counting active containers.
    speechmatics_client_docker.stop_docker_container(instance_id)
    dockers_running = len(speechmatics_client_docker.get_docker_containers())
    stream_quantity = license_docker.get_license_stream_quantity(license_status)
    if dockers_running >= stream_quantity:
        instances_word = 'instance' if stream_quantity == 1 else 'instances'
        errors.forbidden(f'You can only run {stream_quantity} Lexi {instances_word} at a time!')

    lexi_status_data = get_lexi_status_data()
    lexi_status = lexi_status_data.get('available', False)

    if not lexi_status:
        parsed_error_message = get_lexi_status_message(lexi_status_data)

        error = parsed_error_message.get('error')
        message = parsed_error_message.get('message')
        parsed_message = parsed_error_message.get('parsed_message')
        if 'LocalDR' in parsed_message:
            unit_is_disaster_recover = license_docker.is_disaster_recover_unit()
            page_name = 'Usage Reporting' if not unit_is_disaster_recover else 'Disaster Recovery'
            parsed_message = parsed_message.replace('LocalDR', page_name)

        api_error_str = parsed_message if parsed_message \
            else message if message \
            else error if error else 'Lexi is not avaialbe'

        errors.forbidden(api_error_str)

    base_model = instance_settings.get('settings', {}).get('base_model', 'en') if instance_settings is not None else 'en'
    instance_language = base_model
    # "en-US_BroadbandModel" -> "en-US"
    if '_' in instance_language:
        instance_language = instance_language.split('_')[0]
    # Remove locale if any
    docker_language = instance_language.split('-')[0] if '-' in instance_language else instance_language

    spawn_success, p9000, container_id = speechmatics_client_docker.try_spawn_docker_container(instance_id, language=docker_language)

    if spawn_success is False:
        errors.unprocessable('Unable to start lexi - failed to spawn container.')
    instance_settings = get_instance_settings(instance_id, instance_settings)
    with filesystem_lock(instance_id, 'def begin_instance'):
        f = data_store.create_new_pidfile(instance_id)
        if f is None:
            # We couldn't create a new pidfile
            # because a valid one already exists
            # (i.e., the instance is already running).
            errors.unprocessable('Instance already on')

        try:
            # clear any error status for previous run
            remove_status(instance_id, instance_settings)

            environment_variable_name = data_store\
                .get_environment_variable_name()
            print('env var name', environment_variable_name)

            # create status + log dirs
            session_id = str(uuid.uuid4())
            log_dir = log_file_manager.create_instance_log_dir(instance_id)
            log_prefix = '{}_{}'.format(instance_id, session_id)
            status_path = log_file_manager.get_instance_status_path(instance_id)
            utils.remove_file(status_path)

            lexi_settings = instance_settings['settings']
            vocab_file_name = create_vocab_file(instance_id, lexi_settings)
            if vocab_file_name:
                lexi_settings['additional_vocab'] = vocab_file_name

            instance_config = utils.pluck_keys(
                lexi_settings,
                ALLOWED_LEXI_BINARY_FIELDS
            )
            instance_config['sm_diarization'] = instance_config.get('diarization_style') != 'DISABLED'
            # If the cloud language has locale but the local languages don't support it, use the base language
            if not OUTPUT_LOCALE.get(docker_language):
                instance_config['base_model'] = docker_language

            # startup lexi process
            # print('WRAPPER_PATH', WRAPPER_PATH)
            args = [
                WRAPPER_PATH,
                instance_id,
                log_dir,
                log_prefix,
                status_path,
                p9000,
                container_id,
                session_id,
                json.dumps(instance_config),
            ]
            p = subprocess.Popen(
                args,
                env=dict(
                    os.environ,
                    **{environment_variable_name: instance_id}
                )
            )
            # write the subprocess PID to the pidfile
            print('writing pid for {}'.format(instance_id))
            f.write(str(p.pid) + '\n')
            # print('calling create current session')
            create_active_session(
                instance_id,
                session_id=session_id,
                instance_settings=instance_settings,
                initialization_origin=initialization_origin,
                initialization_reason=initialization_reason,
                initialization_user=initialization_user,
            )
        finally:
            f.close()

    return get_instance_representation(instance_id)


def sync_primary_instances():
    success, eeg_cloud_instances = disaster_recovery.get_eeg_cloud_instances()
    existing_cloud_synced_instances = get_cloud_synced_instances()
    custom_models = data_store.get_cloud_custom_models()
    synced_instances = []

    # Do not sync these settings
    PROTECTED_SETTINGS = ['icapco', 'base_model', 'icapuser', 'engine']

    for syncing_instance in eeg_cloud_instances:
        check_ac = icap_client.get_accesscode(syncing_instance['settings']['icapaccesscode'], company=ICAP_COMPANY).json()
        base_model = syncing_instance['settings'].get('base_model')
        if (len(check_ac['accesscodes']) <= 0) or (not topic_models.language_can_be_synced(base_model)):
            continue

        if syncing_instance['instance_id'] in existing_cloud_synced_instances:
            existing_cloud_synced_instances.remove(syncing_instance['instance_id'])

        # custom models are stored by name on cloud and by ID on Lexi Local
        custom_model_name = syncing_instance['settings'].get('model_name')
        if custom_model_name is not None:
            custom_model = next((model for model in custom_models if model['model_name'] == custom_model_name), None)
            if custom_model is not None:
                syncing_instance['settings']['modelID'] = custom_model.get('modelID')

        new_settings = get_valid_settings(syncing_instance['settings'])
        old_settings = get_instance_settings(syncing_instance['instance_id'])
        if old_settings is None:
            syncing_instance.update(create_new_instance(new_settings, cloud_synced=True, cloud_id=syncing_instance['instance_id']))
        else:
            if data_store.instance_is_running(syncing_instance['instance_id']):
                continue

            for setting in PROTECTED_SETTINGS:
                if setting in syncing_instance['settings']:
                    del syncing_instance['settings'][setting]

            old_settings['settings'].update(syncing_instance['settings'])
            syncing_instance = data_store.set_instance_settings(syncing_instance['instance_id'], old_settings)

        synced_instances.append(syncing_instance)

    # If an instance was deleted from the cloud it would be left over in this array, so we double check
    # that it was removed from the cloud and then delete it
    for instance_id in existing_cloud_synced_instances:
        if not disaster_recovery.get_cloud_instance_data(instance_id):
            delete_instance(instance_id)

    return {'success': success, 'synced_instances': synced_instances}


def delete_synced_instances():
    icap_admin_conf = disaster_recovery.get_icap_admin_config()
    synced_instances = get_cloud_synced_instances()
    deleted_instances = []
    for instance_id in synced_instances:
        instance_representation = get_instance_representation(instance_id)
        delete_instance(instance_id, icap_admin_conf.get('company'))
        deleted_instances.append(instance_representation)
    return deleted_instances


def terminate_all_instances(error_message=None):
    running_instances = data_store.get_all_pidfiles()

    if len(running_instances) == 0:
        return

    print('Trying to terminate all running instances')

    for instance in running_instances:
        # Instance data
        instance_id = running_instances[instance].get('instance_id')
        instance_name = get_instance_name(instance_id)

        if instance_id is None:
            errors.internal_error(f'The instance {instance} does not exist')

        print(f'Instance: instance_id -> {instance_id} | instance_name -> {instance_name}')

        terminate_instance(
            instance_id=instance_id,
            user_ended=False,
            termination_origin='Lexi status',
            termination_reason=error_message,
            termination_user='Lexi status'
        )
