"""Security Posture Score for MCP servers.

Holistic security posture scoring (0-100) across 8 dimensions:
code_quality, auth, data_protection, input_validation, logging,
config, dependencies, compliance. Includes grading (A+ to F),
industry benchmarking, and trend analysis.

Zero external dependencies. Python 3.11+ stdlib only.
"""

from __future__ import annotations

import json
import math
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

class PostureDimension(Enum):
    """Security posture dimensions."""

    CODE_QUALITY = "code_quality"
    AUTH = "auth"
    DATA_PROTECTION = "data_protection"
    INPUT_VALIDATION = "input_validation"
    LOGGING = "logging"
    CONFIG = "config"
    DEPENDENCIES = "dependencies"
    COMPLIANCE = "compliance"


DEFAULT_WEIGHTS: dict[str, float] = {
    PostureDimension.CODE_QUALITY.value: 0.15,
    PostureDimension.AUTH.value: 0.15,
    PostureDimension.DATA_PROTECTION.value: 0.15,
    PostureDimension.INPUT_VALIDATION.value: 0.15,
    PostureDimension.LOGGING.value: 0.10,
    PostureDimension.CONFIG.value: 0.10,
    PostureDimension.DEPENDENCIES.value: 0.10,
    PostureDimension.COMPLIANCE.value: 0.10,
}

# Severity penalty weights
_SEVERITY_PENALTY: dict[str, int] = {
    "CRITICAL": 30,
    "HIGH": 18,
    "MEDIUM": 10,
    "LOW": 4,
    "INFO": 1,
}

# Rule prefix -> dimension mapping
_RULE_DIMENSION_MAP: dict[str, PostureDimension] = {
    # Auth
    "AUTH": PostureDimension.AUTH,
    "RATE": PostureDimension.AUTH,
    "PRIV": PostureDimension.AUTH,
    "OAUTH": PostureDimension.AUTH,
    "SESSION": PostureDimension.AUTH,
    # Data protection
    "DATA": PostureDimension.DATA_PROTECTION,
    "LEAK": PostureDimension.DATA_PROTECTION,
    "SENS": PostureDimension.DATA_PROTECTION,
    "PII": PostureDimension.DATA_PROTECTION,
    "SECRET": PostureDimension.DATA_PROTECTION,
    # Input validation
    "INJ": PostureDimension.INPUT_VALIDATION,
    "PROMPT": PostureDimension.INPUT_VALIDATION,
    "DESER": PostureDimension.INPUT_VALIDATION,
    "TRAV": PostureDimension.INPUT_VALIDATION,
    # Logging
    "LOG": PostureDimension.LOGGING,
    # Config
    "GUARD": PostureDimension.CONFIG,
    "CONF": PostureDimension.CONFIG,
    "ENV": PostureDimension.CONFIG,
    "PERM": PostureDimension.CONFIG,
    # Dependencies
    "DEP": PostureDimension.DEPENDENCIES,
    "SUPPLY": PostureDimension.DEPENDENCIES,
    "SBOM": PostureDimension.DEPENDENCIES,
    "LICENSE": PostureDimension.DEPENDENCIES,
    # Compliance
    "OWASP": PostureDimension.COMPLIANCE,
    "MCP": PostureDimension.COMPLIANCE,
    # Code quality (default)
    "CMD": PostureDimension.CODE_QUALITY,
    "PATH": PostureDimension.CODE_QUALITY,
    "FLOW": PostureDimension.CODE_QUALITY,
}

# Industry benchmark percentile data (simulated distribution)
_INDUSTRY_BENCHMARKS: dict[int, int] = {
    # score -> percentile
    95: 95,
    90: 88,
    85: 80,
    80: 70,
    75: 60,
    70: 50,
    65: 40,
    60: 30,
    55: 22,
    50: 15,
    45: 10,
    40: 6,
    35: 3,
    30: 1,
}


# ---------------------------------------------------------------------------
# Data Models
# ---------------------------------------------------------------------------

