"""Automated Fix Planner — Prioritized remediation with sprint planning.

Generates a prioritized, dependency-aware fix plan from scan findings
with effort estimates, trust score impact, code snippets, and sprint
grouping.

Features:
- Dependency-aware execution ordering
- Effort estimation per fix (hours)
- Trust score impact prediction
- Code snippet solutions
- Sprint planning: group fixes into sprints of N hours

CLI: mcpaudit fix-plan <path> [--sprint-hours N] [--format text|json|html]

Zero external dependencies. Python 3.11+ stdlib only.
Part of mcpaudit v3.0.0.
"""

from __future__ import annotations

import json
from collections import Counter
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum, unique
from typing import Any


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

@unique
class FixPriority(Enum):
    """Priority level for a fix item."""
    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"
    INFO = "info"

    @property
    def weight(self) -> int:
        return {
            FixPriority.CRITICAL: 0,
            FixPriority.HIGH: 1,
            FixPriority.MEDIUM: 2,
            FixPriority.LOW: 3,
            FixPriority.INFO: 4,
        }[self]

    @classmethod
    def from_severity(cls, severity: str) -> FixPriority:
        mapping = {
            "CRITICAL": cls.CRITICAL,
            "HIGH": cls.HIGH,
            "MEDIUM": cls.MEDIUM,
            "LOW": cls.LOW,
            "INFO": cls.INFO,
        }
        return mapping.get(severity.upper(), cls.MEDIUM)

    def __lt__(self, other: FixPriority) -> bool:
        return self.weight < other.weight

    def __le__(self, other: FixPriority) -> bool:
        return self.weight <= other.weight


@unique
class FixCategory(Enum):
    """Category of fix action."""
    CODE_CHANGE = "code_change"
    CONFIG_CHANGE = "config_change"
    ARCHITECTURE = "architecture"
    PROCESS = "process"
    MANUAL_REVIEW = "manual_review"


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

@dataclass
class CodeSnippet:
    """A before/after code snippet for a fix."""
    language: str = "python"
    before: str = ""
    after: str = ""
    description: str = ""
    file_pattern: str = ""

    def to_dict(self) -> dict[str, Any]:
        result: dict[str, Any] = {
            "language": self.language,
            "description": self.description,
        }
        if self.before:
            result["before"] = self.before
        if self.after:
            result["after"] = self.after
        if self.file_pattern:
            result["file_pattern"] = self.file_pattern
        return result


@dataclass
class FixItem:
    """A single fix item in the plan."""
    fix_id: str
    rule_id: str
    title: str
    description: str
    priority: FixPriority
    category: FixCategory
    effort_hours: float = 0.5
    trust_score_impact: int = 0
    depends_on: list[str] = field(default_factory=list)
    affected_files: list[str] = field(default_factory=list)
    finding_count: int = 1
    snippets: list[CodeSnippet] = field(default_factory=list)
    tags: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "fix_id": self.fix_id,
            "rule_id": self.rule_id,
            "title": self.title,
            "description": self.description,
            "priority": self.priority.value,
            "category": self.category.value,
            "effort_hours": self.effort_hours,
            "trust_score_impact": self.trust_score_impact,
            "depends_on": list(self.depends_on),
            "affected_files": list(self.affected_files),
            "finding_count": self.finding_count,
            "snippets": [s.to_dict() for s in self.snippets],
            "tags": list(self.tags),
        }


@dataclass
class Sprint:
    """A sprint grouping of fix items."""
    sprint_number: int
    name: str
    items: list[FixItem] = field(default_factory=list)
    total_hours: float = 0.0
    max_hours: float = 0.0

    @property
    def item_count(self) -> int:
        return len(self.items)

    @property
    def remaining_hours(self) -> float:
        return max(0.0, self.max_hours - self.total_hours)

    @property
    def total_trust_impact(self) -> int:
        return sum(i.trust_score_impact for i in self.items)

    @property
    def priority_breakdown(self) -> dict[str, int]:
        counts: Counter[str] = Counter()
        for item in self.items:
            counts[item.priority.value] += 1
        return dict(counts)

    def to_dict(self) -> dict[str, Any]:
        return {
            "sprint_number": self.sprint_number,
            "name": self.name,
            "item_count": self.item_count,
            "total_hours": round(self.total_hours, 1),
            "max_hours": self.max_hours,
            "remaining_hours": round(self.remaining_hours, 1),
            "total_trust_impact": self.total_trust_impact,
            "priority_breakdown": self.priority_breakdown,
            "items": [i.to_dict() for i in self.items],
        }


