"""Comparative orchestration service for the canonical backend route."""

from __future__ import annotations

import asyncio
import json
import logging
from typing import Any

from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel
from openai.lib._pydantic import to_strict_json_schema

from app.services.ai.ai_gateway import create_ai_gateway
from app.services.analysis.comparative_retriever import (
    ComparativeRetriever,
    ComparativeRetrieverRequest,
)
from app.services.analysis.comparative_schemas import (
    AnalisisKomparatifResponse,
    AnalisisPoin,
    DivergensiResponse,
    KeterbatasanResponse,
    KonvergensiResponse,
    KritikResponse,
    PoinKritik,
    SintesisMakroResponse,
)

logger = logging.getLogger(__name__)

DEFAULT_COMPARATIVE_ASPECTS = (
    "background",
    "methodology",
    "results",
)
DEFAULT_TOP_K = 5
MIN_TOP_K = 1
MAX_TOP_K = 50
DEFAULT_SIMILARITY_THRESHOLD = -1.0
MIN_SIMILARITY_THRESHOLD = -1.0
MAX_SIMILARITY_THRESHOLD = 1.0

# ---------------------------------------------------------------------------
# AGENT PROMPTS: 5 Specialized Agents for Map-Reduce Comparative Synthesis
# ---------------------------------------------------------------------------

_PROMPT_KONVERGENSI = """[ROLE]
Anda adalah "Senior Epistemologist". Fokus tunggal Anda adalah mengekstrak poin KESAMAAN, KONSENSUS, dan KESELARASAN antar dokumen pada aspek: {aspect}.

[MANDATORY RULES]
1. ZERO TOLERANCE FOR XML: Dilarang keras menuliskan tag <dokumen id="...">. Sebutkan judul artikel secara natural (misal: "Penelitian oleh [Judul] sejalan dengan...").
2. NO DIVERGENCE: Jangan bahas perbedaan di sini. Jika tidak ada kesamaan, kembalikan array kosong.
3. THE "WHY" FACTOR: Jelaskan alasan epistemologis di balik kesamaan tersebut (5 kalimat). Apakah mereka menggunakan standar emas yang sama? Apakah asumsi teoritis mereka identik?
4. MULTI-SOURCE EVIDENCE: Untuk setiap poin, Anda WAJIB memberikan bukti dari SETIAP dokumen yang terlibat.
   - Field 'kutipan_verbatim' HARUS berisi kutipan teks asli yang mendukung poin tersebut, BUKAN hanya judul artikel saja.

[INSTRUCTION]
Gunakan data dokumen di bawah untuk menyusun poin KONVERGENSI pada aspek {aspect}. Jangan berikan jawaban generik seperti "Kedua penelitian". Sebutkan judul spesifik setiap saat.

[EXPECTED JSON SCHEMA]
{json_schema_string}

[INPUT DATA]
- Relation Context: {relation_desc}
- Articles Content: {articles_text}"""

_PROMPT_DIVERGENSI = """[ROLE]
Anda adalah "Academic Dialectician". Fokus tunggal Anda adalah mengekstrak poin PERBEDAAN dan KONTRADIKSI hasil antar dokumen pada aspek: {aspect}.

[MANDATORY RULES]
1. HIGHLIGHT THE FRICTION: Jangan melembutkan bahasa. Jika hasilnya bertolak belakang, katakan secara tajam mengapa itu terjadi (perbedaan instrumen, sampling, atau lokus penelitian). 
2. NO CONVERGENCE: Dilarang membahas kesamaan di sini. Jika tidak ada perbedaan, kembalikan array kosong.
3. VERBATIM PROOF: Field 'kutipan_verbatim' merupakan kutipan teks asli dari teks dokumen sebagai bukti kontradiksi. Dilarang hanya menulis judul artikel.
4. ANALYTICAL DEPTH: Jelaskan implikasi dari perbedaan ini terhadap validitas teori (5 kalimat).
5. NO XML TAGS: Gunakan judul asli secara natural tanpa atribut tag <dokumen id="...">.

[INSTRUCTION]
Ekstrak poin DIVERGENSI pada aspek {aspect}. Jika dokumen memiliki temuan atau metode yang saling meniadakan, bedah secara mendalam.

[EXPECTED JSON SCHEMA]
{json_schema_string}

[INPUT DATA]
- Relation Context: {relation_desc}
- Articles Content: {articles_text}"""

