"""Shape and diagram rendering for python-pptx."""

from __future__ import annotations

from typing import Optional, Sequence

from lxml import etree
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import PP_ALIGN
from pptx.oxml.ns import qn
from pptx.slide import Slide
from pptx.util import Inches, Pt

from pptx_mcp.models.slides import (
    ConnectorElement,
    Position,
    ShapeElement,
    ShapeType,
    TextStyle,
)
from pptx_mcp.utils.logging import get_logger

logger = get_logger("engine.shapes")

SHAPE_TYPE_MAP: dict[ShapeType, MSO_SHAPE] = {
    ShapeType.RECTANGLE: MSO_SHAPE.RECTANGLE,
    ShapeType.ROUNDED_RECTANGLE: MSO_SHAPE.ROUNDED_RECTANGLE,
    ShapeType.CIRCLE: MSO_SHAPE.OVAL,
    ShapeType.OVAL: MSO_SHAPE.OVAL,
    ShapeType.ARROW_RIGHT: MSO_SHAPE.RIGHT_ARROW,
    ShapeType.ARROW_LEFT: MSO_SHAPE.LEFT_ARROW,
    ShapeType.ARROW_UP: MSO_SHAPE.UP_ARROW,
    ShapeType.ARROW_DOWN: MSO_SHAPE.DOWN_ARROW,
    ShapeType.CHEVRON: MSO_SHAPE.CHEVRON,
    ShapeType.DIAMOND: MSO_SHAPE.DIAMOND,
    ShapeType.TRIANGLE: MSO_SHAPE.ISOSCELES_TRIANGLE,
    ShapeType.PENTAGON: MSO_SHAPE.PENTAGON,
    ShapeType.HEXAGON: MSO_SHAPE.HEXAGON,
    ShapeType.CALLOUT: MSO_SHAPE.ROUNDED_RECTANGULAR_CALLOUT,
}

ALIGN_MAP = {
    "left": PP_ALIGN.LEFT,
    "center": PP_ALIGN.CENTER,
    "right": PP_ALIGN.RIGHT,
    "justify": PP_ALIGN.JUSTIFY,
}


def parse_hex_color(hex_color: str) -> RGBColor:
    """Convert a hex color string to an RGBColor."""
    color = hex_color.lstrip("#")
    if len(color) != 6:
        raise ValueError(f"Invalid hex color: {hex_color}")
    r, g, b = int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16)
    return RGBColor(r, g, b)


def default_position(index: int) -> Position:
    """Generate a default position for a shape based on its index."""
    col = index % 4
    row = index // 4
    return Position(
        left=1.0 + col * 3.0,
        top=2.0 + row * 2.0,
        width=2.5,
        height=1.5,
    )


def add_shape_to_slide(
    slide: Slide,
    element: ShapeElement,
    index: int = 0,
) -> None:
    """Add a geometric shape to a slide.

    Args:
        slide: The python-pptx Slide object.
        element: Shape definition.
        index: Element index for default positioning.
    """
    pos = element.position or default_position(index)
    mso_shape = SHAPE_TYPE_MAP.get(element.shape_type, MSO_SHAPE.RECTANGLE)

    if element.shape_type == ShapeType.LINE:
        shape = slide.shapes.add_connector(
            1,  # MSO_CONNECTOR_TYPE.STRAIGHT
            Inches(pos.left),
            Inches(pos.top),
            Inches(pos.left + pos.width),
            Inches(pos.top + pos.height),
        )
        if element.border_color:
            shape.line.color.rgb = parse_hex_color(element.border_color)
        if element.border_width:
            shape.line.width = Pt(element.border_width)
        return

    shape = slide.shapes.add_shape(
        mso_shape,
        Inches(pos.left),
        Inches(pos.top),
        Inches(pos.width),
        Inches(pos.height),
    )

    if element.fill_color:
        shape.fill.solid()
        shape.fill.fore_color.rgb = parse_hex_color(element.fill_color)
    else:
        shape.fill.background()

    if element.border_color:
        shape.line.color.rgb = parse_hex_color(element.border_color)
    if element.border_width:
        shape.line.width = Pt(element.border_width)
    else:
        shape.line.fill.background()

    if element.rotation is not None:
        shape.rotation = element.rotation

    if element.text:
        tf = shape.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = element.text
        p.alignment = PP_ALIGN.CENTER
        _apply_text_style(p.runs[0], element.style)

    if element.hyperlink:
        shape.click_action.hyperlink.address = element.hyperlink


