from dataclasses import dataclass
from typing import Any, Dict, List, Optional

from monarch_py.datamodels.solr import HistoPhenoKeys, SolrQuery
from monarch_py.datamodels.category_enums import AssociationPredicate
from monarch_py.utils.association_type_utils import AssociationTypeMappings, get_solr_query_fragment
from monarch_py.utils.utils import escape


@dataclass
class AssociationCountSuffixes:
    """Suffix strings used to build and parse facet queries for association counts."""

    direct_subject: str
    direct_object: str
    closure_subject: str
    closure_object: str
    ortho_subject: Optional[str] = None
    ortho_object: Optional[str] = None

    @property
    def all_suffixes(self) -> Dict[str, str]:
        """Return a mapping of count_level+direction to suffix string."""
        result = {
            "direct_subject": self.direct_subject,
            "direct_object": self.direct_object,
            "closure_subject": self.closure_subject,
            "closure_object": self.closure_object,
        }
        if self.ortho_subject:
            result["orthologs_subject"] = self.ortho_subject
        if self.ortho_object:
            result["orthologs_object"] = self.ortho_object
        return result


def build_association_count_suffixes(entities: List[str]) -> AssociationCountSuffixes:
    """Build the suffix strings used in facet queries for association counts.

    Shared between query building and response parsing to keep them in sync.

    Args:
        entities: List of entity IDs (primary entity first, then orthologs).
    """
    entity = entities[0]

    direct_subject = f'AND subject:"{entity}"'
    direct_object = f'AND (object:"{entity}" OR disease_context_qualifier:"{entity}")'
    closure_subject = f'AND (subject:"{entity}" OR subject_closure:"{entity}")'
    closure_object = f'AND (object:"{entity}" OR object_closure:"{entity}" OR disease_context_qualifier:"{entity}" OR disease_context_qualifier_closure:"{entity}")'

    ortho_subject = None
    ortho_object = None
    if len(entities) > 1:
        all_subjects = " OR ".join(f'subject:"{e}" OR subject_closure:"{e}"' for e in entities)
        all_objects = " OR ".join(
            f'object:"{e}" OR object_closure:"{e}" OR disease_context_qualifier:"{e}" OR disease_context_qualifier_closure:"{e}"'
            for e in entities
        )
        ortho_subject = f"AND ({all_subjects})"
        ortho_object = f"AND ({all_objects})"

    return AssociationCountSuffixes(
        direct_subject=direct_subject,
        direct_object=direct_object,
        closure_subject=closure_subject,
        closure_object=closure_object,
        ortho_subject=ortho_subject,
        ortho_object=ortho_object,
    )