_PROMPT_KRITIK = """[ROLE]
Anda adalah "Objective Academic Auditor". Fokus tunggal Anda adalah melakukan audit terhadap LIMITASI dan CELAH METODOLOGIS pada aspek: {aspect}.

[MANDATORY RULES]
1. ATOMIC AUDIT: Satu entri kritik HANYA untuk SATU dokumen. Dilarang mencampur kritik.
2. SURGICAL STRIKE: Kritik harus sangat spesifik terhadap data (5-10 kalimat).
3. NO XML TAGS: Gunakan judul asli artikel yang dikritik tanpa atribut tag <dokumen id="...">.
4. NO PRAISE: Jangan puji penulis. Fokus 100% pada kelemahan.
5. EVIDENCE: Kutip kalimat asli yang menunjukkan kelemahan tersebut secara verbatim, jangan hanya mengambil judul artikel saja.

[INSTRUCTION]
Lakukan audit kritis terhadap setiap dokumen secara independen pada aspek {aspect}. Fokus pada kualitas data dan ketajaman klaim.

[EXPECTED JSON SCHEMA]
{json_schema_string}

[INPUT DATA]
- Relation Context: {relation_desc}
- Articles Content: {articles_text}"""

_PROMPT_SINTESIS = """[ROLE]
Anda adalah "Lead Academic Synthesizer". Tugas Anda adalah menyatukan temuan konvergensi, divergensi, dan kritik menjadi satu konklusi makro pada aspek: {aspect}.

[MANDATORY RULES]
1. INTEGRATED VIEW: Jangan ulangi detail teknis. Berikan pandangan helikopter tentang posisi ilmu pengetahuan saat ini berdasarkan kumpulan dokumen ini.
2. PARADIGM SHIFT: Apakah dokumen-dokumen ini secara kolektif mendukung sebuah tren baru? Atau justru menunjukkan ketidakpastian di bidang ini?
3. NO XML TAGS: Gunakan judul-judul artikel sebagai rujukan utama secara naratif tanpa atribut tag <dokumen id="...">.
4. DENSITY: Tulis dalam 2-3 paragraf padat (minimal 150-200 kata) yang sangat berwawasan luas.

[INSTRUCTION]
Tuliskan SINTESIS OBJEKTIF (Kesimpulan komparatif makro).

[EXPECTED JSON SCHEMA]
{json_schema_string}

[INPUT DATA]
- Relation Context: {relation_desc}
- Articles Content: {articles_text}"""

_PROMPT_KETERBATASAN = """[ROLE]
Anda adalah "Epistemological Auditor". Tugas spesifik Anda adalah mencari TITIK BUTA (Blind-Spots) dan KETERBATASAN dari dokumen-dokumen yang diberikan pada aspek: {aspect}.

[MANDATORY RULES]
1. IDENTIFY THE VOID: Fokuslah pada apa yang GAGAL dijawab oleh teks-teks ini. Apakah ada data kuantitatif/kualitatif yang hilang? Apakah ada konteks geografis/demografis yang tidak terwakili?
2. PROHIBITION OF PRAISE: Anda dilarang memuji atau membahas apa yang sudah berhasil dicapai penulis. Fokus 100% pada area abu-abu dan limitasi instrumen atau data mereka.
3. CONCISE CRITIQUE: Jelaskan titik buta ini secara eksplisit dan analitis dalam 1-2 paragraf padat.
4. NO XML TAGS: Gunakan judul-judul artikel sebagai rujukan utama secara naratif tanpa atribut tag <dokumen id="...">.

[INSTRUCTION]
Tuliskan KETERBATASAN ANALISIS (Hal-hal yang tidak bisa dijawab atau belum terwakili oleh teks ini) berdasarkan konteks dokumen di bawah.

[EXPECTED JSON SCHEMA]
{json_schema_string}

[INPUT DATA]
- Relation Context: {relation_desc}
- Articles Content: {articles_text}"""


class ComparativeServiceError(RuntimeError):
    """Base service error for comparative runtime."""


class ComparativeValidationError(ComparativeServiceError):
    """Raised when canonical comparative inputs are invalid."""


class ComparativeServiceRequestContext(BaseModel):
    """Service-level canonical request context validation model."""

    project_id: str
    file_hashes: list[str]
    aspects_requested: list[str]


