# coding=utf8

import json
import time

from auto_keyworder_modules.ai_services import ai_service, imagga, deepva, cloudsight
from auto_keyworder_modules import api, easydb_api, util


def easydb_server_start(easydb_context):
    easydb_context.register_callback(
        'process',
        {
            'name': 'easydb-auto-keyworder-plugin',
        },
    )

    # endpoint: <server>/api/plugin/base/easydb-auto-keyworder-plugin/start_now
    easydb_context.register_callback(
        'api',
        {
            'name': 'start_now',
            'callback': 'start_now',
        },
    )


def start_now(easydb_context, parameters):
    logger = easydb_context.get_logger('pf.plugin.base.auto_keyworder.api')
    util.log_debug(logger, f'[/start_now] parameters: {util.dumpjs(parameters)}')

    return api.api_start_now(easydb_context, logger, parameters)


def run(easydb_context):
    logger = easydb_context.get_logger('pf.plugin.base.auto_keyworder')

    util.log_info(
        logger, 'starting auto_keyworder process, wait for server to start...'
    )

    # get server config and plugin base config
    conn = easydb_context.db_connect('easydb-auto-keyworder-plugin')
    server_config = easydb_context.get_config(conn)
    conn.close()

    ez_external_url = util.get_json_value(server_config, "system.server.external_url")
    if not ez_external_url:
        util.log_error(logger, f'could not load system.server.external_url from config')
        return

    # before starting directly after the server start, poll the settings endpoint to ensure the server is running properly
    while True:
        status_code, resp_text = easydb_api.easydb_get(
            server=ez_external_url,
            endpoint='settings',
        )
        if status_code == 200:
            util.log_info(logger, f'{ez_external_url}/settings returned 200')
            time.sleep(2)
            break

        util.log_info(
            logger,
            f'{ez_external_url}/settings returned {status_code}, keep waiting...',
        )
        time.sleep(5)

    first_run = True

    (
        poll_start_hour,
        poll_every_days,
        baseconfig_poll_interval_sec,
        search_chunk_size,
    ) = parse_server_config(server_config)

    api.set_start_now(easydb_context, False, logger)

    while True:

        # get server config and plugin base config
        conn = easydb_context.db_connect('easydb-auto-keyworder-plugin')
        server_config = easydb_context.get_config(conn)
        conn.close()

        base_config_json = util.get_json_value(
            server_config, 'base.system.auto_keyworder'
        )
        if not base_config_json:
            util.log_error(
                logger, f'could not load base.system.auto_keyworder from config'
            )
            return

        # util.log_debug(logger, f'loaded base config: {util.dumpjs(base_config_json)}')
        util.log_debug(logger, f'loaded base config')

        # check if the whole process is disabled in the base config
        if not base_config_json.get('enabled', False):
            util.log_debug(
                logger,
                f'disabled in base config, check again in {baseconfig_poll_interval_sec} seconds',
            )
            time.sleep(baseconfig_poll_interval_sec)
            continue

        next_run = calculate_next_runtime(
            poll_start_hour,
            poll_every_days,
            first_run,
        )
        util.log_debug(
            logger, f'next run scheduled for: {next_run} (first run: {first_run})'
        )
        first_run = False

        setup_done = False
        setup_failed = False

        ez_datamodel = None
        ez_token = None

        collected_errors: list = []

        # load base config
        # check all configs and update field information
        configurations_by_ai_service = {
            'cloudsight': [],
            'deepva': [],
            'imagga': [],
        }

        start_now = False
        for service_name in configurations_by_ai_service:
            configs = util.parse_base_config(
                f'configs_{service_name}', base_config_json
            )
            if len(configs) == 0:
                continue

            n = 0
            for config in configs:
                if setup_failed:
                    break

                # use first config to get easydb login information, load datamodel etc
                # these information is the same in all configurations
                if not setup_done:

                    # authenticate at easydb, get token
                    login = config.get_easydb_login()
                    if not login:
                        collected_errors.append(
                            f'could not get easydb login from config'
                        )
                        setup_failed = True
                        break

                    password = config.get_easydb_password()
                    if not password:
                        collected_errors.append(
                            f'could not get easydb password from config'
                        )
                        setup_failed = True
                        break

                    ez_token, resp = easydb_api.easydb_authenticate(
                        server=ez_external_url,
                        login=login,
                        password=password,
                    )
                    if ez_token is None:
                        collected_errors.append(
                            f'could not authenticate at easydb {ez_external_url}: {resp}'
                        )
                        setup_failed = True
                        break
                    else:
                        if resp is not None:
                            resp, err = easydb_api.confirm_pending_tasks(
                                server=ez_external_url,
                                easydb_token=ez_token,
                                auth_response=resp,
                            )
                            if err is not None:
                                collected_errors.append(
                                    f'could not confirm pending tasks at easydb {ez_external_url}: {err}'
                                )
                                setup_failed = True
                                break

                    # load datamodel from easydb api
                    ez_datamodel = {}

                    # load schema
                    statuscode, resp = easydb_api.easydb_get(
                        server=ez_external_url,
                        endpoint='schema/user/CURRENT',
                        params={
                            'format': 'json',
                        },
                        easydb_token=ez_token,
                    )
                    if statuscode != 200:
                        collected_errors.append(
                            f'could not load datamodel from easydb {ez_external_url}: {resp}'
                        )
                        setup_failed = True
                        break
                    if not isinstance(resp, dict):
                        collected_errors.append(
                            f'could not parse datamodel from response from easydb {ez_external_url}: {resp}'
                        )
                        setup_failed = True
                        break

                    ez_datamodel['schema'] = resp

                    # load maskset
                    statuscode, resp = easydb_api.easydb_get(
                        server=ez_external_url,
                        endpoint='mask/CURRENT',
                        params={
                            'format': 'json',
                        },
                        easydb_token=ez_token,
                    )
                    if statuscode != 200:
                        collected_errors.append(
                            f'could not load maskset from easydb {ez_external_url}: {resp}'
                        )
                        setup_failed = True
                        break
                    if not isinstance(resp, dict):
                        collected_errors.append(
                            f'could not parse maskset from response from easydb {ez_external_url}: {resp}'
                        )
                        setup_failed = True
                        break

                    ez_datamodel['maskset'] = resp

                    setup_done = True

                # ----------------------------------------

                # check if the config is valid
                # check of objecttype is valid table
                # check if the asset and timestamp field is a valid field in the table
                # check if the mapping fields are valid fields in the table
                try:
                    config.check_objecttype_valid(ez_datamodel)
                    config.update_asset_field_info(ez_datamodel)
                    config.update_timestamp_field_info(ez_datamodel)
                    config.update_mapping_fields_info(ez_datamodel)
                except Exception as e:
                    collected_errors.append(
                        f'service: {service_name}: config #{n}: skip config: {e}'
                    )
                    continue

                api_url = config.get_api_url()
                if not api_url:
                    collected_errors.append(
                        f'service: {service_name}: config #{n}: skip config: api url is not set'
                    )
                    continue

                n += 1

                # config is not invalid, add it to collected configs for this service
                configurations_by_ai_service[service_name].append(config)

                if config.do_start_now():
                    start_now = True

            util.log_debug(
                logger,
                f'{service_name}: {len(configurations_by_ai_service[service_name])} valid config(s)',
            )

        for err in collected_errors:
            util.log_warn(logger, err)
        if setup_failed:
            util.log_error(logger, 'setup failed, stop')
            return

        util.log_debug(logger, 'setup from server and base config completed')

        # ------------------------------
        # main loop

        if start_now or next_run <= util.now():
            for service_name in configurations_by_ai_service:
                util.log_debug(
                    logger,
                    f'service: {service_name}: {len(configurations_by_ai_service[service_name])}',
                )

                for config in configurations_by_ai_service[service_name]:
                    if not config.is_enabled():
                        util.log_debug(logger, f'- config: skip (not enabled)')
                        continue

                    if service_name == "cloudsight":
                        service = cloudsight.CloudsightAiService(
                            config,
                            easydb_server=ez_external_url,
                            easydb_token=ez_token,
                            logger=logger,
                        )
                    elif service_name == "deepva":
                        service = deepva.DeepvaAiService(
                            config,
                            easydb_server=ez_external_url,
                            easydb_token=ez_token,
                            logger=logger,
                        )
                    elif service_name == "imagga":
                        service = imagga.ImaggaAiService(
                            config,
                            easydb_server=ez_external_url,
                            easydb_token=ez_token,
                            logger=logger,
                        )
                    else:
                        util.log_warn(logger, f'- config: skip (unknown)')
                        continue

                    try:
                        ai_service.run(
                            name=service_name,
                            service=service,
                            search_chunk_size=search_chunk_size,
                        )
                    except Exception as e:
                        util.print_traceback(
                            logger,
                            e,
                        )

        # have to reset the "start now" checkbox in the base config before the next run
        if start_now:
            util.log_debug(
                logger,
                'base config: auto_keyworder.start_now is True, set to False...',
            )
            api.set_start_now(easydb_context, False, logger)

        util.log_debug(logger, f'sleep for {baseconfig_poll_interval_sec} seconds')
        time.sleep(baseconfig_poll_interval_sec)


