"""PPTX parser — reads existing .pptx files into structured PresentationDefinition."""

from __future__ import annotations

from pathlib import Path
from typing import Optional

from pptx import Presentation

from pptx_mcp.models.slides import (
    ChartData,
    ChartDataSeries,
    ChartElement,
    ChartType,
    ColumnContent,
    Paragraph,
    Position,
    PresentationDefinition,
    PresentationMetadata,
    ShapeElement,
    ShapeType,
    SlideDefinition,
    SlideElement,
    SlideType,
    TableCell,
    TableElement,
    TextBox,
    TextRun,
    TextStyle,
)
from pptx_mcp.utils.errors import ParseError
from pptx_mcp.utils.logging import get_logger

logger = get_logger("engine.parser")


def _emu_to_inches(emu: int | None) -> float:
    """Convert EMU to inches."""
    if emu is None:
        return 0.0
    return round(emu / 914400, 3)


def _extract_position(shape: object) -> Position:
    """Extract position from a pptx shape object."""
    return Position(
        left=_emu_to_inches(getattr(shape, "left", 0)),
        top=_emu_to_inches(getattr(shape, "top", 0)),
        width=_emu_to_inches(getattr(shape, "width", 0)),
        height=_emu_to_inches(getattr(shape, "height", 0)),
    )


def _extract_text_style(run: object) -> Optional[TextStyle]:
    """Extract text style from a run object."""
    font = getattr(run, "font", None)
    if font is None:
        return None

    color = None
    try:
        rgb = font.color.rgb
        if rgb is not None:
            color = f"#{rgb}"
    except (AttributeError, TypeError):
        pass

    size = None
    if font.size is not None:
        size = int(font.size.pt)

    return TextStyle(
        font_name=font.name,
        font_size=size,
        bold=bool(font.bold),
        italic=bool(font.italic),
        color=color,
    )


def _extract_paragraph(para: object) -> Paragraph:
    """Extract a Paragraph model from a pptx paragraph."""
    runs = []
    for run in para.runs:  # type: ignore[attr-defined]
        style = _extract_text_style(run)
        runs.append(TextRun(text=run.text, style=style))

    alignment = None
    if para.alignment is not None:  # type: ignore[attr-defined]
        align_map = {0: "left", 1: "center", 2: "right", 3: "justify"}
        alignment = align_map.get(int(para.alignment), None)  # type: ignore[attr-defined]

    return Paragraph(
        runs=runs,
        level=para.level if hasattr(para, "level") else 0,  # type: ignore[attr-defined]
        alignment=alignment,
    )


def _classify_slide(shapes: list, slide_index: int) -> SlideType:
    """Heuristic classification of slide type based on shape contents."""
    text_shapes = [s for s in shapes if hasattr(s, "text_frame")]
    chart_shapes = [s for s in shapes if hasattr(s, "has_chart") and s.has_chart]
    table_shapes = [s for s in shapes if hasattr(s, "has_table") and s.has_table]

    if chart_shapes:
        return SlideType.CHART
    if table_shapes:
        return SlideType.TABLE

    total_text = " ".join(s.text_frame.text for s in text_shapes if hasattr(s, "text_frame"))
    word_count = len(total_text.split())

    if slide_index == 0 and word_count < 30:
        return SlideType.TITLE

    if len(text_shapes) == 1 and word_count < 15:
        return SlideType.SECTION

    if len(text_shapes) >= 3:
        lefts = [_emu_to_inches(s.left) for s in text_shapes if hasattr(s, "left")]
        if lefts:
            unique_lefts = len(set(round(l, 0) for l in lefts))
            if unique_lefts >= 2:
                return SlideType.TWO_COLUMN

    return SlideType.CONTENT


def parse_presentation(path: str | Path) -> PresentationDefinition:
    """Parse an existing PPTX file into a PresentationDefinition.

    Args:
        path: Path to the .pptx file.

    Returns:
        Parsed PresentationDefinition.

    Raises:
        ParseError: If the file cannot be read or parsed.
    """
    path = Path(path)
    if not path.exists():
        raise ParseError(f"File not found: {path.name}")

    try:
        prs = Presentation(str(path))
    except Exception as e:
        raise ParseError(f"Failed to open PPTX file '{path.name}'") from e

    metadata = PresentationMetadata(
        title=prs.core_properties.title or path.stem,
        author=prs.core_properties.author,
        subject=prs.core_properties.subject,
        category=prs.core_properties.category,
    )

    slides: list[SlideDefinition] = []

    for slide_index, slide in enumerate(prs.slides):
        try:
            slide_def = _parse_slide(slide, slide_index)
            slides.append(slide_def)
        except Exception as e:
            logger.warning("Failed to parse slide %d: %s", slide_index, e)
            slides.append(SlideDefinition(
                slide_type=SlideType.BLANK,
                speaker_notes="[Parse error on this slide]",
            ))

    width_inches = round(prs.slide_width / 914400, 3) if prs.slide_width else 13.333
    height_inches = round(prs.slide_height / 914400, 3) if prs.slide_height else 7.5

    return PresentationDefinition(
        metadata=metadata,
        slides=slides,
        width_inches=width_inches,
        height_inches=height_inches,
    )


