import json
import requests

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


class CloudsightAiService(ai_service.AiServiceInterface):

    languages: dict = {
        'ar': 'ar',
        'cs': 'cs-CZ',
        'de': 'de-DE',
        'en': 'en-US',
        'es': 'es-ES',
        'fa': 'fa',
        'fr': 'fr-FR',
        'it': 'it-IT',
        'ja': 'ja-Jpan',
        'ka': 'ka-GE',
        'ko': 'ko-Kore',
        'nl': 'nl-NL',
        'pl': 'pl-PL',
        'ru': 'ru-RU',
        'zh': 'zh-Hans',
    }

    states_wait: list[str] = ['not completed']
    states_success: list[str] = ['completed']
    states_fail: list[str] = []

    def __init__(
        self,
        config: ai_service_configuration.CloudsightConfiguration,
        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 format_headers(self) -> dict[str]:
        return {
            'authority': 'api.cloudsight.ai',
            'authorization': 'CloudSight ' + self.config.get_api_key(),
        }

    def check_status(self, response_json: dict) -> tuple[int, str, list[str]]:
        skipped = response_json.get('skipped')
        if skipped is not None:
            return self.STATE_FAIL, 'skipped', [str(skipped)]

        state = response_json.get('status')

        if state in self.states_success:
            return self.STATE_SUCCESS, state, []

        if state in self.states_fail:
            return self.STATE_FAIL, state, []

        return self.STATE_WAIT, state, []

    def parse_ai_data_from_response(self, response_json: dict) -> dict:
        ai_data = {}
        name = response_json.get('name')
        if isinstance(name, str) and name != '':
            ai_data['subject'] = name

        nsfw = response_json.get('nsfw')
        if isinstance(nsfw, bool):
            ai_data['nsfw'] = nsfw

        categories = response_json.get('categories')
        if isinstance(categories, list) and len(categories) > 0:
            ai_data['categories'] = categories

        similar_objects = response_json.get('similar_objects')
        if isinstance(similar_objects, list) and len(similar_objects) > 0:
            ai_data['similar_objects'] = similar_objects

        structured_output = response_json.get('structured_output')
        if isinstance(structured_output, dict):
            for k in structured_output:
                ai_data[k] = structured_output[k]

        return ai_data

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

        url = f'{self.config.get_api_url()}/images'
        base64_data = self.format_base64_image_string(
            base64_image=asset_status.base64_data,
            img_type=asset_status.img_type,
        )

        response = requests.post(
            url=url,
            headers=self.format_headers(),
            data={
                'image': base64_data,
                '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_CLOUDSIGHT,
                asset_status=asset_status,
                request_method='POST',
                request_url=url,
                request_body={
                    'image': f'{base64_data[:120]}...',
                    'language': self.language_param(),
                },
                response_status_code=response.status_code,
                response_content=response.text,
            ),
        )

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

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

            # check status
            state, status_name, errors = self.check_status(response_json)
            if state == self.STATE_FAIL:
                asset_status.failure_reason = (
                    f'/images: response has status "{status_name}"'
                )
                if len(errors) > 0:
                    asset_status.failure_reason += f', errors: {"; ".join(errors)}'
                asset_status.failed = True
                return

            # there can already be results in the response
            if state == self.STATE_SUCCESS:
                ai_data = self.parse_ai_data_from_response(response_json)
                # check if there is already actual data
                if ai_data == {}:
                    return
                asset_status.ai_data = ai_data
                asset_status.finished = True
                return

            # get job token
            job_token = response_json.get('token')
            if not isinstance(job_token, str) or job_token == '':
                asset_status.failure_reason = (
                    f'/images: could not get a valid token from response'
                )
                asset_status.failed = True
                return

            asset_status.ai_job_id = job_token

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

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

        # if there was already an ai result in the response to the image upload, there is nothing to do
        if asset_status.finished:
            return

        url = f'{self.config.get_api_url()}/images/{asset_status.ai_job_id}'

        response = requests.get(
            url=url,
            headers=self.format_headers(),
        )

        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_CLOUDSIGHT,
                asset_status=asset_status,
                request_method='GET',
                request_url=url,
                request_body=None,
                response_status_code=response.status_code,
                response_content=response.text,
            ),
        )

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

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

            # check status
            state, status_name, errors = self.check_status(response_json)
            if state == self.STATE_FAIL:
                asset_status.failure_reason = (
                    f'/images: response has status "{status_name}"'
                )
                if len(errors) > 0:
                    asset_status.failure_reason += f', errors: {"; ".join(errors)}'
                asset_status.failed = True
                return

            if state == self.STATE_SUCCESS:
                ai_data = self.parse_ai_data_from_response(response_json)
                if ai_data == {}:
                    return
                asset_status.ai_data = ai_data
                asset_status.finished = True

        except Exception as e:
            asset_status.failure_reason = (
                f'/images: 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:
        objecttype = asset_status.easydb_obj_objecttype()
        if objecttype != self.config.get_objecttype():
            return False

        language = self.get_easydb_language(self.language_param())
        object_updated = False

        # subject: simple/l10n text
        subject = asset_status.ai_data.get('subject')
        if subject:
            target_field_info = self.config.get_field_info(
                field_variable='subject_field',
                expected=False,
            )
            if target_field_info:
                if self.merge_into_field(
                    asset_status=asset_status,
                    target_field_info=target_field_info,
                    values=[subject],
                    linked_object_cache=linked_object_cache,
                    language=language,
                ):
                    object_updated = True

        # map: field variable in config -> key in ai data
        keyword_fields = {
            'keyword_gender_field': 'gender',
            'keyword_quantity_field': 'quantity',
            'keyword_material_field': 'material',
            'keyword_color_field': 'color',
            'keyword_categories_field': 'categories',
            'keyword_similar_objects_field': 'similar_objects',
        }
        for field_variable in keyword_fields:
            values = asset_status.ai_data.get(keyword_fields[field_variable])
            if not isinstance(values, list):
                continue
            if len(values) == 0:
                continue

            target_field_info = self.config.get_field_info(
                field_variable,
                expected=False,
            )
            if not target_field_info:
                continue

            if self.merge_into_field(
                asset_status=asset_status,
                target_field_info=target_field_info,
                values=values,
                linked_object_cache=linked_object_cache,
                language=language,
            ):
                object_updated = True

        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

    def merge_into_field(
        self,
        asset_status: eas.AssetStatus,
        target_field_info: datamodel.FieldInfo,
        values: list[str],
        linked_object_cache: dict[str, dict[str, dict[str, dict]]],
        language: str,
    ) -> bool:

        if len(values) == 0:
            return False

        objecttype = asset_status.easydb_obj_objecttype()

        # check if in nested table or single field
        if target_field_info.is_in_nested():
            new_nested = []
            for value in values:
                if target_field_info.is_linked():
                    # find/create a matching linked objects:
                    # look in cache, else search, else create a new linked object
                    link = self.handle_linked_object(
                        ai_data_to_use={
                            language: value,
                        },
                        asset_status=asset_status,
                        language=language,
                        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=value,
                    )

                else:
                    # simple field in nested table
                    # check if l10n
                    if target_field_info.is_l10n():
                        new_nested.append(
                            {
                                target_field_info.name: {
                                    language: value,
                                },
                            }
                        )
                        asset_status.add_to_diff(
                            field_info=target_field_info,
                            field_value=value,
                        )

                    else:
                        new_nested.append(
                            {
                                target_field_info.name: value,
                            }
                        )
                        asset_status.add_to_diff(
                            field_info=target_field_info,
                            field_value=value,
                        )

            if len(new_nested) == 0:
                return False

            asset_status.easydb_obj[objecttype][target_field_info.nested] = new_nested
            return True

        # not in nested -> concatenate alphabetically ordered values into single string
        value = ', '.join(sorted(values))

        if target_field_info.is_linked():
            # find/create a matching linked object:
            # look in cache, else search, else create a new linked object
            link = self.handle_linked_object(
                ai_data_to_use={
                    language: value,
                },
                asset_status=asset_status,
                language=language,
                linked_object_cache=linked_object_cache,
                field_info=target_field_info,
            )
            if link is None:
                return False
            asset_status.easydb_obj[objecttype][target_field_info.link_name] = link
            asset_status.add_to_diff(
                field_info=target_field_info,
                field_value=value,
            )
            return True

        # simple field
        # check if l10n
        if target_field_info.is_l10n():
            asset_status.easydb_obj[objecttype][target_field_info.name] = {
                language: value,
            }
            asset_status.add_to_diff(
                field_info=target_field_info,
                field_value=value,
            )
        else:
            asset_status.easydb_obj[objecttype][target_field_info.name] = value
            asset_status.add_to_diff(
                field_info=target_field_info,
                field_value=value,
            )

        return True
