import asyncio
import logging
from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta
from dateutil import parser as date_parser

from ...domain.entities.consultor import (
    Consultor,
    CoordenacaoCapes,
    Consultoria,
    IES,
    VinculoConsultoria,
    Inscricao,
    AvaliacaoComissao,
    Premiacao,
    BolsaCNPQ,
    Participacao,
    Orientacao,
    MembroBanca,
)
from ...domain.repositories.consultor_repository import ConsultorRepository
from ...domain.services.calculador_pontuacao import CalculadorPontuacao
from ...domain.value_objects.periodo import Periodo, mesclar_periodos
from ..cache import ranking_cache
from ..elasticsearch.client import ElasticsearchClient

logger = logging.getLogger(__name__)


class ConsultorRepositoryImpl(ConsultorRepository):
    def __init__(self, es_client: ElasticsearchClient, oracle_client=None):
        self.es_client = es_client
        self.oracle_client = oracle_client
        self.calculador = CalculadorPontuacao()
        self.es_disponivel = True

    def _parse_date(self, date_str: Optional[str]) -> Optional[datetime]:
        if not date_str:
            return None
        try:
            return date_parser.parse(date_str, dayfirst=True)
        except (ValueError, TypeError):
            return None

    def _inferir_tipo_coordenacao(self, coord: Dict[str, Any]) -> str:
        dados_coord = coord.get("dadosCoordenacaoArea", {}) or {}
        tipo_coord = dados_coord.get("tipo", "").lower()

        if "câmara" in tipo_coord or "camara" in tipo_coord:
            return "CAM"
        elif "adjunt" in tipo_coord:
            if "profissional" in tipo_coord or "mestrado" in tipo_coord:
                return "CAJ_MP"
            return "CAJ"
        elif "coordenador de área" in tipo_coord:
            return "CA"

        descricao = coord.get("descricao", "").lower()
        nome = coord.get("nome", "").lower()
        texto = f"{descricao} {nome}"

        if "câmara" in texto or "camara" in texto:
            return "CAM"
        elif "mestrado profissional" in texto:
            return "CAJ_MP"
        elif "adjunt" in texto:
            return "CAJ"
        elif "coordenador" in texto or "área" in texto or "area" in texto:
            return "CA"

        logger.warning(f"Tipo de coordenação desconhecido, inferindo CA: tipo='{tipo_coord}', texto='{texto[:100]}'")
        return "CA"

    def _extrair_coordenacoes_capes(self, atuacoes: List[Dict[str, Any]]) -> List[CoordenacaoCapes]:
        coordenacoes = [
            a for a in atuacoes
            if a.get("tipo") in ["Coordenação de Área de Avaliação", "Histórico de Coordenação de Área de Avaliação"]
        ]

        resultado = []
        for coord in coordenacoes:
            dados_coord = coord.get("dadosCoordenacaoArea", {}) or {}
            tipo_coord_raw = str(dados_coord.get("tipo", "") or coord.get("nome", "") or "")
            presidente = "presidente" in tipo_coord_raw.lower()
            inicio = self._parse_date(dados_coord.get("inicioVinculacao")) or self._parse_date(coord.get("inicio"))
            if not inicio:
                continue

            codigo = self._inferir_tipo_coordenacao(coord)
            fim = self._parse_date(dados_coord.get("fimVinculacao")) or self._parse_date(coord.get("fim"))

            if inicio and fim and fim < inicio:
                fim = None

            area_avaliacao_obj = dados_coord.get("areaAvaliacao", {}) or {}
            area_avaliacao = area_avaliacao_obj.get("nome") if isinstance(area_avaliacao_obj, dict) else coord.get("areaAvaliacao", "N/A")
            if not area_avaliacao:
                area_avaliacao = coord.get("descricao", "N/A").split(" - ")[0] if coord.get("descricao") else "N/A"

            resultado.append(CoordenacaoCapes(
                codigo=codigo,
                tipo=codigo,
                area_avaliacao=area_avaliacao,
                periodo=Periodo(inicio=inicio, fim=fim),
                areas_adicionais=[],
                ja_coordenou_antes=len(resultado) > 0,
                presidente=presidente,
            ))

        return resultado

    @staticmethod
    def _inferir_premiacao_tipo(texto: str) -> Optional[str]:
        """
        Retorna o tipo de premiação em forma normalizada para uso em selos/hints.
        """
        t = (texto or "").lower()
        if "grande prêmio" in t or "grande premio" in t:
            return "GP"
        if "menção" in t or "mencao" in t or "honrosa" in t:
            return "MENCAO"
        if "prêmio" in t or "premio" in t:
            return "PREMIO"
        return None

    def _tem_coordenacao_ppg(self, atuacoes: List[Dict[str, Any]]) -> bool:
        """
        Selo PPG_COORD: marca consultor que possui atuação de gestão/coordenação de programa no ATUACAPES.
        A pontuação do PPG é reservada (V1), mas o selo é exibido.
        """
        for a in atuacoes:
            if a.get("dadosGestaoPrograma"):
                return True
            tipo = str(a.get("tipo", "") or "").lower()
            if "programa" in tipo and ("coord" in tipo or "gest" in tipo):
                return True
        return False

    def _extrair_consultoria(self, atuacoes: List[Dict[str, Any]]) -> Optional[Consultoria]:
        consultorias = [a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]]
        if not consultorias:
            return None

        vistos: Dict[tuple, Dict[str, Any]] = {}
        for c in consultorias:
            dc = c.get("dadosConsultoria", {}) or {}
            ies_id = (dc.get("ies", {}) or {}).get("id")
            inicio = c.get("inicio") or dc.get("inicioVinculacao") or dc.get("inicioSituacao")
            chave = (ies_id, inicio)
            if chave not in vistos:
                vistos[chave] = c
            else:
                existente_fim = vistos[chave].get("fim")
                novo_fim = c.get("fim")
                if existente_fim and not novo_fim:
                    vistos[chave] = c
        consultorias = list(vistos.values()) if vistos else consultorias

        periodos: List[Periodo] = []
        vinculos: List[VinculoConsultoria] = []
        situacoes: List[str] = []
        areas: List[str] = []

        for c in consultorias:
            dc = c.get("dadosConsultoria", {}) or {}
            situacao = dc.get("situacaoConsultoria") or c.get("situacaoConsultoria")
            if situacao:
                situacoes.append(situacao)

            inicio = (
                self._parse_date(dc.get("inicioVinculacao"))
                or self._parse_date(dc.get("inicioSituacao"))
                or self._parse_date(c.get("inicio"))
            )
            fim = (
                self._parse_date(dc.get("fimVinculacao"))
                or self._parse_date(dc.get("inativacaoSituacao"))
                or self._parse_date(c.get("fim"))
            )

            if inicio and fim and fim < inicio:
                fim = None

            ies_data = dc.get("ies", {}) or {}
            ies = None
            if ies_data and (ies_data.get("id") or ies_data.get("nome")):
                ies = IES(
                    id=str(ies_data.get("id", "")),
                    nome=ies_data.get("nome", ""),
                    sigla=ies_data.get("sigla"),
                )

            if inicio:
                try:
                    periodo = Periodo(inicio=inicio, fim=fim)
                    periodos.append(periodo)
                    vinculos.append(VinculoConsultoria(
                        periodo=periodo,
                        ies=ies,
                        situacao=situacao or "",
                    ))
                except ValueError:
                    continue

            area = dc.get("areaAvaliacao") or c.get("areaAvaliacao")
            if area:
                areas.append(area)

        periodos = [p for p in periodos if p.inicio]
        if not periodos:
            return None

        vinculos.sort(key=lambda v: v.periodo.inicio, reverse=True)

        mesclados = mesclar_periodos(periodos)
        periodo_ativo = next((p for p in mesclados if p.ativo), None)
        anos_consecutivos = periodo_ativo.anos_completos(datetime.now()) if periodo_ativo else 0
        retornos = max(0, len(mesclados) - 1)
        ativo = any(p.ativo for p in periodos)

        situacao_final = situacoes[-1] if situacoes else "N/A"
        is_ativo = ativo or "atividade" in situacao_final.lower() or "ativo" in situacao_final.lower()
        is_falecido = "falecido" in situacao_final.lower()

        if is_falecido:
            codigo = "CONS_FALECIDO"
        elif is_ativo:
            codigo = "CONS_ATIVO"
        else:
            codigo = "CONS_HIST"

        primeiro_evento = min(p.inicio for p in periodos)
        fim_final = None
        if not ativo:
            fins = [p.fim for p in mesclados if p.fim]
            fim_final = max(fins) if fins else datetime.now()
        areas = list(set(areas)) if areas else ["N/A"]

        return Consultoria(
            codigo=codigo,
            situacao=situacao_final,
            periodo=Periodo(inicio=primeiro_evento, fim=fim_final),
            periodos=mesclados,
            vinculos=vinculos,
            areas=areas,
            anos_consecutivos=anos_consecutivos,
            retornos=retornos,
        )

    def _extrair_inscricoes(self, atuacoes: List[Dict[str, Any]]) -> List[Inscricao]:
        inscricoes = []
        for a in atuacoes:
            if a.get("tipo") != "Inscrição Prêmio":
                continue

            dados = a.get("dadosParticipacaoInscricaoPremio", {}) or {}
            tipo_part = dados.get("tipo", "")
            nome_premio = dados.get("nomePremio") or dados.get("premio") or a.get("descricao", "")
            ano = dados.get("ano")
            if not ano:
                inicio = self._parse_date(a.get("inicio"))
                ano = inicio.year if inicio else datetime.now().year

            is_institucional = "coordenador" in tipo_part.lower() or "ppg" in tipo_part.lower()
            codigo = "INSC_INST_AUTOR" if is_institucional else "INSC_AUTOR"

            inscricoes.append(Inscricao(
                codigo=codigo,
                tipo=tipo_part,
                premio=nome_premio,
                ano=ano,
                situacao=dados.get("situacao", ""),
            ))

        return inscricoes

    def _extrair_avaliacoes_comissao(self, atuacoes: List[Dict[str, Any]]) -> List[AvaliacaoComissao]:
        avaliacoes = []
        for a in atuacoes:
            if a.get("tipo") != "Avaliação Prêmio":
                continue

            dados = a.get("dadosParticipacaoPremio", {}) or {}
            tipo_part = dados.get("tipo", "")
            nome_premio = dados.get("nomePremio") or dados.get("premio") or a.get("descricao", "")
            ano = dados.get("ano")
            if not ano:
                inicio = self._parse_date(a.get("inicio"))
                ano = inicio.year if inicio else datetime.now().year

            comissao = dados.get("comissao", {}) or {}
            comissao_tipo = comissao.get("tipo", "") if isinstance(comissao, dict) else ""
            nome_comissao = comissao.get("nome", "") if isinstance(comissao, dict) else ""

            is_grande_premio = "grande" in nome_premio.lower()
            is_coordenador = "coordenador" in tipo_part.lower() or "presidente" in tipo_part.lower()

            if is_coordenador:
                codigo = "COORD_COMIS_GP" if is_grande_premio else "COORD_COMIS_PREMIO"
            else:
                codigo = "AVAL_COMIS_GP" if is_grande_premio else "AVAL_COMIS_PREMIO"

            avaliacoes.append(AvaliacaoComissao(
                codigo=codigo,
                tipo=tipo_part,
                premio=nome_premio,
                ano=ano,
                comissao_tipo=comissao_tipo,
                nome_comissao=nome_comissao,
            ))

        return avaliacoes

    def _extrair_premiacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Premiacao]:
        premiacoes = []
        for a in atuacoes:
            if a.get("tipo") != "Premiação Prêmio":
                continue

            dados = a.get("dadosPremiacaoPremio", {}) or a.get("dadosPremio", {}) or {}
            tipo_premiacao = dados.get("tipoPremiacao") or dados.get("premiacao") or ""
            nome_premio = dados.get("nomePremio") or dados.get("evento") or a.get("descricao", "")
            papel = dados.get("papelPessoa") or dados.get("papel") or None
            ano = dados.get("ano")
            if not ano:
                inicio = self._parse_date(a.get("inicio"))
                ano = inicio.year if inicio else datetime.now().year

            tipo_lower = str(tipo_premiacao).lower()
            nome_lower = str(nome_premio).lower()
            tipo_norm = self._inferir_premiacao_tipo(f"{tipo_lower} {nome_lower}")

            if tipo_norm == "GP":
                codigo = "PREMIACAO_GP_AUTOR"
            elif tipo_norm == "MENCAO":
                codigo = "MENCAO_AUTOR"
            else:
                codigo = "PREMIACAO_AUTOR"

            premiacoes.append(Premiacao(
                codigo=codigo,
                tipo=tipo_premiacao,
                nome_premio=nome_premio,
                ano=ano,
                papel=papel,
            ))

        return premiacoes

    def _extrair_bolsas_cnpq(self, atuacoes: List[Dict[str, Any]]) -> List[BolsaCNPQ]:
        bolsas = []
        for a in atuacoes:
            if a.get("tipo") != "Bolsa CNPQ" and "bolsa" not in a.get("tipo", "").lower():
                continue

            dados = a.get("dadosBolsa", {}) or {}
            nivel = dados.get("nivel", "") or dados.get("categoria", "") or ""
            area = dados.get("areaConhecimento", "") or ""

            bolsas.append(BolsaCNPQ(
                codigo="BOL_BPQ_NIVEL",
                nivel=nivel,
                area=area,
            ))

        return bolsas

    def _extrair_participacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Participacao]:
        participacoes = []

        for a in atuacoes:
            tipo = a.get("tipo", "")

            if tipo == "Evento":
                participacoes.append(Participacao(
                    codigo="EVENTO",
                    tipo="Evento",
                    descricao=a.get("descricao", ""),
                    ano=self._parse_date(a.get("inicio")).year if self._parse_date(a.get("inicio")) else None,
                ))
            elif tipo == "Projeto" or "projeto" in tipo.lower():
                participacoes.append(Participacao(
                    codigo="PROJ",
                    tipo="Projeto",
                    descricao=a.get("descricao", ""),
                    ano=self._parse_date(a.get("inicio")).year if self._parse_date(a.get("inicio")) else None,
                ))

        return participacoes

    def _extrair_orientacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Orientacao]:
        orientacoes = []

        for a in atuacoes:
            tipo = a.get("tipo", "")
            tipo_lower = tipo.lower()

            if tipo == "Orientação de Discentes":
                dados = a.get("dadosOrientacaoDiscente", {}) or {}
                total_mestrado = dados.get("totalOrientacaoFinalizadaMestrado") or 0
                total_doutorado = dados.get("totalOrientacaoFinalizadaDoutorado") or 0
                total_pos_doc = dados.get("totalAcompanhamentoPosDoutorado") or 0

                for _ in range(int(total_pos_doc)):
                    orientacoes.append(Orientacao(
                        codigo="ORIENT_POS_DOC",
                        tipo="Orientação Pós-Doutorado",
                        nivel="Pós-Doutorado",
                        ano=None,
                        coorientacao=False,
                        premiada=False,
                        premiacao_tipo=None,
                    ))

                for _ in range(int(total_doutorado)):
                    orientacoes.append(Orientacao(
                        codigo="ORIENT_TESE",
                        tipo="Orientação Doutorado",
                        nivel="Doutorado",
                        ano=None,
                        coorientacao=False,
                        premiada=False,
                        premiacao_tipo=None,
                    ))

                for _ in range(int(total_mestrado)):
                    orientacoes.append(Orientacao(
                        codigo="ORIENT_DISS",
                        tipo="Orientação Mestrado",
                        nivel="Mestrado",
                        ano=None,
                        coorientacao=False,
                        premiada=False,
                        premiacao_tipo=None,
                    ))
                continue

            if "orientação" not in tipo_lower and "orientacao" not in tipo_lower:
                continue
            if "co-orientação" in tipo_lower or "coorientação" in tipo_lower or "co_orient" in tipo_lower:
                continue

            dados = a.get("dadosOrientacao", {}) or {}
            nivel = dados.get("nivel", "") or dados.get("tipo", "") or ""
            premio_texto = (
                dados.get("premiacao")
                or dados.get("tipoPremiacao")
                or dados.get("premio")
                or dados.get("resultado")
                or ""
            )
            tipo_prem = self._inferir_premiacao_tipo(str(premio_texto))
            premiada = tipo_prem is not None
            ano = dados.get("ano")
            if not ano:
                inicio = self._parse_date(a.get("inicio"))
                ano = inicio.year if inicio else None

            nivel_lower = nivel.lower()
            if "pós-doc" in nivel_lower or "pos-doc" in nivel_lower or "posdoc" in nivel_lower:
                codigo = "ORIENT_POS_DOC"
            elif "tese" in nivel_lower or "doutorado" in nivel_lower:
                codigo = "ORIENT_TESE"
            else:
                codigo = "ORIENT_DISS"

            orientacoes.append(Orientacao(
                codigo=codigo,
                tipo=tipo,
                nivel=nivel,
                ano=ano,
                coorientacao=False,
                premiada=premiada,
                premiacao_tipo=tipo_prem,
            ))

        return orientacoes

    def _extrair_coorientacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Orientacao]:
        coorientacoes = []

        for a in atuacoes:
            tipo = a.get("tipo", "").lower()
            if "co-orientação" not in tipo and "coorientação" not in tipo and "co_orient" not in tipo:
                continue

            dados = a.get("dadosOrientacao", {}) or a.get("dadosCoorientacao", {}) or {}
            nivel = dados.get("nivel", "") or dados.get("tipo", "") or ""
            premio_texto = (
                dados.get("premiacao")
                or dados.get("tipoPremiacao")
                or dados.get("premio")
                or dados.get("resultado")
                or ""
            )
            tipo_prem = self._inferir_premiacao_tipo(str(premio_texto))
            premiada = tipo_prem is not None
            ano = dados.get("ano")
            if not ano:
                inicio = self._parse_date(a.get("inicio"))
                ano = inicio.year if inicio else None

            nivel_lower = nivel.lower()
            if "pós-doc" in nivel_lower or "pos-doc" in nivel_lower or "posdoc" in nivel_lower:
                codigo = "CO_ORIENT_POS_DOC"
            elif "tese" in nivel_lower or "doutorado" in nivel_lower:
                codigo = "CO_ORIENT_TESE"
            else:
                codigo = "CO_ORIENT_DISS"

            coorientacoes.append(Orientacao(
                codigo=codigo,
                tipo=tipo,
                nivel=nivel,
                ano=ano,
                coorientacao=True,
                premiada=premiada,
                premiacao_tipo=tipo_prem,
            ))

        return coorientacoes

    def _extrair_membros_banca(self, atuacoes: List[Dict[str, Any]]) -> List[MembroBanca]:
        membros = []

        for a in atuacoes:
            tipo = a.get("tipo", "").lower()
            if "banca" not in tipo:
                continue

            dados = a.get("dadosBanca", {}) or {}
            nivel = dados.get("nivel", "") or dados.get("tipo", "") or ""
            ano = dados.get("ano")
            if not ano:
                inicio = self._parse_date(a.get("inicio"))
                ano = inicio.year if inicio else None

            nivel_lower = nivel.lower()
            if "pós-doc" in nivel_lower or "pos-doc" in nivel_lower or "posdoc" in nivel_lower:
                codigo = "MB_BANCA_POS_DOC"
            elif "tese" in nivel_lower or "doutorado" in nivel_lower:
                codigo = "MB_BANCA_TESE"
            else:
                codigo = "MB_BANCA_DISS"

            membros.append(MembroBanca(
                codigo=codigo,
                tipo=tipo,
                nivel=nivel,
                ano=ano,
            ))

        return membros

    async def _construir_consultor(self, doc: Dict[str, Any]) -> Consultor:
        id_pessoa = doc["id"]
        dados_pessoais = doc.get("dadosPessoais", {})
        atuacoes = doc.get("atuacoes", [])

        coordenacoes_capes = self._extrair_coordenacoes_capes(atuacoes)
        consultoria = self._extrair_consultoria(atuacoes)
        inscricoes = self._extrair_inscricoes(atuacoes)
        avaliacoes = self._extrair_avaliacoes_comissao(atuacoes)
        premiacoes = self._extrair_premiacoes(atuacoes)
        bolsas = self._extrair_bolsas_cnpq(atuacoes)
        participacoes = self._extrair_participacoes(atuacoes)
        orientacoes = self._extrair_orientacoes(atuacoes)
        coorientacoes = self._extrair_coorientacoes(atuacoes)
        membros_banca = self._extrair_membros_banca(atuacoes)
        coordenador_ppg = self._tem_coordenacao_ppg(atuacoes)

        consultor = Consultor(
            id_pessoa=id_pessoa,
            nome=dados_pessoais.get("nome", "N/A"),
            cpf=dados_pessoais.get("cpf"),
            coordenador_ppg=coordenador_ppg,
            coordenacoes_capes=coordenacoes_capes,
            consultoria=consultoria,
            inscricoes=inscricoes,
            avaliacoes_comissao=avaliacoes,
            premiacoes=premiacoes,
            bolsas_cnpq=bolsas,
            participacoes=participacoes,
            orientacoes=orientacoes + coorientacoes,
            membros_banca=membros_banca,
        )

        consultor.pontuacao = self.calculador.calcular_pontuacao_completa(consultor)

        return consultor

    async def buscar_por_id(self, id_pessoa: int) -> Optional[Consultor]:
        try:
            doc = await self.es_client.buscar_por_id(id_pessoa)
        except Exception as e:
            logger.warning(f"Elasticsearch: falha ao buscar consultor {id_pessoa}: {e}")
            return None
        if not doc:
            return None
        return await self._construir_consultor(doc)

    async def buscar_todos(
        self, limite: int = 100, offset: int = 0, filtro_ativo: Optional[bool] = None
    ) -> List[Consultor]:
        if not self.es_client._client or getattr(self.es_client._client, "is_closed", False):
            self.es_disponivel = False
            return []
        try:
            docs = await self.es_client.buscar_com_atuacoes(size=limite, from_=offset)
            self.es_disponivel = True
        except Exception as e:
            logger.warning(f"Elasticsearch: falha ao buscar consultores: {e}")
            self.es_disponivel = False
            return []
        consultores = [await self._construir_consultor(doc) for doc in docs]

        if filtro_ativo is not None:
            consultores = [c for c in consultores if c.ativo == filtro_ativo]

        return consultores

    async def buscar_ranking(
        self, limite: int = 100, componente: Optional[str] = None
    ) -> List[Consultor]:
        if ranking_cache.is_valid():
            consultores_ordenados = ranking_cache.get()
            return consultores_ordenados[:limite]

        async with ranking_cache._lock:
            if ranking_cache.is_valid():
                return ranking_cache.get()[:limite]

            tamanho_busca = max(limite * 3, 1000)
            docs = await self.es_client.buscar_candidatos_ranking(size=tamanho_busca)

            consultores = []
            for doc in docs:
                consultor = await self._construir_consultor(doc)
                consultores.append(consultor)

            consultores_ordenados = sorted(
                consultores, key=lambda c: c.pontuacao_total, reverse=True
            )
            ranking_cache.set(consultores_ordenados)

        return consultores_ordenados[:limite]

    async def contar_total(self, filtro_ativo: Optional[bool] = None) -> int:
        if not self.es_disponivel:
            return 0
        if not self.es_client._client or getattr(self.es_client._client, "is_closed", False):
            self.es_disponivel = False
            return 0
        try:
            return await self.es_client.contar_com_atuacoes()
        except Exception as e:
            logger.warning(f"Elasticsearch: falha ao contar consultores: {e}")
            self.es_disponivel = False
            return 0
