from __future__ import annotations

from dataclasses import asdict
from datetime import datetime, timezone
import html
import json
from pathlib import Path

from auraindex.models import ScanResult


def build_report_dict(result: ScanResult, *, dialect: str, config: dict[str, object]) -> dict[str, object]:
    return {
        "report_version": "0.4",
        "generated_at_utc": datetime.now(timezone.utc).isoformat(),
        "summary": {
            "dialect": dialect,
            "scanned_files": len(result.scanned_files),
            "query_candidates": len(result.candidates),
            "parsed_queries": len(result.parsed_queries),
            "parse_failures": len(result.parse_failures),
            "suggestions": len(result.suggestions),
        },
        "config": config,
        "scan_metadata": result.metadata,
        "warnings": result.warnings,
        "scanned_files": result.scanned_files,
        "candidates": [
            {
                "sql": candidate.sql,
                "confidence": candidate.confidence,
                "context": asdict(candidate.context),
                "metadata": candidate.metadata,
            }
            for candidate in result.candidates
        ],
        "parse_failures": [
            {
                "error": failure.error,
                "sql": failure.query.sql,
                "context": asdict(failure.query.context),
            }
            for failure in result.parse_failures
        ],
        "workload": _workload_to_dict(result),
        "suggestions": [
            {
                "table": suggestion.table,
                "columns": suggestion.columns,
                "index_name": suggestion.index_name,
                "kind": suggestion.kind,
                "score": suggestion.score,
                "confidence": suggestion.confidence,
                "reason": suggestion.reason,
                "evidence": suggestion.evidence,
                "metadata": suggestion.metadata,
                "ddl": suggestion.ddl,
                "needs_manual_review": suggestion.needs_manual_review,
                "orm_snippets": suggestion.orm_snippets,
            }
            for suggestion in result.suggestions
        ],
        "detailed_index_evidence": _detailed_index_evidence(result),
    }


def _display_path(path_value: object) -> str:
    raw = "." if path_value in (None, "") else str(path_value)
    try:
        return str(Path(raw).expanduser().resolve())
    except (OSError, RuntimeError, ValueError):
        return raw


def render_ddl(result: ScanResult) -> str:
    lines = [
        "-- AuraIndex generated index recommendations",
        "-- Review statements before applying in production.",
    ]
    if not result.suggestions:
        lines.append("-- No index candidates matched the configured thresholds.")
    else:
        for suggestion in result.suggestions:
            lines.append(suggestion.ddl)
    return "\n".join(lines) + "\n"


def render_migration(result: ScanResult, *, dialect: str) -> str:
    lines = [
        "-- AuraIndex migration script",
        "-- Review before applying in production.",
        "-- Up",
    ]
    if not result.suggestions:
        lines.append("-- No index candidates matched the configured thresholds.")
    else:
        for suggestion in result.suggestions:
            lines.append(suggestion.ddl)

    lines.extend(["", "-- Down"])
    if not result.suggestions:
        lines.append("-- No index rollback statements generated.")
    else:
        for suggestion in result.suggestions:
            lines.append(_render_drop_index(suggestion.table, suggestion.index_name, dialect))
    return "\n".join(lines) + "\n"


def render_orm_snippets(result: ScanResult) -> str:
    lines = [
        "# AuraIndex ORM Index Snippets",
        "",
        "Review and adapt before applying.",
        "",
    ]
    if not result.suggestions:
        lines.append("No snippets generated.")
        return "\n".join(lines) + "\n"

    for suggestion in result.suggestions:
        lines.append(f"## {suggestion.index_name}")
        lines.append(f"- Table: `{suggestion.table}`")
        lines.append(f"- Columns: `{', '.join(suggestion.columns)}`")
        lines.append(f"- Kind: `{suggestion.kind}`")
        lines.append(f"- Confidence: `{suggestion.confidence}`")
        lines.append("")
        lines.append("```python")
        lines.append(f"# SQLAlchemy\n{suggestion.orm_snippets['sqlalchemy']}")
        lines.append("")
        lines.append(f"# Django\n{suggestion.orm_snippets['django']}")
        lines.append("```")
        lines.append("")

    return "\n".join(lines)


