from __future__ import annotations

import json
from dataclasses import asdict, dataclass, field
from html import escape
from pathlib import Path
from typing import Any, Literal

import pandas as pd

Severity = Literal["critical", "high", "medium", "low", "info"]


@dataclass
class LeakIssue:
    """
    Standard issue emitted by all FeatureLeak rules.
    """

    severity: str
    check: str
    title: str
    summary: str
    affected_columns: list[str] = field(default_factory=list)
    details: dict[str, Any] = field(default_factory=dict)
    suggested_fix: str = ""

    def __post_init__(self) -> None:
        self.severity = str(self.severity).lower().strip()
        self.check = str(self.check).strip()
        self.title = str(self.title).strip()
        self.summary = str(self.summary).strip()
        self.affected_columns = [str(col) for col in self.affected_columns]

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


# Backwards-compatible alias for older rule code
WarningItem = LeakIssue


@dataclass
class LeakReport:
    """
    Final report returned by LeakScanner.
    """

    issues: list[LeakIssue] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)

    # Backwards-compatible fields expected by older code
    high_risk_features: list[str] = field(default_factory=list)
    medium_risk_features: list[str] = field(default_factory=list)
    low_risk_features: list[str] = field(default_factory=list)

    def add_issue(self, item: LeakIssue) -> None:
        self.issues.append(item)

    # Backwards-compatible method name
    def add_warning(self, item: LeakIssue) -> None:
        self.add_issue(item)

    @property
    def warnings(self) -> list[LeakIssue]:
        # Backwards-compatible alias
        return self.issues

    def issues_by_check(self) -> dict[str, int]:
        counts: dict[str, int] = {}
        for issue in self.issues:
            counts[issue.check] = counts.get(issue.check, 0) + 1
        return counts

    def issues_by_severity(self) -> dict[str, int]:
        counts: dict[str, int] = {
            "critical": 0,
            "high": 0,
            "medium": 0,
            "low": 0,
            "info": 0,
        }
        for issue in self.issues:
            sev = issue.severity.lower()
            counts[sev] = counts.get(sev, 0) + 1
        return counts

    def score(self) -> int:
        weights = {
            "critical": 25,
            "high": 12,
            "medium": 5,
            "low": 2,
            "info": 0,
        }
        total = 0
        for issue in self.issues:
            total += weights.get(issue.severity.lower(), 0)
        return min(int(total), 100)

    def risk_level(self) -> str:
        score = self.score()
        if score >= 65:
            return "high"
        if score >= 35:
            return "moderate"
        if score >= 15:
            return "guarded"
        return "low"

    def to_dict(self) -> dict[str, Any]:
        return {
            "issues": [issue.to_dict() for issue in self.issues],
            "warnings": [issue.to_dict() for issue in self.issues],  # backwards-compatible
            "high_risk_features": self.high_risk_features,
            "medium_risk_features": self.medium_risk_features,
            "low_risk_features": self.low_risk_features,
            "metadata": self.metadata,
            "score": self.score(),
            "risk_level": self.risk_level(),
            "issues_by_check": self.issues_by_check(),
            "issues_by_severity": self.issues_by_severity(),
            "warning_counts_by_rule": self.issues_by_check(),       # backwards-compatible
            "warning_counts_by_severity": self.issues_by_severity(),  # backwards-compatible
        }

    def to_json(self, path: str | Path | None = None, indent: int = 2) -> str:
        payload = json.dumps(self.to_dict(), indent=indent, default=str)
        if path is not None:
            Path(path).write_text(payload, encoding="utf-8")
        return payload

    def to_dataframe(self) -> pd.DataFrame:
        if not self.issues:
            return pd.DataFrame(
                columns=[
                    "severity",
                    "check",
                    "title",
                    "summary",
                    "affected_columns",
                    "suggested_fix",
                    "details",
                ]
            )

        rows: list[dict[str, Any]] = []
        for issue in self.issues:
            rows.append(
                {
                    "severity": issue.severity.upper(),
                    "check": issue.check,
                    "title": issue.title,
                    "summary": issue.summary,
                    "affected_columns": ", ".join(issue.affected_columns),
                    "suggested_fix": issue.suggested_fix,
                    "details": issue.details,
                }
            )
        return pd.DataFrame(rows)

    def summary(self) -> str:
        if not self.issues:
            return "FeatureLeak Report\n==================\nNo obvious leakage risks detected."

        counts = self.issues_by_severity()
        lines = [
            "FeatureLeak Report",
            "==================",
            f"Risk Score: {self.score()}/100 ({self.risk_level().title()})",
            f"Total issues: {len(self.issues)}",
            f"Critical: {counts.get('critical', 0)}",
            f"High: {counts.get('high', 0)}",
            f"Medium: {counts.get('medium', 0)}",
            f"Low: {counts.get('low', 0)}",
            f"Info: {counts.get('info', 0)}",
            "",
        ]

        for issue in self.issues:
            lines.append(f"{issue.severity.upper():<8} {issue.check}")
            lines.append(f"  {issue.summary}")
            if issue.affected_columns:
                lines.append(f"  columns: {', '.join(issue.affected_columns)}")
            if issue.suggested_fix:
                lines.append(f"  fix: {issue.suggested_fix}")
            if issue.details:
                metrics = ", ".join(f"{k}={v}" for k, v in issue.details.items())
                lines.append(f"  details: {metrics}")
            lines.append("")

        return "\n".join(lines).rstrip()

    def _repr_html_(self) -> str:
        severity_bg = {
            "CRITICAL": "#fef2f2",
            "HIGH": "#fee2e2",
            "MEDIUM": "#fef3c7",
            "LOW": "#dbeafe",
            "INFO": "#ecfeff",
        }
        severity_fg = {
            "CRITICAL": "#991b1b",
            "HIGH": "#b91c1c",
            "MEDIUM": "#b45309",
            "LOW": "#1d4ed8",
            "INFO": "#155e75",
        }

        def pill(text: str, bg: str = "#f3f4f6", fg: str = "#111827") -> str:
            return (
                f"<span style='display:inline-block; margin:4px 6px 0 0; "
                f"padding:4px 10px; border-radius:999px; background:{bg}; "
                f"color:{fg}; font-size:12px; border:1px solid #e5e7eb;'>"
                f"{escape(text)}</span>"
            )

        def render_issue(issue: LeakIssue) -> str:
            sev = issue.severity.upper()
            bg = severity_bg.get(sev, "#f3f4f6")
            fg = severity_fg.get(sev, "#374151")

            columns_html = (
                "".join(pill(col, "#ffffff", "#111827") for col in issue.affected_columns)
                if issue.affected_columns
                else "<span style='color:#6b7280;'>None</span>"
            )

            details_html = ""
            if issue.details:
                detail_items = "".join(
                    f"<li><b>{escape(str(k))}</b>: {escape(str(v))}</li>"
                    for k, v in issue.details.items()
                )
                details_html = f"""
                <div style="margin-top:10px;">
                    <div style="font-weight:600; margin-bottom:4px;">Details</div>
                    <ul style="margin:0 0 0 18px; padding:0;">{detail_items}</ul>
                </div>
                """

            fix_html = ""
            if issue.suggested_fix:
                fix_html = f"""
                <div style="margin-top:10px; padding:10px 12px; background:white;
                            border-left:4px solid {fg}; border-radius:8px;">
                    <div style="font-weight:600; margin-bottom:4px;">Suggested fix</div>
                    <div>{escape(issue.suggested_fix)}</div>
                </div>
                """

            return f"""
            <div style="background:{bg}; border:1px solid #e5e7eb; border-radius:14px;
                        padding:16px; margin-bottom:12px;">
                <div style="display:flex; align-items:center; gap:10px; margin-bottom:8px; flex-wrap:wrap;">
                    <span style="display:inline-block; padding:4px 10px; border-radius:999px;
                                 color:white; background:{fg}; font-size:12px; font-weight:700;">
                        {escape(sev)}
                    </span>
                    <span style="font-weight:700; font-size:15px;">{escape(issue.check)}</span>
                    <span style="color:#6b7280; font-size:13px;">{escape(issue.title)}</span>
                </div>
                <div style="margin-bottom:10px; font-size:14px; line-height:1.5;">
                    {escape(issue.summary)}
                </div>
                <div style="margin-top:8px;">
                    <div style="font-weight:600; margin-bottom:4px;">Columns</div>
                    <div>{columns_html}</div>
                </div>
                {details_html}
                {fix_html}
            </div>
            """

        n_rows = self.metadata.get("n_rows", "Unknown")
        n_columns = self.metadata.get("n_columns", "Unknown")
        target = self.metadata.get("target", "Unknown")
        time_col = self.metadata.get("time_col")
        step_results = self.metadata.get("step_results", [])
        counts = self.issues_by_severity()

        if self.issues:
            issues_html = "".join(render_issue(issue) for issue in self.issues)
        else:
            issues_html = """
            <div style="padding:16px; background:#ecfdf5; border:1px solid #a7f3d0;
                        border-radius:14px; color:#065f46; font-weight:600;">
                No obvious leakage risks detected.
            </div>
            """

        step_results_html = ""
        if step_results:
            rows = []
            for step in step_results:
                status = str(step.get("status", "unknown"))
                name = str(step.get("name", "Unknown step"))
                elapsed = step.get("elapsed", 0.0)
                n_step_issues = step.get("n_issues", step.get("n_warnings", 0))
                icon = {"ok": "✔", "warning": "⚠", "error": "✖"}.get(status, "•")
                rows.append(
                    f"""
                    <tr>
                        <td style="padding:8px 10px; border-top:1px solid #e5e7eb;">{escape(icon)}</td>
                        <td style="padding:8px 10px; border-top:1px solid #e5e7eb;">{escape(name)}</td>
                        <td style="padding:8px 10px; border-top:1px solid #e5e7eb;">{escape(status)}</td>
                        <td style="padding:8px 10px; border-top:1px solid #e5e7eb;">{escape(f"{float(elapsed):.2f}s")}</td>
                        <td style="padding:8px 10px; border-top:1px solid #e5e7eb;">{escape(str(n_step_issues))}</td>
                    </tr>
                    """
                )

            step_results_html = f"""
            <div style="margin-top:18px;">
                <h3 style="margin:0 0 12px 0;">Analysis Steps</h3>
                <div style="border:1px solid #e5e7eb; border-radius:14px; overflow:hidden; background:white;">
                    <table style="width:100%; border-collapse:collapse; font-size:14px;">
                        <thead style="background:#f9fafb;">
                            <tr>
                                <th style="text-align:left; padding:10px;">Status</th>
                                <th style="text-align:left; padding:10px;">Step</th>
                                <th style="text-align:left; padding:10px;">Result</th>
                                <th style="text-align:left; padding:10px;">Time</th>
                                <th style="text-align:left; padding:10px;">Issues</th>
                            </tr>
                        </thead>
                        <tbody>{''.join(rows)}</tbody>
                    </table>
                </div>
            </div>
            """

        return f"""
        <div style="font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
                    max-width:1000px; padding:8px;">
            <div style="margin-bottom:18px;">
                <h2 style="margin:0 0 8px 0;">FeatureLeak Report</h2>
                <div style="color:#374151; font-size:14px; line-height:1.7;">
                    <b>Rows:</b> {escape(str(n_rows))} &nbsp;&nbsp;
                    <b>Columns:</b> {escape(str(n_columns))} &nbsp;&nbsp;
                    <b>Target:</b> {escape(str(target))} &nbsp;&nbsp;
                    <b>Time column:</b> {escape(str(time_col)) if time_col else "None"} &nbsp;&nbsp;
                    <b>Risk score:</b> {escape(str(self.score()))}/100 &nbsp;&nbsp;
                    <b>Risk level:</b> {escape(self.risk_level().title())}
                </div>
            </div>

            <div style="display:grid; grid-template-columns:repeat(5, 1fr); gap:12px; margin-bottom:18px;">
                <div style="padding:14px; border-radius:14px; background:#fef2f2; border:1px solid #fecaca;">
                    <div style="font-size:12px; color:#991b1b; font-weight:700;">CRITICAL</div>
                    <div style="margin-top:8px; font-size:22px; font-weight:700;">{counts.get('critical', 0)}</div>
                </div>
                <div style="padding:14px; border-radius:14px; background:#fee2e2; border:1px solid #fecaca;">
                    <div style="font-size:12px; color:#b91c1c; font-weight:700;">HIGH</div>
                    <div style="margin-top:8px; font-size:22px; font-weight:700;">{counts.get('high', 0)}</div>
                </div>
                <div style="padding:14px; border-radius:14px; background:#fef3c7; border:1px solid #fde68a;">
                    <div style="font-size:12px; color:#92400e; font-weight:700;">MEDIUM</div>
                    <div style="margin-top:8px; font-size:22px; font-weight:700;">{counts.get('medium', 0)}</div>
                </div>
                <div style="padding:14px; border-radius:14px; background:#dbeafe; border:1px solid #bfdbfe;">
                    <div style="font-size:12px; color:#1d4ed8; font-weight:700;">LOW</div>
                    <div style="margin-top:8px; font-size:22px; font-weight:700;">{counts.get('low', 0)}</div>
                </div>
                <div style="padding:14px; border-radius:14px; background:#ecfeff; border:1px solid #a5f3fc;">
                    <div style="font-size:12px; color:#155e75; font-weight:700;">INFO</div>
                    <div style="margin-top:8px; font-size:22px; font-weight:700;">{counts.get('info', 0)}</div>
                </div>
            </div>

            <div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:12px; margin-bottom:18px;">
                <div style="padding:14px; border-radius:14px; background:#fef2f2; border:1px solid #fecaca;">
                    <div style="font-size:12px; color:#991b1b; font-weight:700;">HIGH RISK FEATURES</div>
                    <div style="margin-top:8px;">
                        {''.join(pill(x) for x in self.high_risk_features) if self.high_risk_features else "<span style='color:#6b7280;'>None</span>"}
                    </div>
                </div>
                <div style="padding:14px; border-radius:14px; background:#fffbeb; border:1px solid #fde68a;">
                    <div style="font-size:12px; color:#92400e; font-weight:700;">MEDIUM RISK FEATURES</div>
                    <div style="margin-top:8px;">
                        {''.join(pill(x) for x in self.medium_risk_features) if self.medium_risk_features else "<span style='color:#6b7280;'>None</span>"}
                    </div>
                </div>
                <div style="padding:14px; border-radius:14px; background:#eff6ff; border:1px solid #bfdbfe;">
                    <div style="font-size:12px; color:#1d4ed8; font-weight:700;">LOW RISK FEATURES</div>
                    <div style="margin-top:8px;">
                        {''.join(pill(x) for x in self.low_risk_features) if self.low_risk_features else "<span style='color:#6b7280;'>None</span>"}
                    </div>
                </div>
            </div>

            <div>
                <h3 style="margin:0 0 12px 0;">Issues</h3>
                {issues_html}
            </div>

            {step_results_html}
        </div>
        """

    def display(self) -> None:
        try:
            from IPython.display import HTML, display

            display(HTML(self._repr_html_()))
        except Exception:
            print(self.summary())

    def show(self) -> None:
        self.display()