def build_association_query(
    category: Optional[List[str]] = None,
    subject: Optional[List[str]] = None,
    subject_closure: Optional[str] = None,
    subject_category: Optional[List[str]] = None,
    subject_namespace: Optional[List[str]] = None,
    subject_taxon: Optional[List[str]] = None,
    predicate: Optional[List[str]] = None,
    object: Optional[List[str]] = None,
    object_closure: Optional[str] = None,
    object_category: Optional[List[str]] = None,
    object_namespace: Optional[List[str]] = None,
    object_taxon: Optional[List[str]] = None,
    entity: Optional[List[str]] = None,
    primary_knowledge_source: Optional[List[str]] = None,
    direct: bool = False,
    q: Optional[str] = None,
    sort: Optional[List[str]] = None,
    facet_fields: Optional[List[str]] = None,
    facet_queries: Optional[List[str]] = None,
    filter_queries: Optional[List[str]] = None,
    facet_mincount: int = 1,
    offset: int = 0,
    limit: int = 20,
) -> SolrQuery:
    """Populate a SolrQuery object with association filters"""
    entity_fields = ["subject", "object", "disease_context_qualifier"]
    query = SolrQuery(start=offset, rows=limit)
    query.add_field_filter_query("category", None if not category else [c for c in category])
    query.add_field_filter_query("predicate", None if not predicate else [p for p in predicate])
    query.add_field_filter_query("subject_closure", subject_closure)
    query.add_field_filter_query("subject_category", None if not subject_category else [c for c in subject_category])
    query.add_field_filter_query("subject_namespace", subject_namespace)
    query.add_field_filter_query("subject_taxon", subject_taxon)
    query.add_field_filter_query("object_closure", object_closure)
    query.add_field_filter_query("object_category", None if not object_category else [c for c in object_category])
    query.add_field_filter_query("object_namespace", object_namespace)
    query.add_field_filter_query("object_taxon", object_taxon)
    query.add_field_filter_query("primary_knowledge_source", primary_knowledge_source)
    if subject:
        if direct:
            query.add_field_filter_query("subject", " OR ".join(subject))
        else:
            query.add_filter_query(" OR ".join([f'subject:"{s}" OR subject_closure:"{s}"' for s in subject]))
    if object:
        if direct:
            query.add_field_filter_query("object", " OR ".join(object))
        else:
            query.add_filter_query(" OR ".join([f'object:"{o}" OR object_closure:"{o}"' for o in object]))
    if entity:
        if direct:
            query.add_filter_query(" OR ".join([f'{field}:"{e}"' for e in entity for field in entity_fields]))
        else:
            query.add_filter_query(
                " OR ".join([f'{field}:"{e}" OR {field}_closure:"{e}"' for e in entity for field in entity_fields])
            )
    if q:
        # We don't yet have tokenization strategies for the association index, initially we'll limit searching to
        # the visible fields in an association table plus their ID equivalents and use a wildcard query for substring matching
        query.q = q
        query.def_type = "edismax"
        query.hl = True
        query.query_fields = association_search_query_fields()
    if sort:
        query.sort = ", ".join(sort)
    if facet_fields:
        query.facet_fields = facet_fields
    if facet_queries:
        query.facet_queries = facet_queries
    if filter_queries:
        query.filter_queries.extend(filter_queries)
    query.facet_mincount = facet_mincount
    return query


def build_association_table_query(
    entity: List[str],
    category: str,
    direct: bool = False,
    q: Optional[str] = None,
    facet_fields: List[str] = None,
    facet_queries: List[str] = None,
    filter_queries: List[str] = None,
    offset: int = 0,
    limit: int = 5,
    sort: List[str] = None,
) -> SolrQuery:
    if sort is None:
        sort = [
            "frequency_computed_sortable_float desc",
            "evidence_count desc",
            "subject_label asc",
            "predicate asc",
            "object_label asc",
            "primary_knowledge_source asc",
        ]

    query = build_association_query(
        entity=entity,
        category=[category],
        q=q,
        sort=sort,
        offset=offset,
        limit=limit,
        direct=direct,
        facet_fields=facet_fields,
        facet_queries=facet_queries,
        filter_queries=filter_queries,
    )
    return query


def build_association_counts_query(entities: List[str]) -> SolrQuery:
    """Build a query that produces three levels of association counts via facet queries.

    Args:
        entities: List of entity IDs. The first is the primary entity; the rest are orthologs.
                  If only one entity is provided, ortholog counts are skipped.
    """
    suffixes = build_association_count_suffixes(entities)

    facet_queries = []
    for suffix in suffixes.all_suffixes.values():
        for agm in AssociationTypeMappings.get_mappings():
            association_type_query = get_solr_query_fragment(agm)
            facet_queries.append(f"({association_type_query}) {suffix}")

    query = build_association_query(entity=entities, facet_queries=facet_queries)
    return query


def build_histopheno_query(subject: str) -> SolrQuery:
    query = build_association_query(
        subject=[subject],
        offset=0,
        limit=0,
    )
    hpkeys = [i for i in HistoPhenoKeys]
    query.facet_queries = [f'object_closure:"{(i.value)}"' for i in hpkeys]
    return query