@dataclass
class FixPlan:
    """Complete fix plan with ordered items."""
    items: list[FixItem] = field(default_factory=list)
    total_effort_hours: float = 0.0
    total_trust_impact: int = 0
    priority_counts: dict[str, int] = field(default_factory=dict)
    category_counts: dict[str, int] = field(default_factory=dict)
    execution_order: list[str] = field(default_factory=list)
    timestamp: str = ""

    def __post_init__(self):
        if not self.timestamp:
            self.timestamp = datetime.now(timezone.utc).isoformat()

    @property
    def item_count(self) -> int:
        return len(self.items)

    @property
    def critical_count(self) -> int:
        return self.priority_counts.get("critical", 0)

    @property
    def high_count(self) -> int:
        return self.priority_counts.get("high", 0)

    def get_item(self, fix_id: str) -> FixItem | None:
        for item in self.items:
            if item.fix_id == fix_id:
                return item
        return None

    def to_dict(self) -> dict[str, Any]:
        return {
            "item_count": self.item_count,
            "total_effort_hours": round(self.total_effort_hours, 1),
            "total_trust_impact": self.total_trust_impact,
            "priority_counts": dict(self.priority_counts),
            "category_counts": dict(self.category_counts),
            "execution_order": list(self.execution_order),
            "items": [i.to_dict() for i in self.items],
            "timestamp": self.timestamp,
        }


# ---------------------------------------------------------------------------
# Fix knowledge base
# ---------------------------------------------------------------------------