def _parse_slide(slide: object, slide_index: int) -> SlideDefinition:
    """Parse a single slide into a SlideDefinition."""
    shapes = list(slide.shapes)  # type: ignore[attr-defined]
    slide_type = _classify_slide(shapes, slide_index)

    title = None
    body_parts: list[str] = []
    bullets: list[str] = []
    elements: list[SlideElement] = []

    # Extract speaker notes
    notes = None
    try:
        if slide.has_notes_slide:  # type: ignore[attr-defined]
            notes_slide = slide.notes_slide  # type: ignore[attr-defined]
            notes = notes_slide.notes_text_frame.text.strip() or None
    except Exception:
        pass

    for shape in shapes:
        # Charts
        if hasattr(shape, "has_chart") and shape.has_chart:
            chart_elem = _parse_chart(shape)
            if chart_elem:
                elements.append(chart_elem)
            continue

        # Tables
        if hasattr(shape, "has_table") and shape.has_table:
            table_elem = _parse_table(shape)
            if table_elem:
                elements.append(table_elem)
            continue

        # Text shapes
        if hasattr(shape, "text_frame"):
            tf = shape.text_frame
            text = tf.text.strip()

            if not text:
                continue

            # Heuristic: first large text is the title
            if title is None and len(text) < 100:
                first_run = None
                for para in tf.paragraphs:
                    if para.runs:
                        first_run = para.runs[0]
                        break
                if first_run and first_run.font.size and first_run.font.size.pt >= 24:
                    title = text
                    continue
                elif title is None and _emu_to_inches(shape.top) < 1.5:
                    title = text
                    continue

            # Check for bullet lists
            has_bullets = False
            for para in tf.paragraphs:
                if para.text.strip():
                    pPr = para._p.find(
                        "{http://schemas.openxmlformats.org/drawingml/2006/main}pPr"
                    )
                    if pPr is not None:
                        buChar = pPr.find(
                            "{http://schemas.openxmlformats.org/drawingml/2006/main}buChar"
                        )
                        if buChar is not None:
                            has_bullets = True
                            bullets.append(para.text.strip())

            if not has_bullets:
                paragraphs = []
                for para in tf.paragraphs:
                    p = _extract_paragraph(para)
                    paragraphs.append(p)

                if paragraphs:
                    elements.append(TextBox(
                        paragraphs=paragraphs,
                        position=_extract_position(shape),
                    ))
                    body_parts.append(text)

        # Other shapes
        elif hasattr(shape, "shape_type"):
            elements.append(ShapeElement(
                shape_type=ShapeType.RECTANGLE,
                text=getattr(shape, "text", None),
                position=_extract_position(shape),
            ))

    return SlideDefinition(
        slide_type=slide_type,
        title=title,
        body=body_parts[0] if body_parts and slide_type == SlideType.CONTENT else None,
        bullets=bullets,
        elements=elements,
        speaker_notes=notes,
    )


def _parse_chart(shape: object) -> Optional[ChartElement]:
    """Parse a chart shape into a ChartElement."""
    try:
        chart = shape.chart  # type: ignore[attr-defined]
        chart_type_val = chart.chart_type

        type_map = {
            51: ChartType.BAR,   # BAR_CLUSTERED
            4: ChartType.LINE,   # LINE_MARKERS
            5: ChartType.PIE,    # PIE
            57: ChartType.COLUMN,  # COLUMN_CLUSTERED
            1: ChartType.AREA,   # AREA
        }
        chart_type = type_map.get(int(chart_type_val), ChartType.BAR)

        categories = [str(c) for c in chart.plots[0].categories]
        series_list = []
        for series in chart.series:
            values = list(series.values)
            series_list.append(ChartDataSeries(
                name=str(series.tx.text) if hasattr(series, "tx") and series.tx else "Series",
                values=[float(v) if v is not None else 0.0 for v in values],
            ))

        title = None
        if chart.has_title:
            title = chart.chart_title.text_frame.text

        return ChartElement(
            chart_type=chart_type,
            data=ChartData(categories=categories, series=series_list),
            title=title,
            position=_extract_position(shape),
        )
    except Exception as e:
        logger.warning("Failed to parse chart: %s", e)
        return None


def _parse_table(shape: object) -> Optional[TableElement]:
    """Parse a table shape into a TableElement."""
    try:
        table = shape.table  # type: ignore[attr-defined]
        rows = []
        for row in table.rows:
            cells = []
            for cell in row.cells:
                text = cell.text_frame.text.strip() if cell.text_frame else ""
                cells.append(TableCell(text=text))
            rows.append(cells)

        return TableElement(
            rows=rows,
            position=_extract_position(shape),
            header_row=True,
        )
    except Exception as e:
        logger.warning("Failed to parse table: %s", e)
        return None