def build_multi_entity_association_query(
    entity: str,
    counterpart_category: Optional[str] = None,
    # predicate: List[str] = None,
    offset: int = 0,
    limit: int = 20,
) -> SolrQuery:
    """Populate a SolrQuery object with association filters"""
    query = SolrQuery(start=offset, rows=limit)
    if counterpart_category:
        query.add_filter_query(
            f'(subject:"{escape(entity)}" AND object_category:"{escape(counterpart_category)}") OR (object:"{escape(entity)}" AND subject_category:"{escape(counterpart_category)}")'
        )
    else:
        query.add_filter_query(f'(subject:"{escape(entity)}") OR (object:"{escape(entity)}")')
    return query


def build_search_query(
    q: str = "*:*",
    offset: int = 0,
    limit: int = 20,
    category: List[str] = None,
    in_taxon_label: List[str] = None,
    facet_fields: List[str] = None,
    facet_queries: List[str] = None,
    filter_queries: List[str] = None,
    highlighting: bool = False,
    sort: Optional[str] = None,
) -> SolrQuery:
    query = SolrQuery(start=offset, rows=limit, sort=sort)
    query.q = q
    query.def_type = "edismax"
    query.query_fields = entity_query_fields()
    query.hl = highlighting
    query.boost = entity_boost(empty_search=(q == "*:*"))
    if category:
        query.add_filter_query(" OR ".join(f'category:"{cat}"' for cat in category))
    if in_taxon_label:
        query.add_filter_query(" OR ".join([f'in_taxon_label:"{t}"' for t in in_taxon_label]))
    if facet_fields:
        query.facet_fields = facet_fields
    if facet_queries:
        query.facet_queries = facet_queries
    if filter_queries:
        query.filter_queries.extend(filter_queries)
    # Filter out entities that don't have names (required field, but some don't have them)
    query.add_filter_query("name:*")
    return query


def build_autocomplete_query(
    q: str, category: List[str] = None, prioritized_predicates: List[AssociationPredicate] = None
) -> SolrQuery:
    query = SolrQuery(q=q, rows=10, start=0)
    query.q = q
    if category:
        query.add_filter_query(" OR ".join(f'category:"{cat}"' for cat in category))
    # match the query fields to start with
    query.query_fields = entity_query_fields()
    query.def_type = "edismax"
    query.boost = entity_boost(prioritized_predicates=prioritized_predicates, empty_search=(q == "*:*"))
    return query


def build_mapping_query(
    entity_id: Optional[List[str]] = None,
    subject_id: Optional[List[str]] = None,
    predicate_id: Optional[List[str]] = None,
    object_id: Optional[List[str]] = None,
    mapping_justification: Optional[List[str]] = None,
    offset: int = 0,
    limit: int = 20,
) -> SolrQuery:
    query = SolrQuery(start=offset, rows=limit)
    if entity_id:
        query.add_filter_query(" OR ".join([f'subject_id:"{escape(i)}" OR object_id:"{escape(i)}"' for i in entity_id]))
    if subject_id:
        query.add_filter_query(" OR ".join([f'subject_id:"{escape(i)}"' for i in subject_id]))
    if predicate_id:
        query.add_filter_query(" OR ".join([f'predicate_id:"{escape(i)}"' for i in predicate_id]))
    if object_id:
        query.add_filter_query(" OR ".join([f'object_id:"{escape(i)}"' for i in object_id]))
    if mapping_justification:
        query.add_filter_query(" OR ".join([f'mapping_justification:"{escape(i)}"' for i in mapping_justification]))
    return query


def build_grounding_query(text: str) -> SolrQuery:
    query = SolrQuery(q=text, rows=10, start=0)
    query.q = f'"{text}"'  # quoting so that the complete text is matched as a unit
    # rather than _t (text) or _ac (autocomplete/starts-with), just use keyword fields
    query.query_fields = (
        "id^100 name^10 symbol^10 synonym name_grounding full_name_grounding symbol_grounding synonym_grounding"
    )
    query.def_type = "edismax"
    query.boost = obsolete_unboost(multiplier=0.001)
    return query