_FIX_KB: dict[str, dict[str, Any]] = {
    "CMD-001": {
        "title": "Fix shell command injection",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 1.0,
        "trust_impact": 15,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "os_system(f'ls {user_input}')",
                "after": "subprocess.run(['ls', user_input], check=True)",
                "description": "Replace os_system with subprocess.run using list args",
            },
        ],
        "tags": ["injection", "critical"],
    },
    "CMD-002": {
        "title": "Sanitize command arguments",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 0.5,
        "trust_impact": 10,
        "depends_on": ["CMD-001"],
        "snippets": [
            {
                "language": "python",
                "before": "cmd = f'echo {user_input}'",
                "after": "cmd = f'echo {shlex.quote(user_input)}'",
                "description": "Apply shlex.quote to user-supplied arguments",
            },
        ],
        "tags": ["injection", "sanitization"],
    },
    "SQL-001": {
        "title": "Fix SQL injection vulnerability",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 1.0,
        "trust_impact": 15,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "cursor.execute(f'SELECT * FROM users WHERE id={uid}')",
                "after": "cursor.execute('SELECT * FROM users WHERE id=?', (uid,))",
                "description": "Use parameterized queries",
            },
        ],
        "tags": ["injection", "sql"],
    },
    "PATH-001": {
        "title": "Fix path traversal vulnerability",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 0.75,
        "trust_impact": 10,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "open(user_path)",
                "after": (
                    "resolved = Path(user_path).resolve()\n"
                    "assert str(resolved).startswith(str(ALLOWED_DIR))\n"
                    "open(resolved)"
                ),
                "description": "Resolve and validate paths against allowlist",
            },
        ],
        "tags": ["traversal", "filesystem"],
    },
    "AUTH-001": {
        "title": "Implement authentication",
        "category": FixCategory.ARCHITECTURE,
        "effort_hours": 4.0,
        "trust_impact": 20,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "@tool\ndef my_tool(args):\n    return process(args)",
                "after": "@tool\n@require_auth\ndef my_tool(args):\n    return process(args)",
                "description": "Add authentication decorator to tool handlers",
            },
        ],
        "tags": ["auth", "architecture"],
    },
    "AUTHZ-001": {
        "title": "Add authorization checks",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 2.0,
        "trust_impact": 15,
        "depends_on": ["AUTH-001"],
        "snippets": [
            {
                "language": "python",
                "before": "def access_resource(user, rid):\n    return db.get(rid)",
                "after": (
                    "def access_resource(user, rid):\n"
                    "    if not user.can_access(rid):\n"
                    "        raise PermissionError('Unauthorized')\n"
                    "    return db.get(rid)"
                ),
                "description": "Verify user permissions before resource access",
            },
        ],
        "tags": ["auth", "authz"],
    },
    "SSRF-001": {
        "title": "Prevent SSRF attacks",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 1.5,
        "trust_impact": 12,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "requests.get(user_url)",
                "after": (
                    "parsed = urlparse(user_url)\n"
                    "if parsed.hostname not in ALLOWED_HOSTS:\n"
                    "    raise ValueError('Host not allowed')\n"
                    "requests.get(user_url)"
                ),
                "description": "Validate URLs against domain allowlist",
            },
        ],
        "tags": ["ssrf", "network"],
    },
    "DESER-001": {
        "title": "Fix unsafe deserialization",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 1.0,
        "trust_impact": 12,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "data = unsafe_loads(raw)",
                "after": "data = json.loads(raw)",
                "description": "Replace unsafe deserializer with JSON",
            },
        ],
        "tags": ["deserialization"],
    },
    "SEC-001": {
        "title": "Remove hardcoded secrets",
        "category": FixCategory.CONFIG_CHANGE,
        "effort_hours": 0.5,
        "trust_impact": 8,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "API_KEY = 'sk-abc123'",
                "after": "API_KEY = os.environ['API_KEY']",
                "description": "Move secrets to environment variables",
            },
        ],
        "tags": ["secrets", "config"],
    },
    "PERM-001": {
        "title": "Fix excessive permissions",
        "category": FixCategory.CONFIG_CHANGE,
        "effort_hours": 0.5,
        "trust_impact": 5,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "os.chmod(path, 0o777)",
                "after": "os.chmod(path, 0o644)",
                "description": "Use restrictive file permissions",
            },
        ],
        "tags": ["permissions"],
    },
    "RATE-001": {
        "title": "Add rate limiting",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 2.0,
        "trust_impact": 8,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "@tool\ndef my_tool(args): ...",
                "after": "@tool\n@rate_limit(max_calls=100, window=60)\ndef my_tool(args): ...",
                "description": "Add rate limiting decorator",
            },
        ],
        "tags": ["rate-limiting", "dos"],
    },
    "VAL-001": {
        "title": "Add input validation",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 1.0,
        "trust_impact": 8,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "def process(data): ...",
                "after": "def process(data):\n    validate_schema(data, EXPECTED_SCHEMA)\n    ...",
                "description": "Add schema validation for external input",
            },
        ],
        "tags": ["validation", "input"],
    },
    "IDOR-001": {
        "title": "Fix insecure direct object reference",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 1.5,
        "trust_impact": 10,
        "depends_on": ["AUTHZ-001"],
        "snippets": [],
        "tags": ["idor", "authz"],
    },
    "ERR-001": {
        "title": "Implement proper error handling",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 1.0,
        "trust_impact": 5,
        "depends_on": [],
        "snippets": [],
        "tags": ["error-handling"],
    },
    "INFO-001": {
        "title": "Remove information disclosure",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 0.5,
        "trust_impact": 5,
        "depends_on": ["ERR-001"],
        "snippets": [],
        "tags": ["info-disclosure"],
    },
    "LOG-001": {
        "title": "Sanitize log output",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 0.5,
        "trust_impact": 3,
        "depends_on": [],
        "snippets": [],
        "tags": ["logging"],
    },
    "CRYPTO-001": {
        "title": "Use strong cryptographic algorithms",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 1.0,
        "trust_impact": 8,
        "depends_on": [],
        "snippets": [
            {
                "language": "python",
                "before": "hashlib.md5(data)",
                "after": "hashlib.sha256(data)",
                "description": "Replace weak hash with SHA-256",
            },
        ],
        "tags": ["crypto"],
    },
    "CORS-001": {
        "title": "Restrict CORS configuration",
        "category": FixCategory.CONFIG_CHANGE,
        "effort_hours": 0.5,
        "trust_impact": 5,
        "depends_on": [],
        "snippets": [],
        "tags": ["cors", "config"],
    },
    "RES-001": {
        "title": "Implement resource limits",
        "category": FixCategory.CONFIG_CHANGE,
        "effort_hours": 1.5,
        "trust_impact": 8,
        "depends_on": [],
        "snippets": [],
        "tags": ["resources", "dos"],
    },
    "REDOS-001": {
        "title": "Fix ReDoS-vulnerable regex",
        "category": FixCategory.CODE_CHANGE,
        "effort_hours": 1.0,
        "trust_impact": 5,
        "depends_on": [],
        "snippets": [],
        "tags": ["regex", "dos"],
    },
}