class ComparativeServiceDiagnostics(BaseModel):
    """Service-level canonical diagnostics validation model."""

    total_documents: int
    total_chunks_used: int
    llm_provider: str
    llm_model: str
    top_k: int
    similarity_threshold: float


class ComparativeServiceResponse(BaseModel):
    """Service-level canonical response validation gate."""

    request_context: ComparativeServiceRequestContext
    konvergensi: list[AnalisisPoin]
    divergensi: list[AnalisisPoin]
    kritik_akademis: list[PoinKritik]
    sintesis_objektif: str
    keterbatasan_analisis: str
    diagnostics: ComparativeServiceDiagnostics


class ComparativeService:
    """Comparative service skeleton aligned to the canonical request flow."""

    def __init__(
        self,
        retriever: ComparativeRetriever | None = None,
    ) -> None:
        """Initialize comparative service dependencies.

        Args:
            retriever: Optional retriever override for testing or composition.
        """
        self._gateway = create_ai_gateway()
        self._retriever = retriever or ComparativeRetriever()

    async def run_comparative(
        self,
        *,
        user_id: str,
        project_id: str,
        file_hashes: list[str],
        aspects: list[str] | None = None,
        llm_model: str | None = None,
        top_k: int | None = None,
        similarity_threshold: float | None = None,
    ) -> dict[str, Any]:
        """Run the comparative skeleton pipeline.

        Args:
            user_id: Tenant user identifier derived from JWT.
            project_id: Canonical project identifier.
            file_hashes: Canonical comparative file hashes.
            aspects: Optional aspect list from caller.
            llm_model: Optional model override within active provider family.
            top_k: Optional bounded retrieval tuning hint.
            similarity_threshold: Optional bounded retrieval threshold hint.

        Returns:
            dict[str, Any]: Canonical comparative response payload.
        """
        normalized_hashes = _normalize_file_hashes(file_hashes)
        normalized_aspects = _normalize_aspects(aspects)
        normalized_top_k = _normalize_top_k(top_k)
        normalized_threshold = _normalize_similarity_threshold(similarity_threshold)

        if len(normalized_hashes) < 2:
            raise ComparativeValidationError(
                "file_hashes must contain at least two unique non-empty values."
            )

        from app.core.database import _driver
        title_mapping = {}
        def fetch_titles(tx):
            cypher = """
            MATCH (n:Literature)
            WHERE n.hash IN $file_hashes
              AND n.project_id = $project_id
              AND (n.user_id = $user_id OR n.user_id IS NULL)
            RETURN n.hash AS hash, coalesce(n.title, n.filename) AS title
            """
            return [record for record in tx.run(cypher, file_hashes=normalized_hashes, project_id=project_id, user_id=user_id)]

        with _driver.session() as session:
            try:
                records = await run_in_threadpool(session.execute_read, fetch_titles)
                for rec in records:
                    title_mapping[rec["hash"]] = rec["title"]
            except Exception as e:
                logger.warning(f"Failed to fetch titles from Neo4j: {e}")

        embedding_engine = self._gateway.get_embedding_engine()
        
        semantic_queries = _build_semantic_queries(normalized_aspects)
        sem = asyncio.Semaphore(5)

        async def _retrieve_for_query(q_dict: dict[str, str]) -> dict[str, Any]:
            async with sem:
                aspect_key = q_dict["aspect_key"]
                aspect_query = q_dict["text"]
                
                embedding = await run_in_threadpool(embedding_engine.embed, aspect_query)
                aspect_embeddings = {
                    aspect_query: embedding,
                }
                retriever_request = ComparativeRetrieverRequest(
                    user_id=user_id,
                    project_id=project_id,
                    file_hashes=tuple(normalized_hashes),
                    aspect_query=aspect_query,
                    top_k=normalized_top_k,
                    similarity_threshold=normalized_threshold,
                )
                retrieval = await self._retriever.retrieve(
                    request=retriever_request,
                    aspect_embeddings=aspect_embeddings,
                )
                return {
                    "aspect_key": aspect_key,
                    "aspect_query": aspect_query,
                    "category": q_dict["category"],
                    "retrieval": retrieval,
                }

        retrieval_tasks = [_retrieve_for_query(q) for q in semantic_queries]
        aspect_evidence_blocks = await asyncio.gather(*retrieval_tasks)

        active_provider = self._gateway.active_llm_provider
        comparative_models = self._gateway.routing_policy["tasks"]["comparative"]["models"]
        resolved_model = llm_model or comparative_models[0]
        if llm_model is not None and llm_model not in comparative_models:
            raise ComparativeValidationError(
                "llm_model must belong to the comparative task family."
            )

        total_chunks_used = sum(
            block["retrieval"]["total_chunks_used"]
            for block in aspect_evidence_blocks
        )

        # -- Phase 3: Multi-Agent Synthesis Engine (Map-Reduce) --
        relation_desc = "Comparative analysis between selected research articles"

        agent_configs: list[dict[str, Any]] = [
            {
                "name": "konvergensi",
                "prompt": _PROMPT_KONVERGENSI,
                "schema_class": KonvergensiResponse,
            },
            {
                "name": "divergensi",
                "prompt": _PROMPT_DIVERGENSI,
                "schema_class": DivergensiResponse,
            },
            {
                "name": "kritik",
                "prompt": _PROMPT_KRITIK,
                "schema_class": KritikResponse,
            },
            {
                "name": "sintesis",
                "prompt": _PROMPT_SINTESIS,
                "schema_class": SintesisMakroResponse,
            },
            {
                "name": "keterbatasan",
                "prompt": _PROMPT_KETERBATASAN,
                "schema_class": KeterbatasanResponse,
            },
        ]

        llm_sem = asyncio.Semaphore(10)

        async def _run_agent(config: dict[str, Any], aspect: str, articles_text: str) -> dict[str, Any]:
            async with llm_sem:
                agent_name = config["name"]
                prompt_template = config["prompt"]
                schema_cls = config["schema_class"]

                messages = self._build_agent_messages(
                    prompt_template=prompt_template,
                    schema_class=schema_cls,
                    aspect=aspect.replace("_", " ").title(),
                    articles_text=articles_text,
                    relation_desc=relation_desc,
                )

                response_format = {
                    "type": "json_schema",
                    "json_schema": {
                        "name": schema_cls.__name__,
                        "strict": True,
                        "schema": to_strict_json_schema(schema_cls),
                    },
                }

                logger.info(
                    "Sending agent request to AI Gateway",
                    extra={
                        "agent": agent_name,
                        "aspect": aspect,
                        "model": resolved_model,
                        "articles_text_length": len(articles_text),
                    },
                )

                agent_response = await self._gateway.agenerate_llm_response(
                    messages=messages,
                    task_name="comparative",
                    model=resolved_model,
                    temperature=1.0,
                    max_tokens=4096,
                    response_format=response_format,
                    timeout=180.0,
                )

                raw = agent_response.choices[0].message.content.strip()

                # Markdown Guard
                if raw.startswith("```json"):
                    raw = raw[7:]
                if raw.startswith("```"):
                    raw = raw[3:]
                if raw.endswith("```"):
                    raw = raw[:-3]
                raw = raw.strip()

                parsed = schema_cls.model_validate(json.loads(raw))

                logger.info(
                    "Agent completed",
                    extra={"agent": agent_name, "aspect": aspect},
                )

                return {
                    "agent_name": agent_name,
                    "aspect": aspect,
                    "data": parsed.model_dump()
                }

        agent_tasks = []
        for aspect in normalized_aspects:
            aspect_blocks = [b for b in aspect_evidence_blocks if b["aspect_key"] == aspect]
            articles_text = self._build_articles_text(
                aspect_evidence_blocks=aspect_blocks,
                file_hashes=normalized_hashes,
                title_mapping=title_mapping,
            )
            for cfg in agent_configs:
                agent_tasks.append(_run_agent(cfg, aspect, articles_text))

        agent_results = await asyncio.gather(*agent_tasks)

        # -- Reduce: Merge multi-aspect agent outputs into unified response --
        merged = self._aggregate_results(agent_results)
        synthesis_data = AnalisisKomparatifResponse.model_validate(merged)

        # Kalkulasi deterministik untuk log observabilitas backend
        jumlah_konvergensi = len(synthesis_data.konvergensi)
        jumlah_divergensi = len(synthesis_data.divergensi)
        total_poin = jumlah_konvergensi + jumlah_divergensi

        if total_poin > 0:
            sim_pct = round((jumlah_konvergensi / total_poin) * 100, 2)
            div_pct = round((jumlah_divergensi / total_poin) * 100, 2)
        else:
            sim_pct = 0.0
            div_pct = 0.0

        logger.info(
            "Multi-agent synthesis completed",
            extra={
                "similarity_pct": sim_pct,
                "divergence_pct": div_pct,
                "agents_executed": len(agent_results),
            },
        )

        # -- Map merged output into service-level response envelope --
        result = {
            "request_context": {
                "project_id": project_id,
                "file_hashes": normalized_hashes,
                "aspects_requested": normalized_aspects,
            },
            **synthesis_data.model_dump(),
            "diagnostics": {
                "total_documents": len(normalized_hashes),
                "total_chunks_used": total_chunks_used,
                "llm_provider": active_provider,
                "llm_model": resolved_model,
                "top_k": normalized_top_k,
                "similarity_threshold": normalized_threshold,
                "parallel_tasks_executed": len(agent_results),
            },
        }
        return ComparativeServiceResponse.model_validate(result).model_dump()

    @staticmethod
    def _aggregate_results(agent_results: list[dict[str, Any]]) -> dict[str, Any]:
        """Aggregate outputs from parallel aspect-based agent tasks."""
        merged: dict[str, Any] = {
            "konvergensi": [],
            "divergensi": [],
            "kritik_akademis": [],
            "sintesis_objektif": "",
            "keterbatasan_analisis": ""
        }
        
        sintesis_list = []
        keterbatasan_list = []

        for res in agent_results:
            data = res["data"]
            aspect_title = res["aspect"].replace("_", " ").title()
            
            if res["agent_name"] == "konvergensi":
                merged["konvergensi"].extend(data.get("konvergensi", []))
            elif res["agent_name"] == "divergensi":
                merged["divergensi"].extend(data.get("divergensi", []))
            elif res["agent_name"] == "kritik":
                merged["kritik_akademis"].extend(data.get("kritik_akademis", []))
            elif res["agent_name"] == "sintesis":
                sintesis_list.append(f"**Aspek {aspect_title}:**\n{data.get('sintesis_objektif', '')}")
            elif res["agent_name"] == "keterbatasan":
                keterbatasan_list.append(f"**Aspek {aspect_title}:**\n{data.get('keterbatasan_analisis', '')}")
                
        merged["sintesis_objektif"] = "\n\n".join(sintesis_list)
        merged["keterbatasan_analisis"] = "\n\n".join(keterbatasan_list)
        return merged

    @staticmethod
    def _build_agent_messages(
        prompt_template: str,
        schema_class: type[BaseModel],
        aspect: str,
        articles_text: str,
        relation_desc: str,
    ) -> list[dict[str, str]]:
        """Build messages payload for a single specialized agent.

        Constructs the system prompt from a per-category prompt template
        by injecting the dynamic variables (aspect, articles text, relation
        description, and the agent-specific JSON schema) into the
        parameterized prompt constant.

        Args:
            prompt_template: One of the 5 agent prompt constants.
            schema_class: The Pydantic wrapper class for this agent.
            aspect: Comma-separated aspect string for this analysis.
            articles_text: Labeled and concatenated article text.
            relation_desc: Description of the relation between articles.

        Returns:
            list[dict[str, str]]: Messages list ready for AI Gateway.
        """
        schema_str = json.dumps(schema_class.model_json_schema(), indent=2)
        system_content = prompt_template.format(
            aspect=aspect,
            relation_desc=relation_desc,
            articles_text=articles_text,
            json_schema_string=schema_str,
        )

        return [
            {"role": "system", "content": system_content},
            {
                "role": "user",
                "content": (
                    "Execute the analysis as instructed in the system prompt. "
                    "Return the result as a JSON object matching the required schema."
                ),
            },
        ]

    @staticmethod
    def _build_articles_text(
        aspect_evidence_blocks: list[dict[str, Any]],
        file_hashes: list[str],
        title_mapping: dict[str, str],
    ) -> str:
        """Build explicitly labeled articles text from retrieval evidence blocks.

        Assembles a structured, labeled text payload from retrieved chunks
        that is injected into the Senior Reviewer system prompt. Each document
        is assigned a sequential label (Artikel A, Artikel B, ...) based on its
        position in file_hashes to satisfy the prompt constraint that requires
        explicit article identity in every analytical point.

        Args:
            aspect_evidence_blocks: List of evidence block dicts from the
                retrieval phase, each containing 'aspect_key', 'aspect_query',
                and 'retrieval' with 'evidence_groups'.
            file_hashes: Ordered list of normalized file hashes used for
                consistent label assignment.

        Returns:
            str: Labeled and concatenated articles text ready for prompt
                injection into {articles_text}.
        """
        label_map: dict[str, str] = {}
        for idx, file_hash in enumerate(file_hashes):
            label_map[file_hash] = f"Artikel {chr(65 + idx)}"

        sections: list[str] = []
        for file_hash in file_hashes:
            doc_title = title_mapping.get(file_hash, f"Artikel {label_map.get(file_hash, 'Unknown')}")
            doc_chunks_text: list[str] = []
            seen_chunk_ids: set[str] = set()

            for block in aspect_evidence_blocks:
                evidence_groups = block.get("retrieval", {}).get(
                    "evidence_groups", []
                )
                for group in evidence_groups:
                    if group.get("file_hash") != file_hash:
                        continue
                    for chunk in group.get("chunks", []):
                        chunk_id = chunk.get("id")
                        if chunk_id and chunk_id in seen_chunk_ids:
                            continue
                        if chunk_id:
                            seen_chunk_ids.add(chunk_id)
                            
                        header = chunk.get("metadata_header", "")
                        content = chunk.get("content", "")
                        if not content:
                            continue
                        if header:
                            doc_chunks_text.append(
                                f"[{header}]\n{content}"
                            )
                        else:
                            doc_chunks_text.append(content)

            if doc_chunks_text:
                body = "\n\n".join(doc_chunks_text)
            else:
                body = "(Tidak ada data yang ditemukan untuk artikel ini.)"

            safe_title = doc_title.replace('"', '&quot;') if doc_title else "Unknown"
            xml_str = (
                f'<dokumen id="{safe_title}">\n'
                f'{body}\n'
                f'</dokumen>'
            )
            
            sections.append(xml_str)

        return "\n\n".join(sections)