### Search helper functions ###


def obsolete_unboost(multiplier=0.1):
    return f'if(termfreq(deprecated,"true"),{multiplier},1)'


def entity_boost(prioritized_predicates: List[AssociationPredicate] = None, empty_search: bool = False) -> str:
    """Shared boost function between search and autocomplete"""
    phenotype_boost = category_boost("biolink:PhenotypicFeature", 1.1)
    disease_boost = category_boost("biolink:Disease", 1.3)
    human_gene_boost = category_boost("biolink:Gene", 1.1, taxon="NCBITaxon:9606")

    boosts = [phenotype_boost, disease_boost, human_gene_boost, obsolete_unboost()]
    if prioritized_predicates:
        boosts.append(entity_predicate_boost(prioritized_predicates, 2.0))
    if empty_search:
        boosts.append(blank_search_boost())
    return f"product({','.join(boosts)})"


def entity_predicate_boost(prioritized_predicates: List[AssociationPredicate], multiplier: float) -> str:
    boosts = []
    for predicate in prioritized_predicates:
        field_root = predicate.value.replace("biolink:", "")
        count_field = field_root + "_count"
        boosts.append(f"if({count_field},{multiplier},1)")
    return ",".join(boosts)


def category_boost(category: str, multiplier: float, taxon: Optional[str] = None) -> str:
    if taxon:
        return f'if(and(termfreq(in_taxon,"{taxon}"),termfreq(category,"{category}")),{multiplier},1)'
    else:
        return f'if(termfreq(category,"{category}"),{multiplier},1)'


def blank_search_boost() -> str:
    """
    Boost specific nodes that we'd like to see as site examples to the top for empty searches
    """
    example_nodes = [
        "MONDO:0007523",  # Ehlers-Danlos syndrome, hypermobility type
        "MONDO:0019391",  # Fanconi anemia
        "MONDO:0018954",  # Loeys-Dietz syndrome
        "MONDO:0011518",  # Wiedemann-Steiner syndrome
        "HP:0001166",  # Arachnodactyly
        "HP:0001631",  # Atrial septal defect
        "UBERON:0000948",  # heart
        "UBERON:0006585",  # vestibular organ
        "HGNC:4851",  # HTT
        "HGNC:3603",  # FBN1
    ]
    # boost score by 1.5 + i/10 for these nodes
    boosts = [f'if(termfreq(id,"{node}"),{len(example_nodes) - i + 2},1)' for i, node in enumerate(example_nodes)]
    boost = ",".join(boosts)
    return f"product({boost})"


def autocomplete_query_fields():
    """
    Fields and boosts used for autocomplete
    """
    return "id^100 name^10 name_t^5 name_ac symbol^10 symbol_t^5 symbol_ac synonym synonym_t synonym_ac in_taxon_label_t"


def entity_query_fields():
    """
    Fields and boosts used for entity search, includes autocomplete fields and expands upon them beyond pure name fields
    """
    return f"{autocomplete_query_fields()} description_t xref"


def association_search_query_fields():
    """
    Shared field list for free text search on associations (e.g. for the association table)
    """

    return (
        "subject subject_label^2 subject_label_t subject_closure subject_closure_label subject_closure_label_t"
        " predicate predicate_t"
        " object object_label^2 object_label_t object_closure object_closure_label object_closure_label_t"
        " publications has_evidence primary_knowledge_source aggregator_knowledge_source provided_by "
    )


