from collections.abc import Mapping, Sequence

from kb_dashboard_core.panels.charts.lens.columns.view import (
    KbnLensCustomIntervalsDimensionColumn,
    KbnLensCustomIntervalsDimensionColumnParams,
    KbnLensCustomIntervalsDimensionColumnParentFormat,
    KbnLensCustomIntervalsDimensionColumnParentFormatParams,
    KbnLensDateHistogramDimensionColumn,
    KbnLensDateHistogramDimensionColumnParams,
    KbnLensDimensionColumnTypes,
    KbnLensFiltersDimensionColumn,
    KbnLensFiltersDimensionColumnParams,
    KbnLensFiltersFilter,
    KbnLensIntervalsDimensionColumn,
    KbnLensIntervalsDimensionColumnParams,
    KbnLensIntervalsRange,
    KbnLensMetricColumnTypes,
    KbnLensTermsDimensionColumn,
    KbnLensTermsDimensionColumnParams,
    KbnLensTermsOrderBy,
    KbnLensTermsParentFormat,
)
from kb_dashboard_core.panels.charts.lens.dimensions.config import (
    LensDateHistogramDimension,
    LensDimensionTypes,
    LensFiltersDimension,
    LensIntervalsDimension,
    LensMultiTermsDimension,
    LensTermsDimension,
)
from kb_dashboard_core.queries.compile import compile_nonesql_query
from kb_dashboard_core.shared.config import Sort
from kb_dashboard_core.shared.defaults import default_false, default_true

# Maps user-friendly granularity levels (1=finest to 7=coarsest) to Kibana's
# maxBars parameter. These values control histogram bucketing precision:
# - Lower granularity (1-3): More buckets, finer detail, slower queries
# - Higher granularity (5-7): Fewer buckets, coarser detail, faster queries
# Values are calibrated to match Kibana's native granularity slider behavior
GRANULARITY_TO_BARS = {
    1: 1,  # Finest: Maximum detail
    2: 167.5,
    3: 334,
    4: 499.5,  # Medium: Balanced
    5: 666,
    6: 833.5,
    7: 1000,  # Coarsest: Minimum buckets
}


def _resolve_order_by(
    sort: Sort | None,
    kbn_column_label_to_id: dict[str, str],
    kbn_metric_column_by_id: Mapping[str, KbnLensMetricColumnTypes],
) -> KbnLensTermsOrderBy:
    """Resolve ordering for terms dimensions.

    Args:
        sort: The sort configuration from the dimension.
        kbn_column_label_to_id: Mapping of column labels to their IDs.
        kbn_metric_column_by_id: Mapping of column IDs to metric columns.

    Returns:
        KbnLensTermsOrderBy: The resolved order_by configuration.

    Raises:
        ValueError: If the sort column is not found in the metric columns.

    """
    if sort is not None:
        # Look up by label first, then by column ID directly
        if sort.by in kbn_column_label_to_id:
            column_id = kbn_column_label_to_id[sort.by]
        elif sort.by in kbn_metric_column_by_id:
            column_id = sort.by
        else:
            msg = f"Sort column '{sort.by}' not found in available metric columns"
            raise ValueError(msg)
        metric = kbn_metric_column_by_id[column_id]
        if metric.operationType in ('formula', 'static_value'):
            return KbnLensTermsOrderBy(
                type='alphabetical',
                fallback=True,
            )

        return KbnLensTermsOrderBy(
            type='column',
            columnId=column_id,
        )

    if len(kbn_metric_column_by_id) > 0:
        # Default to ordering by first metric column if it's not a formula
        # Formula columns cannot be used for aggregation ordering in Elasticsearch
        first_metric_id = next(iter(kbn_metric_column_by_id.keys()))
        first_metric = kbn_metric_column_by_id[first_metric_id]

        if first_metric.operationType in ('formula', 'static_value'):
            # Formula and static value columns are computed post-aggregation,
            # use alphabetical ordering.
            return KbnLensTermsOrderBy(
                type='alphabetical',
                fallback=True,
            )

        # Non-formula metrics can be used for ordering
        return KbnLensTermsOrderBy(
            type='column',
            columnId=first_metric_id,
        )

    # No metrics available, fall back to alphabetical
    return KbnLensTermsOrderBy(
        type='alphabetical',
        fallback=True,
    )