def parse_server_config(server_config: dict) -> tuple[int, int, int, int]:
    """
    auto_keyworder:
        baseconfig_poll_interval_sec: 10
        poll_start_hour: 0
        poll_every_days: 1
        search_chunk_size: 3
    """

    baseconfig_poll_interval_sec = util.get_json_value(
        server_config,
        'system.auto_keyworder.baseconfig_poll_interval_sec',
    )
    if not isinstance(baseconfig_poll_interval_sec, int):
        baseconfig_poll_interval_sec = 10
    if baseconfig_poll_interval_sec < 1:
        baseconfig_poll_interval_sec = 10

    poll_start_hour = util.get_json_value(
        server_config,
        'system.auto_keyworder.poll_start_hour',
    )
    if not isinstance(poll_start_hour, int):
        poll_start_hour = 0
    if poll_start_hour < 0:
        poll_start_hour = 0

    poll_every_days = util.get_json_value(
        server_config,
        'system.auto_keyworder.poll_every_days',
    )
    if not isinstance(poll_every_days, int):
        poll_every_days = 0
    if poll_every_days < 0:
        poll_every_days = 0

    search_chunk_size = util.get_json_value(
        server_config,
        'system.auto_keyworder.search_chunk_size',
    )
    if not isinstance(search_chunk_size, int):
        search_chunk_size = 50
    if search_chunk_size < 1:
        search_chunk_size = 50
    if search_chunk_size > 1000:
        search_chunk_size = 1000

    return (
        poll_start_hour,
        poll_every_days,
        baseconfig_poll_interval_sec,
        search_chunk_size,
    )


def calculate_next_runtime(poll_start_hour: int, poll_every_days: int, first_run: bool):
    # calculate the next run time for the cloudsight worker
    if first_run:
        # for the first run, calculate the next possible full hour
        return util.get_next_run(poll_start_hour)
    else:
        # calculate the next possible full hour after the specified number of poll_every_days pause
        return util.get_next_run(poll_start_hour, poll_every_days)