def build_case_phenotype_query(
    disease_id: str,
    direct_only: bool,
    rows: int = 50000,
) -> Dict[str, Any]:
    """Build Solr query for case-phenotype matrix.

    Uses Solr's JOIN query parser to find all phenotype associations
    for cases that have a specific disease association.

    The query structure:
    1. Main query: JOIN from CaseToDiseaseAssociation to CaseToPhenotypicFeatureAssociation
       - Joins on `subject` field (the case ID)
       - Inner query filters diseases by object (direct) or object_closure (indirect)
    2. Filter query: Restrict results to phenotype associations only
    3. Facet queries: Count phenotypes per HistoPheno bin

    Args:
        disease_id: MONDO disease ID to query
        direct_only: If True, only cases with exactly this disease;
                     If False, includes cases with descendant diseases
        rows: Maximum results to return

    Returns:
        Dict of Solr query parameters ready for requests

    Example direct query:
        {!join from=subject to=subject}(
            category:"biolink:CaseToDiseaseAssociation"
            AND object:"MONDO:0007078"
        )

    Example indirect query (via closure):
        {!join from=subject to=subject}(
            category:"biolink:CaseToDiseaseAssociation"
            AND object_closure:"MONDO:0007078"
        )
    """
    disease_field = "object" if direct_only else "object_closure"

    join_query = (
        '{!join from=subject to=subject}'
        f'(category:"biolink:CaseToDiseaseAssociation" '
        f'AND {disease_field}:"{escape(disease_id)}")'
    )

    facet_queries = [
        f'object_closure:"{bin_key.value}"'
        for bin_key in HistoPhenoKeys
    ]

    return {
        "q": join_query,
        "fq": 'category:"biolink:CaseToPhenotypicFeatureAssociation"',
        "rows": rows,
        "fl": ",".join([
            "subject",           # Case ID
            "subject_label",     # Case label
            "object",            # Phenotype ID
            "object_label",      # Phenotype label
            "object_closure",    # For bin assignment
            "negated",           # Explicit absence
            "publications",      # Supporting evidence
            "onset_qualifier",   # Age of onset (ISO8601)
            "onset_qualifier_label",
        ]),
        "facet": "true",
        "facet.query": facet_queries,
    }


def build_case_disease_query(
    disease_id: str,
    direct_only: bool,
    rows: int = 50000,
) -> Dict[str, Any]:
    """Build Solr query to get cases for a disease.

    This is a simpler query to get CaseToDiseaseAssociation documents,
    which we need to determine:
    1. Total case count (for limit checking before full matrix fetch)
    2. Which specific disease each case has (for direct/indirect flagging)

    Args:
        disease_id: MONDO disease ID to query
        direct_only: If True, only cases with exactly this disease
        rows: Maximum results to return

    Returns:
        Dict of Solr query parameters
    """
    disease_field = "object" if direct_only else "object_closure"

    return {
        "q": f'{disease_field}:"{escape(disease_id)}"',
        "fq": 'category:"biolink:CaseToDiseaseAssociation"',
        "rows": rows,
        "fl": "subject,subject_label,object,object_label",
    }


def build_grid_column_query(
    context_id: str,
    config: "GridTypeConfig",
    direct_only: bool,
    filter_empty_columns: bool = True,
    rows: int = 50000,
) -> Dict[str, Any]:
    """Build Solr query to get column entities for a grid.

    First hop: Context Entity -> Column Entities

    Args:
        context_id: The context entity ID (e.g., disease ID, gene ID)
        config: Grid type configuration
        direct_only: If True, only direct associations; if False, use closure
        filter_empty_columns: If True, exclude columns with no row associations
        rows: Maximum results to return

    Returns:
        Dict of Solr query parameters
    """
    from monarch_py.datamodels.grid_configs import GridTypeConfig

    context_field = config.context_field if direct_only else config.context_closure_field

    # Base filter for column association category (handles single or multi-category configs)
    column_categories = config.get_column_assoc_categories()
    if len(column_categories) == 1:
        category_filter = f'category:"{column_categories[0].value}"'
    else:
        category_filter = "(" + " OR ".join(f'category:"{c.value}"' for c in column_categories) + ")"
    fq = [category_filter]

    # Build main query
    if filter_empty_columns:
        # Use JOIN to find column associations where the column entity
        # appears in row associations. This filters out columns with no rows.
        # JOIN: from=row_context_field (e.g., subject in phenotype assocs)
        #       to=column_field (e.g., object in homology assocs)
        existence_join = (
            f'{{!join from={config.row_context_field} to={config.column_field}}}'
            f'category:"{config.row_assoc_category.value}"'
        )
        q = existence_join
        # Add context filter to fq instead of q
        fq.append(f'{context_field}:"{context_id}"')
    else:
        q = f'{context_field}:"{context_id}"'

    return {
        "q": q,
        "fq": fq,
        "rows": rows,
        "fl": ",".join([
            config.column_field,
            f"{config.column_field}_label",
            config.context_field,
            f"{config.context_field}_label",
            "subject_taxon",
            "subject_taxon_label",
            "object_taxon",
            "object_taxon_label",
        ]),
    }


