"""Design rules for presentation review."""

from __future__ import annotations

from pptx_mcp.models.brand import BrandConfig
from pptx_mcp.models.review import ReviewCategory, ReviewIssue, Severity
from pptx_mcp.models.slides import SlideDefinition, SlideType


def check_word_count(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check that slides don't have too many words."""
    issues: list[ReviewIssue] = []
    all_text = _gather_text(slide)
    word_count = len(all_text.split())

    max_words = brand.layout_rules.max_words_per_slide
    if word_count > max_words:
        issues.append(ReviewIssue(
            severity=Severity.WARNING,
            category=ReviewCategory.CONTENT,
            message=f"Slide has {word_count} words (max {max_words}). Consider reducing text.",
            slide_index=slide_index,
            suggestion="Break content into multiple slides or use visuals instead of text.",
        ))

    if word_count > max_words * 1.5:
        issues[-1].severity = Severity.ERROR

    return issues


def check_bullet_count(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check that slides don't have too many bullets."""
    issues: list[ReviewIssue] = []
    max_bullets = brand.layout_rules.max_bullets_per_slide

    if len(slide.bullets) > max_bullets:
        issues.append(ReviewIssue(
            severity=Severity.WARNING,
            category=ReviewCategory.CONTENT,
            message=(
                f"Slide has {len(slide.bullets)} bullets (max {max_bullets}). "
                "Audiences lose focus with too many points."
            ),
            slide_index=slide_index,
            suggestion="Group related points or split across slides.",
        ))

    for i, bullet in enumerate(slide.bullets):
        words = len(bullet.split())
        max_bw = brand.layout_rules.max_words_per_bullet
        if words > max_bw:
            issues.append(ReviewIssue(
                severity=Severity.INFO,
                category=ReviewCategory.CONTENT,
                message=f"Bullet {i + 1} has {words} words (max {max_bw}).",
                slide_index=slide_index,
                element_description=f'Bullet: "{bullet[:50]}..."' if len(bullet) > 50 else f'Bullet: "{bullet}"',
                suggestion="Shorten to a concise phrase.",
            ))

    return issues


def check_title_presence(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check that each slide has a title."""
    if brand.layout_rules.title_every_slide and not slide.title:
        if slide.slide_type not in (SlideType.BLANK,):
            return [ReviewIssue(
                severity=Severity.WARNING,
                category=ReviewCategory.LAYOUT,
                message="Slide is missing a title.",
                slide_index=slide_index,
                suggestion="Add a descriptive title to guide the audience.",
            )]
    return []


def check_empty_slide(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check for slides with no content."""
    all_text = _gather_text(slide)
    if not all_text.strip() and not slide.elements and slide.slide_type != SlideType.BLANK:
        return [ReviewIssue(
            severity=Severity.ERROR,
            category=ReviewCategory.CONTENT,
            message="Slide appears to be empty.",
            slide_index=slide_index,
            suggestion="Add content or remove this slide.",
        )]
    return []


def check_column_balance(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check that multi-column slides have balanced content."""
    issues: list[ReviewIssue] = []

    if slide.slide_type in (SlideType.TWO_COLUMN, SlideType.COMPARISON) and slide.columns:
        lengths = [len(col.bullets) for col in slide.columns]
        if lengths and max(lengths) > 0:
            ratio = min(lengths) / max(lengths) if max(lengths) > 0 else 1.0
            if ratio < 0.3:
                issues.append(ReviewIssue(
                    severity=Severity.INFO,
                    category=ReviewCategory.WHITESPACE,
                    message="Column content is significantly unbalanced.",
                    slide_index=slide_index,
                    suggestion="Try to balance the amount of content across columns.",
                ))

    if slide.slide_type == SlideType.PROS_CONS:
        pro_count = len(slide.pros)
        con_count = len(slide.cons)
        if pro_count > 0 and con_count > 0:
            ratio = min(pro_count, con_count) / max(pro_count, con_count)
            if ratio < 0.3:
                issues.append(ReviewIssue(
                    severity=Severity.INFO,
                    category=ReviewCategory.WHITESPACE,
                    message=(
                        f"Pros ({pro_count}) and Cons ({con_count}) are significantly unbalanced."
                    ),
                    slide_index=slide_index,
                    suggestion="Consider balancing or using a different layout.",
                ))

    return issues


def check_narrative_flow(
    slides: list[SlideDefinition],
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check deck-level narrative flow."""
    issues: list[ReviewIssue] = []

    if not slides:
        return [ReviewIssue(
            severity=Severity.ERROR,
            category=ReviewCategory.NARRATIVE,
            message="Presentation has no slides.",
            suggestion="Add slides to create a presentation.",
        )]

    # First slide should be a title
    if slides[0].slide_type != SlideType.TITLE:
        issues.append(ReviewIssue(
            severity=Severity.SUGGESTION,
            category=ReviewCategory.NARRATIVE,
            message="Presentation does not start with a title slide.",
            slide_index=0,
            suggestion="Start with a title slide to set context.",
        ))

    # Check for consecutive identical types (monotony)
    for i in range(1, len(slides)):
        if (
            slides[i].slide_type == slides[i - 1].slide_type
            and slides[i].slide_type == SlideType.CONTENT
        ):
            if i >= 2 and slides[i - 2].slide_type == SlideType.CONTENT:
                issues.append(ReviewIssue(
                    severity=Severity.INFO,
                    category=ReviewCategory.NARRATIVE,
                    message=f"Three consecutive content slides (slides {i - 1}-{i + 1}). Consider varying layout.",
                    slide_index=i,
                    suggestion="Insert a visual, chart, or section break for variety.",
                ))

    # Deck length
    if len(slides) > 25:
        issues.append(ReviewIssue(
            severity=Severity.WARNING,
            category=ReviewCategory.NARRATIVE,
            message=f"Presentation has {len(slides)} slides. Executive decks work best under 20.",
            suggestion="Consider moving detailed content to an appendix.",
        ))

    return issues


def check_consistency(
    slides: list[SlideDefinition],
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check for cross-slide consistency issues."""
    issues: list[ReviewIssue] = []

    # Collect title patterns
    titles = [s.title for s in slides if s.title]
    if titles:
        cases = set()
        for t in titles:
            if t == t.upper():
                cases.add("upper")
            elif t == t.title():
                cases.add("title")
            else:
                cases.add("mixed")
        if len(cases) > 1:
            issues.append(ReviewIssue(
                severity=Severity.INFO,
                category=ReviewCategory.CONSISTENCY,
                message=f"Inconsistent title casing detected: {', '.join(sorted(cases))}.",
                suggestion="Use consistent title casing throughout the deck.",
            ))

    return issues


def _gather_text(slide: SlideDefinition) -> str:
    """Gather all text content from a slide definition."""
    parts: list[str] = []
    if slide.title:
        parts.append(slide.title)
    if slide.subtitle:
        parts.append(slide.subtitle)
    if slide.body:
        parts.append(slide.body)
    parts.extend(slide.bullets)
    parts.extend(slide.pros)
    parts.extend(slide.cons)
    for col in slide.columns:
        if col.title:
            parts.append(col.title)
        parts.extend(col.bullets)
    for card in slide.cards:
        parts.append(card.title)
        if card.body:
            parts.append(card.body)
        parts.extend(card.bullets)
    for kpi in slide.kpis:
        parts.append(kpi.value)
        parts.append(kpi.label)
    for icon in slide.icons:
        parts.append(icon.title)
        if icon.description:
            parts.append(icon.description)
    for member in slide.team_members:
        parts.append(member.name)
        if member.role:
            parts.append(member.role)
        if member.bio:
            parts.append(member.bio)
    if slide.callout:
        if slide.callout.title:
            parts.append(slide.callout.title)
        parts.append(slide.callout.body)
    for stage in slide.funnel_stages:
        parts.append(stage.label)
        if stage.description:
            parts.append(stage.description)
    for item in slide.agenda_items:
        parts.append(item.title)
        if item.description:
            parts.append(item.description)
    if slide.swot:
        parts.extend(slide.swot.strengths)
        parts.extend(slide.swot.weaknesses)
        parts.extend(slide.swot.opportunities)
        parts.extend(slide.swot.threats)
    for step in slide.steps:
        parts.append(step.title)
        if step.description:
            parts.append(step.description)
    # Diagram fields (v0.6.0)
    if slide.org_chart:
        def _gather_org(node) -> None:
            parts.append(node.name)
            if node.role:
                parts.append(node.role)
            for child in node.children:
                _gather_org(child)
        _gather_org(slide.org_chart)
    if slide.matrix:
        for q in [slide.matrix.top_left, slide.matrix.top_right,
                   slide.matrix.bottom_left, slide.matrix.bottom_right]:
            parts.append(q.label)
            parts.extend(q.items)
    for layer in slide.pyramid_layers:
        parts.append(layer.label)
        if layer.description:
            parts.append(layer.description)
    if slide.venn:
        for c in slide.venn.circles:
            parts.append(c.label)
            parts.extend(c.items)
        if slide.venn.intersection_label:
            parts.append(slide.venn.intersection_label)
    for bar in slide.waterfall_bars:
        parts.append(bar.label)
    if slide.hub_spoke_center:
        parts.append(slide.hub_spoke_center)
    for spoke in slide.spokes:
        parts.append(spoke.label)
        if spoke.description:
            parts.append(spoke.description)
    for cs in slide.cycle_steps:
        parts.append(cs.label)
        if cs.description:
            parts.append(cs.description)
    for task in slide.gantt_tasks:
        parts.append(task.label)
    parts.extend(slide.gantt_phases)
    return " ".join(parts)


def check_card_count(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check that card/KPI counts don't exceed recommended limits."""
    issues: list[ReviewIssue] = []

    if slide.slide_type == SlideType.CARD_GRID and len(slide.cards) > 6:
        issues.append(ReviewIssue(
            severity=Severity.WARNING,
            category=ReviewCategory.CONTENT,
            message=f"Card grid has {len(slide.cards)} cards (recommended max 6). Audience may struggle to absorb all cards.",
            slide_index=slide_index,
            suggestion="Split into multiple slides or prioritize the most important cards.",
        ))

    if slide.slide_type == SlideType.DASHBOARD and len(slide.kpis) > 8:
        issues.append(ReviewIssue(
            severity=Severity.WARNING,
            category=ReviewCategory.CONTENT,
            message=f"Dashboard has {len(slide.kpis)} KPIs (recommended max 8). Too many metrics dilute focus.",
            slide_index=slide_index,
            suggestion="Focus on the most critical metrics or split into multiple dashboards.",
        ))

    return issues


def check_image_alt_text(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check that images have alt text for accessibility."""
    issues: list[ReviewIssue] = []
    for elem in slide.elements:
        if getattr(elem, "type", None) == "image" and not getattr(elem, "alt_text", None):
            issues.append(ReviewIssue(
                severity=Severity.WARNING,
                category=ReviewCategory.ACCESSIBILITY,
                message="Image missing alt text description for accessibility",
                slide_index=slide_index,
                suggestion="Add descriptive alt text for screen readers.",
            ))
    return issues


def check_font_hierarchy(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Validate that brand font sizes follow a clear hierarchy."""
    if slide_index != 0:
        return []
    fonts = brand.fonts
    if not (fonts.title_size > fonts.heading_size > fonts.body_size > fonts.caption_size):
        return [ReviewIssue(
            severity=Severity.INFO,
            category=ReviewCategory.TYPOGRAPHY,
            message=(
                f"Font hierarchy not strictly decreasing: title={fonts.title_size}, "
                f"heading={fonts.heading_size}, body={fonts.body_size}, caption={fonts.caption_size}."
            ),
            slide_index=slide_index,
            suggestion="Ensure title > heading > body > caption font sizes for clear visual hierarchy.",
        )]
    return []


def check_accent_overuse(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check for excessive use of accent color."""
    if not brand.layout_rules.use_accent_sparingly:
        return []
    accent = brand.colors.accent.lstrip("#").upper()
    count = 0
    for elem in slide.elements:
        fill = getattr(elem, "fill_color", None)
        if fill and fill.lstrip("#").upper() == accent:
            count += 1
    if count > 2:
        return [ReviewIssue(
            severity=Severity.INFO,
            category=ReviewCategory.COLOR,
            message=f"Accent color used on {count} elements; sparingly is preferred.",
            slide_index=slide_index,
            suggestion="Reserve accent color for 1-2 focal elements per slide.",
        )]
    return []


def check_chart_brand_colors(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Suggest defining chart colors when charts are present but none configured."""
    has_chart = any(
        getattr(elem, "type", None) == "chart" for elem in slide.elements
    )
    if has_chart and not brand.colors.chart_colors:
        return [ReviewIssue(
            severity=Severity.SUGGESTION,
            category=ReviewCategory.COLOR,
            message="Chart present but no brand chart_colors defined.",
            slide_index=slide_index,
            suggestion="Define chart_colors in brand config for consistent data visualization.",
        )]
    return []


def check_logo_presence(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Check for logo image when brand logo is configured."""
    if slide_index != 0:
        return []
    if not brand.logo or not brand.logo.path:
        return []
    has_logo_image = any(
        getattr(elem, "type", None) == "image"
        for elem in slide.elements
    )
    if not has_logo_image:
        return [ReviewIssue(
            severity=Severity.INFO,
            category=ReviewCategory.BRAND,
            message="Brand logo configured but no image element found on title slide.",
            slide_index=slide_index,
            suggestion="Add the brand logo to reinforce identity.",
        )]
    return []


def check_shadow_border_conflict(
    slide: SlideDefinition,
    slide_index: int,
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Detect thick borders combined with shadows."""
    if brand.shape_style.shadow_preset == "none":
        return []
    issues: list[ReviewIssue] = []
    for elem in slide.elements:
        bw = getattr(elem, "border_width", None)
        if bw is not None and bw >= 2.0:
            issues.append(ReviewIssue(
                severity=Severity.SUGGESTION,
                category=ReviewCategory.LAYOUT,
                message="Thick border combined with shadow may create visual noise.",
                slide_index=slide_index,
                suggestion="Use either a prominent border or a shadow, not both.",
            ))
    return issues


def check_visual_rhythm(
    slides: list[SlideDefinition],
    brand: BrandConfig,
) -> list[ReviewIssue]:
    """Flag 3+ consecutive slides of the same type for visual monotony."""
    issues: list[ReviewIssue] = []
    skip_types = {SlideType.SECTION, SlideType.TITLE, SlideType.BLANK}
    filtered = [
        (i, s.slide_type) for i, s in enumerate(slides) if s.slide_type not in skip_types
    ]
    if len(filtered) < 3:
        return issues

    run_start = 0
    for j in range(1, len(filtered)):
        if filtered[j][1] != filtered[run_start][1]:
            run_length = j - run_start
            if run_length >= 3:
                start_idx = filtered[run_start][0]
                end_idx = filtered[j - 1][0]
                stype = filtered[run_start][1].value
                issues.append(ReviewIssue(
                    severity=Severity.SUGGESTION,
                    category=ReviewCategory.NARRATIVE,
                    message=f"Slides {start_idx + 1}-{end_idx + 1} are all {stype}; vary layout for visual rhythm.",
                    suggestion="Insert a visual, chart, or section break for variety.",
                ))
            run_start = j

    # Check final run
    run_length = len(filtered) - run_start
    if run_length >= 3:
        start_idx = filtered[run_start][0]
        end_idx = filtered[-1][0]
        stype = filtered[run_start][1].value
        issues.append(ReviewIssue(
            severity=Severity.SUGGESTION,
            category=ReviewCategory.NARRATIVE,
            message=f"Slides {start_idx + 1}-{end_idx + 1} are all {stype}; vary layout for visual rhythm.",
            suggestion="Insert a visual, chart, or section break for variety.",
        ))

    return issues


# Registry of all per-slide rule functions
SLIDE_RULES = [
    check_word_count,
    check_bullet_count,
    check_title_presence,
    check_empty_slide,
    check_column_balance,
    check_card_count,
    check_image_alt_text,
    check_font_hierarchy,
    check_accent_overuse,
    check_chart_brand_colors,
    check_logo_presence,
    check_shadow_border_conflict,
]

# Registry of all deck-level rule functions
DECK_RULES = [
    check_narrative_flow,
    check_consistency,
    check_visual_rhythm,
]
