#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Brain Validators: Neuroscience principle enforcement (V43.11)

Validates: Chunking (Miller's Law), deduplication, confidence decay,
contradiction detection across all brain memory files.

Usage:
    python .claude/automation/brain_validators.py --validate-all
    python .claude/automation/brain_validators.py --chunking
    python .claude/automation/brain_validators.py --dedup
    python .claude/automation/brain_validators.py --decay
    python .claude/automation/brain_validators.py --contradictions
Dependencies: Python stdlib only (Law 4)
"""

import sys
from datetime import datetime, timezone
from pathlib import Path

from pulse.core.brain_utils import (load_config, load_json, save_json, text_similarity,
                         compute_confidence_decay, count_md_sections,
                         EVIDENCE_DIR, PATTERNS_DIR, LESSONS_DIR,
                         DOMAINS_DIR, WINS_DIR, FAILS_DIR, 
                         PRIVATE_PATTERNS_DIR, PRIVATE_LESSONS_DIR, 
                         HIPPOCAMPUS_DIR)


# === CHUNKING VALIDATOR (Miller's Law: 4+-1 items per section) ===

def validate_chunking(apply: bool = False) -> list:
    """Check markdown files for sections exceeding chunk limits.

    When apply=True, auto-trims oversized sections to keep only the last
    N items (most recent, assuming append-at-bottom convention).
    """
    config = load_config()
    max_chunk = config.get("validation", {}).get("max_chunk_size", 5)
    violations = []

    # Scan all fractal markdown chambers
    chambers = [
        PATTERNS_DIR, LESSONS_DIR, DOMAINS_DIR,
        WINS_DIR, FAILS_DIR,
        PRIVATE_PATTERNS_DIR, PRIVATE_LESSONS_DIR
    ]

    for directory in chambers:
        if not directory.exists():
            continue
        for md_file in sorted(directory.rglob("*.md")):
            if md_file.name in ("patterns.md", "lessons.md"):
                continue
            current_section = None
            item_count = 0
            file_violations = []
            with open(md_file, "r", encoding="utf-8") as f:
                for line in f:
                    if line.startswith("## "):
                        if current_section and item_count > max_chunk:
                            file_violations.append({
                                "file": md_file.name,
                                "section": current_section,
                                "items": item_count,
                                "max": max_chunk,
                                "severity": "warning"
                            })
                        current_section = line.strip().lstrip("# ").strip()
                        item_count = 0
                    elif line.startswith("- ") or line.startswith("* "):
                        item_count += 1
            if current_section and item_count > max_chunk:
                file_violations.append({
                    "file": md_file.name, "section": current_section,
                    "items": item_count, "max": max_chunk, "severity": "warning"
                })
            violations.extend(file_violations)

            # Auto-repair: trim oversized sections
            if apply and file_violations:
                _trim_chunking(md_file, max_chunk)

    return violations


def _trim_chunking(md_file: Path, max_chunk: int):
    """Auto-trim sections exceeding max_chunk items (keep last N)."""
    text = md_file.read_text(encoding="utf-8", errors="replace")
    sections = text.split("\n## ")
    if len(sections) < 2:
        return

    rebuilt = [sections[0]]
    for section in sections[1:]:
        lines = section.split("\n")
        title = lines[0]
        body_lines = lines[1:]

        item_indices = [i for i, ln in enumerate(body_lines) if ln.startswith("- ") or ln.startswith("* ")]
        if len(item_indices) <= max_chunk:
            rebuilt.append(section)
            continue

        # Keep only the last max_chunk items (most recent at bottom)
        keep_indices = set(item_indices[-max_chunk:])
        new_body = [ln for i, ln in enumerate(body_lines) if i not in set(item_indices) or i in keep_indices]
        rebuilt.append(title + "\n" + "\n".join(new_body))

    md_file.write_text("\n## ".join(rebuilt), encoding="utf-8")


# === DEDUPLICATION VALIDATOR ===

def validate_dedup() -> list:
    """Find near-duplicate evidence items across files."""
    config = load_config()
    threshold = config.get("validation", {}).get("dedup_similarity_threshold", 0.8)
    duplicates = []

    # Scan all evidence (Episodic + Private)
    all_items = []
    evidence_chambers = [EVIDENCE_DIR, HIPPOCAMPUS_DIR / "Private" / "Episodic"]
    
    for chamber in evidence_chambers:
        if not chamber.exists():
            continue
        for ef in sorted(chamber.rglob("*.json")):
            items = load_json(ef)
            if isinstance(items, list):
                for item in items:
                    # Support both 'evidence' and 'description' keys
                    text = item.get("evidence") or item.get("description") or item.get("want") or ""
                    if text:
                        all_items.append({"file": ef.name,
                                          "text": text,
                                          "pattern": item.get("pattern", "")})

    for i in range(len(all_items)):
        for j in range(i + 1, len(all_items)):
            sim = text_similarity(all_items[i]["text"], all_items[j]["text"])
            if sim >= threshold:
                duplicates.append({
                    "file_a": all_items[i]["file"],
                    "file_b": all_items[j]["file"],
                    "similarity": round(sim, 3),
                    "text_a": all_items[i]["text"][:60],
                    "text_b": all_items[j]["text"][:60]
                })
    return duplicates


# === CONFIDENCE DECAY VALIDATOR ===

def validate_decay(apply: bool = False) -> list:
    """Check and optionally apply confidence decay to evidence."""
    config = load_config()
    decay_rate = config.get("decay", {}).get("confidence_decay_rate", 0.005)
    max_age = config.get("validation", {}).get("max_evidence_age_days", 365)
    now = datetime.now(timezone.utc)
    decayed = []

    if not EVIDENCE_DIR.exists():
        return decayed

    for ef in sorted(EVIDENCE_DIR.rglob("*.json")):
        items = load_json(ef)
        if not isinstance(items, list):
            continue
        modified = False
        for item in items:
            ts = item.get("date") or item.get("timestamp")
            if not ts:
                continue
            try:
                ts_str = str(ts).replace("Z", "+00:00")
                if "T" in ts_str:
                    d = datetime.fromisoformat(ts_str)
                else:
                    d = datetime.fromisoformat(ts_str).replace(tzinfo=timezone.utc)
                days_old = (now - d).days
            except (ValueError, TypeError):
                continue

            if days_old <= 0:
                continue

            original = item.get("confidence", 0.8)
            new_conf = compute_confidence_decay(original, days_old, decay_rate)

            if new_conf != original:
                decayed.append({
                    "file": ef.name,
                    "days_old": days_old,
                    "original": original,
                    "decayed": new_conf,
                    "stale": days_old > max_age
                })
                if apply:
                    item["confidence"] = new_conf
                    item["decay_applied"] = now.isoformat()
                    modified = True

        if modified and apply:
            save_json(ef, items)
    return decayed


# === CONTRADICTION DETECTOR ===

def validate_contradictions() -> list:
    """Find WIN vs FAIL evidence with high textual similarity."""
    config = load_config()
    threshold = config.get("validation", {}).get(
        "contradiction_similarity_threshold", 0.5)
    contradictions = []

    if not EVIDENCE_DIR.exists():
        return contradictions

    domain_items = {}
    for ef in sorted(EVIDENCE_DIR.rglob("*.json")):
        items = load_json(ef)
        if not isinstance(items, list):
            continue
        domain = ef.stem.split("_")[0]
        domain_items.setdefault(domain, {"wins": [], "fails": []})
        for item in items:
            t = item.get("type", "")
            if t == "win":
                domain_items[domain]["wins"].append(item)
            elif t == "fail":
                domain_items[domain]["fails"].append(item)

    for domain, groups in domain_items.items():
        for w in groups["wins"]:
            for f in groups["fails"]:
                sim = text_similarity(
                    w.get("evidence", ""), f.get("evidence", ""))
                if sim >= threshold:
                    contradictions.append({
                        "domain": domain,
                        "similarity": round(sim, 3),
                        "win": w.get("evidence", "")[:80],
                        "fail": f.get("evidence", "")[:80],
                        "severity": "critical" if sim > 0.7 else "warning"
                    })
    return contradictions


# === MAIN ===

def run_all_validations() -> dict:
    """Run all validators and return combined report."""
    return {
        "chunking": validate_chunking(),
        "duplicates": validate_dedup(),
        "decay": validate_decay(apply=False),
        "contradictions": validate_contradictions()
    }


def main():
    print("=== Brain Validators V43.11 ===")

    if "--chunking" in sys.argv:
        apply = "--apply" in sys.argv
        results = validate_chunking(apply=apply)
        print(f"  Chunking violations: {len(results)} {'(applied)' if apply else '(dry-run)'}")
        for v in results:
            print(f"    {v['file']}: {v['section']} ({v['items']} items, max {v['max']})")
        return

    if "--dedup" in sys.argv:
        results = validate_dedup()
        print(f"  Duplicates found: {len(results)}")
        for d in results:
            print(f"    {d['file_a']} <-> {d['file_b']}: {d['similarity']:.0%}")
        return

    if "--decay" in sys.argv:
        apply = "--apply" in sys.argv
        results = validate_decay(apply=apply)
        print(f"  Decayed items: {len(results)} {'(applied)' if apply else '(dry-run)'}")
        for d in results:
            print(f"    {d['file']}: {d['original']:.3f} -> {d['decayed']:.3f} ({d['days_old']}d)")
        return

    if "--contradictions" in sys.argv:
        results = validate_contradictions()
        print(f"  Contradictions: {len(results)}")
        for c in results:
            print(f"    [{c['domain']}] {c['severity']}: {c['similarity']:.0%}")
        return

    if "--validate-all" in sys.argv:
        report = run_all_validations()
        total = sum(len(v) for v in report.values())
        print(f"  Total findings: {total}")
        for name, items in report.items():
            status = "CLEAN" if not items else f"{len(items)} issue(s)"
            print(f"    {name}: {status}")
        return

    print("  Usage: --validate-all | --chunking | --dedup | --decay [--apply] | --contradictions")


if __name__ == "__main__":
    main()
