#!/usr/bin/env python3
"""
Validate risk_scoring sections in Argus analysis files.
Checks:
  1. Context profile referenced exists in context_profiles.yaml
  2. Multiplier sources are valid keys from context_profiles.risk_multipliers
  3. Multiplier values match the referenced profile
  4. final_score = base_severity × multiplier_applied × confidence (±0.01)
  5. Ranks are sequential (1..N) with no gaps
  6. Summary counts are consistent with scored findings

Usage:
  python tests/validate_risk_scoring.py [--verbose] [--quiet]
"""
import sys
import math
import logging
from pathlib import Path

try:
    import yaml
except ImportError:
    print("Missing dependency: pip install pyyaml")
    sys.exit(1)

from config import (
    VALID_MULTIPLIER_SOURCES, CRITICAL_THRESHOLD,
    HIGH_THRESHOLD, SCORE_TOLERANCE,
)

logger = logging.getLogger(__name__)

ROOT = Path(__file__).resolve().parent.parent
CONTEXT_PROFILES_PATH = ROOT / "memory" / "semantic" / "context_profiles.yaml"
ANALYSES_DIR = ROOT / "memory" / "episodic" / "analyses"


def load_context_profiles() -> dict:
    """Load context profiles and return {profile_id: {multiplier_key: value}}."""
    with open(CONTEXT_PROFILES_PATH, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)

    profiles = {}
    for profile in data.get("profiles", []):
        pid = profile.get("id", "")
        multipliers = profile.get("risk_multipliers", {})
        profiles[pid] = multipliers
    return profiles


def validate_risk_scoring(analysis_path: Path, profiles: dict, verbose: bool = False) -> list[str]:
    """Validate risk_scoring section of a single analysis file. Returns list of error strings."""
    errors = []
    with open(analysis_path, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)

    if data is None:
        return []

    # La section risk_scoring est optionnelle dans les analyses —
    # seules les analyses avec findings quantifiés en ont une
    scoring = data.get("risk_scoring")
    if scoring is None:
        return []

    try:
        rel = str(analysis_path.relative_to(ROOT))
    except ValueError:
        rel = str(analysis_path)

    # 1. Context profile exists
    ctx_id = scoring.get("context_profile", "")
    if ctx_id not in profiles:
        errors.append(f"  [{rel}] context_profile '{ctx_id}' not found in context_profiles.yaml. "
                       f"Available: {sorted(profiles.keys())}")

    profile_multipliers = profiles.get(ctx_id, {})

    findings_scored = scoring.get("findings_scored", [])
    if not findings_scored:
        return errors  # No findings to validate

    ranks_seen = []
    computed_scores = []

    for i, fs in enumerate(findings_scored):
        fid = fs.get("finding_id", f"item[{i}]")

        # 2. Multiplier source is a valid key
        msrc = fs.get("multiplier_source", "")
        if msrc not in VALID_MULTIPLIER_SOURCES:
            errors.append(f"  [{rel}] {fid}: multiplier_source '{msrc}' invalid. "
                           f"Must be one of {sorted(VALID_MULTIPLIER_SOURCES)}")

        # 3. Multiplier matches profile value
        mult_applied = fs.get("multiplier_applied", 0)
        if not isinstance(mult_applied, (int, float)):
            errors.append(f"  [{rel}] {fid}: multiplier_applied must be numeric, got {type(mult_applied).__name__}")
            mult_applied = 0
        if profile_multipliers and msrc in profile_multipliers:
            expected_mult = profile_multipliers[msrc]
            if abs(mult_applied - expected_mult) > SCORE_TOLERANCE:
                errors.append(f"  [{rel}] {fid}: multiplier_applied={mult_applied} "
                               f"but {ctx_id}.{msrc}={expected_mult}")

        # Vérification 4 : formule de scoring Argus
        # final_score = base_severity × multiplier_applied × confidence
        # La tolérance SCORE_TOLERANCE absorbe les erreurs d'arrondi flottant
        base = fs.get("base_severity", 0)
        conf = fs.get("confidence", 0)
        final = fs.get("final_score", 0)
        for field_name, field_val in [("base_severity", base), ("confidence", conf), ("final_score", final)]:
            if not isinstance(field_val, (int, float)):
                errors.append(f"  [{rel}] {fid}: {field_name} must be numeric, got {type(field_val).__name__}")
        if isinstance(base, (int, float)) and isinstance(conf, (int, float)) and isinstance(final, (int, float)):
            expected_score = base * mult_applied * conf
            if abs(final - expected_score) > SCORE_TOLERANCE:
                errors.append(f"  [{rel}] {fid}: final_score={final} "
                               f"but {base} × {mult_applied} × {conf} = {expected_score:.4f}")

        # Collect for summary/rank checks
        ranks_seen.append(fs.get("rank", 0))
        computed_scores.append(final)

    # Vérification 5 : les rangs doivent être séquentiels 1..N sans trous
    # (le rang 1 = finding le plus critique)
    expected_ranks = list(range(1, len(findings_scored) + 1))
    if sorted(ranks_seen) != expected_ranks:
        errors.append(f"  [{rel}] ranks should be 1..{len(findings_scored)}, "
                       f"got {sorted(ranks_seen)}")

    # Vérification 6 : le summary (total_scored, critical_count, high_count,
    # max_score, avg_score) doit refléter fidèlement les findings_scored
    summary = scoring.get("summary")
    if summary:
        total = summary.get("total_scored", 0)
        if total != len(findings_scored):
            errors.append(f"  [{rel}] summary.total_scored={total} but {len(findings_scored)} findings scored")

        expected_critical = sum(1 for s in computed_scores if s >= CRITICAL_THRESHOLD)
        actual_critical = summary.get("critical_count", 0)
        if actual_critical != expected_critical:
            errors.append(f"  [{rel}] summary.critical_count={actual_critical} "
                           f"but {expected_critical} findings have score >= {CRITICAL_THRESHOLD}")

        expected_high = sum(1 for s in computed_scores if s >= HIGH_THRESHOLD)
        actual_high = summary.get("high_count", 0)
        if actual_high != expected_high:
            errors.append(f"  [{rel}] summary.high_count={actual_high} "
                           f"but {expected_high} findings have score >= {HIGH_THRESHOLD}")

        if computed_scores:
            expected_max = max(computed_scores)
            actual_max = summary.get("max_score", 0)
            if abs(actual_max - expected_max) > SCORE_TOLERANCE:
                errors.append(f"  [{rel}] summary.max_score={actual_max} but computed max={expected_max:.4f}")

            expected_avg = sum(computed_scores) / len(computed_scores)
            actual_avg = summary.get("avg_score", 0)
            if abs(actual_avg - expected_avg) > SCORE_TOLERANCE:
                errors.append(f"  [{rel}] summary.avg_score={actual_avg} but computed avg={expected_avg:.4f}")

    return errors