def compile_lens_dimension(
    dimension: LensDimensionTypes,
    *,
    kbn_metric_column_by_id: Mapping[str, KbnLensMetricColumnTypes],
) -> tuple[str, KbnLensDimensionColumnTypes]:
    """Compile a single LensDimensionTypes object into its Kibana view model.

    Args:
        dimension (LensDimensionTypes): The LensDimensionTypes object to compile.
        kbn_metric_column_by_id (Mapping[str, KbnLensMetricColumnTypes]): A mapping of compiled KbnLensFieldMetricColumn objects.

    Returns:
        tuple[str, KbnLensDimensionColumnTypes]: A tuple containing the dimension ID and the compiled Kibana view model.

    """
    kbn_column_label_to_id = {column.label: column_id for column_id, column in kbn_metric_column_by_id.items()}

    custom_label = True if dimension.label is not None else None

    if isinstance(dimension, LensDateHistogramDimension):
        dimension_id = dimension.get_id()

        return dimension_id, KbnLensDateHistogramDimensionColumn(
            label=dimension.label if dimension.label is not None else dimension.field,
            customLabel=custom_label,
            dataType='date',
            operationType='date_histogram',
            sourceField=dimension.field,
            scale='interval',
            params=KbnLensDateHistogramDimensionColumnParams(
                interval=dimension.minimum_interval if dimension.minimum_interval is not None else 'auto',
                includeEmptyRows=True,
                dropPartials=not default_true(dimension.partial_intervals),
            ),
        )
    if isinstance(dimension, LensTermsDimension):
        dimension_id = dimension.get_id()
        # Use 3 as the default size to match Kibana's behavior when size is not specified
        effective_size = dimension.size if dimension.size is not None else 3
        label = dimension.label if dimension.label is not None else f'Top {effective_size} values of {dimension.field}'

        order_by = _resolve_order_by(
            sort=dimension.sort,
            kbn_column_label_to_id=kbn_column_label_to_id,
            kbn_metric_column_by_id=kbn_metric_column_by_id,
        )

        return dimension_id, KbnLensTermsDimensionColumn(
            label=label,
            customLabel=custom_label,
            dataType='string',
            operationType='terms',
            scale='ordinal',
            sourceField=dimension.field,
            params=KbnLensTermsDimensionColumnParams(
                size=effective_size,
                orderBy=order_by,
                orderDirection=dimension.sort.direction if dimension.sort is not None else 'desc',
                otherBucket=default_true(dimension.show_other_bucket),
                missingBucket=default_false(dimension.include_missing_values),
                parentFormat=KbnLensTermsParentFormat(id='terms'),
                include=dimension.include if dimension.include is not None else [],
                exclude=dimension.exclude if dimension.exclude is not None else [],
                includeIsRegex=dimension.include_is_regex if dimension.include_is_regex is not None else False,
                excludeIsRegex=dimension.exclude_is_regex if dimension.exclude_is_regex is not None else False,
                secondaryFields=None,
            ),
        )
    if isinstance(dimension, LensMultiTermsDimension):
        primary_field = dimension.fields[0]
        secondary_fields = dimension.fields[1:]
        dimension_id = dimension.get_id()
        # Use 3 as the default size to match Kibana's behavior when size is not specified
        effective_size = dimension.size if dimension.size is not None else 3

        # Generate label
        if dimension.label is not None:
            label = dimension.label
        else:
            num_others = len(secondary_fields)
            if num_others == 1:
                label = f'Top values of {primary_field} + 1 other'
            else:
                label = f'Top values of {primary_field} + {num_others} others'

        order_by = _resolve_order_by(
            sort=dimension.sort,
            kbn_column_label_to_id=kbn_column_label_to_id,
            kbn_metric_column_by_id=kbn_metric_column_by_id,
        )

        return dimension_id, KbnLensTermsDimensionColumn(
            label=label,
            customLabel=custom_label,
            dataType='string',
            operationType='terms',
            scale='ordinal',
            sourceField=primary_field,
            params=KbnLensTermsDimensionColumnParams(
                size=effective_size,
                orderBy=order_by,
                orderDirection=dimension.sort.direction if dimension.sort is not None else 'desc',
                otherBucket=default_true(dimension.show_other_bucket),
                missingBucket=default_false(dimension.include_missing_values),
                parentFormat=KbnLensTermsParentFormat(id='multi_terms'),
                include=dimension.include if dimension.include is not None else [],
                exclude=dimension.exclude if dimension.exclude is not None else [],
                includeIsRegex=dimension.include_is_regex if dimension.include_is_regex is not None else False,
                excludeIsRegex=dimension.exclude_is_regex if dimension.exclude_is_regex is not None else False,
                secondaryFields=secondary_fields,
            ),
        )
    if isinstance(dimension, LensFiltersDimension):
        dimension_id = dimension.get_id()
        return dimension_id, KbnLensFiltersDimensionColumn(
            label=dimension.label if dimension.label is not None else 'Filters',
            customLabel=custom_label,
            dataType='string',
            operationType='filters',
            scale='ordinal',
            params=KbnLensFiltersDimensionColumnParams(
                filters=[
                    KbnLensFiltersFilter(label=f.label if f.label is not None else '', input=compile_nonesql_query(f.query))
                    for f in dimension.filters
                ]
            ),
        )

    # This check is necessary even though it appears redundant to type checkers
    # because dimension could be a more specific subclass at runtime
    if isinstance(dimension, LensIntervalsDimension):  # pyright: ignore[reportUnnecessaryIsInstance]
        dimension_id = dimension.get_id()

        if dimension.intervals is None:
            return dimension_id, KbnLensIntervalsDimensionColumn(
                label=dimension.label if dimension.label is not None else dimension.field,
                customLabel=custom_label,
                sourceField=dimension.field,
                params=KbnLensIntervalsDimensionColumnParams(
                    includeEmptyRows=default_true(dimension.include_empty_intervals),
                    type='histogram',
                    ranges=[KbnLensIntervalsRange(from_value=0, to_value=1000, label='')],
                    maxBars=GRANULARITY_TO_BARS[dimension.granularity] if dimension.granularity is not None else 'auto',
                ),
            )
        ranges = [
            KbnLensIntervalsRange(
                from_value=interval.from_value,
                to_value=interval.to_value,
                label=interval.label if interval.label is not None else '',
            )
            for interval in dimension.intervals
        ]
        return dimension_id, KbnLensCustomIntervalsDimensionColumn(
            label=dimension.label if dimension.label is not None else dimension.field,
            customLabel=custom_label,
            sourceField=dimension.field,
            params=KbnLensCustomIntervalsDimensionColumnParams(
                ranges=ranges,
                maxBars=499.5,
                parentFormat=KbnLensCustomIntervalsDimensionColumnParentFormat(
                    id='range',
                    params=KbnLensCustomIntervalsDimensionColumnParentFormatParams(
                        template='arrow_right',
                        replaceInfinity=True,
                    ),
                ),
            ),
        )

    # All LensDimensionTypes have been handled above, this is unreachable
    # but kept for type safety in case new types are added
    msg = f'Unsupported dimension type: {type(dimension)}'  # pyright: ignore[reportUnreachable]
    raise NotImplementedError(msg)


def compile_lens_dimensions(
    dimensions: Sequence[LensDimensionTypes],
    *,
    kbn_metric_column_by_id: Mapping[str, KbnLensMetricColumnTypes],
) -> dict[str, KbnLensDimensionColumnTypes]:
    """Compile a sequence of LensDimensionTypes objects into their Kibana view model representation.

    Args:
        dimensions (Sequence[LensDimensionTypes]): The sequence of LensDimensionTypes objects to compile.
        kbn_metric_column_by_id (Mapping[str, KbnLensMetricColumnTypes]): A mapping of compiled KbnLensFieldMetricColumn objects.

    Returns:
        dict[str, KbnLensDimensionColumnTypes]: A dictionary of compiled KbnLensDimensionColumnTypes objects.

    """
    return dict(compile_lens_dimension(dimension, kbn_metric_column_by_id=kbn_metric_column_by_id) for dimension in dimensions)