def render_markdown_report(
    result: ScanResult,
    *,
    dialect: str,
    config: dict[str, object],
    suggestion_limit: int | None = None,
    max_parse_failures: int = 20,
) -> str:
    display_path = _display_path(config.get("path"))
    lines = [
        "# AuraIndex Detailed Report",
        "",
        f"- Generated (UTC): `{datetime.now(timezone.utc).isoformat()}`",
        f"- Dialect: `{dialect}`",
        f"- Target path: `{display_path}`",
        f"- Extensions: `{', '.join(config.get('extensions', []))}`",
        "",
        "## Summary",
        "",
        f"- Scanned files: **{len(result.scanned_files)}**",
        f"- Query candidates: **{len(result.candidates)}**",
        f"- Parsed queries: **{len(result.parsed_queries)}**",
        f"- Parse failures: **{len(result.parse_failures)}**",
        f"- Suggested indexes: **{len(result.suggestions)}**",
        "",
        "## Suggested Indexes (Ranked)",
        "",
    ]
    scan_meta = result.metadata or {}

    if not result.suggestions:
        lines.append("No index suggestions met current thresholds.")
        lines.append("")
    else:
        ranked_suggestions = _apply_suggestion_limit(result.suggestions, suggestion_limit)
        if suggestion_limit is not None and len(ranked_suggestions) < len(result.suggestions):
            lines.append(
                f"Showing **{len(ranked_suggestions)}** of **{len(result.suggestions)}** suggestions "
                "(`--limit-suggestions`)."
            )
            lines.append("")
        lines.append("| Rank | Table | Columns | Kind | Score | Confidence | Manual Review |")
        lines.append("|---:|---|---|---|---:|---:|---|")
        for idx, suggestion in enumerate(ranked_suggestions, start=1):
            lines.append(
                f"| {idx} | `{suggestion.table}` | `{', '.join(suggestion.columns)}` | `{suggestion.kind}` | "
                f"{suggestion.score:.2f} | {suggestion.confidence:.2f} | "
                f"{'Yes' if suggestion.needs_manual_review else 'No'} |"
            )
        lines.append("")
        lines.extend(
            [
                "Column guide:",
                "- `Rank`: order after scoring and all enabled adjustments (impact/schema checks/limits).",
                "- `Table`, `Columns`: exact target where index should be created.",
                "- `Kind`: reason class (`filter_key`, `join_key`, `composite_filter`, `where_order_composite`, etc.).",
                "- `Score`: ranking priority from workload evidence + enabled rank adjustments.",
                "- `Confidence`: strength of static evidence for this recommendation.",
                "- `Manual Review`: `Yes` means verify carefully before rollout.",
                "",
            ]
        )

    schema_summary = ((scan_meta.get("schema_checks") or {}).get("summary", {})) if scan_meta else {}
    if schema_summary:
        lines.extend(
            [
                "## Schema-aware Checks",
                "",
                f"- Existing indexes discovered: **{schema_summary.get('discovered_existing_indexes', 0)}**",
                f"- Suggestions kept: **{schema_summary.get('kept_suggestions', len(result.suggestions))}**",
                f"- Suggestions skipped (existing coverage): **{schema_summary.get('skipped_suggestions', 0)}**",
                "",
            ]
        )
        sources = ((scan_meta.get("schema_checks") or {}).get("sources", {})) if scan_meta else {}
        if sources:
            lines.append(
                f"- Existing index sources: code=`{sources.get('code_discovered', 0)}`, "
                f"live_db=`{sources.get('live_discovered', 0)}`"
            )
            lines.append("")

    live_db_summary = scan_meta.get("live_db_introspection") if scan_meta else None
    if live_db_summary:
        lines.extend(
            [
                "## Live Database Introspection",
                "",
                f"- Dialect: `{live_db_summary.get('dialect', 'unknown')}`",
                f"- Connected: **{live_db_summary.get('connected', False)}**",
                f"- Discovered indexes: **{live_db_summary.get('discovered_indexes', 0)}**",
                f"- Host: `{live_db_summary.get('host')}`",
                f"- Database: `{live_db_summary.get('database')}`",
                f"- Schema: `{live_db_summary.get('schema')}`",
                "",
            ]
        )

    explain_summary = scan_meta.get("explain_validation") if scan_meta else None
    if explain_summary:
        lines.extend(
            [
                "## EXPLAIN Validation",
                "",
                f"- Engine: `{explain_summary.get('engine', 'unknown')}`",
                f"- Validated: **{explain_summary.get('validated', 0)}**",
                f"- Errors: **{explain_summary.get('errors', 0)}**",
                f"- Skipped: **{explain_summary.get('skipped', 0)}**",
                "",
            ]
        )

    drift_summary = ((scan_meta.get("schema_drift") or {}).get("summary", {})) if scan_meta else {}
    if drift_summary:
        lines.extend(
            [
                "## Schema Drift Watch",
                "",
                f"- Possibly unused existing indexes: **{drift_summary.get('possibly_unused_existing_indexes', 0)}**",
                f"- Missing index candidates (from current scan): **{drift_summary.get('missing_index_candidates', 0)}**",
                "",
            ]
        )
        drift_details = scan_meta.get("schema_drift", {})
        maybe_unused = drift_details.get("possibly_unused_existing_indexes", [])
        if maybe_unused:
            lines.append("Top possibly unused existing indexes:")
            for item in maybe_unused[:10]:
                lines.append(
                    f"- `{item.get('name')}` on `{item.get('table')}` ({', '.join(item.get('columns', []))}) "
                    f"[reason: {item.get('reason')}]"
                )
            lines.append("")

    advisory_meta = (scan_meta.get("query_advisories") or {}) if scan_meta else {}
    advisory_items = advisory_meta.get("items", [])
    if advisory_items:
        lines.extend(
            [
                "## Query Optimization Advisories",
                "",
                f"- Advisory count: **{advisory_meta.get('count', len(advisory_items))}**",
                "",
            ]
        )
        for item in advisory_items[:25]:
            lines.append(
                f"- `{item.get('file_path')}:{item.get('line')}` [{item.get('severity')}] "
                f"`{item.get('rule_id')}`: {item.get('message')}"
            )
        lines.append("")

    evidence_map = _detailed_index_evidence(result)
    lines.extend(
        [
            "## Index Evidence Details",
            "",
            "Each suggestion includes supporting query locations and usage counters.",
            "",
        ]
    )
    if not evidence_map:
        lines.append("No supporting evidence records were generated.")
        lines.append("")
    else:
        for index_name, details in evidence_map.items():
            lines.append(f"### `{index_name}`")
            lines.append(f"- DDL: `{details['ddl']}`")
            lines.append(f"- Reason: {details['reason']}")
            lines.append(f"- Evidence counters: `{details['evidence']}`")
            lines.append(f"- Support count: **{details['support_count']}**")
            lines.append(f"- Matched query occurrences: **{details['matched_query_count']}**")
            impact = details.get("impact", {})
            if impact:
                lines.append(
                    "- Impact score: "
                    f"level=`{impact.get('impact_level')}`, "
                    f"net={impact.get('net_impact')}, "
                    f"read_benefit={impact.get('read_benefit')}, "
                    f"write_cost={impact.get('write_cost')}, "
                    f"write_ratio={impact.get('write_ratio')}"
                )
            index_type = details.get("index_type", {})
            if index_type:
                lines.append(
                    f"- Index type hint: recommended=`{index_type.get('recommended', 'btree')}` "
                    f"(reason: {index_type.get('reason', 'n/a')})"
                )
                typed_ddl = index_type.get("typed_ddl")
                if typed_ddl:
                    lines.append(f"- Typed DDL variant: `{typed_ddl}`")
            schema = details.get("schema_check", {})
            if schema:
                lines.append(
                    f"- Schema check: status=`{schema.get('status', 'unknown')}`, "
                    f"unique_overlap=`{schema.get('unique_overlap', False)}`, "
                    f"partial_overlap=`{schema.get('partial_overlap', False)}`"
                )
            explain = details.get("explain_validation", {})
            if explain:
                lines.append(f"- EXPLAIN validation: status=`{explain.get('status', 'unknown')}`")
            lines.append("- Supporting query locations:")
            contexts = details.get("supporting_contexts", [])
            if contexts:
                for context in contexts:
                    lines.append(f"  - `{context}`")
            else:
                lines.append("  - None")
            matched_queries = details.get("matched_queries", [])
            if matched_queries:
                lines.append("- Query excerpts:")
                for item in matched_queries[:5]:
                    lines.append(
                        f"  - `{item['file_path']}:{item['line']}` ({item['source_kind']}): "
                        f"`{item['sql_excerpt']}`"
                    )
            lines.append("")

    lines.extend(["## Workload by Table", ""])
    if not result.workload.table_query_count:
        lines.append("No workload statistics available.")
        lines.append("")
    else:
        for table, count in sorted(result.workload.table_query_count.items(), key=lambda item: (-item[1], item[0])):
            lines.append(f"### `{table}`")
            lines.append(f"- Query count: **{count}**")
            usage = result.workload.column_usage.get(table, {})
            if usage:
                top_cols = sorted(
                    usage.items(),
                    key=lambda item: (
                        item[1].where_count + item[1].join_count + item[1].order_count + item[1].group_count,
                        item[0],
                    ),
                    reverse=True,
                )[:12]
                lines.append("- Column signals:")
                for col, stats in top_cols:
                    lines.append(
                        f"  - `{col}`: where={stats.where_count}, join={stats.join_count}, "
                        f"order={stats.order_count}, group={stats.group_count}"
                    )
            else:
                lines.append("- Column signals: none")
            lines.append("")

    lines.extend(["## Parse Failures", ""])
    if not result.parse_failures:
        lines.append("No parse failures.")
        lines.append("")
    else:
        lines.append(
            f"Showing {min(len(result.parse_failures), max_parse_failures)} of {len(result.parse_failures)} failures."
        )
        lines.append("")
        for failure in result.parse_failures[:max_parse_failures]:
            ctx = failure.query.context
            lines.append(f"- `{ctx.file_path}:{ctx.line}`")
            lines.append(f"  - Error: `{failure.error}`")
            lines.append(f"  - SQL: `{failure.query.sql[:240]}`")
            lines.append("")

    if result.warnings:
        lines.extend(["## Warnings", ""])
        for warning in result.warnings:
            lines.append(f"- {warning}")
        lines.append("")

    lines.extend(["## Migration Script (Up/Down)", "", "```sql"])
    lines.append(render_migration(result, dialect=dialect).strip())
    lines.extend(["```", ""])

    return "\n".join(lines)