def main():
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    verbose = "--verbose" in sys.argv or "-v" in sys.argv
    quiet = "--quiet" in sys.argv or "-q" in sys.argv

    if not CONTEXT_PROFILES_PATH.exists():
        print(f"ERROR: Context profiles not found at {CONTEXT_PROFILES_PATH}")
        return 1

    profiles = load_context_profiles()

    if not quiet:
        print(f"{'='*70}")
        print(f"  ARGUS — Risk Scoring Validation")
        print(f"  Context profiles loaded: {len(profiles)} ({', '.join(sorted(profiles.keys()))})")
        print(f"{'='*70}\n")

    total_errors = 0
    validated = 0
    with_scoring = 0

    for yaml_file in sorted(ANALYSES_DIR.glob("*.yaml")):
        if yaml_file.name.startswith("_"):
            continue  # skip templates
        validated += 1
        errors = validate_risk_scoring(yaml_file, profiles, verbose)

        # Check if file has risk_scoring
        with open(yaml_file, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)
        if data and data.get("risk_scoring"):
            with_scoring += 1

        if errors:
            total_errors += len(errors)
            if not quiet:
                print(f"✗ {yaml_file.relative_to(ROOT)}")
                for e in errors:
                    print(e)
                print()
        elif verbose:
            print(f"✓ {yaml_file.relative_to(ROOT)}")

    # Also validate template
    template_path = ANALYSES_DIR / "_template_analysis.yaml"
    if template_path.exists():
        errors = validate_risk_scoring(template_path, profiles, verbose)
        if errors:
            total_errors += len(errors)
            if not quiet:
                print(f"✗ {template_path.relative_to(ROOT)} (template)")
                for e in errors:
                    print(e)
                print()

    if not quiet:
        print(f"\n{'─'*70}")
        print(f"  Analyses validated: {validated}")
        print(f"  With risk_scoring: {with_scoring}")
        print(f"  Errors: {total_errors}")
        print(f"{'─'*70}")

    return 1 if total_errors > 0 else 0


if __name__ == "__main__":
    sys.exit(main())