def build_multi_category_column_query(
    context_id: str,
    column_assoc_categories: List[str],
    context_field: str,
    context_closure_field: str,
    column_field: str,
    direct_only: bool,
    row_assoc_categories: Optional[List[str]] = None,
    row_context_field: Optional[str] = None,
    filter_empty_columns: bool = True,
    rows: int = 50000,
) -> Dict[str, Any]:
    """Build Solr query to get column entities from multiple association categories.

    First hop: Context Entity -> Column Entities (with multiple category support)

    Args:
        context_id: The context entity ID (e.g., gene ID)
        column_assoc_categories: List of association category values
        context_field: Field in associations containing context ID
        context_closure_field: Field for indirect context matching
        column_field: Field containing column entity ID
        direct_only: If True, only direct associations; if False, use closure
        row_assoc_categories: List of row association categories (for empty column filtering)
        row_context_field: Field in row associations linking to columns (for filtering)
        filter_empty_columns: If True, exclude columns with no row associations
        rows: Maximum results to return

    Returns:
        Dict of Solr query parameters
    """
    ctx_field = context_field if direct_only else context_closure_field

    # Build OR query for multiple column categories
    column_category_filter = " OR ".join(f'category:"{cat}"' for cat in column_assoc_categories)

    # Base filter queries
    fq = [f'({column_category_filter})']

    # Build main query
    if filter_empty_columns and row_assoc_categories and row_context_field:
        # Use JOIN to find column associations where the column entity
        # appears in row associations. This filters out columns with no rows.
        row_category_filter = " OR ".join(f'category:"{cat}"' for cat in row_assoc_categories)
        existence_join = (
            f'{{!join from={row_context_field} to={column_field}}}'
            f'({row_category_filter})'
        )
        q = existence_join
        # Add context filter to fq instead of q
        fq.append(f'{ctx_field}:"{context_id}"')
    else:
        q = f'{ctx_field}:"{context_id}"'

    return {
        "q": q,
        "fq": fq,
        "rows": rows,
        "fl": ",".join([
            column_field,
            f"{column_field}_label",
            context_field,
            f"{context_field}_label",
            "subject_taxon",
            "subject_taxon_label",
            "object_taxon",
            "object_taxon_label",
            # Include source association fields
            "category",
            "predicate",
            "publications",
            "has_evidence",
            "primary_knowledge_source",
        ]),
    }