@dataclass
class DimensionResult:
    """Score for a single posture dimension."""

    dimension: str
    score: int  # 0-100
    weight: float = 0.125
    finding_count: int = 0
    penalty: int = 0
    max_penalty: int = 100
    grade: str = "A+"
    details: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "dimension": self.dimension,
            "score": self.score,
            "weight": self.weight,
            "finding_count": self.finding_count,
            "penalty": self.penalty,
            "grade": self.grade,
            "details": self.details,
        }


@dataclass
class TrendPoint:
    """A historical posture score point."""

    timestamp: str
    score: int
    grade: str

    def to_dict(self) -> dict[str, Any]:
        return {
            "timestamp": self.timestamp,
            "score": self.score,
            "grade": self.grade,
        }


@dataclass
class PostureScore:
    """Complete security posture score result."""

    overall_score: int = 100
    grade: str = "A+"
    dimensions: list[DimensionResult] = field(default_factory=list)
    percentile: int = 95
    trend: str = "stable"  # improving, degrading, stable
    trend_points: list[TrendPoint] = field(default_factory=list)
    total_findings: int = 0
    scan_target: str = ""
    timestamp: str = ""
    weights_used: dict[str, float] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "overall_score": self.overall_score,
            "grade": self.grade,
            "dimensions": [d.to_dict() for d in self.dimensions],
            "percentile": self.percentile,
            "trend": self.trend,
            "trend_points": [t.to_dict() for t in self.trend_points],
            "total_findings": self.total_findings,
            "scan_target": self.scan_target,
            "timestamp": self.timestamp,
            "weights_used": self.weights_used,
        }


# ---------------------------------------------------------------------------
# Dimension classification
# ---------------------------------------------------------------------------

def classify_dimension(rule_id: str) -> PostureDimension:
    """Classify a finding's rule_id into a posture dimension."""
    prefix = rule_id.split("-")[0] if "-" in rule_id else rule_id
    return _RULE_DIMENSION_MAP.get(prefix, PostureDimension.CODE_QUALITY)


# ---------------------------------------------------------------------------
# Score computation
# ---------------------------------------------------------------------------

def _compute_dimension_score(
    findings: list[dict[str, Any]],
    dimension: PostureDimension,
) -> DimensionResult:
    """Compute score for a single dimension from relevant findings."""
    relevant = [
        f for f in findings
        if classify_dimension(f.get("rule_id", "")) == dimension
    ]

    penalty = 0
    details: list[str] = []
    for f in relevant:
        sev = f.get("severity", "MEDIUM")
        p = _SEVERITY_PENALTY.get(sev, 5)
        penalty += p
        details.append(f"{f.get('rule_id', '?')}: {sev} (-{p})")

    score = max(0, 100 - penalty)

    return DimensionResult(
        dimension=dimension.value,
        score=score,
        finding_count=len(relevant),
        penalty=penalty,
        grade=_score_to_grade(score),
        details=details,
    )


def _score_to_grade(score: int) -> str:
    """Convert a score to a letter grade."""
    if score >= 95:
        return "A+"
    if score >= 90:
        return "A"
    if score >= 80:
        return "B"
    if score >= 70:
        return "C"
    if score >= 60:
        return "D"
    return "F"


def _compute_percentile(score: int) -> int:
    """Estimate industry percentile from score."""
    # Find closest benchmark
    best_match = 1
    for threshold, pctl in sorted(_INDUSTRY_BENCHMARKS.items(), reverse=True):
        if score >= threshold:
            best_match = pctl
            break
    return best_match