_DEFAULT_FIX: dict[str, Any] = {
    "title": "Review and remediate finding",
    "category": FixCategory.MANUAL_REVIEW,
    "effort_hours": 2.0,
    "trust_impact": 5,
    "depends_on": [],
    "snippets": [],
    "tags": ["manual"],
}


# ---------------------------------------------------------------------------
# Topological sort for execution order
# ---------------------------------------------------------------------------

def _topological_sort(items: list[FixItem]) -> list[str]:
    """Topological sort of fix items by their dependencies.

    Returns fix_ids in dependency-respecting execution order.
    Items with unresolvable deps are placed at the end.
    """
    item_ids = {i.fix_id for i in items}
    item_map = {i.fix_id: i for i in items}

    # Build adjacency: dep -> [dependents]
    in_degree: dict[str, int] = {i.fix_id: 0 for i in items}
    graph: dict[str, list[str]] = {i.fix_id: [] for i in items}

    for item in items:
        for dep in item.depends_on:
            if dep in item_ids:
                graph[dep].append(item.fix_id)
                in_degree[item.fix_id] += 1

    # Kahn's algorithm
    queue: list[str] = []
    for fix_id, deg in in_degree.items():
        if deg == 0:
            queue.append(fix_id)

    # Sort queue by priority for deterministic ordering among peers
    queue.sort(key=lambda fid: item_map[fid].priority.weight)

    result: list[str] = []
    while queue:
        # Pick the highest-priority item with 0 in-degree
        queue.sort(key=lambda fid: item_map[fid].priority.weight)
        current = queue.pop(0)
        result.append(current)

        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    # Handle items not reached (cycles or missing deps) — append them
    remaining = [i.fix_id for i in items if i.fix_id not in set(result)]
    remaining.sort(key=lambda fid: item_map[fid].priority.weight)
    result.extend(remaining)

    return result


# ---------------------------------------------------------------------------
# Trust score impact estimation
# ---------------------------------------------------------------------------

_SEVERITY_TRUST_IMPACT: dict[str, int] = {
    "CRITICAL": 15,
    "HIGH": 10,
    "MEDIUM": 5,
    "LOW": 2,
    "INFO": 1,
}


def _estimate_trust_impact(rule_id: str, severity: str) -> int:
    """Estimate trust score improvement from fixing a finding."""
    kb = _FIX_KB.get(rule_id)
    if kb:
        return kb.get("trust_impact", 5)
    return _SEVERITY_TRUST_IMPACT.get(severity.upper(), 5)


# ---------------------------------------------------------------------------
# Core planner
# ---------------------------------------------------------------------------

