"""Chart rendering for python-pptx."""

from __future__ import annotations

import copy

from lxml import etree
from pptx.chart.data import BubbleChartData as PptxBubbleChartData
from pptx.chart.data import CategoryChartData
from pptx.chart.data import XyChartData as PptxXyChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_DATA_LABEL_POSITION, XL_LEGEND_POSITION
from pptx.oxml.ns import qn
from pptx.slide import Slide
from pptx.util import Inches, Pt

from pptx_mcp.engine.shapes import parse_hex_color
from pptx_mcp.models.slides import (
    AxisFormat,
    BubbleChartDataModel,
    ChartElement,
    ChartType,
    DataLabelConfig,
    LegendConfig,
    Position,
    XyChartData,
)
from pptx_mcp.utils.logging import get_logger

logger = get_logger("engine.charts")

CHART_TYPE_MAP: dict[ChartType, XL_CHART_TYPE] = {
    ChartType.BAR: XL_CHART_TYPE.BAR_CLUSTERED,
    ChartType.LINE: XL_CHART_TYPE.LINE_MARKERS,
    ChartType.PIE: XL_CHART_TYPE.PIE,
    ChartType.COLUMN: XL_CHART_TYPE.COLUMN_CLUSTERED,
    ChartType.AREA: XL_CHART_TYPE.AREA,
    ChartType.SCATTER: XL_CHART_TYPE.XY_SCATTER,
    ChartType.RADAR: XL_CHART_TYPE.RADAR,
    ChartType.DOUGHNUT: XL_CHART_TYPE.DOUGHNUT,
    ChartType.STACKED_BAR: XL_CHART_TYPE.BAR_STACKED,
    ChartType.STACKED_COLUMN: XL_CHART_TYPE.COLUMN_STACKED,
}

# Chart types that use point-based coloring (like pie)
_POINT_COLOR_TYPES = {ChartType.PIE, ChartType.DOUGHNUT}


def default_chart_position() -> Position:
    """Default chart position centered on the slide."""
    return Position(left=1.5, top=2.0, width=10.0, height=5.0)


def add_chart_to_slide(
    slide: Slide,
    element: ChartElement,
    chart_colors: list[str] | None = None,
) -> None:
    """Add a chart to a slide, dispatching to the appropriate renderer."""
    if element.chart_type == ChartType.SCATTER:
        _add_scatter_chart(slide, element, chart_colors)
    elif element.chart_type == ChartType.BUBBLE:
        _add_bubble_chart(slide, element, chart_colors)
    elif element.chart_type == ChartType.COMBO:
        _add_combo_chart(slide, element, chart_colors)
    else:
        _add_category_chart(slide, element, chart_colors)


# ------------------------------------------------------------------
# Category-based chart (bar, line, pie, column, area, radar, etc.)
# ------------------------------------------------------------------


def _add_category_chart(
    slide: Slide,
    element: ChartElement,
    chart_colors: list[str] | None = None,
) -> None:
    """Render a category-based chart (all types except scatter/bubble/combo)."""
    pos = element.position or default_chart_position()
    xl_chart_type = CHART_TYPE_MAP.get(element.chart_type, XL_CHART_TYPE.COLUMN_CLUSTERED)

    chart_data = CategoryChartData()
    chart_data.categories = element.data.categories

    for series in element.data.series:
        chart_data.add_series(series.name, series.values)

    chart_frame = slide.shapes.add_chart(
        xl_chart_type,
        Inches(pos.left),
        Inches(pos.top),
        Inches(pos.width),
        Inches(pos.height),
        chart_data,
    )

    chart = chart_frame.chart
    _apply_common_formatting(chart, element, chart_colors)


# ------------------------------------------------------------------
# Scatter chart
# ------------------------------------------------------------------


def _add_scatter_chart(
    slide: Slide,
    element: ChartElement,
    chart_colors: list[str] | None = None,
) -> None:
    """Render an XY scatter chart."""
    pos = element.position or default_chart_position()
    chart_data = PptxXyChartData()

    if element.xy_data:
        for s in element.xy_data.series:
            series = chart_data.add_series(s.name)
            for pt in s.points:
                series.add_data_point(pt.x, pt.y)
    elif element.data:
        # Fallback: treat categories as x-values, series values as y-values
        x_values = []
        for cat in element.data.categories:
            try:
                x_values.append(float(cat))
            except (ValueError, TypeError):
                x_values.append(float(len(x_values)))
        for s in element.data.series:
            series = chart_data.add_series(s.name)
            for x, y in zip(x_values, s.values):
                series.add_data_point(x, y)

    chart_frame = slide.shapes.add_chart(
        XL_CHART_TYPE.XY_SCATTER,
        Inches(pos.left),
        Inches(pos.top),
        Inches(pos.width),
        Inches(pos.height),
        chart_data,
    )

    chart = chart_frame.chart
    _apply_common_formatting(chart, element, chart_colors)