def add_connector_to_slide(
    slide: Slide,
    element: ConnectorElement,
    shapes_on_slide: list,
) -> None:
    """Add a connector line between two shapes.

    Args:
        slide: The python-pptx Slide object.
        element: Connector definition.
        shapes_on_slide: List of shape objects already on the slide.
    """
    if element.start_shape_index >= len(shapes_on_slide):
        logger.warning("Connector start index %d out of range", element.start_shape_index)
        return
    if element.end_shape_index >= len(shapes_on_slide):
        logger.warning("Connector end index %d out of range", element.end_shape_index)
        return

    start = shapes_on_slide[element.start_shape_index]
    end = shapes_on_slide[element.end_shape_index]

    start_x = start.left + start.width // 2
    start_y = start.top + start.height // 2
    end_x = end.left + end.width // 2
    end_y = end.top + end.height // 2

    connector = slide.shapes.add_connector(
        1,  # straight connector
        start_x,
        start_y,
        end_x,
        end_y,
    )

    if element.color:
        connector.line.color.rgb = parse_hex_color(element.color)
    if element.width:
        connector.line.width = Pt(element.width)


def _apply_text_style(
    run: object,
    style: Optional[TextStyle],
) -> None:
    """Apply text styling to a run object."""
    if style is None:
        return
    font = run.font  # type: ignore[attr-defined]
    if style.font_name:
        font.name = style.font_name
    if style.font_size:
        font.size = Pt(style.font_size)
    if style.bold:
        font.bold = True
    if style.italic:
        font.italic = True
    if style.underline:
        font.underline = True
    if style.color:
        font.color.rgb = parse_hex_color(style.color)


def apply_gradient_fill(
    shape: object,
    colors: Sequence[str],
    angle: float = 90.0,
    stops: Optional[Sequence[float]] = None,
) -> None:
    """Apply a linear gradient fill to a shape via direct XML manipulation.

    python-pptx doesn't expose gradient fills natively, so we build the
    ``a:gradFill`` XML element directly.

    Args:
        shape: A python-pptx shape object (must have ``._element``).
        colors: Hex color strings (e.g. ``["#1B2A4A", "#3A5A8A"]``).
            At least 2 colors required.
        angle: Gradient angle in degrees (0 = left→right, 90 = top→bottom).
        stops: Gradient stop positions as fractions 0.0–1.0.
            If omitted, colors are evenly spaced.
    """
    if len(colors) < 2:
        raise ValueError("Gradient requires at least 2 colors")

    if stops is None:
        stops = [i / (len(colors) - 1) for i in range(len(colors))]
    if len(stops) != len(colors):
        raise ValueError("stops must have the same length as colors")

    sp = shape._element  # type: ignore[attr-defined]

    # Remove existing fill properties
    spPr = sp.find(qn("a:spPr"))
    if spPr is None:
        spPr = sp.find(qn("p:spPr"))
    if spPr is None:
        return

    for tag in ("a:solidFill", "a:noFill", "a:gradFill", "a:pattFill"):
        for old in spPr.findall(qn(tag)):
            spPr.remove(old)

    # Build gradient fill element
    grad_fill = etree.SubElement(spPr, qn("a:gradFill"))

    gs_lst = etree.SubElement(grad_fill, qn("a:gsLst"))
    for color, stop in zip(colors, stops):
        pos = int(stop * 100000)  # thousandths of a percent
        gs = etree.SubElement(gs_lst, qn("a:gs"))
        gs.set("pos", str(pos))
        srgb = etree.SubElement(gs, qn("a:srgbClr"))
        srgb.set("val", color.lstrip("#"))

    # Linear gradient direction (EMUs: 60000 per degree)
    lin = etree.SubElement(grad_fill, qn("a:lin"))
    lin.set("ang", str(int(angle * 60000)))
    lin.set("scaled", "1")