def render_html_report(
    result: ScanResult,
    *,
    dialect: str,
    config: dict[str, object],
    suggestion_limit: int | None = None,
    max_parse_failures: int = 20,
) -> str:
    generated = datetime.now(timezone.utc).isoformat()
    evidence_map = _detailed_index_evidence(result)
    scan_meta = result.metadata or {}
    display_path = _display_path(config.get("path"))

    def esc(value: object) -> str:
        return html.escape(str(value), quote=True)

    def search_blob_for(suggestion: object) -> str:
        table = getattr(suggestion, "table", "")
        columns = list(getattr(suggestion, "columns", []))
        kind = getattr(suggestion, "kind", "")
        needs_manual_review = bool(getattr(suggestion, "needs_manual_review", False))
        manual_token = "yes" if needs_manual_review else "no"
        tokens = [
            getattr(suggestion, "index_name", ""),
            table,
            kind,
            *columns,
            f"table:{table}",
            f"kind:{kind}",
            f"manual:{manual_token}",
            *(f"column:{column}" for column in columns),
        ]
        return " ".join(str(token).strip().lower() for token in tokens if str(token).strip())

    summary_cards = [
        ("Scanned files", len(result.scanned_files)),
        ("Query candidates", len(result.candidates)),
        ("Parsed queries", len(result.parsed_queries)),
        ("Parse failures", len(result.parse_failures)),
        ("Index suggestions", len(result.suggestions)),
    ]
    summary_html = "".join(
        f"<div class='summary-card'><div class='k'>{esc(key)}</div><div class='v'>{esc(value)}</div></div>"
        for key, value in summary_cards
    )

    ranked_suggestions = _apply_suggestion_limit(result.suggestions, suggestion_limit)
    ranked_rows = []
    for index, suggestion in enumerate(ranked_suggestions, start=1):
        search_blob = search_blob_for(suggestion)
        ranked_rows.append(
            f"<tr class='rank-row' data-search='{esc(search_blob)}'>"
            f"<td>{index}</td>"
            f"<td><code>{esc(suggestion.index_name)}</code></td>"
            f"<td><code>{esc(suggestion.table)}</code></td>"
            f"<td><code>{esc(', '.join(suggestion.columns))}</code></td>"
            f"<td>{esc(suggestion.kind)}</td>"
            f"<td>{suggestion.score:.2f}</td>"
            f"<td>{suggestion.confidence:.2f}</td>"
            f"<td>{'Yes' if suggestion.needs_manual_review else 'No'}</td>"
            "</tr>"
        )
    ranked_table_html = "\n".join(ranked_rows) if ranked_rows else "<tr><td colspan='8'>No suggestions.</td></tr>"
    ranked_scope_html = ""
    if suggestion_limit is not None and len(ranked_suggestions) < len(result.suggestions):
        ranked_scope_html = (
            "<div class='sub'>"
            f"Showing {esc(len(ranked_suggestions))} of {esc(len(result.suggestions))} suggestions "
            "(limited by <code>--limit-suggestions</code>)."
            "</div>"
        )

    ranked_column_guide_html = (
        "<details open>"
        "<summary>How to read ranked table columns</summary>"
        "<ul>"
        "<li><code>#</code>: ranking order after workload scoring and enabled adjustments.</li>"
        "<li><code>Index</code>: suggested index name for migration/DDL.</li>"
        "<li><code>Table</code>: target table where index applies.</li>"
        "<li><code>Columns</code>: indexed column order (important for composites).</li>"
        "<li><code>Kind</code>: recommendation type (<code>filter_key</code>, <code>join_key</code>, "
        "<code>composite_filter</code>, <code>where_order_composite</code>, ...).</li>"
        "<li><code>Score</code>: priority signal used for ranking.</li>"
        "<li><code>Confidence</code>: strength of static evidence (higher = stronger repeated signal).</li>"
        "<li><code>Manual</code>: <code>Yes</code> means human verification is recommended before rollout.</li>"
        "</ul>"
        "</details>"
    )

    search_options: set[str] = set()
    table_filters = sorted({f"table:{suggestion.table.lower()}" for suggestion in result.suggestions})
    kind_filters = sorted({f"kind:{suggestion.kind.lower()}" for suggestion in result.suggestions})
    manual_filters = sorted(
        {f"manual:{'yes' if suggestion.needs_manual_review else 'no'}" for suggestion in result.suggestions}
    )
    for suggestion in result.suggestions:
        search_options.update(
            {
                suggestion.index_name.lower(),
                suggestion.table.lower(),
                suggestion.kind.lower(),
                *[column.lower() for column in suggestion.columns],
                f"table:{suggestion.table.lower()}",
                f"kind:{suggestion.kind.lower()}",
                f"manual:{'yes' if suggestion.needs_manual_review else 'no'}",
                *[f"column:{column.lower()}" for column in suggestion.columns],
            }
        )

    datalist_options_html = "".join(
        f"<option value='{esc(option)}'></option>" for option in sorted(search_options)[:300]
    )
    quick_filter_values = [*manual_filters, *table_filters[:10], *kind_filters[:8]]
    quick_filters_html = ""
    if quick_filter_values:
        quick_filters_html = (
            "<div class='quick-filters'>"
            "<span class='muted'>Quick filters:</span>"
            + "".join(
                f"<button type='button' class='quick-filter' onclick=\"applyQuickFilter('{esc(value)}')\">{esc(value)}</button>"
                for value in quick_filter_values
            )
            + "</div>"
        )

    schema_summary = ((scan_meta.get("schema_checks") or {}).get("summary", {})) if scan_meta else {}
    schema_summary_html = ""
    if schema_summary:
        sources = (scan_meta.get("schema_checks") or {}).get("sources", {})
        sources_html = ""
        if sources:
            sources_html = (
                "<div class='meta'>"
                f"<span class='pill'>code indexes: {esc(sources.get('code_discovered', 0))}</span>"
                f"<span class='pill'>live db indexes: {esc(sources.get('live_discovered', 0))}</span>"
                "</div>"
            )
        schema_summary_html = (
            "<section class='card'>"
            "<h2>Schema-aware checks</h2>"
            "<div class='meta'>"
            f"<span class='pill'>existing indexes: {esc(schema_summary.get('discovered_existing_indexes', 0))}</span>"
            f"<span class='pill'>kept suggestions: {esc(schema_summary.get('kept_suggestions', 0))}</span>"
            f"<span class='pill'>skipped as covered: {esc(schema_summary.get('skipped_suggestions', 0))}</span>"
            "</div>"
            f"{sources_html}"
            "</section>"
        )

    live_db_summary_html = ""
    live_db_summary = scan_meta.get("live_db_introspection") if scan_meta else None
    if isinstance(live_db_summary, dict):
        live_db_summary_html = (
            "<section class='card'>"
            "<h2>Live database introspection</h2>"
            "<div class='meta'>"
            f"<span class='pill'>dialect: {esc(live_db_summary.get('dialect', 'unknown'))}</span>"
            f"<span class='pill'>connected: {esc(live_db_summary.get('connected', False))}</span>"
            f"<span class='pill'>discovered indexes: {esc(live_db_summary.get('discovered_indexes', 0))}</span>"
            f"<span class='pill'>host: {esc(live_db_summary.get('host'))}</span>"
            f"<span class='pill'>database: {esc(live_db_summary.get('database'))}</span>"
            f"<span class='pill'>schema: {esc(live_db_summary.get('schema'))}</span>"
            "</div>"
            "</section>"
        )

    explain_summary = scan_meta.get("explain_validation") if scan_meta else None
    explain_summary_html = ""
    if explain_summary:
        explain_summary_html = (
            "<section class='card'>"
            "<h2>EXPLAIN validation</h2>"
            "<div class='meta'>"
            f"<span class='pill'>engine: {esc(explain_summary.get('engine', 'unknown'))}</span>"
            f"<span class='pill'>validated: {esc(explain_summary.get('validated', 0))}</span>"
            f"<span class='pill'>errors: {esc(explain_summary.get('errors', 0))}</span>"
            f"<span class='pill'>skipped: {esc(explain_summary.get('skipped', 0))}</span>"
            "</div>"
            "</section>"
        )

    drift_summary_html = ""
    drift_meta = scan_meta.get("schema_drift") if scan_meta else None
    if isinstance(drift_meta, dict):
        summary = drift_meta.get("summary", {})
        if summary:
            top_unused = drift_meta.get("possibly_unused_existing_indexes", [])
            unused_html = ""
            if top_unused:
                unused_html = (
                    "<details><summary>Possibly unused existing indexes (top)</summary><ul>"
                    + "".join(
                        f"<li><code>{esc(item.get('name'))}</code> on <code>{esc(item.get('table'))}</code> "
                        f"({esc(', '.join(item.get('columns', [])))}): {esc(item.get('reason'))}</li>"
                        for item in top_unused[:10]
                    )
                    + "</ul></details>"
                )
            drift_summary_html = (
                "<section class='card'>"
                "<h2>Schema drift watch</h2>"
                "<div class='meta'>"
                f"<span class='pill'>possibly unused existing indexes: {esc(summary.get('possibly_unused_existing_indexes', 0))}</span>"
                f"<span class='pill'>missing index candidates: {esc(summary.get('missing_index_candidates', 0))}</span>"
                "</div>"
                f"{unused_html}"
                "</section>"
            )

    advisory_summary_html = ""
    advisory_meta = scan_meta.get("query_advisories") if scan_meta else None
    if isinstance(advisory_meta, dict) and advisory_meta.get("items"):
        items = advisory_meta.get("items", [])
        advisory_summary_html = (
            "<section class='card'>"
            "<h2>Query optimization advisories</h2>"
            "<div class='meta'>"
            f"<span class='pill'>advisories: {esc(advisory_meta.get('count', len(items)))}</span>"
            "</div>"
            "<ul>"
            + "".join(
                f"<li><code>{esc(item.get('file_path'))}:{esc(item.get('line'))}</code> "
                f"<span class='pill'>{esc(item.get('severity'))}</span> "
                f"<code>{esc(item.get('rule_id'))}</code>: {esc(item.get('message'))}</li>"
                for item in items[:30]
            )
            + "</ul>"
            "</section>"
        )

    cards: list[str] = []
    for idx, suggestion in enumerate(result.suggestions, start=1):
        details = evidence_map.get(suggestion.index_name, {})
        detail_id = f"index_{idx}"
        ddl_id = f"{detail_id}_ddl"
        orm_id = f"{detail_id}_orm"
        evidence_id = f"{detail_id}_evidence"
        search_blob = search_blob_for(suggestion)
        support_contexts = details.get("supporting_contexts", [])
        matched_queries = details.get("matched_queries", [])
        support_count = details.get("support_count", 0)
        matched_query_count = details.get("matched_query_count", 0)
        impact = details.get("impact", {}) if isinstance(details.get("impact"), dict) else {}
        index_type = details.get("index_type", {}) if isinstance(details.get("index_type"), dict) else {}
        schema = details.get("schema_check", {}) if isinstance(details.get("schema_check"), dict) else {}
        explain = details.get("explain_validation", {}) if isinstance(details.get("explain_validation"), dict) else {}

        contexts_html = (
            "<ul>"
            + "".join(f"<li><code>{esc(context)}</code></li>" for context in support_contexts)
            + "</ul>"
            if support_contexts
            else "<p class='muted'>No direct source locations captured.</p>"
        )
        matched_queries_html = (
            "<div class='query-grid'>"
            + "".join(
                "<div class='query-item'>"
                f"<div><code>{esc(item['file_path'])}:{esc(item['line'])}</code> "
                f"<span class='pill'>{esc(item['source_kind'])}</span></div>"
                f"<pre>{esc(item['sql_excerpt'])}</pre>"
                "</div>"
                for item in matched_queries[:20]
            )
            + "</div>"
            if matched_queries
            else "<p class='muted'>No query excerpts captured.</p>"
        )
        orm_block = (
            f"# SQLAlchemy\n{suggestion.orm_snippets['sqlalchemy']}\n\n"
            f"# Django\n{suggestion.orm_snippets['django']}"
        )
        typed_ddl_section = ""
        if index_type.get("typed_ddl"):
            typed_ddl_section = (
                "<details><summary>Typed DDL variant</summary>"
                f"<pre>{esc(index_type.get('typed_ddl'))}</pre>"
                "</details>"
            )
        impact_html = ""
        if impact:
            impact_html = (
                "<div class='meta'>"
                f"<span class='pill'>impact: {esc(impact.get('impact_level', 'unknown'))}</span>"
                f"<span class='pill'>net: {esc(impact.get('net_impact', 0))}</span>"
                f"<span class='pill'>read benefit: {esc(impact.get('read_benefit', 0))}</span>"
                f"<span class='pill'>write cost: {esc(impact.get('write_cost', 0))}</span>"
                f"<span class='pill'>write ratio: {esc(impact.get('write_ratio', 0))}</span>"
                "</div>"
            )
        index_type_html = ""
        if index_type:
            index_type_html = (
                "<div class='meta'>"
                f"<span class='pill'>index type hint: {esc(index_type.get('recommended', 'btree'))}</span>"
                f"<span class='pill'>{esc(index_type.get('reason', ''))}</span>"
                "</div>"
            )
        schema_html = ""
        if schema:
            schema_html = (
                "<div class='meta'>"
                f"<span class='pill'>schema status: {esc(schema.get('status', 'unknown'))}</span>"
                f"<span class='pill'>unique overlap: {esc(schema.get('unique_overlap', False))}</span>"
                f"<span class='pill'>partial overlap: {esc(schema.get('partial_overlap', False))}</span>"
                "</div>"
            )
        explain_html = ""
        if explain:
            explain_html = (
                "<div class='meta'>"
                f"<span class='pill'>explain status: {esc(explain.get('status', 'unknown'))}</span>"
                f"<span class='pill'>engine: {esc(explain.get('engine', 'n/a'))}</span>"
                "</div>"
            )
        cards.append(
            f"<section class='card index-card' data-search='{esc(search_blob)}'>"
            f"<h3>{idx}. <code>{esc(suggestion.index_name)}</code></h3>"
            "<div class='meta'>"
            f"<span class='pill'>table: {esc(suggestion.table)}</span>"
            f"<span class='pill'>columns: {esc(', '.join(suggestion.columns))}</span>"
            f"<span class='pill'>kind: {esc(suggestion.kind)}</span>"
            f"<span class='pill'>score: {suggestion.score:.2f}</span>"
            f"<span class='pill'>confidence: {suggestion.confidence:.2f}</span>"
            f"<span class='pill'>{'manual review' if suggestion.needs_manual_review else 'auto candidate'}</span>"
            "</div>"
            f"<p><strong>Why needed:</strong> {esc(details.get('reason', suggestion.reason))}</p>"
            "<div class='meta'>"
            f"<span class='pill'>support count: {esc(support_count)}</span>"
            f"<span class='pill'>matched query occurrences: {esc(matched_query_count)}</span>"
            "</div>"
            f"{impact_html}"
            f"{index_type_html}"
            f"{schema_html}"
            f"{explain_html}"
            "<details open><summary>Evidence counters and code locations</summary>"
            f"<pre id='{evidence_id}'>{esc(json.dumps(details.get('evidence', suggestion.evidence), indent=2))}</pre>"
            f"<button class='copy' onclick=\"copyFromId('{evidence_id}', this)\">Copy evidence JSON</button>"
            "<h4>Code locations where index helps</h4>"
            f"{contexts_html}"
            "</details>"
            "<details open><summary>Matched query excerpts</summary>"
            f"{matched_queries_html}"
            "</details>"
            "<details><summary>DDL (SQL index script)</summary>"
            f"<pre id='{ddl_id}'>{esc(suggestion.ddl)}</pre>"
            f"<button class='copy' onclick=\"copyFromId('{ddl_id}', this)\">Copy DDL</button>"
            "</details>"
            f"{typed_ddl_section}"
            "<details><summary>ORM snippets</summary>"
            f"<pre id='{orm_id}'>{esc(orm_block)}</pre>"
            f"<button class='copy' onclick=\"copyFromId('{orm_id}', this)\">Copy ORM snippet</button>"
            "</details>"
            "</section>"
        )
    cards_html = "\n".join(cards) if cards else "<p>No index suggestions generated.</p>"
    migration_sql = render_migration(result, dialect=dialect).strip()

    parse_failure_html = ""
    if result.parse_failures:
        blocks: list[str] = []
        for failure in result.parse_failures[:max_parse_failures]:
            context = failure.query.context
            blocks.append(
                "<div class='parse-failure'>"
                f"<div><code>{esc(context.file_path)}:{context.line}</code></div>"
                f"<div class='muted'>{esc(failure.error)}</div>"
                f"<pre>{esc(failure.query.sql[:500])}</pre>"
                "</div>"
            )
        parse_failure_html = (
            "<section class='card'>"
            f"<h2>Parse failures ({len(result.parse_failures)})</h2>"
            + "".join(blocks)
            + "</section>"
        )

    warnings_html = ""
    if result.warnings:
        warning_items = "".join(f"<li>{esc(warning)}</li>" for warning in result.warnings)
        warnings_html = (
            "<section class='card'>"
            f"<h2>Warnings ({len(result.warnings)})</h2>"
            f"<ul>{warning_items}</ul>"
            "</section>"
        )

    return f"""<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>AuraIndex Report</title>
  <style>
    :root {{
      color-scheme: dark;
      --bg: #0b1020;
      --panel: #121a33;
      --text: #e7ecff;
      --muted: #97a5d6;
      --line: #2b3b77;
      --accent: #7ea2ff;
      --ok: #6be38f;
      --warn: #ffca65;
    }}
    body {{ margin: 0; font-family: Inter, Segoe UI, Arial, sans-serif; background: var(--bg); color: var(--text); }}
    header {{ padding: 20px 24px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: rgba(11,16,32,0.95); backdrop-filter: blur(6px); z-index: 5; }}
    h1 {{ margin: 0 0 8px; font-size: 22px; }}
    .sub {{ color: var(--muted); font-size: 13px; }}
    main {{ padding: 20px 24px 48px; max-width: 1400px; margin: 0 auto; }}
    .summary-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 16px; }}
    .summary-card {{ border: 1px solid var(--line); background: var(--panel); border-radius: 10px; padding: 10px 12px; }}
    .summary-card .k {{ color: var(--muted); font-size: 12px; }}
    .summary-card .v {{ font-weight: 700; font-size: 18px; }}
    .card {{ border: 1px solid var(--line); background: var(--panel); border-radius: 12px; padding: 16px; margin-bottom: 14px; }}
    .pill {{ display: inline-block; border: 1px solid var(--line); border-radius: 999px; padding: 2px 8px; margin: 2px 6px 2px 0; color: var(--muted); font-size: 12px; }}
    .meta {{ margin-bottom: 8px; }}
    .muted {{ color: var(--muted); }}
    input[type='search'] {{
      background: #0e1530; border: 1px solid var(--line); color: var(--text); border-radius: 8px; padding: 8px 10px; min-width: 300px;
    }}
    .search-row {{ display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; align-items: center; }}
    .quick-filters {{ display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-top: 10px; }}
    .quick-filter {{
      border: 1px solid var(--line); background: #101c44; color: var(--text); border-radius: 999px; padding: 5px 10px; cursor: pointer;
    }}
    .quick-filter:hover {{ background: #192b63; }}
    #filter-status {{ margin-top: 8px; }}
    table {{ border-collapse: collapse; width: 100%; font-size: 13px; }}
    th, td {{ border: 1px solid var(--line); padding: 8px; text-align: left; vertical-align: top; }}
    th {{ background: #1a2650; }}
    pre {{ white-space: pre-wrap; word-break: break-word; background: #0d1430; border: 1px solid var(--line); border-radius: 8px; padding: 10px; }}
    details {{ margin: 8px 0; }}
    summary {{ cursor: pointer; color: var(--accent); }}
    .copy {{
      border: 1px solid var(--line); background: #192754; color: var(--text); border-radius: 8px; padding: 6px 10px; cursor: pointer;
    }}
    .copy:hover {{ background: #22336d; }}
    .query-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 10px; }}
    .query-item {{ border: 1px solid var(--line); border-radius: 8px; padding: 8px; background: #0f1735; }}
    .parse-failure {{ border: 1px solid var(--line); border-radius: 8px; padding: 8px; margin-bottom: 8px; }}
  </style>
</head>
<body>
  <header>
    <h1>AuraIndex Interactive Report</h1>
    <div class="sub">Generated: {esc(generated)} | Dialect: {esc(dialect)} | Target path (absolute): {esc(display_path)}</div>
  </header>
  <main>
    <section class="card">
      <div class="summary-grid">{summary_html}</div>
      <div class="sub">
        Filter by free text or tokens such as <code>table:users</code>, <code>column:status</code>,
        <code>kind:join_key</code>, <code>manual:yes</code>.
      </div>
      <div class="search-row">
        <input
          id="index-search"
          type="search"
          list="index-search-options"
          placeholder="Filter suggestions (name/table/column/kind/manual)..."
          oninput="filterCards()"
        />
        <button type="button" class="quick-filter" onclick="clearFilter()">Clear</button>
      </div>
      <datalist id="index-search-options">
        {datalist_options_html}
      </datalist>
      {quick_filters_html}
      <div id="filter-status" class="sub"></div>
      <div id="filter-empty" class="muted" style="display:none;">No suggestions match the current filter.</div>
    </section>

    <section class="card">
      <h2>Ranked Suggestions</h2>
      {ranked_scope_html}
      <table>
        <thead>
          <tr><th>#</th><th>Index</th><th>Table</th><th>Columns</th><th>Kind</th><th>Score</th><th>Confidence</th><th>Manual</th></tr>
        </thead>
        <tbody>
          {ranked_table_html}
        </tbody>
      </table>
      {ranked_column_guide_html}
    </section>

    <section class="card">
      <h2>Why each index is suggested</h2>
      {cards_html}
    </section>

    {schema_summary_html}
    {live_db_summary_html}
    {explain_summary_html}
    {drift_summary_html}
    {advisory_summary_html}

    <section class="card">
      <h2>Migration Script (Up/Down)</h2>
      <pre id="migration_script">{esc(migration_sql)}</pre>
      <button class="copy" onclick="copyFromId('migration_script', this)">Copy migration SQL</button>
    </section>

    {parse_failure_html}
    {warnings_html}
  </main>

  <script>
    function fallbackCopy(text) {{
      const ta = document.createElement('textarea');
      ta.value = text;
      document.body.appendChild(ta);
      ta.select();
      document.execCommand('copy');
      document.body.removeChild(ta);
    }}
    async function copyFromId(id, button) {{
      const el = document.getElementById(id);
      if (!el) return;
      const text = el.innerText;
      try {{
        if (navigator.clipboard && navigator.clipboard.writeText) {{
          await navigator.clipboard.writeText(text);
        }} else {{
          fallbackCopy(text);
        }}
        const original = button.innerText;
        button.innerText = 'Copied';
        setTimeout(() => button.innerText = original, 1200);
      }} catch (e) {{
        fallbackCopy(text);
      }}
    }}
    function matchesTerms(hay, terms) {{
      if (!terms.length) {{
        return true;
      }}
      return terms.every((term) => hay.includes(term));
    }}
    function filterCards() {{
      const value = (document.getElementById('index-search').value || '').toLowerCase().trim();
      const terms = value ? value.split(/\\s+/g) : [];
      const cards = document.querySelectorAll('.index-card');
      const rows = document.querySelectorAll('.rank-row');
      let visible = 0;
      cards.forEach((card) => {{
        const hay = card.getAttribute('data-search') || '';
        const show = matchesTerms(hay, terms);
        card.style.display = show ? 'block' : 'none';
        if (show) {{
          visible += 1;
        }}
      }});
      rows.forEach((row) => {{
        const hay = row.getAttribute('data-search') || '';
        const show = matchesTerms(hay, terms);
        row.style.display = show ? 'table-row' : 'none';
      }});
      const status = document.getElementById('filter-status');
      if (status) {{
        status.textContent = `${{visible}} of ${{cards.length}} suggestions shown`;
      }}
      const empty = document.getElementById('filter-empty');
      if (empty) {{
        empty.style.display = cards.length > 0 && visible === 0 ? 'block' : 'none';
      }}
    }}
    function applyQuickFilter(value) {{
      const input = document.getElementById('index-search');
      if (!input) {{
        return;
      }}
      input.value = value;
      filterCards();
      input.focus();
    }}
    function clearFilter() {{
      const input = document.getElementById('index-search');
      if (!input) {{
        return;
      }}
      input.value = '';
      filterCards();
      input.focus();
    }}
    document.addEventListener('DOMContentLoaded', () => {{
      filterCards();
    }});
  </script>
</body>
</html>
"""