# ------------------------------------------------------------------
# Bubble chart
# ------------------------------------------------------------------


def _add_bubble_chart(
    slide: Slide,
    element: ChartElement,
    chart_colors: list[str] | None = None,
) -> None:
    """Render a bubble chart."""
    pos = element.position or default_chart_position()
    chart_data = PptxBubbleChartData()

    if element.bubble_data:
        for s in element.bubble_data.series:
            series = chart_data.add_series(s.name)
            for pt in s.points:
                series.add_data_point(pt.x, pt.y, pt.size)
    elif element.data:
        # Fallback: treat categories as x, values as y, uniform bubble size
        x_values = []
        for cat in element.data.categories:
            try:
                x_values.append(float(cat))
            except (ValueError, TypeError):
                x_values.append(float(len(x_values)))
        for s in element.data.series:
            series = chart_data.add_series(s.name)
            for x, y in zip(x_values, s.values):
                series.add_data_point(x, y, 1.0)

    chart_frame = slide.shapes.add_chart(
        XL_CHART_TYPE.BUBBLE,
        Inches(pos.left),
        Inches(pos.top),
        Inches(pos.width),
        Inches(pos.height),
        chart_data,
    )

    chart = chart_frame.chart
    _apply_common_formatting(chart, element, chart_colors)


# ------------------------------------------------------------------
# Combo chart (primary column/bar + secondary line overlay)
# ------------------------------------------------------------------


def _add_combo_chart(
    slide: Slide,
    element: ChartElement,
    chart_colors: list[str] | None = None,
) -> None:
    """Render a combo chart with primary bars and secondary line overlay.

    Creates a COLUMN_CLUSTERED chart, then uses XML manipulation to move
    the last series into a separate line chart group. If secondary_axis
    is True, that line series is placed on a secondary value axis.
    """
    pos = element.position or default_chart_position()

    chart_data = CategoryChartData()
    chart_data.categories = element.data.categories

    for series in element.data.series:
        chart_data.add_series(series.name, series.values)

    chart_frame = slide.shapes.add_chart(
        XL_CHART_TYPE.COLUMN_CLUSTERED,
        Inches(pos.left),
        Inches(pos.top),
        Inches(pos.width),
        Inches(pos.height),
        chart_data,
    )

    chart = chart_frame.chart

    # Convert the last series to a line chart via XML
    if len(element.data.series) >= 2:
        _convert_last_series_to_line(chart, element.secondary_axis)

    _apply_common_formatting(chart, element, chart_colors)


def _convert_last_series_to_line(chart: object, secondary_axis: bool) -> None:
    """Move the last series from the bar/column group into a new line chart group."""
    try:
        chart_space = chart._chartSpace  # type: ignore[attr-defined]
        plot_area = chart_space.find(qn("c:chart")).find(qn("c:plotArea"))

        # Find the column chart group
        bar_chart = plot_area.find(qn("c:barChart"))
        if bar_chart is None:
            return

        # Get all series in the bar chart
        all_ser = bar_chart.findall(qn("c:ser"))
        if len(all_ser) < 2:
            return

        # Remove last series from bar chart
        last_ser = all_ser[-1]
        bar_chart.remove(last_ser)

        # Create a lineChart element
        line_chart = etree.SubElement(plot_area, qn("c:lineChart"))
        grouping = etree.SubElement(line_chart, qn("c:grouping"))
        grouping.set("val", "standard")

        # Clone the series into the line chart
        line_ser = copy.deepcopy(last_ser)
        line_chart.append(line_ser)

        # Add marker
        marker = etree.SubElement(line_chart, qn("c:marker"))
        marker_val = etree.SubElement(marker, qn("c:val"))
        marker_val.text = "1"

        if secondary_axis:
            # Create secondary value axis
            existing_val_axes = plot_area.findall(qn("c:valAx"))
            existing_cat_axes = plot_area.findall(qn("c:catAx"))

            # Get primary axis IDs
            primary_val_ax_id = "1"
            primary_cat_ax_id = "0"
            if existing_val_axes:
                ax_id_el = existing_val_axes[0].find(qn("c:axId"))
                if ax_id_el is not None:
                    primary_val_ax_id = ax_id_el.get("val", "1")
            if existing_cat_axes:
                ax_id_el = existing_cat_axes[0].find(qn("c:axId"))
                if ax_id_el is not None:
                    primary_cat_ax_id = ax_id_el.get("val", "0")

            sec_cat_ax_id = "10"
            sec_val_ax_id = "11"

            # Point bar chart axes at primary
            _ensure_ax_id_refs(bar_chart, primary_cat_ax_id, primary_val_ax_id)

            # Point line chart at secondary axes
            _ensure_ax_id_refs(line_chart, sec_cat_ax_id, sec_val_ax_id)

            # Add secondary category axis (hidden)
            sec_cat_ax = etree.SubElement(plot_area, qn("c:catAx"))
            _add_axis_xml(sec_cat_ax, sec_cat_ax_id, sec_val_ax_id, delete=True)

            # Add secondary value axis (visible, on right)
            sec_val_ax = etree.SubElement(plot_area, qn("c:valAx"))
            _add_axis_xml(sec_val_ax, sec_val_ax_id, sec_cat_ax_id, delete=False, position="r")
        else:
            # Share the same axes as the bar chart
            bar_ax_ids = bar_chart.findall(qn("c:axId"))
            for ax_ref in bar_ax_ids:
                ax_copy = copy.deepcopy(ax_ref)
                line_chart.append(ax_copy)

    except Exception as e:
        logger.warning("Could not convert series to line overlay: %s", e)


