import httpx
from typing import Optional, Dict, Any


class ElasticsearchClient:
    def __init__(
        self,
        url: str,
        index: str,
        user: str = "",
        password: str = "",
        verify_ssl: bool = True,
    ):
        self.url = url.rstrip("/")
        self.index = index
        self.user = user
        self.password = password
        self.verify_ssl = verify_ssl
        self._client: Optional[httpx.AsyncClient] = None

    async def connect(self) -> None:
        auth = None
        if self.user and self.password:
            auth = httpx.BasicAuth(self.user, self.password)

        self._client = httpx.AsyncClient(
            auth=auth,
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json"
            },
            verify=self.verify_ssl,
            timeout=httpx.Timeout(
                connect=10.0,
                read=60.0,
                write=30.0,
                pool=10.0,
            )
        )

    async def close(self) -> None:
        if self._client:
            await self._client.aclose()

    @property
    def client(self) -> httpx.AsyncClient:
        if not self._client or self._client.is_closed:
            raise RuntimeError("Cliente Elasticsearch não conectado ou fechado. Execute connect() primeiro.")
        return self._client

    async def buscar_por_id(self, id_pessoa: int) -> Optional[dict]:
        try:
            query = {
                "query": {"term": {"id": id_pessoa}},
                "_source": ["id", "dadosPessoais", "atuacoes"],
                "size": 1,
            }

            response = await self.client.post(
                f"{self.url}/{self.index}/_search",
                json=query
            )
            response.raise_for_status()

            data = response.json()
            hits = data.get("hits", {}).get("hits", [])
            return hits[0]["_source"] if hits else None
        except Exception as e:
            raise RuntimeError(f"Erro ao buscar consultor {id_pessoa}: {e}")

    async def buscar_documento_completo(self, id_pessoa: int) -> Optional[dict]:
        try:
            query = {
                "query": {"term": {"id": id_pessoa}},
                "size": 1,
            }

            response = await self.client.post(
                f"{self.url}/{self.index}/_search",
                json=query
            )
            response.raise_for_status()

            data = response.json()
            hits = data.get("hits", {}).get("hits", [])
            if hits:
                hit = hits[0]
                return {
                    "_index": hit.get("_index"),
                    "_id": hit.get("_id"),
                    "_score": hit.get("_score"),
                    "_source": hit.get("_source"),
                }
            return None
        except Exception as e:
            raise RuntimeError(f"Erro ao buscar documento completo {id_pessoa}: {e}")

    async def buscar_com_atuacoes(self, size: int = 1000, from_: int = 0) -> list:
        try:
            query = {
                "query": {
                    "nested": {
                        "path": "atuacoes",
                        "query": {"exists": {"field": "atuacoes.tipo"}}
                    }
                },
                "_source": ["id", "dadosPessoais", "atuacoes"],
                "size": size,
                "from": from_,
                "sort": [{"id": "asc"}],
            }

            response = await self.client.post(
                f"{self.url}/{self.index}/_search",
                json=query
            )
            response.raise_for_status()

            data = response.json()
            return [hit["_source"] for hit in data.get("hits", {}).get("hits", [])]
        except Exception as e:
            raise RuntimeError(f"Erro ao buscar consultores: {e}")

    async def contar_com_atuacoes(self) -> int:
        try:
            query = {
                "query": {
                    "nested": {
                        "path": "atuacoes",
                        "query": {"exists": {"field": "atuacoes.tipo"}}
                    }
                }
            }

            response = await self.client.post(
                f"{self.url}/{self.index}/_count",
                json=query
            )
            response.raise_for_status()

            data = response.json()
            return data.get("count", 0)
        except Exception as e:
            raise RuntimeError(f"Erro ao contar consultores: {e}")

    async def buscar_candidatos_ranking(self, size: int = 100) -> list:
        query = {
            "size": size,
            "query": {
                "bool": {
                    "should": [
                        {
                            "nested": {
                                "path": "atuacoes",
                                "query": {
                                    "bool": {
                                        "should": [
                                            {"term": {"atuacoes.tipo": {"value": "Coordenação de Área de Avaliação", "boost": 10}}},
                                            {"term": {"atuacoes.tipo": {"value": "Histórico de Coordenação de Área de Avaliação", "boost": 5}}}
                                        ]
                                    }
                                },
                                "score_mode": "sum"
                            }
                        },
                        {
                            "nested": {
                                "path": "atuacoes",
                                "query": {
                                    "bool": {
                                        "should": [
                                            {"term": {"atuacoes.tipo": {"value": "Consultor", "boost": 2}}},
                                            {"term": {"atuacoes.tipo": {"value": "Histórico de Consultoria", "boost": 1}}}
                                        ]
                                    }
                                },
                                "score_mode": "sum"
                            }
                        },
                        {
                            "nested": {
                                "path": "atuacoes",
                                "query": {
                                    "bool": {
                                        "should": [
                                            {"term": {"atuacoes.tipo": {"value": "Premiação Prêmio", "boost": 3}}},
                                            {"term": {"atuacoes.tipo": {"value": "Avaliação Prêmio", "boost": 2}}},
                                            {"term": {"atuacoes.tipo": {"value": "Inscrição Prêmio", "boost": 1}}}
                                        ]
                                    }
                                },
                                "score_mode": "sum"
                            }
                        }
                    ],
                    "minimum_should_match": 1
                }
            },
            "_source": ["id", "dadosPessoais", "atuacoes"],
            "sort": [{"_score": "desc"}]
        }

        try:
            response = await self.client.post(
                f"{self.url}/{self.index}/_search",
                json=query,
                timeout=120.0
            )
            response.raise_for_status()

            data = response.json()
            results = []
            for hit in data.get("hits", {}).get("hits", []):
                doc = hit["_source"]
                doc["_score_es"] = hit.get("_score", 0)
                results.append(doc)
            return results
        except Exception as e:
            raise RuntimeError(f"Erro ao buscar candidatos ranking: {e}")

    async def buscar_ranking_com_score_old(self, size: int = 100) -> list:
        painless_script = """
            double score = 0;
            long now = System.currentTimeMillis();
            long doisAnos = 730L * 24 * 60 * 60 * 1000;

            // Componente A - Coordenação CAPES
            double compA = 0;
            for (def a : params._source.atuacoes) {
                String tipo = a.tipo;
                if (tipo == null) continue;

                if (tipo.contains('Coordenação de Área')) {
                    String inicio = a.inicio;
                    String fim = a.fim;
                    if (inicio == null) continue;

                    boolean ativo = (fim == null || fim.length() == 0);
                    if (!ativo) continue;

                    // Parse data inicio (formato DD/MM/YYYY ou YYYY-MM-DD)
                    long inicioMs = 0;
                    try {
                        if (inicio.contains('/')) {
                            String[] parts = inicio.substring(0, 10).split('/');
                            int dia = Integer.parseInt(parts[0]);
                            int mes = Integer.parseInt(parts[1]);
                            int ano = Integer.parseInt(parts[2]);
                            inicioMs = (ano - 1970) * 365L * 24 * 60 * 60 * 1000;
                        }
                    } catch (Exception e) {}

                    double anos = (now - inicioMs) / (365.25 * 24 * 60 * 60 * 1000);

                    // Determinar tipo de coordenação
                    double base = 100;
                    double tempoMax = 50;
                    double bonusAtual = 10;

                    def dados = a.dadosCoordenacaoArea;
                    if (dados == null) dados = a.dadosHistoricoCoordenacaoArea;
                    if (dados != null && dados.tipo != null) {
                        String tipoCoord = dados.tipo.toLowerCase();
                        if (tipoCoord.contains('coordenador') && !tipoCoord.contains('adjunt')) {
                            base = 200; tempoMax = 100; bonusAtual = 30;
                        } else if (tipoCoord.contains('adjunt')) {
                            if (tipoCoord.contains('profissional')) {
                                base = 120; tempoMax = 60; bonusAtual = 15;
                            } else {
                                base = 150; tempoMax = 80; bonusAtual = 20;
                            }
                        } else if (tipoCoord.contains('presidente') || tipoCoord.contains('camara')) {
                            base = 100; tempoMax = 50; bonusAtual = 10;
                        }
                    }

                    double tempo = Math.min(anos * 10, tempoMax);
                    double thisCompA = base + tempo + bonusAtual;
                    if (thisCompA > compA) compA = thisCompA;
                }
            }
            score += Math.min(compA, 450);

            // Componente C - Consultoria
            double compC = 0;
            int totalEventos = 0;
            int eventosRecentes = 0;
            int vezesResponsavel = 0;
            long primeiroEvento = now;
            def areasConsultoria = new HashSet();

            for (def a : params._source.atuacoes) {
                String tipo = a.tipo;
                if (tipo == null) continue;

                if (tipo == 'Consultor' || tipo == 'Histórico de Consultoria') {
                    totalEventos++;

                    String inicio = a.inicio;
                    if (inicio != null && inicio.length() >= 10) {
                        try {
                            String[] parts = inicio.substring(0, 10).split('/');
                            if (parts.length == 3) {
                                int ano = Integer.parseInt(parts[2]);
                                long inicioMs = (ano - 1970) * 365L * 24 * 60 * 60 * 1000;
                                if (inicioMs < primeiroEvento) primeiroEvento = inicioMs;
                            }
                        } catch (Exception e) {}
                    }

                    String fim = a.fim;
                    if (fim != null && fim.length() >= 10) {
                        try {
                            String[] parts = fim.substring(0, 10).split('/');
                            if (parts.length == 3) {
                                int ano = Integer.parseInt(parts[2]);
                                long fimMs = (ano - 1970) * 365L * 24 * 60 * 60 * 1000;
                                if (now - fimMs < doisAnos) eventosRecentes++;
                            }
                        } catch (Exception e) {}
                    }

                    def dados = a.dadosConsultoria;
                    if (dados != null) {
                        if (dados.responsavel == true) vezesResponsavel++;
                        if (dados.areaAvaliacao != null) areasConsultoria.add(dados.areaAvaliacao);
                    }
                }
            }

            if (totalEventos > 0) {
                double base = (eventosRecentes > 0) ? 150 : 100;
                double anosConsultoria = (now - primeiroEvento) / (365.25 * 24 * 60 * 60 * 1000);
                double tempo = Math.min(anosConsultoria * 5, 50);
                double extrasEventos = Math.min(totalEventos * 2, 20);
                double extrasResp = Math.min(vezesResponsavel * 5, 25);
                double extrasAreas = (areasConsultoria.size() > 1) ? Math.min((areasConsultoria.size() - 1) * 10, 30) : 0;
                compC = base + tempo + extrasEventos + extrasResp + extrasAreas;
            }
            score += Math.min(compC, 230);

            // Componente D - Premiações
            double compD = 0;
            for (def a : params._source.atuacoes) {
                String tipo = a.tipo;
                if (tipo == null) continue;

                if (tipo.contains('Prêmio') || tipo.contains('Premio')) {
                    if (tipo.contains('Premiação')) {
                        compD += 60;
                    } else if (tipo.contains('Avaliação')) {
                        compD += 40;
                    } else if (tipo.contains('Inscrição')) {
                        compD += 20;
                    }
                }
            }
            score += Math.min(compD, 180);

            return score;
        """

        query = {
            "size": size,
            "query": {
                "function_score": {
                    "query": {
                        "nested": {
                            "path": "atuacoes",
                            "query": {"exists": {"field": "atuacoes.tipo"}}
                        }
                    },
                    "script_score": {
                        "script": {
                            "source": painless_script,
                            "lang": "painless"
                        }
                    },
                    "boost_mode": "replace"
                }
            },
            "_source": ["id", "dadosPessoais", "atuacoes"],
            "sort": [{"_score": "desc"}]
        }

        try:
            response = await self.client.post(
                f"{self.url}/{self.index}/_search",
                json=query
            )
            response.raise_for_status()

            data = response.json()
            results = []
            for hit in data.get("hits", {}).get("hits", []):
                doc = hit["_source"]
                doc["_score_es"] = hit.get("_score", 0)
                results.append(doc)
            return results
        except Exception as e:
            raise RuntimeError(f"Erro ao buscar ranking com score: {e}")

    async def iniciar_scroll(self, size: int = 1000, scroll_timeout: str = "5m") -> dict:
        """
        Inicia um scroll para iterar por todos os documentos com atuações.
        Retorna o scroll_id e a primeira página de resultados.
        """
        query = {
            "query": {
                "nested": {
                    "path": "atuacoes",
                    "query": {"exists": {"field": "atuacoes.tipo"}}
                }
            },
            "_source": ["id", "dadosPessoais", "atuacoes"],
            "size": size,
            "sort": [{"id": "asc"}]
        }

        try:
            response = await self.client.post(
                f"{self.url}/{self.index}/_search?scroll={scroll_timeout}",
                json=query,
                timeout=120.0
            )
            response.raise_for_status()

            data = response.json()
            return {
                "scroll_id": data.get("_scroll_id"),
                "hits": [hit["_source"] for hit in data.get("hits", {}).get("hits", [])],
                "total": data.get("hits", {}).get("total", {}).get("value", 0)
            }
        except Exception as e:
            raise RuntimeError(f"Erro ao iniciar scroll: {e}")

    async def continuar_scroll(self, scroll_id: str, scroll_timeout: str = "5m") -> dict:
        """
        Continua um scroll existente usando o scroll_id.
        Retorna a próxima página de resultados.
        """
        try:
            response = await self.client.post(
                f"{self.url}/_search/scroll",
                json={
                    "scroll": scroll_timeout,
                    "scroll_id": scroll_id
                },
                timeout=120.0
            )
            response.raise_for_status()

            data = response.json()
            return {
                "scroll_id": data.get("_scroll_id"),
                "hits": [hit["_source"] for hit in data.get("hits", {}).get("hits", [])]
            }
        except Exception as e:
            raise RuntimeError(f"Erro ao continuar scroll: {e}")

    async def limpar_scroll(self, scroll_id: str) -> None:
        """
        Limpa o contexto de scroll após uso.
        """
        try:
            await self.client.delete(
                f"{self.url}/_search/scroll",
                json={"scroll_id": scroll_id},
                timeout=30.0
            )
        except Exception:
            pass

    async def buscar_todos_consultores(self, callback, batch_size: int = 1000):
        """
        Itera por TODOS os consultores do índice usando Scroll API.
        Chama callback(batch, progress) para cada batch de documentos.

        Args:
            callback: função assíncrona que recebe (docs: list, progress: dict)
            batch_size: tamanho do batch (padrão 1000)

        Progress dict contém:
            - total: total de documentos
            - processados: documentos processados até agora
            - batch_atual: número do batch atual
            - percentual: percentual de progresso (0-100)
        """
        scroll_id = None
        try:
            result = await self.iniciar_scroll(size=batch_size)
            scroll_id = result["scroll_id"]
            total = result["total"]
            hits = result["hits"]

            processados = 0
            batch_atual = 1

            while hits:
                processados += len(hits)
                percentual = int((processados / total) * 100) if total > 0 else 100

                progress = {
                    "total": total,
                    "processados": processados,
                    "batch_atual": batch_atual,
                    "percentual": percentual
                }

                await callback(hits, progress)

                if len(hits) < batch_size:
                    break

                result = await self.continuar_scroll(scroll_id)
                scroll_id = result["scroll_id"]
                hits = result["hits"]
                batch_atual += 1

            return {
                "total": total,
                "processados": processados,
                "batches": batch_atual
            }
        finally:
            if scroll_id:
                await self.limpar_scroll(scroll_id)

    async def sugerir_consultores(
        self,
        tema: str,
        area_avaliacao: Optional[str] = None,
        apenas_ativos: bool = True,
        size: int = 20
    ) -> list:
        must_conditions = []
        should_conditions = []

        must_conditions.append({
            "nested": {
                "path": "atuacoes",
                "query": {
                    "bool": {
                        "must": [
                            {"term": {"atuacoes.tipo": "Consultor"}}
                        ],
                        "should": [
                            {"match": {"atuacoes.dadosConsultoria.areaConhecimentoPos.nome": {"query": tema, "boost": 3}}},
                            {"match": {"atuacoes.dadosConsultoria.areaConhecimentoPos.areaAvaliacao.nome": {"query": tema, "boost": 5}}},
                            {"match": {"atuacoes.dadosConsultoria.areaPesquisa.descricao": {"query": tema, "boost": 2}}},
                            {"match": {"atuacoes.dadosConsultoria.areaConhecimentoPos.especialidade": {"query": tema, "boost": 2}}}
                        ],
                        "minimum_should_match": 1
                    }
                },
                "score_mode": "max"
            }
        })

        should_conditions.extend([
            {
                "nested": {
                    "path": "atuacoes",
                    "query": {
                        "bool": {
                            "should": [
                                {"term": {"atuacoes.tipo": {"value": "Coordenação de Área de Avaliação", "boost": 3}}},
                                {"term": {"atuacoes.tipo": {"value": "Histórico de Coordenação de Área de Avaliação", "boost": 1}}}
                            ]
                        }
                    },
                    "score_mode": "sum"
                }
            },
            {
                "nested": {
                    "path": "atuacoes",
                    "query": {
                        "bool": {
                            "should": [
                                {"term": {"atuacoes.tipo": {"value": "Premiação Prêmio", "boost": 2}}},
                                {"term": {"atuacoes.tipo": {"value": "Avaliação Prêmio", "boost": 1}}}
                            ]
                        }
                    },
                    "score_mode": "sum"
                }
            }
        ])

        if area_avaliacao:
            must_conditions.append({
                "nested": {
                    "path": "atuacoes",
                    "query": {
                        "bool": {
                            "must": [
                                {"term": {"atuacoes.tipo": "Consultor"}},
                                {"match": {"atuacoes.dadosConsultoria.areaConhecimentoPos.areaAvaliacao.nome": area_avaliacao}}
                            ]
                        }
                    }
                }
            })

        if apenas_ativos:
            must_conditions.append({
                "nested": {
                    "path": "atuacoes",
                    "query": {
                        "bool": {
                            "must": [
                                {"term": {"atuacoes.tipo": "Consultor"}},
                                {
                                    "bool": {
                                        "should": [
                                            {"match_phrase": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Atividade Contínua"}},
                                            {"term": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Ativo"}}
                                        ],
                                        "minimum_should_match": 1
                                    }
                                }
                            ],
                            "must_not": [
                                {"match_phrase": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Inatividade"}}
                            ]
                        }
                    }
                }
            })

        query = {
            "size": size,
            "query": {
                "bool": {
                    "must": must_conditions if must_conditions else [{"match_all": {}}],
                    "should": should_conditions,
                    "minimum_should_match": 1
                }
            },
            "_source": ["id", "dadosPessoais", "atuacoes"],
            "sort": [{"_score": "desc"}]
        }

        try:
            response = await self.client.post(
                f"{self.url}/{self.index}/_search",
                json=query,
                timeout=60.0
            )
            response.raise_for_status()

            data = response.json()
            results = []
            for hit in data.get("hits", {}).get("hits", []):
                doc = hit["_source"]
                doc["_score_match"] = hit.get("_score", 0)
                results.append(doc)
            return results
        except Exception as e:
            raise RuntimeError(f"Erro ao sugerir consultores: {e}")

    async def listar_areas_avaliacao(self) -> list:
        areas_conhecidas = [
            "ADMINISTRAÇÃO PÚBLICA E DE EMPRESAS, CIÊNCIAS CONTÁBEIS E TURISMO",
            "ANTROPOLOGIA E ARQUEOLOGIA",
            "ARQUITETURA, URBANISMO E DESIGN",
            "ARTES",
            "ASTRONOMIA E FÍSICA",
            "BIODIVERSIDADE",
            "BIOTECNOLOGIA",
            "CIÊNCIA DA COMPUTAÇÃO",
            "CIÊNCIA DE ALIMENTOS",
            "CIÊNCIA POLÍTICA E RELAÇÕES INTERNACIONAIS",
            "CIÊNCIAS AGRÁRIAS I",
            "CIÊNCIAS AMBIENTAIS",
            "CIÊNCIAS BIOLÓGICAS I",
            "CIÊNCIAS BIOLÓGICAS II",
            "CIÊNCIAS BIOLÓGICAS III",
            "CIÊNCIAS DA RELIGIÃO E TEOLOGIA",
            "COMUNICAÇÃO E INFORMAÇÃO E MUSEOLOGIA",
            "DIREITO",
            "ECONOMIA",
            "EDUCAÇÃO",
            "EDUCAÇÃO FÍSICA",
            "ENFERMAGEM",
            "ENGENHARIAS I",
            "ENGENHARIAS II",
            "ENGENHARIAS III",
            "ENGENHARIAS IV",
            "ENSINO",
            "FARMÁCIA",
            "FILOSOFIA",
            "GEOGRAFIA",
            "GEOCIÊNCIAS",
            "HISTÓRIA",
            "INTERDISCIPLINAR",
            "LETRAS E LINGUÍSTICA",
            "MATEMÁTICA E ESTATÍSTICA",
            "MATERIAIS",
            "MEDICINA I",
            "MEDICINA II",
            "MEDICINA III",
            "MEDICINA VETERINÁRIA",
            "NUTRIÇÃO",
            "ODONTOLOGIA",
            "PLANEJAMENTO URBANO E REGIONAL E DEMOGRAFIA",
            "PSICOLOGIA",
            "QUÍMICA",
            "SAÚDE COLETIVA",
            "SERVIÇO SOCIAL",
            "SOCIOLOGIA",
            "ZOOTECNIA E RECURSOS PESQUEIROS"
        ]
        return [{"nome": area, "count": 0} for area in areas_conhecidas]
