import json
import requests

from auto_keyworder_modules.ai_services import ai_service, ai_service_configuration
from auto_keyworder_modules import util, eas, event


class ImaggaAiService(ai_service.AiServiceInterface):

    languages: dict = {
        'ar': 'ar',
        'ca': 'ca',
        'cs': 'cs-CZ',
        'de': 'de-DE',
        'en': 'en-US',
        'es': 'es-ES',
        'fa': 'fa',
        'fi': 'fi-FI',
        'fr': 'fr-FR',
        'he': 'he',
        'hi': 'hi',
        'it': 'it-IT',
        'ja': 'ja-Jpan',
        'ko': 'ko-Kore',
        'nl': 'nl-NL',
        'pl': 'pl-PL',
        'pt': 'pt',
        'ru': 'ru-RU',
        'sv': 'sv-SE',
        'tr': 'tr-TR',
        'uk': 'uk',
        'ur': 'ur',
        'zh_chs': 'zh-Hans',
        'zh_cht': 'zh-Hant',
    }

    def __init__(
        self,
        config: ai_service_configuration.ImaggaConfiguration,
        easydb_server: str,
        easydb_token: str,
        logger=None,
    ) -> None:
        super().__init__(config, easydb_server, easydb_token, logger)

    def api_uses_base64(self) -> bool:
        return True

    def get_api_language(self, easydb_lang_code: str) -> str:
        for lc in self.languages:
            if self.languages[lc] == easydb_lang_code:
                return lc
        return None

    def has_easydb_language(self, easydb_lang_code: str) -> bool:
        return self.get_api_language(easydb_lang_code) is not None

    def get_easydb_language(self, api_lang_code: str) -> str:
        return self.languages.get(api_lang_code)

    def has_api_language(self, api_lang_code: str) -> bool:
        return self.get_easydb_language(api_lang_code) is not None

    def language_param(self) -> str:
        easydb_lang_code = self.config.get_string('language')
        if not self.has_easydb_language(easydb_lang_code=easydb_lang_code):
            return 'en'
        return self.get_api_language(easydb_lang_code=easydb_lang_code)

    def upload_image_to_service_api(
        self,
        asset_status: eas.AssetStatus,
    ) -> None:

        url = f'{self.config.get_api_url()}/uploads'

        response = requests.post(
            url=url,
            auth=(
                self.config.get_api_key(),
                self.config.get_api_secret(),
            ),
            files={
                'image_base64': asset_status.base64_data,
            },
        )

        event.log_event(
            easydb_server=self.easydb_server,
            easydb_token=self.easydb_token,
            event_data=event.format_request_event(
                event_type=event.AUTO_KEYWORDER_REQUEST_IMAGGA,
                asset_status=asset_status,
                request_method='POST',
                request_url=url,
                request_body={
                    'image_base64': f'{asset_status.base64_data[:120]}...',
                },
                response_status_code=response.status_code,
                response_content=response.text,
            ),
        )

        if response.status_code != 200:
            asset_status.failure_reason = f'/uploads: request failed with status={response.status_code}, response={response.text}'
            asset_status.failed = True
            return

        try:
            response_json = json.loads(response.text)

            # check status
            status_type = util.get_json_value(response_json, 'status.type')
            if status_type != 'success':
                asset_status.failure_reason = f'/uploads: response has status.type "{status_type}": "{util.get_json_value(response_json, "status.text")}"'
                asset_status.failed = True
                return

            upload_id = util.get_json_value(response_json, 'result.upload_id')
            if not isinstance(upload_id, str) or upload_id == "":
                asset_status.failure_reason = f'/uploads: could not find valid result.upload_id in response={response.text}'
                asset_status.failed = True
                return

            asset_status.ai_job_id = upload_id

        except Exception as e:
            asset_status.failure_reason = (
                f'/uploads: could not parse response={response.text}: {e}'
            )
            asset_status.failed = True

    def get_ai_data_from_service_api(
        self,
        asset_status: eas.AssetStatus,
    ) -> int:

        url = f'{self.config.get_api_url()}/tags'

        response = requests.get(
            url=url,
            auth=(
                self.config.get_api_key(),
                self.config.get_api_secret(),
            ),
            params={
                'image_upload_id': asset_status.ai_job_id,
                'limit': self.config.get_int('num_keywords', default=5),
                'language': self.language_param(),
            },
        )

        event.log_event(
            easydb_server=self.easydb_server,
            easydb_token=self.easydb_token,
            event_data=event.format_request_event(
                event_type=event.AUTO_KEYWORDER_REQUEST_IMAGGA,
                asset_status=asset_status,
                request_method='GET',
                request_url=url,
                request_body={
                    'image_upload_id': asset_status.ai_job_id,
                    'limit': self.config.get_int('num_keywords', default=5),
                    'language': self.language_param(),
                },
                response_status_code=response.status_code,
                response_content=response.text,
            ),
        )

        if response.status_code != 200:
            asset_status.failure_reason = f'/tags: request failed with status={response.status_code}, response={response.text}'
            asset_status.failed = True
            return

        try:
            response_json = json.loads(response.text)

            # check status
            status_type = util.get_json_value(response_json, 'status.type')
            if status_type != 'success':
                asset_status.failure_reason = f'/tags: response has status.type "{status_type}": "{util.get_json_value(response_json, "status.text")}"'
                asset_status.failed = True
                return

            tags = util.get_json_value(response_json, 'result.tags')
            if not isinstance(tags, list) or len(tags) < 1:
                asset_status.failure_reason = (
                    f'/tags: could not find result.tags in response={response.text}'
                )
                asset_status.failed = True
                return

            min_confidence = self.config.get_int('min_confidence', 100)

            parsed_tags = []
            parsed_tags_by_confidence = {}
            seen_tags = {}
            for t in tags:
                tag = t.get('tag')
                if not isinstance(tag, dict):
                    continue

                confidence = t.get('confidence')
                if not (isinstance(confidence, float) or isinstance(confidence, int)):
                    continue
                if confidence < min_confidence:
                    continue

                _tag = {}
                for lang_code in tag:
                    easydb_lang_code = self.get_easydb_language(lang_code)
                    if not easydb_lang_code:
                        continue

                    if not easydb_lang_code in seen_tags:
                        seen_tags[easydb_lang_code] = set()

                    if tag[lang_code] in seen_tags[easydb_lang_code]:
                        continue
                    seen_tags[easydb_lang_code].add(tag[lang_code])

                    _tag[easydb_lang_code] = tag[lang_code]

                if _tag == {}:
                    continue

                parsed_tags_by_confidence[confidence] = _tag

            if parsed_tags_by_confidence == {}:
                asset_status.failure_reason = f'/tags: no entry in result.tags reached the minimum confidence of {min_confidence}'
                asset_status.failed = True
                return

            # order by confidence, highest first
            for confidence in sorted(parsed_tags_by_confidence.keys(), reverse=True):
                parsed_tags.append(parsed_tags_by_confidence[confidence])

            asset_status.ai_data = {
                'tags': parsed_tags,
            }
            asset_status.finished = True

        except Exception as e:
            asset_status.failure_reason = (
                f'/tags: could not parse response={response.text}: {e}'
            )
            asset_status.failed = True

    def merge_ai_data_into_object(
        self,
        asset_status: eas.AssetStatus,
        linked_object_cache: dict[str, dict[str, dict[str, dict]]],
    ) -> bool:
        ot = asset_status.easydb_obj_objecttype()
        if ot != self.config.get_objecttype():
            return False

        target_field_info = self.config.get_field_info(
            field_variable='target_field',
            expected=False,
        )
        if not target_field_info:
            return False

        ai_tags = []
        # check if the ai data is valid
        if not isinstance(asset_status.ai_data, dict):
            return False
        tags = asset_status.ai_data.get('tags')
        if not isinstance(tags, list):
            return False
        for ai_tag in tags:
            if ai_tag == {}:
                continue
            ai_tags.append(ai_tag)
        if len(ai_tags) == 0:
            return False

        sub_obj = asset_status.easydb_obj.get(ot)

        object_updated = False

        # iterate over the collected tags, append/insert into mapped field

        # check if in nested table or single field
        if target_field_info.is_in_nested():
            new_nested = []
            for ai_tag in ai_tags:
                if target_field_info.is_linked():
                    # find/create a matching linked objects:
                    # look in cache, else search, else create a new linked object
                    for lang in ai_tag:
                        # ai_data should only have a single language code, use first that matches
                        link = self.handle_linked_object(
                            ai_data_to_use=ai_tag,
                            asset_status=asset_status,
                            language=lang,
                            linked_object_cache=linked_object_cache,
                            field_info=target_field_info,
                        )
                        if link is None:
                            continue
                        new_nested.append({target_field_info.link_name: link})
                        asset_status.add_to_diff(
                            field_info=target_field_info,
                            field_value=ai_tag[lang],
                        )

                        break

                else:
                    # simple field in nested table
                    # check if l10n
                    if target_field_info.is_l10n():
                        # ai_data already has language codes, use as is
                        new_nested.append({target_field_info.name: ai_tag})
                        asset_status.add_to_diff(
                            field_info=target_field_info,
                            field_value=ai_tag,
                        )
                    else:
                        # ai_data should only have a single language code, use first
                        for lang in ai_tag:
                            new_nested.append({target_field_info.name: ai_tag[lang]})
                            asset_status.add_to_diff(
                                field_info=target_field_info,
                                field_value=ai_tag[lang],
                            )
                            break

            if len(new_nested) == 0:
                return False

            sub_obj[target_field_info.nested] = new_nested
            object_updated = True

        else:
            # not in nested -> concatenate values (ordered by confidence) into single string
            ai_label_values_by_lang = {}

            for ai_tag in ai_tags:
                for lang in ai_tag:
                    if not lang in ai_label_values_by_lang:
                        ai_label_values_by_lang[lang] = []
                    ai_label_values_by_lang[lang].append(ai_tag[lang])

            for lang in ai_label_values_by_lang:
                ai_label_values_by_lang[lang] = ', '.join(ai_label_values_by_lang[lang])

            if target_field_info.is_linked():
                # find/create a matching linked object:
                # look in cache, else search, else create a new linked object
                for lang in ai_label_values_by_lang:
                    # ai_data should only have a single language code, use first
                    link = self.handle_linked_object(
                        ai_data_to_use=ai_label_values_by_lang,
                        asset_status=asset_status,
                        language=lang,
                        linked_object_cache=linked_object_cache,
                        field_info=target_field_info,
                    )
                    if link is None:
                        return False
                    sub_obj[target_field_info.link_name] = link
                    asset_status.add_to_diff(
                        field_info=target_field_info,
                        field_value=ai_tag,
                    )
                    object_updated = True
                    break

            else:
                # simple field
                # check if l10n
                if target_field_info.is_l10n():
                    # ai_data already has language codes, use as is
                    sub_obj[target_field_info.name] = ai_label_values_by_lang
                    asset_status.add_to_diff(
                        field_info=target_field_info,
                        field_value=ai_label_values_by_lang,
                    )
                    object_updated = True
                else:
                    # ai_data should only have a single language code, use first
                    for lang in ai_label_values_by_lang:
                        sub_obj[target_field_info.name] = ai_label_values_by_lang[lang]
                        asset_status.add_to_diff(
                            field_info=target_field_info,
                            field_value=ai_label_values_by_lang[lang],
                        )
                        object_updated = True
                        break

        if not object_updated:
            # no new data was merged into the object
            return False

        self.update_main_object_info(asset_status.easydb_obj)
        return True
