"""Baseline-based quality enforcement infrastructure.

This module provides the foundation for "no new debt" quality gates.
Instead of allowing N violations, we compare against a frozen baseline
of existing violations. Any new violation fails immediately.

Usage:
    from tests.quality._baseline import Violation, assert_no_new_violations

    violations = [
        Violation(
            rule="high_complexity",
            relative_path="src/noteflow/grpc/service.py",
            identifier="StreamTranscription",
            detail="complexity=18",
        )
    ]
    assert_no_new_violations("high_complexity", violations)
"""

from __future__ import annotations

import hashlib
import json
import os
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path

BASELINE_PATH = Path(__file__).parent / "baselines.json"
SCHEMA_VERSION = 1


@dataclass(frozen=True)
class Violation:
    """Represents a quality rule violation with stable identity.

    Attributes:
        rule: The rule name (e.g., "high_complexity", "thin_wrapper")
        relative_path: Path relative to project root
        identifier: Stable identifier (function/class name or content hash)
        detail: Optional detail (wrapped call, metric value, etc.)
    """

    rule: str
    relative_path: str
    identifier: str
    detail: str = ""

    @property
    def stable_id(self) -> str:
        """Generate stable ID for baseline comparison.

        Format: rule|relative_path|identifier[|detail]
        """
        parts = [self.rule, self.relative_path, self.identifier]
        if self.detail:
            parts.append(self.detail)
        return "|".join(parts)

    def __str__(self) -> str:
        """Return human-readable representation."""
        base = f"{self.relative_path}:{self.identifier}"
        return f"{base} ({self.detail})" if self.detail else base


@dataclass
class BaselineResult:
    """Result of baseline comparison."""

    new_violations: list[Violation]
    fixed_violations: list[str]
    current_count: int
    baseline_count: int

    @property
    def passed(self) -> bool:
        """Return True if no new violations introduced."""
        return len(self.new_violations) == 0


def load_baseline() -> dict[str, set[str]]:
    """Load baseline violations from JSON file.

    Returns:
        Mapping of rule names to sets of stable violation IDs.

    Raises:
        ValueError: If baseline schema version doesn't match.
    """
    if not BASELINE_PATH.exists():
        return {}

    data = json.loads(BASELINE_PATH.read_text(encoding="utf-8"))

    schema_version = data.get("schema_version", 0)
    if schema_version != SCHEMA_VERSION:
        raise ValueError(
            f"Baseline schema version mismatch: "
            f"expected {SCHEMA_VERSION}, got {schema_version}"
        )

    return {rule: set(ids) for rule, ids in data.get("rules", {}).items()}


def save_baseline(violations_by_rule: dict[str, list[Violation]]) -> None:
    """Save current violations as new baseline.

    This should only be called manually when intentionally updating the baseline.

    Args:
        violations_by_rule: Mapping of rule names to violation lists.
    """
    data = {
        "schema_version": SCHEMA_VERSION,
        "generated_at": datetime.now(UTC).isoformat(),
        "rules": {
            rule: sorted(v.stable_id for v in violations)
            for rule, violations in violations_by_rule.items()
        },
    }

    BASELINE_PATH.write_text(
        json.dumps(data, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )


def assert_no_new_violations(
    rule: str,
    current_violations: list[Violation],
    *,
    max_new_allowed: int = 0,
) -> BaselineResult:
    """Assert no new violations beyond the frozen baseline.

    Args:
        rule: The rule name (e.g., "high_complexity", "thin_wrapper")
        current_violations: List of violations found in current scan
        max_new_allowed: Allow up to N new violations (default 0)

    Returns:
        BaselineResult with comparison details.

    Raises:
        AssertionError: If new violations exceed max_new_allowed.
    """
    # Check for baseline generation mode
    if os.environ.get("QUALITY_GENERATE_BASELINE") == "1":
        return BaselineResult(
            new_violations=[],
            fixed_violations=[],
            current_count=len(current_violations),
            baseline_count=0,
        )

    baseline = load_baseline()
    allowed_ids = baseline.get(rule, set())

    current_ids = {v.stable_id for v in current_violations}

    new_ids = current_ids - allowed_ids
    fixed_ids = allowed_ids - current_ids

    new_violations = [v for v in current_violations if v.stable_id in new_ids]

    result = BaselineResult(
        new_violations=sorted(new_violations, key=lambda v: v.stable_id),
        fixed_violations=sorted(fixed_ids),
        current_count=len(current_violations),
        baseline_count=len(allowed_ids),
    )

    if len(new_violations) > max_new_allowed:
        message_parts = [
            f"[{rule}] {len(new_violations)} NEW violation(s) introduced "
            f"(baseline: {len(allowed_ids)}, current: {len(current_violations)}):",
        ]
        message_parts.extend(f"  + {v}" for v in new_violations[:20])
        if len(new_violations) > 20:
            message_parts.append(f"  ... and {len(new_violations) - 20} more")

        if fixed_ids:
            message_parts.append(
                f"\nFixed {len(fixed_ids)} violation(s) (can update baseline):"
            )
            message_parts.extend(f"  - {fid}" for fid in list(fixed_ids)[:5])
            if len(fixed_ids) > 5:
                message_parts.append(f"  ... and {len(fixed_ids) - 5} more")

        raise AssertionError("\n".join(message_parts))

    return result


def content_hash(content: str, length: int = 8) -> str:
    """Generate short hash of content for stable line-level IDs.

    Args:
        content: The content to hash.
        length: Length of the hash to return (default 8).

    Returns:
        Hexadecimal hash string.
    """
    return hashlib.sha256(content.encode()).hexdigest()[:length]


def collect_violations_for_baseline(
    test_functions: list[tuple[str, list[Violation]]],
) -> None:
    """Collect violations from multiple tests and save baseline.

    This is a helper for generating the initial baseline.

    Args:
        test_functions: List of (rule_name, violations) tuples.
    """
    violations_by_rule: dict[str, list[Violation]] = dict(test_functions)
    save_baseline(violations_by_rule)