def build_multi_category_row_query(
    context_id: str,
    column_assoc_categories: List[str],
    row_assoc_categories: List[str],
    context_field: str,
    context_closure_field: str,
    column_field: str,
    row_context_field: str,
    row_entity_field: str,
    grouping: "RowGroupingConfig",
    direct_only: bool,
    rows: int = 50000,
) -> Dict[str, Any]:
    """Build Solr query for grid row entities using JOIN with multiple column categories.

    Second hop: Column Entities -> Row Entities (supporting multiple column categories)

    Args:
        context_id: The context entity ID
        column_assoc_categories: List of association categories for the column hop
        row_assoc_categories: List of association categories for the row hop
        context_field: Field in column associations containing context ID
        context_closure_field: Field for indirect context matching
        column_field: Field containing column entity ID
        row_context_field: Field in row associations to join on
        row_entity_field: Field containing row entity ID
        grouping: Row grouping configuration (for facet queries)
        direct_only: If True, only direct associations; if False, use closure
        rows: Maximum results to return

    Returns:
        Dict of Solr query parameters
    """
    from monarch_py.datamodels.grid_groupings import RowGroupingConfig

    ctx_field = context_field if direct_only else context_closure_field

    # Build OR query for multiple categories in the JOIN
    category_filter = " OR ".join(f'category:"{cat}"' for cat in column_assoc_categories)

    # JOIN query: Find row associations for column entities associated with context
    join_query = (
        f'{{!join from={column_field} to={row_context_field}}}'
        f'(({category_filter}) '
        f'AND {ctx_field}:"{context_id}")'
    )

    # Build facet queries for bin counts
    facet_queries = [
        f'{row_entity_field}_closure:"{bin_id}"'
        for bin_id in grouping.bin_ids
    ]

    # Build OR query for multiple row categories
    row_category_filter = " OR ".join(f'category:"{cat}"' for cat in row_assoc_categories)

    return {
        "q": join_query,
        "fq": f'({row_category_filter})',
        "rows": rows,
        "fl": ",".join([
            row_context_field,                      # Column entity ID
            f"{row_context_field}_label",           # Column entity label
            row_entity_field,                       # Row entity ID
            f"{row_entity_field}_label",            # Row entity label
            f"{row_entity_field}_closure",          # For bin assignment
            "negated",
            "publications",
            "onset_qualifier",
            "onset_qualifier_label",
        ]),
        "facet": "true",
        "facet.query": facet_queries,
    }


def build_grid_row_query(
    context_id: str,
    config: "GridTypeConfig",
    grouping: "RowGroupingConfig",
    direct_only: bool,
    rows: int = 50000,
) -> Dict[str, Any]:
    """Build Solr query for grid row entities using JOIN.

    Second hop: Column Entities -> Row Entities

    Uses Solr's JOIN query parser to find all row associations
    for column entities that are associated with the context entity.

    Args:
        context_id: The context entity ID
        config: Grid type configuration
        grouping: Row grouping configuration (for facet queries)
        direct_only: If True, only direct associations; if False, use closure
        rows: Maximum results to return

    Returns:
        Dict of Solr query parameters
    """
    from monarch_py.datamodels.grid_configs import GridTypeConfig
    from monarch_py.datamodels.grid_groupings import RowGroupingConfig

    context_field = config.context_field if direct_only else config.context_closure_field

    # Build column category filter (handles single or multi-category configs)
    column_categories = config.get_column_assoc_categories()
    if len(column_categories) == 1:
        col_cat_filter = f'category:"{column_categories[0].value}"'
    else:
        col_cat_filter = "(" + " OR ".join(f'category:"{c.value}"' for c in column_categories) + ")"

    # JOIN query: Find row associations for column entities associated with context
    join_query = (
        f'{{!join from={config.column_field} to={config.row_context_field}}}'
        f'({col_cat_filter} '
        f'AND {context_field}:"{context_id}")'
    )

    # Build facet queries for bin counts
    facet_queries = [
        f'{config.row_entity_field}_closure:"{bin_id}"'
        for bin_id in grouping.bin_ids
    ]

    return {
        "q": join_query,
        "fq": f'category:"{config.row_assoc_category.value}"',
        "rows": rows,
        "fl": ",".join([
            config.row_context_field,                      # Column entity ID
            f"{config.row_context_field}_label",           # Column entity label
            config.row_entity_field,                       # Row entity ID
            f"{config.row_entity_field}_label",            # Row entity label
            f"{config.row_entity_field}_closure",          # For bin assignment
            "negated",
            "publications",
            "onset_qualifier",
            "onset_qualifier_label",
        ]),
        "facet": "true",
        "facet.query": facet_queries,
    }
