#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Auto-Synthesis Daemon: Evidence -> Patterns (Neuroscience-Enhanced V43.11)

Enhancements: Novelty scoring (#25), Error weighting (#33-34),
Transfer detection (#16, #29), Per-domain thresholds (#15), Timing (#21).

Usage:
    python .claude/automation/synthesizer.py [--dry-run] [--threshold N]
Dependencies: Python stdlib only (Law 4)
"""

import sys
import time
from collections import Counter
from datetime import datetime, date, timezone
from pathlib import Path

# Add Utilities to path for brain_utils

from pulse.core.brain_utils import (load_config, load_json, save_json, load_json_dict,
                         extract_domain, compute_weighted_count,
                         text_similarity, EVIDENCE_DIR, PATTERNS_DIR,
                         PRIVATE_PATTERNS_DIR, GROWTH_METRICS, 
                         BRAIN_MAP, REFACTOR_LOG)
from pulse.core.pulse_crdt import EvidenceCRDT

# Initialize CRDT handler
CRDT = EvidenceCRDT()


def get_pattern_file(domain: str, visibility: str = "public") -> Path:
    base_dir = PRIVATE_PATTERNS_DIR if visibility == "personal" else PATTERNS_DIR
    if domain.lower() in ["pulse", "automation", "brain"]:
        return base_dir / "PULSE_Patterns" / f"{domain}_patterns.md"
    return base_dir / f"{domain}_patterns.md"


def find_existing_patterns(pattern_file: Path) -> set:
    existing = set()
    if pattern_file.exists():
        with open(pattern_file, "r", encoding="utf-8") as f:
            for line in f:
                if line.startswith("## "):
                    existing.add(line.strip().lstrip("# ").strip())
    return existing


def detect_transfer_patterns(all_domain_groups: dict) -> list:
    """Find patterns appearing across multiple domains (#16, #29)."""
    pattern_domains = {}
    for domain, groups in all_domain_groups.items():
        for pname in groups:
            pattern_domains.setdefault(pname, set()).add(domain)
    return [{"pattern": p, "domains": sorted(d), "count": len(d),
             "universal": len(d) >= 3}
            for p, d in pattern_domains.items() if len(d) >= 2]


def get_related_links(items: list) -> list:
    """Generate Wikilinks for related memory chambers based on evidence."""
    links = []
    types = set(i.get("type") for i in items)
    
    if "win" in types:
        links.append("[[Hippocampus/Lessons/pulse_wins.md|Related Wins]]")
    if "fail" in types:
        links.append("[[Hippocampus/Failures/pulse_fails.md|Related Fails]]")
    
    # Heuristic: Check for key concepts in descriptions
    text_blob = " ".join([str(i.get("description", "") or i.get("evidence", "")) for i in items]).lower()
    if "security" in text_blob or "auth" in text_blob:
        links.append("[[Corpus_Callosum/CONSTITUTION.md|Constitution]]")
    if "speed" in text_blob or "performance" in text_blob:
        links.append("[[Hippocampus/Patterns/performance_patterns.md|Performance]]")
    
    return sorted(list(set(links)))


def synthesize_pattern(pattern_name: str, items: list, config: dict) -> str:
    """Create markdown pattern section with neuroscience metadata."""
    today_str = date.today().isoformat()
    confidence = sum(i.get("confidence", 0.8) for i in items) / len(items)
    types = Counter(i.get("type", "observation") for i in items)
    wins, fails = types.get("win", 0), types.get("fail", 0)
    weighted = compute_weighted_count(items, config)

    lines = [
        f"## {pattern_name}", "",
        f"**Synthesized:** {today_str} | **Evidence:** {len(items)} items "
        f"(weighted: {weighted:.1f}) | **Confidence:** {confidence:.2f}",
        f"**Wins:** {wins} | **Fails:** {fails}", "",
        "**Evidence Summary:**",
    ]
    # Sort: FAIL first (error-driven learning)
    sorted_items = sorted(items, key=lambda x: (
        0 if x.get("type") == "fail" else 1, -x.get("confidence", 0.5)))
    for item in sorted_items:
        marker = {"win": "[WIN]", "fail": "[FAIL]"}.get(item.get("type"), "[OBS]")
        # Check for multiple possible evidence description keys
        desc = item.get("evidence") or item.get("description") or item.get("want") or item.get("dont_want") or "No description"
        entry = f"- {marker} {desc}"
        if item.get("source"):
            entry += f" *(Source: {item['source']})*"
        lines.append(entry)

    # [Phase 1.2] Hyperlinked Neurons
    links = get_related_links(items)
    if links:
        lines.append("")
        lines.append("**Neural Links:**")
        lines.extend([f"- {link}" for link in links])

    # Derive principle
    if wins and fails:
        principle = f"Learned from {wins} successes and {fails} failures."
    elif wins:
        principle = f"Consistent success across {wins} observations. Replicate."
    elif fails:
        principle = f"Recurring failure across {fails} observations. Avoid."
    else:
        principle = f"Emerging pattern from {len(items)} observations. Monitor."
    lines.extend(["", f"**Principle:** {principle}", ""])
    return chr(10).join(lines)


def update_brain_map(domain: str, pattern_name: str, evidence_file: str):
    brain_map = load_json_dict(BRAIN_MAP)
    brain_map.setdefault("synthesis_connections", {})
    brain_map.setdefault("evidence_to_patterns", {})
    key = f"evidence/{evidence_file}"
    if key not in brain_map["evidence_to_patterns"]:
        brain_map["evidence_to_patterns"][key] = []
    pf_ref = f"PATTERNS/{domain}_patterns.md"
    if pf_ref not in brain_map["evidence_to_patterns"][key]:
        brain_map["evidence_to_patterns"][key].append(pf_ref)
    brain_map["synthesis_connections"][f"{domain}/{pattern_name}"] = {
        "source": evidence_file, "date": date.today().isoformat()}
    save_json(BRAIN_MAP, brain_map)


def update_growth_metrics(domain: str, new_patterns: int, ms: int):
    metrics = load_json_dict(GROWTH_METRICS)
    pk = f"PATTERNS/{domain}_patterns.md"
    files = metrics.setdefault("files", {})
    if pk in files:
        files[pk]["patterns"] = files[pk].get("patterns", 0) + new_patterns
        files[pk]["last_updated"] = date.today().isoformat()
    totals = metrics.setdefault("totals", {})
    totals["patterns"] = totals.get("patterns", 0) + new_patterns
    timing = metrics.setdefault("synthesis_timing", [])
    timing.append({"date": datetime.now(timezone.utc).isoformat(),
                    "domain": domain, "patterns": new_patterns, "duration_ms": ms})
    if len(timing) > 100:
        metrics["synthesis_timing"] = timing[-50:]
    save_json(GROWTH_METRICS, metrics)


def log_synthesis(evidence_file: str, domain: str, patterns_created: list,
                  transfers: list = None):
    log = load_json(REFACTOR_LOG) if REFACTOR_LOG.exists() else []
    if not isinstance(log, list):
        log = []
    entry = {"date": date.today().isoformat(), "action": "auto_synthesis",
             "reason": f"Threshold reached in {evidence_file}",
             "patterns_created": patterns_created,
             "outcome": f"Synthesized {len(patterns_created)} pattern(s)"}
    if transfers:
        entry["transfer_patterns"] = transfers
    log.append(entry)
    save_json(REFACTOR_LOG, log)


def run_synthesis(threshold_override=None, dry_run=False):
    config = load_config()
    default_t = threshold_override or config.get("synthesis", {}).get("default_threshold", 5)
    domain_t = config.get("synthesis", {}).get("domain_thresholds", {})
    start = time.time()
    summary = {"scanned": 0, "synthesized": 0, "patterns_created": [],
               "skipped": [], "transfers": []}
    if not EVIDENCE_DIR.exists():
        return summary

    all_domain_groups = {}
    for ef in sorted(EVIDENCE_DIR.glob("*.json")):
        summary["scanned"] += 1
        items = load_json(ef)
        if not items or not isinstance(items, list):
            continue
        
        # [PHASE 1.3] Deduplicate evidence via CRDT before grouping
        # Merges identical evidence content using Last-Write-Wins (LWW)
        items = CRDT.merge(items, [])
        
        domain = extract_domain(ef.name)
        threshold = domain_t.get(domain, default_t)
        pf = get_pattern_file(domain)
        existing = find_existing_patterns(pf)
        groups = {}
        for item in items:
            p = item.get("pattern", "uncategorized")
            v = item.get("visibility", "public")
            groups.setdefault((p, v), []).append(item)
        all_domain_groups[domain] = groups

        for (pname, visibility), gitems in groups.items():
            weighted = compute_weighted_count(gitems, config)
            pf = get_pattern_file(domain, visibility)
            existing = find_existing_patterns(pf)
            
            if weighted >= threshold and pname not in existing:
                if not dry_run:
                    content = synthesize_pattern(pname, gitems, config)
                    pf.parent.mkdir(parents=True, exist_ok=True)
                    with open(pf, "a", encoding="utf-8") as f:
                        f.write(chr(10) + content + chr(10))
                    print(f"  [SYNTH] {domain}/{pname} [{visibility}] ({len(gitems)} items, w={weighted:.1f})")
                    update_brain_map(domain, pname, ef.name)
                    ms = int((time.time() - start) * 1000)
                    update_growth_metrics(domain, 1, ms)
                    log_synthesis(ef.name, domain, [pname])
                    summary["synthesized"] += 1
                    summary["patterns_created"].append(f"{domain}/{pname} [{visibility}]")
                else:
                    print(f"  [DRY-RUN] {domain}/{pname} [{visibility}] ({len(gitems)}, w={weighted:.1f})")
            elif pname in existing:
                summary["skipped"].append(f"{domain}/{pname} [{visibility}]")

    # Transfers currently only consider the pattern name, not visibility
    # For now, let's keep it simple and just use the name
    flat_domain_groups = {d: {p: items for (p, v), items in g.items()} for d, g in all_domain_groups.items()}
    transfers = detect_transfer_patterns(flat_domain_groups)
    summary["transfers"] = transfers
    for t in transfers:
        tag = "UNIVERSAL" if t["universal"] else "CROSS-DOMAIN"
        print(f"  [{tag}] '{t['pattern']}' in {t['domains']}")
    return summary


def main():
    dry_run = "--dry-run" in sys.argv
    threshold = None
    if "--threshold" in sys.argv:
        idx = sys.argv.index("--threshold")
        if idx + 1 < len(sys.argv):
            threshold = int(sys.argv[idx + 1])
    config = load_config()
    t = threshold or config.get("synthesis", {}).get("default_threshold", 5)
    print(f"=== Brain Auto-Synthesizer V43.11 (threshold={t}) ===")
    if dry_run:
        print("[MODE] Dry run")
    result = run_synthesis(threshold_override=threshold, dry_run=dry_run)
    print(f"\n=== Done: {result['synthesized']} patterns, "
          f"{len(result['transfers'])} transfers ===")


if __name__ == "__main__":
    main()
