"""Base chart block factory for shared chart functionality."""

from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import pandas as pd

from yera.events.blocks.base.base import _BlockFactory
from yera.events.models import BarChartData, LineChartData


def _is_hex_color(value: str) -> bool:
    """Check if a string is a valid hex color (starts with # and has valid hex digits).

    Raises ValueError if the value is not a valid hex color format.
    """
    if not value.startswith("#"):
        raise ValueError(f"Hex color must start with '#', got: {value!r}")
    if len(value) not in (4, 7):  # #rgb or #rrggbb format
        raise ValueError(
            f"Hex color must be 4 or 7 characters (#rgb or #rrggbb), got {len(value)} characters: {value!r}"
        )
    # Check that all characters after # are valid hex digits (0-9, a-f, A-F)
    hex_digits = set("0123456789abcdefABCDEF")
    invalid_chars = [c for c in value[1:] if c not in hex_digits]
    if invalid_chars:
        raise ValueError(
            f"Hex color contains invalid characters: {invalid_chars}. "
            f"Only 0-9, a-f, A-F are allowed after '#'. Got: {value!r}"
        )
    return True


def _get_default_colors(count: int) -> list[str]:
    """Get default colors for the given number of series."""
    default_palette = ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF"]
    return [default_palette[i % len(default_palette)] for i in range(count)]


def _check_if_colour_column_contains_hex_colors(colour_values: pd.Series) -> bool:
    """Check if all values in a colour column are valid hex colors."""
    try:
        all(_is_hex_color(str(val)) for val in colour_values)
        return True
    except ValueError:
        return False


def _build_x_colour_to_y_value_map(
    df: pd.DataFrame, x_column: str, colour_column: str, y_column: str
) -> dict[tuple[str, str], float | None]:
    """Build a mapping from (x_category, colour) to y_value."""
    value_map = {}
    for _, row in df.iterrows():
        x_val = str(row[x_column])
        colour_val = str(row[colour_column])
        y_val = row[y_column]
        value_map[(x_val, colour_val)] = float(y_val) if y_val is not None else None
    return value_map


def _extract_y_values_for_x_and_colour_categories(
    x_categories: list[str],
    colour_categories: list[str],
    value_map: dict[tuple[str, str], float | None],
) -> list[list[float | None]]:
    """Extract y_values matrix from value map for given x and colour categories."""
    y_values = []
    for x_cat in x_categories:
        row_values = [
            value_map.get((x_cat, colour_cat)) for colour_cat in colour_categories
        ]
        y_values.append(row_values)
    return y_values


def _simplify_to_single_value_per_x_category(
    y_values: list[list[float | None]],
) -> list[list[float | None]]:
    """Simplify y_values when each x-category has only one non-None value."""
    has_single_value_per_x = all(
        sum(1 for v in row if v is not None) == 1 for row in y_values
    )
    if has_single_value_per_x:
        return [[next(v for v in row if v is not None)] for row in y_values]
    return y_values


def _get_y_value_for_x_category(
    df: pd.DataFrame, x_column: str, x_category: str, y_column: str
) -> float | None:
    """Get the y value for a specific x category (assumes single match)."""
    mask = df[x_column] == x_category
    matching_rows = df[mask]
    if len(matching_rows) > 0:
        val = matching_rows[y_column].iloc[0]
        return float(val) if val is not None else None
    return None


def _get_y_values_for_x_categories_with_colour_grouping(
    df: pd.DataFrame,
    x_column: str,
    y_column: str,
    colour_column: str,
    x_categories: list[str],
    colour_categories: list[str],
) -> list[list[float | None]]:
    """Get y_values matrix grouped by x and colour categories."""
    y_values = []
    for x_cat in x_categories:
        row_values = []
        for colour_cat in colour_categories:
            mask = (df[x_column] == x_cat) & (df[colour_column] == colour_cat)
            matching_rows = df[mask]
            if len(matching_rows) > 0:
                val = matching_rows[y_column].iloc[0]
                row_values.append(float(val) if val is not None else None)
            else:
                row_values.append(None)
        y_values.append(row_values)
    return y_values


@dataclass
class _ChartConversionData:
    """Intermediate data structure for chart conversion."""

    x_label: str | None
    x_categories: list[str] | None
    y_label: str | None
    y_values: list[list[float | None]]
    colour_label: str | None
    colour_categories: list[str]
    colour_values: list[str]