def _build_fix_item(
    rule_id: str,
    findings: list[dict[str, Any]],
    fix_counter: int,
) -> FixItem:
    """Build a FixItem from grouped findings."""
    kb = _FIX_KB.get(rule_id, _DEFAULT_FIX)
    first = findings[0]
    severity = first.get("severity", "MEDIUM")

    category = kb.get("category", FixCategory.MANUAL_REVIEW)
    if isinstance(category, str):
        category = FixCategory(category)

    snippets: list[CodeSnippet] = []
    for raw in kb.get("snippets", []):
        snippets.append(CodeSnippet(
            language=raw.get("language", "python"),
            before=raw.get("before", ""),
            after=raw.get("after", ""),
            description=raw.get("description", ""),
            file_pattern=raw.get("file_pattern", ""),
        ))

    effort = kb.get("effort_hours", 2.0)
    trust_impact = _estimate_trust_impact(rule_id, severity)
    affected_files = sorted(set(f.get("file", "") for f in findings if f.get("file")))

    return FixItem(
        fix_id=f"FIX-{fix_counter:03d}",
        rule_id=rule_id,
        title=kb.get("title", first.get("title", f"Fix {rule_id}")),
        description=first.get("description", ""),
        priority=FixPriority.from_severity(severity),
        category=category,
        effort_hours=effort,
        trust_score_impact=trust_impact,
        depends_on=list(kb.get("depends_on", [])),
        affected_files=affected_files,
        finding_count=len(findings),
        snippets=snippets,
        tags=list(kb.get("tags", [])),
    )


def create_fix_plan(
    findings: list[dict[str, Any]],
) -> FixPlan:
    """Create a prioritized fix plan from scan findings.

    Groups findings by rule_id, resolves dependencies,
    estimates effort and trust score impact.

    Args:
        findings: List of finding dicts with at least rule_id and severity.

    Returns:
        FixPlan with ordered items.
    """
    if not findings:
        return FixPlan()

    # Group by rule_id
    by_rule: dict[str, list[dict[str, Any]]] = {}
    for f in findings:
        rid = f.get("rule_id", "UNKNOWN")
        by_rule.setdefault(rid, []).append(f)

    # Build fix items
    items: list[FixItem] = []
    fix_counter = 0
    rule_to_fix: dict[str, str] = {}

    for rule_id, group in by_rule.items():
        fix_counter += 1
        item = _build_fix_item(rule_id, group, fix_counter)
        items.append(item)
        rule_to_fix[rule_id] = item.fix_id

    # Resolve depends_on from rule_ids to fix_ids
    for item in items:
        resolved_deps: list[str] = []
        for dep_rule in item.depends_on:
            if dep_rule in rule_to_fix:
                resolved_deps.append(rule_to_fix[dep_rule])
        item.depends_on = resolved_deps

    # Compute execution order
    exec_order = _topological_sort(items)

    # Reorder items by execution order
    item_map = {i.fix_id: i for i in items}
    ordered = [item_map[fid] for fid in exec_order if fid in item_map]

    # Compute aggregates
    total_effort = sum(i.effort_hours for i in ordered)
    total_impact = sum(i.trust_score_impact for i in ordered)
    priority_counts: Counter[str] = Counter()
    category_counts: Counter[str] = Counter()
    for item in ordered:
        priority_counts[item.priority.value] += 1
        category_counts[item.category.value] += 1

    return FixPlan(
        items=ordered,
        total_effort_hours=total_effort,
        total_trust_impact=total_impact,
        priority_counts=dict(priority_counts),
        category_counts=dict(category_counts),
        execution_order=exec_order,
    )


# ---------------------------------------------------------------------------
# Sprint planning
# ---------------------------------------------------------------------------

def create_sprints(
    plan: FixPlan,
    hours_per_sprint: float = 8.0,
) -> list[Sprint]:
    """Group fix plan items into sprints of a given duration.

    Items are assigned to sprints in execution order.
    An item that exceeds the remaining sprint capacity starts a new sprint.

    Dependency ordering is preserved: if item B depends on item A,
    A's sprint number <= B's sprint number.

    Args:
        plan: FixPlan with ordered items.
        hours_per_sprint: Maximum hours per sprint.

    Returns:
        List of Sprint objects.
    """
    if not plan.items or hours_per_sprint <= 0:
        return []

    sprints: list[Sprint] = []
    current = Sprint(
        sprint_number=1,
        name="Sprint 1",
        max_hours=hours_per_sprint,
    )

    for item in plan.items:
        # If item won't fit and we have items, start new sprint
        if (current.total_hours + item.effort_hours > hours_per_sprint
                and current.items):
            sprints.append(current)
            num = len(sprints) + 1
            current = Sprint(
                sprint_number=num,
                name=f"Sprint {num}",
                max_hours=hours_per_sprint,
            )

        current.items.append(item)
        current.total_hours += item.effort_hours

    if current.items:
        sprints.append(current)

    return sprints


