import json
import requests

from gtranslate.constants import (
    GOOGLE_CODES_TO_LANGUAGES,
    GOOGLE_DISPLAY_LANGUAGES_TO_CODES,
    GOOGLE_LANGUAGES_TO_CODES,
    LANGUAGE_CODE_TO_FLAG,
)
from gtranslate.exceptions import (
    ElementNotFoundInGetRequest,
    LanguageNotSupportedException,
    NotValidPayload,
)
from gtranslate.parent import BaseTranslator


class GoogleTranslator(BaseTranslator):
    """Google Translate wrapper with translation, detection and language metadata helpers."""

    supported_languages = list(GOOGLE_LANGUAGES_TO_CODES.keys())
    _api_base_url = 'https://translate.googleapis.com/translate_a/single'

    def __init__(self, source='auto', target='en'):
        if self.is_language_supported(source, target):
            self._source, self._target = self._map_language_to_code(source, target)

        super(GoogleTranslator, self).__init__(
            base_url=self._api_base_url,
            source=self._source,
            target=self._target,
            payload_key='q',
            client='gtx',
            dt='t',
            sl=self._source,
            tl=self._target,
            dj=0,
        )

    @classmethod
    def _supported_codes_map(cls):
        return {
            str(code).lower(): str(code)
            for code in GOOGLE_LANGUAGES_TO_CODES.values()
        }

    def _map_language_to_code(self, *languages, **kwargs):
        supported_codes_map = self._supported_codes_map()

        mapped = []
        for language in languages:
            normalized = str(language).strip()
            normalized_lower = normalized.lower()

            if normalized_lower == 'auto':
                mapped.append('auto')
            elif normalized_lower in GOOGLE_LANGUAGES_TO_CODES:
                mapped.append(GOOGLE_LANGUAGES_TO_CODES[normalized_lower])
            elif normalized_lower in supported_codes_map:
                mapped.append(supported_codes_map[normalized_lower])
            else:
                raise LanguageNotSupportedException(language)

        return tuple(mapped)

    def is_language_supported(self, *languages, **kwargs):
        supported_codes_map = self._supported_codes_map()

        for lang in languages:
            normalized = str(lang).strip()
            normalized_lower = normalized.lower()

            if normalized_lower == 'auto':
                continue

            if normalized_lower in GOOGLE_LANGUAGES_TO_CODES:
                continue

            if normalized_lower in supported_codes_map:
                continue

            raise LanguageNotSupportedException(lang)

        return True

    def _perform_request(self, payload, **kwargs):
        params = dict(self._url_params)
        if self.payload_key:
            params[self.payload_key] = payload

        headers = {
            'User-Agent': 'Mozilla/5.0',
            'Accept': 'application/json,text/plain,*/*',
        }

        response = requests.get(
            self._api_base_url,
            params=params,
            headers=headers,
            timeout=kwargs.get('timeout', 15),
        )
        response.raise_for_status()
        return response.json()

    def _extract_translation(self, data):
        if not isinstance(data, list) or not data or not isinstance(data[0], list):
            raise ElementNotFoundInGetRequest(data)

        parts = []
        for item in data[0]:
            if isinstance(item, list) and item:
                text = item[0]
                if text:
                    parts.append(text)

        translated_text = ''.join(parts).strip()
        if not translated_text:
            raise ElementNotFoundInGetRequest(data)

        return translated_text

    def _extract_detected_language(self, data):
        if isinstance(data, list) and len(data) > 2 and isinstance(data[2], str) and data[2].strip():
            return data[2].strip()
        raise ElementNotFoundInGetRequest(data)

    @staticmethod
    def _normalize_code(code):
        return str(code).strip().lower()

    @staticmethod
    def _country_code_to_flag(country_code):
        if len(country_code) != 2 or not country_code.isalpha():
            return '🌐'
        base = 127397
        return chr(base + ord(country_code[0].upper())) + chr(base + ord(country_code[1].upper()))

    @classmethod
    def get_supported_languages(cls, as_dict=True, as_json=False, indent=2):
        payload = dict(GOOGLE_DISPLAY_LANGUAGES_TO_CODES)

        if as_json:
            return json.dumps(payload, ensure_ascii=False, indent=indent)

        if as_dict:
            return payload

        return list(payload.keys())

    @classmethod
    def get_language_flag(cls, language_or_code):
        if not language_or_code:
            return '🌐'

        normalized = str(language_or_code).strip()
        normalized_lower = normalized.lower()

        if normalized_lower in GOOGLE_LANGUAGES_TO_CODES:
            code = GOOGLE_LANGUAGES_TO_CODES[normalized_lower]
        else:
            supported_codes_map = cls._supported_codes_map()
            code = supported_codes_map.get(normalized_lower, normalized)

        normalized_code = cls._normalize_code(code)

        if normalized_code in LANGUAGE_CODE_TO_FLAG:
            return LANGUAGE_CODE_TO_FLAG[normalized_code]

        if '-' in str(code):
            region = str(code).split('-')[-1].upper()
            if len(region) == 2 and region.isalpha():
                return cls._country_code_to_flag(region)

        return '🌐'

    @classmethod
    def get_supported_languages_with_flags(cls, as_json=False, indent=2):
        payload = {
            language: {
                'code': code,
                'flag': cls.get_language_flag(code),
            }
            for language, code in GOOGLE_DISPLAY_LANGUAGES_TO_CODES.items()
        }

        if as_json:
            return json.dumps(payload, ensure_ascii=False, indent=indent)

        return payload

    @classmethod
    def get_language_details(cls, language_or_code):
        if not language_or_code:
            raise NotValidPayload(language_or_code)

        normalized = str(language_or_code).strip()
        normalized_lower = normalized.lower()

        if normalized_lower == 'auto':
            return {
                'language': 'Auto Detect',
                'code': 'auto',
                'flag': '🌐',
            }

        if normalized_lower in GOOGLE_LANGUAGES_TO_CODES:
            code = GOOGLE_LANGUAGES_TO_CODES[normalized_lower]
        else:
            supported_codes_map = cls._supported_codes_map()
            code = supported_codes_map.get(normalized_lower, normalized)

        language = (
            GOOGLE_CODES_TO_LANGUAGES.get(code)
            or GOOGLE_CODES_TO_LANGUAGES.get(str(code).lower())
            or normalized
        )

        return {
            'language': language,
            'code': code,
            'flag': cls.get_language_flag(code),
        }

    def translate(self, payload, **kwargs):
        if self._validate_payload(payload):
            payload = payload.strip()
            data = self._perform_request(payload=payload, **kwargs)
            return self._extract_translation(data)

    def detect_language(self, payload, **kwargs):
        if self._validate_payload(payload):
            payload = payload.strip()
            data = self._perform_request(payload=payload, **kwargs)
            return self._extract_detected_language(data)

    def detect(self, payload, **kwargs):
        return self.detect_language(payload=payload, **kwargs)

    def detect_language_details(self, payload, **kwargs):
        code = self.detect_language(payload=payload, **kwargs)
        return self.get_language_details(code)

    def get_target_details(self):
        return self.get_language_details(self._target)

    def get_source_details(self, payload=None, **kwargs):
        if self._source == 'auto':
            if payload is None:
                return {
                    'language': 'Auto Detect',
                    'code': 'auto',
                    'flag': '🌐',
                }
            return self.detect_language_details(payload=payload, **kwargs)

        return self.get_language_details(self._source)

    def get_translation_details(self, payload, **kwargs):
        if not self._validate_payload(payload):
            raise NotValidPayload(payload)

        payload = payload.strip()
        data = self._perform_request(payload=payload, **kwargs)
        translated_text = self._extract_translation(data)

        if self._source == 'auto':
            source_code = self._extract_detected_language(data)
            source_details = self.get_language_details(source_code)
        else:
            source_details = self.get_language_details(self._source)

        target_details = self.get_language_details(self._target)

        return {
            'text': payload,
            'translation': translated_text,
            'source': source_details,
            'target': target_details,
        }

    def translate_with_details(self, payload, **kwargs):
        return self.get_translation_details(payload=payload, **kwargs)

    def get_language_pair_details(self, payload=None, **kwargs):
        return {
            'source': self.get_source_details(payload=payload, **kwargs),
            'target': self.get_target_details(),
        }

    def translate_file(self, path, **kwargs):
        try:
            with open(path, encoding=kwargs.get('encoding', 'utf-8')) as f:
                text = f.read()
            return self.translate(payload=text, **kwargs)
        except Exception as e:
            raise e

    def translate_sentences(self, sentences=None, **kwargs):
        if not sentences:
            raise NotValidPayload(sentences)

        translated_sentences = []
        try:
            for sentence in sentences:
                translated = self.translate(payload=sentence, **kwargs)
                translated_sentences.append(translated)
            return translated_sentences
        except Exception as e:
            raise e