class _ChartBlockFactory(_BlockFactory, ABC):
    """Base factory for chart blocks that process DataFrames."""

    def _convert_default_format(
        self,
        df: pd.DataFrame,
    ) -> _ChartConversionData:
        """Convert DataFrame using default format: index as x, all columns as y series."""
        x_categories = [str(idx) for idx in df.index]
        y_columns = list(df.columns)
        y_values = [
            [
                float(df.iloc[i][col]) if df.iloc[i][col] is not None else None
                for col in y_columns
            ]
            for i in range(len(df))
        ]
        return _ChartConversionData(
            x_label=None,
            x_categories=x_categories,
            y_label=None,
            y_values=y_values,
            colour_label=None,
            colour_categories=y_columns,
            colour_values=_get_default_colors(len(y_columns)),
        )

    def _convert_long_format(
        self,
        df: pd.DataFrame,
        x_column: str,
        y_column: str,
        colour: str | Sequence[str] | None,
    ) -> _ChartConversionData:
        """Convert DataFrame in long format: x and y columns specified, optional colour grouping."""
        x_categories = [str(val) for val in df[x_column].unique()]

        if isinstance(colour, str):
            colour_label = colour
            unique_colour_values = df[colour].unique()

            if _check_if_colour_column_contains_hex_colors(unique_colour_values):
                colour_categories = [str(val) for val in unique_colour_values]
                colour_values = colour_categories.copy()

                value_map = _build_x_colour_to_y_value_map(
                    df, x_column, colour, y_column
                )
                y_values = _extract_y_values_for_x_and_colour_categories(
                    x_categories, colour_categories, value_map
                )
                y_values = _simplify_to_single_value_per_x_category(y_values)
            else:
                colour_categories = sorted([str(val) for val in unique_colour_values])
                colour_values = _get_default_colors(len(colour_categories))
                y_values = _get_y_values_for_x_categories_with_colour_grouping(
                    df, x_column, y_column, colour, x_categories, colour_categories
                )
        else:
            colour_label = None
            colour_categories = [y_column]
            colour_values = _get_default_colors(1)
            y_values = [
                [_get_y_value_for_x_category(df, x_column, x_cat, y_column)]
                for x_cat in x_categories
            ]

        return _ChartConversionData(
            x_label=x_column,
            x_categories=x_categories,
            y_label=y_column,
            y_values=y_values,
            colour_label=colour_label,
            colour_categories=colour_categories,
            colour_values=colour_values,
        )

    def _convert_wide_format(
        self,
        df: pd.DataFrame,
        x_column: str | None,
        y_columns: Sequence[str],
        colour: str | Sequence[str] | None,
    ) -> _ChartConversionData:
        """Convert DataFrame in wide format: x column specified (or None), multiple y columns."""
        if x_column is not None:
            x_categories = [str(val) for val in df[x_column].unique()]
            x_label = x_column
        else:
            # When x is None but y is specified, use index
            x_categories = [str(idx) for idx in df.index]
            x_label = None

        if isinstance(colour, Sequence) and not isinstance(colour, str):
            colour_values = list(colour)
            colour_categories = list(y_columns)
        else:
            colour_values = _get_default_colors(len(y_columns))
            colour_categories = list(y_columns)

        y_values = []
        if x_column is not None:
            for x_cat in x_categories:
                mask = df[x_column] == x_cat
                matching_rows = df[mask]
                if len(matching_rows) > 0:
                    row = matching_rows.iloc[0]
                    y_values.append(
                        [
                            float(row[col]) if row[col] is not None else None
                            for col in y_columns
                        ]
                    )
                else:
                    y_values.append([None] * len(y_columns))
        else:
            # Use index for x
            for i in range(len(df)):
                row = df.iloc[i]
                y_values.append(
                    [
                        float(row[col]) if row[col] is not None else None
                        for col in y_columns
                    ]
                )

        return _ChartConversionData(
            x_label=x_label,
            x_categories=x_categories,
            y_label=None,
            y_values=y_values,
            colour_label=None,
            colour_categories=colour_categories,
            colour_values=colour_values,
        )

    @abstractmethod
    def _create_chart_data_from_conversion(
        self,
        conversion_data: _ChartConversionData,
        chart_specific_params: dict,
    ) -> BarChartData | LineChartData:
        """Convert intermediate data to specific chart data type. Override in subclasses."""


__all__ = ["_ChartBlockFactory"]