def _render_drop_index(table: str, index_name: str, dialect: str) -> str:
    if dialect == "mysql":
        return f"DROP INDEX `{index_name}` ON `{table}`;"
    return f'DROP INDEX IF EXISTS "{index_name}";'


def _workload_to_dict(result: ScanResult) -> dict[str, object]:
    column_usage: dict[str, dict[str, dict[str, int]]] = {}
    for table, columns in result.workload.column_usage.items():
        column_usage[table] = {
            column: {
                "where_count": usage.where_count,
                "join_count": usage.join_count,
                "order_count": usage.order_count,
                "group_count": usage.group_count,
            }
            for column, usage in columns.items()
        }

    composite_filters = {
        table: {"|".join(combo): count for combo, count in combos.items()}
        for table, combos in result.workload.composite_filters.items()
    }
    where_order_patterns = {
        table: {
            f"{'|'.join(where_cols)} -> {'|'.join(order_cols)}": count
            for (where_cols, order_cols), count in patterns.items()
        }
        for table, patterns in result.workload.where_order_patterns.items()
    }

    return {
        "total_queries": result.workload.total_queries,
        "table_query_count": result.workload.table_query_count,
        "table_read_count": result.workload.table_read_count,
        "table_write_count": result.workload.table_write_count,
        "column_usage": column_usage,
        "composite_filters": composite_filters,
        "where_order_patterns": where_order_patterns,
    }


