import time
import json
from typing import Iterator
from importlib.metadata import version

import requests
from requests.models import Response

from nsj_gcf_utils.json_util import convert_to_dumps, json_dumps

from nsj_integracao_api_client.service.integrador_cfg import (
    Environment, TAMANHO_PAGINA, _E_SEND_DATA, AUTH_HEADER,
    TipoVerificacaoIntegridade
)

from nsj_integracao_api_client.infra.debug_utils import DebugUtils as _du

class APIException(Exception):
    _campos_padrao = ["mensagem", "endpoint",  "status", "resposta"]
    _todos_campos =  ["mensagem", "endpoint", "status", "resposta", "dados"]

    def __init__(self, mensagem=None, endpoint=None, status=None, reason=None, response_text=None, dados=None):
        super().__init__(mensagem)
        self.mensagem = mensagem
        self.endpoint = endpoint
        self.status = status
        self.reason = reason
        self.response_text = response_text
        self.dados = dados
        self._campos_visiveis = self._campos_padrao.copy()

    def campos(self, **kwargs):
        # kwargs: campo=True/False
        self._campos_visiveis = [campo for campo, mostrar in kwargs.items() if mostrar and campo in self._todos_campos]
        return self

    def __str__(self):
        campos_dict = {
            "mensagem": self.mensagem,
            "endpoint": self.endpoint,
            "status": f"{self.status} - {self.reason}",
            "resposta": self.response_text,
            "dados": self.dados
        }
        return "\n".join(f"{campo}: {campos_dict.get(campo)}" for campo in self._campos_visiveis if campos_dict.get(campo) is not None)