def _ensure_ax_id_refs(chart_group: etree._Element, cat_ax_id: str, val_ax_id: str) -> None:
    """Ensure a chart group has axId references to the given axes."""
    # Remove existing axId refs
    for old_ref in chart_group.findall(qn("c:axId")):
        chart_group.remove(old_ref)
    ax1 = etree.SubElement(chart_group, qn("c:axId"))
    ax1.set("val", cat_ax_id)
    ax2 = etree.SubElement(chart_group, qn("c:axId"))
    ax2.set("val", val_ax_id)


def _add_axis_xml(
    ax_element: etree._Element,
    ax_id: str,
    cross_ax_id: str,
    delete: bool = False,
    position: str = "b",
) -> None:
    """Populate a catAx or valAx XML element with minimal required children."""
    axid = etree.SubElement(ax_element, qn("c:axId"))
    axid.set("val", ax_id)

    scaling = etree.SubElement(ax_element, qn("c:scaling"))
    orient = etree.SubElement(scaling, qn("c:orientation"))
    orient.set("val", "minMax")

    del_el = etree.SubElement(ax_element, qn("c:delete"))
    del_el.set("val", "1" if delete else "0")

    ax_pos = etree.SubElement(ax_element, qn("c:axPos"))
    ax_pos.set("val", position)

    cross = etree.SubElement(ax_element, qn("c:crossAx"))
    cross.set("val", cross_ax_id)


# ------------------------------------------------------------------
# Shared formatting helpers
# ------------------------------------------------------------------

_LEGEND_POSITION_MAP: dict[str, XL_LEGEND_POSITION] = {
    "bottom": XL_LEGEND_POSITION.BOTTOM,
    "right": XL_LEGEND_POSITION.RIGHT,
    "left": XL_LEGEND_POSITION.LEFT,
    "top": XL_LEGEND_POSITION.TOP,
    "corner": XL_LEGEND_POSITION.CORNER,
}


_DATA_LABEL_POSITION_MAP: dict[str, XL_DATA_LABEL_POSITION] = {
    "outside_end": XL_DATA_LABEL_POSITION.OUTSIDE_END,
    "center": XL_DATA_LABEL_POSITION.CENTER,
    "best_fit": XL_DATA_LABEL_POSITION.BEST_FIT,
    "inside_end": XL_DATA_LABEL_POSITION.INSIDE_END,
    "inside_base": XL_DATA_LABEL_POSITION.INSIDE_BASE,
}


def _apply_axis_format(axis: object, fmt: AxisFormat) -> None:
    """Apply formatting to a chart axis."""
    if fmt.minimum is not None:
        axis.minimum_scale = fmt.minimum  # type: ignore[attr-defined]
    if fmt.maximum is not None:
        axis.maximum_scale = fmt.maximum  # type: ignore[attr-defined]
    if fmt.major_unit is not None:
        axis.major_unit = fmt.major_unit  # type: ignore[attr-defined]
    if fmt.number_format is not None:
        axis.tick_labels.number_format = fmt.number_format  # type: ignore[attr-defined]
    if fmt.title is not None:
        axis.has_title = True  # type: ignore[attr-defined]
        axis.axis_title.text_frame.paragraphs[0].text = fmt.title  # type: ignore[attr-defined]
    if fmt.has_gridlines is not None:
        axis.has_major_gridlines = fmt.has_gridlines  # type: ignore[attr-defined]