def _normalize_file_hashes(file_hashes: list[str]) -> list[str]:
    """Normalize comparative file hashes while preserving order."""
    normalized: list[str] = []
    for file_hash in file_hashes:
        stripped = file_hash.strip()
        if stripped and stripped not in normalized:
            normalized.append(stripped)
    return normalized


def _normalize_aspects(aspects: list[str] | None) -> list[str]:
    """Normalize caller-provided aspects or apply service defaults."""
    source = aspects or list(DEFAULT_COMPARATIVE_ASPECTS)
    normalized: list[str] = []
    for aspect in source:
        stripped = aspect.strip().lower()
        if stripped and stripped not in normalized:
            normalized.append(stripped)
    return normalized


def _build_semantic_queries(aspects: list[str]) -> list[dict[str, str]]:
    """Build static semantic queries per aspect for the 3 comparative categories."""
    queries = []
    for aspect in aspects:
        queries.extend([
            {"aspect_key": aspect, "category": "konvergensi", "text": f"Persamaan pendekatan, kesepakatan teori, dan titik temu temuan mengenai {aspect}"},
            {"aspect_key": aspect, "category": "divergensi", "text": f"Perbedaan metode, kontradiksi eksperimen, dan hasil yang saling berbenturan mengenai {aspect}"},
            {"aspect_key": aspect, "category": "kritik", "text": f"Kelemahan riset, bias data, keterbatasan instrumen, dan celah metodologis mengenai {aspect}"}
        ])
    return queries


def _normalize_top_k(top_k: int | None) -> int:
    """Normalize top-k according to bounded comparative skeleton policy."""
    if top_k is None:
        return DEFAULT_TOP_K
    return max(MIN_TOP_K, min(MAX_TOP_K, top_k))


def _normalize_similarity_threshold(similarity_threshold: float | None) -> float:
    """Normalize similarity threshold according to bounded comparative policy."""
    if similarity_threshold is None:
        return DEFAULT_SIMILARITY_THRESHOLD
    return max(
        MIN_SIMILARITY_THRESHOLD,
        min(MAX_SIMILARITY_THRESHOLD, similarity_threshold),
    )
