"""Running Scorecard — live coverage tracking with curiosity-peak insights.

Accumulates per-file validation results as the streaming scan progresses.
Generates curiosity-peak insights that reveal interesting facts about the
codebase during scan — keeping the user engaged while building completes.

See ADR-105 §Curiosity Peak Design.
"""

# ── graqle:intelligence ──
# module: graqle.intelligence.scorecard
# risk: MEDIUM (impact radius: 7 modules)
# consumers: claude_section, compile, emitter, pipeline, test_claude_section +2 more
# dependencies: __future__, logging, typing, models
# constraints: none
# ── /graqle:intelligence ──

from __future__ import annotations

import logging
from typing import Any

from graqle.intelligence.models import (
    CoverageReport,
    CuriosityInsight,
    FileIntelligenceUnit,
    InsightCategory,
    ModulePacket,
)

logger = logging.getLogger("graqle.intelligence.scorecard")


class RunningScorecard:
    """Live-updating scorecard that tracks scan progress and generates insights.

    Fed one FileIntelligenceUnit at a time. After each file:
    1. Updates running coverage totals
    2. Checks for curiosity-peak insights
    3. Maintains rankings (most imported, largest, etc.)
    """

    def __init__(self) -> None:
        # Running totals
        self.files_scanned: int = 0
        self.total_files: int = 0  # set externally when file list is known
        self.total_nodes: int = 0
        self.total_edges: int = 0
        self.nodes_with_chunks: int = 0
        self.nodes_with_descriptions: int = 0
        self.valid_edges: int = 0
        self.pending_edges: int = 0
        self.dangling_edges: int = 0
        self.auto_repairs: int = 0
        self.degraded_nodes: int = 0
        self.parse_failures: int = 0

        # Rankings for insights
        self._function_counts: dict[str, int] = {}     # module → function count
        self._class_counts: dict[str, int] = {}         # module → class count
        self._consumer_counts: dict[str, int] = {}      # module → consumer count
        self._dependency_counts: dict[str, int] = {}    # module → dependency count
        self._line_counts: dict[str, int] = {}          # module → line count
        self._risk_scores: dict[str, float] = {}        # module → risk score
        self._incidents: dict[str, list[str]] = {}      # module → incidents
        self._all_packets: list[ModulePacket] = []

        # All insights generated
        self.insights: list[CuriosityInsight] = []
        # Dedup key: (normalized_module, category, message_pattern)
        self._seen_insight_keys: set[tuple[str, str, str]] = set()

    def ingest(self, unit: FileIntelligenceUnit) -> list[CuriosityInsight]:
        """Ingest one file's intelligence and return new insights.

        Returns only the NEW insights generated by this file (not cumulative).
        """
        self.files_scanned += 1
        new_insights: list[CuriosityInsight] = []

        # Update coverage totals
        cov = unit.coverage
        self.total_nodes += cov.total_nodes
        self.nodes_with_chunks += cov.nodes_with_chunks
        self.nodes_with_descriptions += cov.nodes_with_descriptions
        self.total_edges += cov.total_edges
        self.valid_edges += cov.valid_edges
        self.pending_edges += cov.pending_edges
        self.dangling_edges += cov.dangling_edges
        self.auto_repairs += cov.auto_repairs
        self.degraded_nodes += cov.degraded_nodes
        if not cov.parse_success:
            self.parse_failures += 1

        # Track module metrics
        pkt = unit.module_packet
        module = pkt.module
        self._function_counts[module] = pkt.function_count
        self._class_counts[module] = pkt.class_count
        self._consumer_counts[module] = pkt.consumer_count
        self._dependency_counts[module] = pkt.dependency_count
        self._line_counts[module] = pkt.line_count
        self._risk_scores[module] = pkt.risk_score
        if pkt.incidents:
            self._incidents[module] = pkt.incidents
        self._all_packets.append(pkt)

        # Generate curiosity-peak insights for this file
        new_insights.extend(self._check_superlatives(pkt))
        new_insights.extend(self._check_warnings(pkt))
        new_insights.extend(self._check_connections(pkt))
        new_insights.extend(self._check_suggestions(pkt))

        # Deduplicate: skip insights for the same normalized module + category + pattern
        # This prevents duplicate insights from source/dist/bundle variants of the same module
        deduped: list[CuriosityInsight] = []
        for insight in new_insights:
            key = self._insight_dedup_key(insight)
            if key not in self._seen_insight_keys:
                self._seen_insight_keys.add(key)
                deduped.append(insight)

        self.insights.extend(deduped)
        return deduped

    @property
    def coverage(self) -> CoverageReport:
        """Current running coverage report."""
        return CoverageReport(
            total_nodes=self.total_nodes,
            nodes_with_chunks=self.nodes_with_chunks,
            nodes_with_descriptions=self.nodes_with_descriptions,
            total_edges=self.total_edges,
            valid_edges=self.valid_edges,
            pending_edges=self.pending_edges,
            dangling_edges=self.dangling_edges,
            auto_repairs=self.auto_repairs,
            degraded_nodes=self.degraded_nodes,
            parse_success=self.parse_failures == 0,
        )

    @property
    def progress_pct(self) -> float:
        if self.total_files == 0:
            return 0.0
        return round(self.files_scanned / self.total_files * 100, 1)

    def to_dict(self) -> dict[str, Any]:
        """Serialize current state for JSON output / WebSocket event."""
        cov = self.coverage
        return {
            "files_scanned": self.files_scanned,
            "total_files": self.total_files,
            "progress_pct": self.progress_pct,
            "total_nodes": self.total_nodes,
            "total_edges": self.total_edges,
            "chunk_coverage": cov.chunk_coverage,
            "description_coverage": cov.description_coverage,
            "edge_integrity": cov.edge_integrity,
            "health": cov.health,
            "auto_repairs": self.auto_repairs,
            "degraded_nodes": self.degraded_nodes,
            "parse_failures": self.parse_failures,
            "insights_count": len(self.insights),
        }

    def recalculate_edge_coverage(self, all_units: list[FileIntelligenceUnit]) -> None:
        """Recalculate edge coverage after second-pass edge resolution."""
        self.total_edges = 0
        self.valid_edges = 0
        self.pending_edges = 0
        for unit in all_units:
            cov = unit.coverage
            self.total_edges += cov.total_edges
            self.valid_edges += cov.valid_edges
            self.pending_edges += cov.pending_edges

    @staticmethod
    def _normalize_module(module: str) -> str:
        """Normalize module name to avoid duplicates from source/dist/bundle variants.

        Examples:
            graqle.core.graph      -> graqle.core.graph
            dist.graqle.core.graph -> graqle.core.graph
            src.graqle.core.graph  -> graqle.core.graph
            build.lib.graqle.x    -> graqle.x
        """
        parts = module.split(".")
        # Strip common build/dist prefixes
        strip_prefixes = {"dist", "build", "src", "lib", "out", "output", "lambda_patch", "lambda_repack"}
        while parts and parts[0].lower().replace("-", "_").replace(" ", "_") in strip_prefixes:
            parts = parts[1:]
        return ".".join(parts) if parts else module

    @staticmethod
    def _insight_dedup_key(insight: CuriosityInsight) -> tuple[str, str, str]:
        """Create a dedup key from insight category + normalized module + message pattern."""
        import re
        # Strip numbers from message to match patterns like "HIGH RISK — impact radius: 5"
        # and "HIGH RISK — impact radius: 5" (same module, same pattern)
        msg_pattern = re.sub(r"\d+", "N", insight.message[:60])
        return (
            RunningScorecard._normalize_module(insight.module),
            insight.category.value,
            msg_pattern,
        )

    # ─── Insight Generators ──────────────────────────────────────────

    def _check_superlatives(self, pkt: ModulePacket) -> list[CuriosityInsight]:
        """Check if this module is the new leader in any category.

        Only emits one insight per category across the entire compile run
        by replacing previous holders when a new leader emerges.
        """
        insights: list[CuriosityInsight] = []

        # Skip modules with very short names (minified/bundled artifacts)
        module_leaf = pkt.module.rsplit(".", 1)[-1]
        if len(module_leaf) <= 2:
            return insights

        # Most consumers (most imported)
        if pkt.consumer_count > 0 and self.files_scanned > 1:
            max_consumers = max(self._consumer_counts.values())
            if pkt.consumer_count == max_consumers and pkt.consumer_count >= 3:
                # Remove previous "MOST IMPORTED" insights — only keep the latest leader
                self.insights = [
                    i for i in self.insights
                    if not (i.category == InsightCategory.SUPERLATIVE and "MOST IMPORTED" in i.message)
                ]
                insights.append(CuriosityInsight(
                    category=InsightCategory.SUPERLATIVE,
                    module=pkt.module,
                    message=f"THE MOST IMPORTED MODULE — {pkt.consumer_count} consumers. "
                            f"Any change here ripples through the codebase.",
                    metric=f"{pkt.consumer_count} consumers",
                    severity="critical" if pkt.consumer_count >= 10 else "warn",
                ))

        # Most functions (largest module)
        if pkt.function_count > 0 and self.files_scanned > 1:
            max_fns = max(self._function_counts.values())
            if pkt.function_count == max_fns and pkt.function_count >= 20:
                self.insights = [
                    i for i in self.insights
                    if not (i.category == InsightCategory.SUPERLATIVE and "LARGEST MODULE" in i.message)
                ]
                insights.append(CuriosityInsight(
                    category=InsightCategory.SUPERLATIVE,
                    module=pkt.module,
                    message=f"YOUR LARGEST MODULE — {pkt.function_count} functions, "
                            f"{pkt.class_count} classes.",
                    metric=f"{pkt.function_count} functions",
                ))

        # Most dependencies (most complex)
        if pkt.dependency_count > 0 and self.files_scanned > 1:
            max_deps = max(self._dependency_counts.values())
            if pkt.dependency_count == max_deps and pkt.dependency_count >= 6:
                self.insights = [
                    i for i in self.insights
                    if not (i.category == InsightCategory.SUPERLATIVE and "MOST COMPLEX" in i.message)
                ]
                insights.append(CuriosityInsight(
                    category=InsightCategory.SUPERLATIVE,
                    module=pkt.module,
                    message=f"MOST COMPLEX MODULE — imports {pkt.dependency_count} other modules.",
                    metric=f"{pkt.dependency_count} dependencies",
                ))

        return insights

    def _check_warnings(self, pkt: ModulePacket) -> list[CuriosityInsight]:
        """Check for warning-level insights."""
        insights: list[CuriosityInsight] = []

        # High risk score
        if pkt.risk_level in ("HIGH", "CRITICAL"):
            insights.append(CuriosityInsight(
                category=InsightCategory.WARNING,
                module=pkt.module,
                message=f"{pkt.risk_level} RISK — impact radius: {pkt.impact_radius} modules. "
                        f"Changes here affect {pkt.impact_radius} other modules.",
                metric=f"impact: {pkt.impact_radius}",
                severity="critical" if pkt.risk_level == "CRITICAL" else "warn",
            ))

        # Incident history
        if pkt.incidents:
            insights.append(CuriosityInsight(
                category=InsightCategory.HISTORY,
                module=pkt.module,
                message=f"INCIDENT HISTORY FOUND — {len(pkt.incidents)} past issue(s). "
                        f"Latest: {pkt.incidents[-1][:80]}",
                metric=f"{len(pkt.incidents)} incidents",
                severity="warn",
            ))

        return insights

    def _check_connections(self, pkt: ModulePacket) -> list[CuriosityInsight]:
        """Check for connection/topology insights."""
        insights: list[CuriosityInsight] = []

        # Hub module (both imports and exports many)
        if pkt.consumer_count >= 3 and pkt.dependency_count >= 3:
            insights.append(CuriosityInsight(
                category=InsightCategory.CONNECTION,
                module=pkt.module,
                message=f"HUB MODULE — {pkt.consumer_count} consumers AND "
                        f"{pkt.dependency_count} dependencies. Central connector in the codebase.",
                metric=f"{pkt.consumer_count}↓ {pkt.dependency_count}↑",
            ))

        return insights

    def _check_suggestions(self, pkt: ModulePacket) -> list[CuriosityInsight]:
        """Check for actionable suggestions."""
        insights: list[CuriosityInsight] = []

        # Large file suggestion
        if pkt.function_count >= 30:
            insights.append(CuriosityInsight(
                category=InsightCategory.SUGGESTION,
                module=pkt.module,
                message=f"{pkt.function_count} functions in one module. Consider splitting "
                        f"into focused sub-modules?",
                metric=f"{pkt.function_count} functions",
            ))

        # Too many classes
        if pkt.class_count >= 8:
            insights.append(CuriosityInsight(
                category=InsightCategory.SUGGESTION,
                module=pkt.module,
                message=f"{pkt.class_count} classes in one module. This might be doing too much.",
                metric=f"{pkt.class_count} classes",
            ))

        return insights