def _apply_data_labels(plot: object, cfg: DataLabelConfig) -> None:
    """Apply data label configuration to a chart plot."""
    plot.has_data_labels = True  # type: ignore[attr-defined]
    data_labels = plot.data_labels  # type: ignore[attr-defined]
    data_labels.show_value = cfg.show_value
    data_labels.show_percentage = cfg.show_percentage
    data_labels.show_category_name = cfg.show_category
    if cfg.position:
        pos = _DATA_LABEL_POSITION_MAP.get(cfg.position)
        if pos is not None:
            data_labels.label_position = pos
    if cfg.number_format:
        data_labels.number_format = cfg.number_format
    if cfg.font_size:
        data_labels.font.size = Pt(cfg.font_size)


def _apply_legend_config(chart: object, cfg: LegendConfig) -> None:
    """Apply legend configuration to a chart."""
    chart.has_legend = True  # type: ignore[attr-defined]
    pos = _LEGEND_POSITION_MAP.get(cfg.position, XL_LEGEND_POSITION.BOTTOM)
    chart.legend.position = pos  # type: ignore[attr-defined]
    chart.legend.include_in_layout = cfg.include_in_layout  # type: ignore[attr-defined]


def _apply_common_formatting(
    chart: object,
    element: ChartElement,
    chart_colors: list[str] | None = None,
) -> None:
    """Apply title, legend, colors, data labels, axes, and style to a chart."""
    if element.title:
        chart.has_title = True  # type: ignore[attr-defined]
        chart.chart_title.text_frame.paragraphs[0].text = element.title  # type: ignore[attr-defined]
        chart.chart_title.text_frame.paragraphs[0].font.size = Pt(14)  # type: ignore[attr-defined]

    # Legend: prefer explicit LegendConfig, fall back to show_legend flag
    if element.legend:
        _apply_legend_config(chart, element.legend)
    elif element.show_legend and element.chart_type not in _POINT_COLOR_TYPES:
        chart.has_legend = True  # type: ignore[attr-defined]
        chart.legend.position = XL_LEGEND_POSITION.BOTTOM  # type: ignore[attr-defined]
        chart.legend.include_in_layout = False  # type: ignore[attr-defined]
    elif not element.show_legend:
        chart.has_legend = False  # type: ignore[attr-defined]

    if chart_colors and element.chart_type in _POINT_COLOR_TYPES:
        _apply_pie_colors(chart, chart_colors)
    elif chart_colors:
        _apply_series_colors(chart, chart_colors)

    # Data labels: prefer explicit DataLabelConfig, fall back to show_data_labels flag
    if element.data_labels:
        try:
            plot = chart.plots[0]  # type: ignore[attr-defined]
            _apply_data_labels(plot, element.data_labels)
        except (IndexError, AttributeError) as e:
            logger.warning("Could not apply data labels: %s", e)
    elif element.show_data_labels:
        for series in chart.series:  # type: ignore[attr-defined]
            series.has_data_labels = True
            series.data_labels.font.size = Pt(10)
            series.data_labels.show_value = True

    # Axis formatting
    if element.value_axis:
        try:
            _apply_axis_format(chart.value_axis, element.value_axis)  # type: ignore[attr-defined]
        except (AttributeError, ValueError) as e:
            logger.warning("Could not apply value axis format: %s", e)
    if element.category_axis:
        try:
            _apply_axis_format(chart.category_axis, element.category_axis)  # type: ignore[attr-defined]
        except (AttributeError, ValueError) as e:
            logger.warning("Could not apply category axis format: %s", e)

    # Chart style (1-48)
    if element.chart_style is not None:
        chart.style = element.chart_style  # type: ignore[attr-defined]

    # Smooth lines for line charts
    if element.smooth_lines:
        try:
            for series in chart.series:  # type: ignore[attr-defined]
                series.smooth = True
        except (AttributeError, TypeError) as e:
            logger.warning("Could not apply smooth lines: %s", e)


def _apply_series_colors(chart: object, colors: list[str]) -> None:
    """Apply colors to chart series."""
    for i, series in enumerate(chart.series):  # type: ignore[attr-defined]
        if i < len(colors):
            series.format.fill.solid()
            series.format.fill.fore_color.rgb = parse_hex_color(colors[i])


def _apply_pie_colors(chart: object, colors: list[str]) -> None:
    """Apply colors to pie/doughnut chart points."""
    try:
        series = chart.series[0]  # type: ignore[attr-defined]
        for i, point in enumerate(series.points):
            if i < len(colors):
                point.format.fill.solid()
                point.format.fill.fore_color.rgb = parse_hex_color(colors[i])
    except (IndexError, AttributeError) as e:
        logger.warning("Could not apply pie chart colors: %s", e)