def _detailed_index_evidence(result: ScanResult) -> dict[str, dict[str, object]]:
    details: dict[str, dict[str, object]] = {}

    for suggestion in result.suggestions:
        matched_queries: list[dict[str, object]] = []
        target_cols = set(suggestion.columns)
        for parsed in result.parsed_queries:
            if suggestion.table not in parsed.tables:
                continue
            referenced_cols = {
                col for table, col in (parsed.where_columns + parsed.join_columns + parsed.order_by_columns + parsed.group_by_columns)
                if table == suggestion.table
            }
            if not (target_cols & referenced_cols):
                continue
            context = parsed.query.context
            function_name = context.function or "<module>"
            matched_queries.append(
                {
                    "file_path": context.file_path,
                    "line": context.line,
                    "function": function_name,
                    "source_kind": context.source_kind,
                    "sql_excerpt": parsed.query.sql[:320],
                    "query_confidence": parsed.query.confidence,
                }
            )

        deduped_queries = _dedupe_matched_queries(matched_queries)
        deduped_contexts = [
            f"{item['file_path']}:{item['line']} ({item.get('function') or '<module>'})"
            for item in deduped_queries
        ]

        support_count = _estimate_support_count(suggestion.evidence, deduped_queries)

        details[suggestion.index_name] = {
            "table": suggestion.table,
            "columns": suggestion.columns,
            "ddl": suggestion.ddl,
            "kind": suggestion.kind,
            "score": suggestion.score,
            "confidence": suggestion.confidence,
            "reason": suggestion.reason,
            "evidence": suggestion.evidence,
            "support_count": support_count,
            "matched_query_count": len(deduped_queries),
            "supporting_contexts": deduped_contexts[:20],
            "matched_queries": deduped_queries[:50],
            "impact": suggestion.metadata.get("impact", {}),
            "index_type": suggestion.metadata.get("index_type", {}),
            "schema_check": suggestion.metadata.get("schema_check", {}),
            "explain_validation": suggestion.metadata.get("explain_validation", {}),
        }

    return details