# ---------------------------------------------------------------------------
# Formatters
# ---------------------------------------------------------------------------

def format_fix_plan_text(plan: FixPlan) -> str:
    """Format a fix plan as plain text."""
    lines: list[str] = []
    lines.append(f"{'=' * 60}")
    lines.append("  Automated Fix Plan")
    lines.append(f"{'=' * 60}")
    lines.append(f"  Items: {plan.item_count}")
    lines.append(f"  Total effort: {plan.total_effort_hours:.1f} hours")
    lines.append(f"  Trust score impact: +{plan.total_trust_impact} points")

    if plan.priority_counts:
        lines.append(f"\n  Priority breakdown:")
        for p in ["critical", "high", "medium", "low", "info"]:
            c = plan.priority_counts.get(p, 0)
            if c > 0:
                lines.append(f"    {p.upper()}: {c}")

    lines.append(f"\n  Execution order:")
    for idx, fix_id in enumerate(plan.execution_order, 1):
        item = plan.get_item(fix_id)
        if item:
            deps = f" (after: {', '.join(item.depends_on)})" if item.depends_on else ""
            lines.append(
                f"    {idx}. [{item.priority.value.upper()}] {fix_id}: "
                f"{item.title} ({item.effort_hours:.1f}h, +{item.trust_score_impact}pts)"
                f"{deps}"
            )

    lines.append(f"\n{'---' * 20}")
    for item in plan.items:
        lines.append(
            f"\n  {item.fix_id}: {item.title}"
        )
        lines.append(f"    Rule: {item.rule_id}")
        lines.append(f"    Priority: {item.priority.value.upper()}")
        lines.append(f"    Category: {item.category.value}")
        lines.append(f"    Effort: {item.effort_hours:.1f} hours")
        lines.append(f"    Trust impact: +{item.trust_score_impact}")
        lines.append(f"    Findings: {item.finding_count}")
        if item.affected_files:
            lines.append(f"    Files: {', '.join(item.affected_files[:5])}")
        if item.depends_on:
            lines.append(f"    Depends on: {', '.join(item.depends_on)}")
        for snippet in item.snippets:
            if snippet.before:
                lines.append(f"    Before: {snippet.before}")
            if snippet.after:
                lines.append(f"    After:  {snippet.after}")

    lines.append(f"\n{'=' * 60}")
    return "\n".join(lines)


def format_fix_plan_json(plan: FixPlan) -> str:
    """Format fix plan as JSON."""
    return json.dumps(plan.to_dict(), indent=2)


def format_sprints_text(sprints: list[Sprint]) -> str:
    """Format sprint plan as plain text."""
    lines: list[str] = []
    lines.append(f"{'=' * 60}")
    lines.append(f"  Sprint Plan ({len(sprints)} sprints)")
    lines.append(f"{'=' * 60}")

    total_hours = sum(s.total_hours for s in sprints)
    total_items = sum(s.item_count for s in sprints)
    total_impact = sum(s.total_trust_impact for s in sprints)

    lines.append(f"  Total items: {total_items}")
    lines.append(f"  Total hours: {total_hours:.1f}")
    lines.append(f"  Total trust impact: +{total_impact}")

    for sprint in sprints:
        lines.append(f"\n  {'---' * 16}")
        lines.append(
            f"  {sprint.name} ({sprint.total_hours:.1f}/{sprint.max_hours:.1f}h)"
        )
        lines.append(f"    Trust impact: +{sprint.total_trust_impact}")
        for item in sprint.items:
            lines.append(
                f"    - [{item.priority.value.upper()}] {item.fix_id}: "
                f"{item.title} ({item.effort_hours:.1f}h)"
            )

    lines.append(f"\n{'=' * 60}")
    return "\n".join(lines)


def format_sprints_json(sprints: list[Sprint]) -> str:
    """Format sprint plan as JSON."""
    return json.dumps([s.to_dict() for s in sprints], indent=2)