def _compute_trend(points: list[TrendPoint]) -> str:
    """Determine trend direction from historical points."""
    if len(points) < 2:
        return "stable"
    recent = points[-3:] if len(points) >= 3 else points
    scores = [p.score for p in recent]
    first_half = scores[: len(scores) // 2 + 1]
    second_half = scores[len(scores) // 2:]
    avg_first = sum(first_half) / len(first_half) if first_half else 0
    avg_second = sum(second_half) / len(second_half) if second_half else 0
    diff = avg_second - avg_first
    if diff > 3:
        return "improving"
    if diff < -3:
        return "degrading"
    return "stable"


def _extract_findings(scan_result: dict[str, Any]) -> list[dict[str, Any]]:
    """Extract all findings from scan results."""
    findings: list[dict[str, Any]] = []
    results = scan_result.get("results", [])
    if isinstance(results, list):
        for r in results:
            if isinstance(r, dict):
                for f in r.get("findings", []):
                    if isinstance(f, dict):
                        findings.append(f)
    return findings


# ---------------------------------------------------------------------------
# Core Functions
# ---------------------------------------------------------------------------

def calculate_posture(
    scan_result: dict[str, Any],
    weights: dict[str, float] | None = None,
    history: list[dict[str, Any]] | None = None,
) -> PostureScore:
    """Calculate security posture score from scan results.

    Args:
        scan_result: Scan result dict with 'results' and optional 'summary'.
        weights: Optional custom weights per dimension (sum should be ~1.0).
        history: Optional list of previous posture scores for trend analysis.

    Returns:
        PostureScore with overall score, grades, and dimension breakdowns.
    """
    w = dict(DEFAULT_WEIGHTS)
    if weights:
        w.update(weights)
    # Normalize weights to sum to 1.0
    total_w = sum(w.values())
    if total_w > 0:
        w = {k: v / total_w for k, v in w.items()}

    findings = _extract_findings(scan_result)

    # Compute per-dimension scores
    dimensions: list[DimensionResult] = []
    for dim in PostureDimension:
        result = _compute_dimension_score(findings, dim)
        result.weight = w.get(dim.value, 0.125)
        dimensions.append(result)

    # Weighted overall score
    overall = sum(d.score * d.weight for d in dimensions)
    overall_int = max(0, min(100, round(overall)))

    # Grade
    grade = _score_to_grade(overall_int)

    # Percentile
    percentile = _compute_percentile(overall_int)

    # Trend
    trend_points: list[TrendPoint] = []
    if history:
        for h in history:
            if isinstance(h, dict):
                trend_points.append(TrendPoint(
                    timestamp=h.get("timestamp", ""),
                    score=h.get("score", 0),
                    grade=h.get("grade", "?"),
                ))
    # Add current point
    ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    trend_points.append(TrendPoint(timestamp=ts, score=overall_int, grade=grade))
    trend = _compute_trend(trend_points)

    target = scan_result.get("summary", {}).get("target", "")
    if not target:
        results = scan_result.get("results", [])
        if results and isinstance(results, list) and results:
            first = results[0]
            if isinstance(first, dict):
                target = first.get("file", "")

    return PostureScore(
        overall_score=overall_int,
        grade=grade,
        dimensions=dimensions,
        percentile=percentile,
        trend=trend,
        trend_points=trend_points,
        total_findings=len(findings),
        scan_target=target,
        timestamp=ts,
        weights_used=w,
    )


# ---------------------------------------------------------------------------
# Format helpers
# ---------------------------------------------------------------------------

def format_posture_json(posture: PostureScore) -> str:
    """Serialize a PostureScore to JSON."""
    return json.dumps(posture.to_dict(), indent=2)


def format_posture_text(posture: PostureScore) -> str:
    """Format a PostureScore as human-readable text."""
    lines = [
        "=" * 60,
        "SECURITY POSTURE SCORE",
        "=" * 60,
        f"Overall: {posture.overall_score}/100 (Grade: {posture.grade})",
        f"Industry Percentile: {posture.percentile}th",
        f"Trend: {posture.trend}",
        f"Findings: {posture.total_findings}",
        "",
        "Dimensions:",
        "-" * 40,
    ]

    for d in posture.dimensions:
        bar = _score_bar(d.score)
        lines.append(
            f"  {d.dimension:<20s} {d.score:>3d}/100 {bar} "
            f"({d.grade}) [{d.finding_count} findings]"
        )

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


def format_posture_html(posture: PostureScore) -> str:
    """Generate an HTML posture score report."""
    import html as html_mod

    target = html_mod.escape(posture.scan_target or "MCP Server")
    grade_color = _grade_color(posture.grade)

    dim_rows = ""
    for d in posture.dimensions:
        bar_w = d.score
        bar_color = _score_color(d.score)
        dim_rows += f"""<tr>
  <td style="font-weight:600;">{html_mod.escape(d.dimension)}</td>
  <td style="text-align:center;">{d.score}</td>
  <td><div style="background:#e2e8f0;border-radius:4px;overflow:hidden;height:20px;">
    <div style="background:{bar_color};width:{bar_w}%;height:100%;"></div>
  </div></td>
  <td style="text-align:center;">{d.grade}</td>
  <td style="text-align:center;">{d.finding_count}</td>
</tr>"""

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Security Posture — {target}</title>
<style>
  body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
         margin: 0; padding: 20px; background: #f8fafc; color: #1e293b; }}
  .header {{ background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
             color: white; padding: 30px; border-radius: 12px; margin-bottom: 24px; }}
  .header h1 {{ margin: 0 0 8px 0; font-size: 24px; }}
  .score-circle {{ display: inline-flex; align-items: center; justify-content: center;
                   width: 80px; height: 80px; border-radius: 50%;
                   background: {grade_color}; color: white; font-size: 32px;
                   font-weight: 700; }}
  .meta {{ color: #94a3b8; font-size: 13px; margin-top: 8px; }}
  .stats {{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;
            margin-bottom: 24px; }}
  .stat {{ background: white; border-radius: 8px; padding: 16px; text-align: center;
           box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
  .stat .value {{ font-size: 24px; font-weight: 700; }}
  .stat .label {{ font-size: 12px; color: #64748b; }}
  table {{ width: 100%; border-collapse: collapse; background: white;
           border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
  th {{ background: #f1f5f9; padding: 12px; text-align: left; font-size: 13px; }}
  td {{ padding: 10px 12px; border-top: 1px solid #f1f5f9; font-size: 13px; }}
  .footer {{ text-align: center; color: #94a3b8; font-size: 12px;
             margin-top: 32px; }}
</style>
</head>
<body>
<div class="header">
  <div style="display:flex;align-items:center;gap:20px;">
    <div class="score-circle">{posture.grade}</div>
    <div>
      <h1>Security Posture: {posture.overall_score}/100</h1>
      <div class="meta">Target: {target} | {posture.timestamp}</div>
    </div>
  </div>
</div>

<div class="stats">
  <div class="stat">
    <div class="value">{posture.percentile}th</div>
    <div class="label">Industry Percentile</div>
  </div>
  <div class="stat">
    <div class="value">{posture.trend}</div>
    <div class="label">Trend</div>
  </div>
  <div class="stat">
    <div class="value">{posture.total_findings}</div>
    <div class="label">Total Findings</div>
  </div>
</div>

<table>
<tr><th>Dimension</th><th>Score</th><th>Bar</th><th>Grade</th><th>Findings</th></tr>
{dim_rows}
</table>

<div class="footer">mcpaudit Security Posture Score</div>
</body>
</html>"""


def _score_bar(score: int) -> str:
    """Text-based bar for terminal output."""
    filled = score // 5
    empty = 20 - filled
    return "[" + "#" * filled + "." * empty + "]"


def _score_color(score: int) -> str:
    if score >= 90:
        return "#22c55e"
    if score >= 75:
        return "#4ade80"
    if score >= 60:
        return "#eab308"
    if score >= 40:
        return "#f97316"
    return "#dc2626"


def _grade_color(grade: str) -> str:
    return {
        "A+": "#22c55e",
        "A": "#4ade80",
        "B": "#eab308",
        "C": "#f97316",
        "D": "#ef4444",
        "F": "#dc2626",
    }.get(grade, "#6b7280")