class ApiClient:


    def __init__(self, env: Environment):
        self._env = env


    def _trace_envio(self, filename, content):
        _du.conditional_trace(
            condition=_E_SEND_DATA,
            func=_du.save_to_file,
            filename=filename,
            content=content
        )


    def _url_base(self) -> str:
        if self._env == Environment.LOCAL:
            return "http://localhost:5000/integracao-pessoas-api/66"
        elif self._env == Environment.DEV:
            return "https://api.nasajon.dev/integracao-pessoas-api/66"
        elif self._env == Environment.QA:
            return "https://api.nasajon.qa/integracao-pessoas-api/66"
        elif self._env == Environment.PROD:
            return "https://api4.nasajon.app/integracao-pessoas-api/66"
        else:
            raise ValueError(f"Ambiente desconhecido: {self._env}")


    def _url_diretorio(self) -> str:
        if self._env == Environment.LOCAL:
            return "http://localhost"
        elif self._env == Environment.DEV:
            return "https://dir.nasajon.dev"
        elif self._env == Environment.QA:
            return "https://dir.nasajon.qa"
        elif self._env == Environment.PROD:
            return "https://diretorio.nasajon.com.br"
        else:
            raise ValueError(f"Ambiente desconhecido: {self._env}")


    def raise_exception_from_response(
        self,
        msg: str,
        response: Response,
        dados: list,
        response_text: str = None
    ):
        response_content = response_text if response_text else response.json() if 'application/json' in response.headers.get('Content-Type', '') else response.text

        raise APIException(
            msg,
            response.url,
            response.status_code,
            response.reason,
            response_content,
            dados
        )


    def sistemas_contratados_ambiente(self, api_key: str, tenant: int):
        """
        Endpoint que efetua a consulta dos sistemas contratados para um ambiente (tenant).
        """

        assert api_key, "Autenticação não foi informada."
        assert tenant, "Ambiente não foi informado."

        s = requests.Session()
        s.headers.update({
            'apiKey': api_key,
            'Accept':'application/json'
        })
        response = s.get(f'{self._url_diretorio()}/v2/api/licenca/tenant/{tenant}')

        if response.status_code == 200:
            try:
                return response.json()
            except Exception as ex:
                raise APIException(f'Retorno desconhecido:{response.text}') from ex

        if response.status_code < 200 or response.status_code > 299:
            if 'application/json' in response.headers.get('Content-Type', ''):
                _json_response = response.json()
                _message = _json_response['erro'] if 'erro' in _json_response else ''
            else:
                _message = response.text
            raise APIException(
                "Erro ao capturar os sistemas contratados",
                response.url,
                response.status_code,
                response.reason,
                _message
            )


    def enviar_dados(self, dict_data: list, entidade: str, api_key: str):
        """
        """
        self._trace_envio(
            f"trace/send_data_{entidade}_{_du.time()}.json",
            json.dumps(dict_data, indent=2, ensure_ascii=False, default=str) if _E_SEND_DATA  else ""
        )

        _rota = entidade.replace('.', '/').lower()

        upsert = True #False if (acao in ["processos"] and self._env == Environment.PROD) else True

        s = requests.Session()
        s.headers.update({'Content-Type':'application/json', AUTH_HEADER: api_key})

        if upsert:
            response = s.put(f'{self._url_base()}/{_rota}?upsert=true', json=convert_to_dumps(dict_data))

            if response.status_code == 413:
                for _item in dict_data:
                    response = s.put(f'{self._url_base()}/{_rota}?upsert=true', json=convert_to_dumps(_item))
                    self._tratar_resposta(response, dict_data)
            else:
                self._tratar_resposta(response, dict_data)

        else:
            for _item in dict_data:
                response = s.post(f'{self._url_base()}/{_rota}', json=convert_to_dumps(_item))
                if response.status_code < 200 or response.status_code > 299:
                    if 'application/json' in response.headers.get('Content-Type', '') and \
                    isinstance(response.json(), list) and 'message' in response.json()[0] and \
                    ('_bt_check_unique' in response.json()[0].get('message', '') or response.status_code == 409):
                        raise APIException(
                            "Erro ao enviar dados ao servidor",
                            response.url,
                            response.status_code,
                            response.reason,
                            response.text,
                            convert_to_dumps(dict_data)
                        )


    def apagar_dados(self, dict_data: list, entidade: str, api_key: str, tenant: int) -> Iterator[str]:
        """
        """
        _rota = entidade.replace('.', '/').lower()

        s = requests.Session()
        s.headers.update({'Content-Type':'application/json', AUTH_HEADER: api_key})
        response = s.delete(f'{self._url_base()}/{_rota}?tenant={tenant}', json=convert_to_dumps(dict_data))

        # Caso algum item não exista no servidor tenta apagar individualmente,
        # ignorando os ausentes
        if response.status_code == 404:
            #Não mais faremos essa tratativa do um a um
            for _item in dict_data:
                response = s.delete(f'{self._url_base()}/{_rota}?tenant={tenant}', json=convert_to_dumps([_item]))
                if response.status_code == 404:
                    #yield f"Registro {entidade} id: {_item} não encontrado para exclusão no tenant {tenant}, ignorando."
                    continue
                else:
                    if (response.status_code < 200 or response.status_code > 299) and response.status_code != 404:
                        if 'application/json' in response.headers.get('Content-Type', ''):
                            _json_response = response.json()
                            if isinstance(_json_response, dict):
                                _message = _json_response['message']if 'message' in _json_response else ''
                            else:
                                _message = _json_response[0]['message'] if 'message' in _json_response[0] else ''

                        else:
                            _message = response.text
                        raise APIException(
                            "Erro ao apagar dados do servidor",
                            response.url,
                            response.status_code,
                            response.reason,
                            _message,
                            convert_to_dumps(dict_data)
                        )

        if (response.status_code < 200 or response.status_code > 299) and response.status_code != 404:
            if 'application/json' in response.headers.get('Content-Type', ''):
                _json_response = response.json()
                if isinstance(_json_response, dict):
                    _message = _json_response['message']if 'message' in _json_response else ''
                else:
                    _message = _json_response[0]['message'] if 'message' in _json_response[0] else ''

            else:
                _message = response.text
            raise APIException(
                "Erro ao apagar dados do servidor",
                response.url,
                response.status_code,
                response.reason,
                _message,
                convert_to_dumps(dict_data)
            )

        return []


    def _validar_response_apagar_dados_bulk(self, response: Response) -> bool:

        # Valida o tipo de response
        if not 'application/json' in response.headers.get('Content-Type', ''):
            return False

        # Valida o formato de response
        _json_response = response.json()
        if  not (isinstance(_json_response, dict) and 'global_status' in _json_response  and 'response' in _json_response):
            return False

        return True


    def apagar_dados_bulk(self, data: list, entidade: str, api_key: str, tenant: int):
        """
        """
        _rota = entidade.replace('.', '/').lower()

        s = requests.Session()
        s.headers.update({'Content-Type':'application/json', AUTH_HEADER: api_key})
        response = s.delete(f'{self._url_base()}/{_rota}/bulk?tenant={tenant}', json=convert_to_dumps(data))

        if not self._validar_response_apagar_dados_bulk(response):
            self.raise_exception_from_response("Formato inválido na resposta da exclusão em massa", response, data)

        _json_response = response.json()

        # Avalia os estatus individuais para lançar erro se pertinente
        if _json_response["global_status"] != "OK":

            _responses = [ f'{_resp["error"]["message"]}' for _resp in _json_response["response"] if _resp["status"] >= 500 ]

            if _responses:
                self.raise_exception_from_response(
                    "Erro ao apagar dados do servidor",
                    response,
                    data,
                    "\n".join(_responses)
                )


    def _dividir_filtros_em_lotes(self, filtros: dict, tamanho_lote: int = 20):
        """
        Divide filtros que são listas/strings longas em lotes menores para evitar URI muito longa.
        Retorna uma lista de dicts de filtros, cada um representando um lote.
        """
        if not filtros:
            return [{}]

        # Identifica filtros que podem ser grandes (valores que são listas ou strings com vírgulas)
        filtro_grande = None
        filtro_grande_chave = None
        outros_filtros = {}

        for chave, valor in filtros.items():
            # Se for uma string com vírgulas (múltiplos valores concatenados)
            if isinstance(valor, str) and ',' in valor:
                filtro_grande = valor.split(',')
                filtro_grande_chave = chave
            else:
                outros_filtros[chave] = valor

        # Se não há filtro grande, retorna como está
        if not filtro_grande:
            return [filtros]

        # Divide o filtro grande em lotes
        lotes = []
        for i in range(0, len(filtro_grande), tamanho_lote):
            lote_valores = filtro_grande[i:i + tamanho_lote]
            lote_filtros = outros_filtros.copy()
            lote_filtros[filtro_grande_chave] = ','.join(lote_valores)
            lotes.append(lote_filtros)

        return lotes

    def consultar_integridade_de(
            self,
            entidade: str,
            filtros: dict,
            filtros_extras: list,
            ultimo_id,
            detalhar_diferencas: bool,
            api_key: str,
            tenant: int,
            tipo_verificacao: TipoVerificacaoIntegridade = TipoVerificacaoIntegridade.HASH):
        """
        Consulta uma página de dados de integridade na API, para os filtros e cursor (ultimo_id)
        informados. Não realiza paginação nem batching internamente — isso é responsabilidade
        do chamador (ver integrador.py).
        """
        _rota = entidade.replace('.', '/').lower()

        s = requests.Session()
        s.headers.update({
            'Content-Type': 'application/json',
            AUTH_HEADER: api_key,
        })

        filtros_str = None
        if filtros:
            filtros_str = "&".join(
                [f"{_chave}={filtros[_chave]}" for _chave in filtros.keys()]
            )

        filtros_extras_str = None
        if filtros_extras:
            filtros_extras_str = f"custom_filter={json_dumps(filtros_extras)}"
        else:
            filtros_extras_str = "custom_filter=[]"

        _url = (
            f'{self._url_base()}/{_rota}/verificacao-integridade?tenant={tenant}&source={detalhar_diferencas}'
            f'{"&" + filtros_str if filtros_str else ""}'
            f'&limit={TAMANHO_PAGINA}'
            f'{"&after="+str(ultimo_id) if ultimo_id else ""}'
            f'{"&" + filtros_extras_str}'
            f'&type={tipo_verificacao.value}'
        )

        _max_retries = 3
        _retry_count = 0
        while _retry_count <= _max_retries:
            response = s.get(_url, headers={'X-NSJ-INTEGRACAO-VERSION': version('nsj_integracao_api_entidades')})
            response_content = response.json() if 'application/json' in response.headers.get('Content-Type', '') else response.text

            if response.status_code < 200 or response.status_code > 299:
                _retry_count += 1
                if _retry_count > _max_retries:
                    if isinstance(response_content, dict):
                        _message = response_content.get('message', '')
                    else:
                        _message = response_content
                    raise APIException(
                        "Erro ao consultar a integridade no servidor",
                        response.url,
                        response.status_code,
                        response.reason,
                        _message
                    )
                else:
                    # Tenta novamente após um pequeno delay
                    time.sleep(2 ** _retry_count)
                    continue
            break

        return response_content


    def gerar_token_tenant(self, chave_ativacao: str) -> str:
        """
        """
        s = requests.Session()
        s.headers.update({
            'Content-Type':'application/x-www-form-urlencoded',
            'Accept':'application/json'
        })
        response = s.post(
            f'{self._url_diretorio()}/v2/api/gerar_token_ativacao_integracao/',
            data={"codigo_ativacao": chave_ativacao})

        if response.status_code == 200:
            _json = response.json()
            if "apiKey" in _json:
                return _json["apiKey"]
            else:
                raise APIException(f'Retorno desconhecido:{_json}')

        if response.status_code < 200 or response.status_code > 299:
            if 'application/json' in response.headers.get('Content-Type', ''):
                _json_response = response.json()
                _message = _json_response['message'] if 'message' in _json_response else ''
            else:
                _message = response.text
            raise APIException(
                "Erro ao gerar o token de integração",
                response.url,
                response.status_code,
                response.reason,
                _message
            )

    def ativar_integracao(self, tenant: int, chave_ativacao: str, grupos_empresariais: list, api_key: str) -> dict:
        """
        Ativa integração para um tenant usando a API.
        
        Args:
            tenant: ID do tenant
            chave_ativacao: Chave de ativação no formato NNNN-NNNN-NNNN-NNNN
            grupos_empresariais: Lista de códigos ou UUIDs dos grupos empresariais
            api_key: API Key para autenticação
            
        Returns:
            dict: Resposta da API com detalhes da ativação
            
        Raises:
            APIException: Em caso de erro na chamada da API
        """
        s = requests.Session()
        s.headers.update(
            {
                "Content-Type": "application/json",
                "Accept": "application/json",
                AUTH_HEADER: api_key,
            }
        )
        
        payload = {
            "tenant": tenant,
            "chave_ativacao": chave_ativacao,
            "grupos_empresariais": grupos_empresariais,
        }
        
        response = s.post(
            f"{self._url_base()}/processos-admin/ativacao-integracao",
            json=payload,
        )
        
        if response.status_code == 201:
            return response.json()
        
        # Tratamento de erros
        if "application/json" in response.headers.get("Content-Type", ""):
            _json_response = response.json()
            _message = _json_response.get("erro", _json_response.get("message", ""))
            _detalhes = _json_response.get("mensagem", _json_response.get("detalhes", ""))
            if _detalhes:
                _message = f"{_message}: {_detalhes}"
        else:
            _message = response.text
            
        raise APIException(
            "Erro ao ativar integração",
            response.url,
            response.status_code,
            response.reason,
            _message,
        )

    def ativar_grupos_empresariais(self, tenant: int, grupos: list, api_key: str) -> dict:
        """
        Ativa grupos empresariais para integração via API.
        
        Args:
            tenant: ID do tenant
            grupos: Lista de códigos ou UUIDs dos grupos empresariais
            api_key: API Key para autenticação
            
        Returns:
            dict: Resposta da API com detalhes da ativação
            
        Raises:
            APIException: Em caso de erro na chamada da API
        """
        s = requests.Session()
        s.headers.update(
            {
                "Content-Type": "application/json",
                "Accept": "application/json",
                AUTH_HEADER: api_key,
            }
        )
        
        payload = {
            "tenant": tenant,
            "grupos": grupos,
        }
        
        response = s.post(
            f"{self._url_base()}/processos-admin/grupos-empresariais",
            json=payload,
        )
        
        if response.status_code == 200:
            return response.json()
        
        # Tratamento de erros
        if "application/json" in response.headers.get("Content-Type", ""):
            _json_response = response.json()
            _message = _json_response.get("erro", _json_response.get("message", ""))
        else:
            _message = response.text
            
        raise APIException(
            "Erro ao ativar grupos empresariais",
            response.url,
            response.status_code,
            response.reason,
            _message,
        )

    def desativar_grupos_empresariais(self, tenant: int, grupos: list, api_key: str) -> dict:
        """
        Desativa grupos empresariais da integração via API.
        
        Args:
            tenant: ID do tenant
            grupos: Lista de códigos ou UUIDs dos grupos empresariais
            api_key: API Key para autenticação
            
        Returns:
            dict: Resposta da API com detalhes da desativação
            
        Raises:
            APIException: Em caso de erro na chamada da API
        """
        s = requests.Session()
        s.headers.update(
            {
                "Content-Type": "application/json",
                "Accept": "application/json",
                AUTH_HEADER: api_key,
            }
        )
        
        payload = {
            "tenant": tenant,
            "grupos": grupos,
        }
        
        response = s.post(
            f"{self._url_base()}/processos-admin/grupos-empresariais/desativar",
            json=payload,
        )
        
        if response.status_code == 200:
            return response.json()
        
        # Tratamento de erros
        if "application/json" in response.headers.get("Content-Type", ""):
            _json_response = response.json()
            _message = _json_response.get("erro", _json_response.get("message", ""))
        else:
            _message = response.text
            
        raise APIException(
            "Erro ao desativar grupos empresariais",
            response.url,
            response.status_code,
            response.reason,
            _message,
        )

    def desativar_integracao(self, tenant: int, api_key: str) -> dict:
        """
        Desativa integração para um tenant via API (remove token).
        
        Args:
            tenant: ID do tenant
            api_key: API Key para autenticação
            
        Returns:
            dict: Resposta da API
            
        Raises:
            APIException: Em caso de erro na chamada da API
        """
        s = requests.Session()
        s.headers.update(
            {
                "Accept": "application/json",
                AUTH_HEADER: api_key,
            }
        )
        
        response = s.delete(
            f"{self._url_base()}/processos-admin/ativacao-integracao?tenant={tenant}"
        )
        
        if response.status_code == 200:
            return response.json()
        
        # Tratamento de erros
        if "application/json" in response.headers.get("Content-Type", ""):
            _json_response = response.json()
            _message = _json_response.get("erro", _json_response.get("message", ""))
            _detalhes = _json_response.get("mensagem", "")
            if _detalhes:
                _message = f"{_message}: {_detalhes}"
        else:
            _message = response.text
            
        raise APIException(
            "Erro ao desativar integração",
            response.url,
            response.status_code,
            response.reason,
            _message,
        )

    def _tratar_resposta(self, response, dict_data):
        if response.status_code < 200 or response.status_code > 299:
            if 'application/json' in response.headers.get('Content-Type', ''):
                _json_response = response.json()
                if isinstance(_json_response, dict):
                    _message = _json_response['message'] if 'message' in _json_response else ''
                else:
                    _message = _json_response[0]['message'] if 'message' in _json_response[0] else ''
            else:
                _message = response.text
            raise APIException(
                "Erro ao enviar dados ao servidor",
                response.url,
                response.status_code,
                response.reason,
                _message,
                convert_to_dumps(dict_data)
            )


    def consultar_hash_blob(self, ids: list, entidade: str, campo: str, tenant: int, api_key: str):
        """
        """

        _rota = entidade.split('.', 1)[1].lower()

        s = requests.Session()
        s.headers.update({'Content-Type':'application/json', AUTH_HEADER: api_key})
        _url = (
            f'{self._url_base()}/blobs-{_rota}-hashes/filtros?tenant={tenant}&field={campo}'
            f'&limit={TAMANHO_PAGINA}'
        )

        _max_retries = 3
        _retry_count = 0
        while _retry_count <= _max_retries:
            response = s.post(_url, json=convert_to_dumps(ids))
            response_content = response.json() if 'application/json' in response.headers.get('Content-Type', '') else response.text

            if response.status_code < 200 or response.status_code > 299:

                _retry_count += 1
                if _retry_count > _max_retries:
                    if isinstance(response_content, dict):
                        _message = response_content.get('message', '')
                    else:
                        _message = response_content
                    raise APIException(
                        "Erro ao consultar os hashes dos blobs no servidor",
                        response.url,
                        response.status_code,
                        response.reason,
                        _message
                    )
                else:
                    # Tenta novamente após um pequeno delay
                    time.sleep(2 ** _retry_count)
                    continue
            break

        if not 'result' in response_content:
            raise APIException(
                "Formato inválido na consulta dos hashes dos blobs no servidor",
                response.url,
                response.status_code,
                response.reason,
                response_content
            )

        return response_content


    def enviar_blobs(self, ids: list, files: list ,entidade: str, campo: str, tenant: int, api_key: str):
        """
        """

        _rota = entidade.split('.', 1)[1].lower()

        s = requests.Session()
        s.headers.update({AUTH_HEADER: api_key})
        _url = f'{self._url_base()}/blobs-{_rota}?field={campo}&tenant={tenant}'

        _files = files  # só os arquivos
        _data = {'ids': json.dumps(ids)}

        response = s.put(_url, files=_files, data=_data)

        if response.status_code == 413:
            # Envia cada arquivo individualmente em caso de erro 413 (Payload Too Large)
            for i, file in enumerate(files):
                _data = {'ids': json.dumps([ids[i]])}
                response = s.put(_url, files=[file], data=_data)
                self._tratar_resposta(response, [ids[i]])
        else:
            self._tratar_resposta(response, ids)


# if __name__ == "__main__":
#     api = ApiClient(Environment.LOCAL)
#     key = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0aXBvIjoidGVuYW50IiwidGVuYW50X2lkIjo5Njd9.jZrhVlEjPYaffhsuZ9UiE0kMoXpVpfrvRTfNx0zdJGk"
#     files = [
#         ('files', ("None", b"file1", 'application/octet-stream'))
#     ]
#     api.enviar_blobs(["6a2dfb51-12a0-4716-aa90-97d333d44f09"], files, "persona.trabalhadores", "foto", 900, key)