def _estimate_support_count(evidence: dict[str, int], matched_queries: list[dict[str, object]]) -> int:
    support_fields = [
        "support_count",
        "composite_count",
        "pattern_count",
        "join_count",
        "where_count",
    ]
    values = [int(evidence[field]) for field in support_fields if field in evidence]
    if values:
        return max(values)
    return len(matched_queries)


def _dedupe_matched_queries(items: list[dict[str, object]]) -> list[dict[str, object]]:
    buckets: dict[tuple[str, str, str], list[dict[str, object]]] = {}

    for raw_item in items:
        file_path = str(raw_item.get("file_path", ""))
        sql_excerpt = str(raw_item.get("sql_excerpt", ""))
        normalized_sql = " ".join(sql_excerpt.split()).strip().lower()
        source_kind = str(raw_item.get("source_kind", ""))
        line = int(raw_item.get("line", 0) or 0)
        item = dict(raw_item)

        key = (file_path, normalized_sql, source_kind)
        bucket = buckets.setdefault(key, [])

        replaced = False
        for index, existing in enumerate(bucket):
            existing_line = int(existing.get("line", 0) or 0)
            existing_source = str(existing.get("source_kind", ""))
            is_near_duplicate = (
                source_kind == "wasm_sql"
                and existing_source == "wasm_sql"
                and abs(existing_line - line) <= 1
            )
            is_exact_duplicate = existing_line == line
            if is_near_duplicate or is_exact_duplicate:
                if line < existing_line:
                    bucket[index] = item
                replaced = True
                break
        if not replaced:
            bucket.append(item)

    deduped = [item for bucket in buckets.values() for item in bucket]
    deduped.sort(
        key=lambda item: (
            str(item.get("file_path", "")),
            int(item.get("line", 0) or 0),
            str(item.get("sql_excerpt", "")),
        )
    )
    return deduped


def _apply_suggestion_limit(items: list[object], limit: int | None) -> list[object]:
    if limit is None or limit >= len(items):
        return items
    return items[:limit]
