#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
D15: Phylogenetic Skill Compiler — Generational Learning DAG

Reads 8 brain data sources, groups by domain, ranks by importance,
synthesizes ordered curricula via LLM. New agents boot with a
domain-specific learning path instead of flat dumps.

Output:
  state/skill_phylogeny.json          — full DAG with edges + metadata
  Hippocampus/Knowledge/curricula.json — compact boot-injectable curricula

Usage:
  python skill_phylogeny.py --once -v [--dry-run]
  python skill_phylogeny.py status
"""
from __future__ import annotations

import logging
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

if os.name == "nt" and __name__ == "__main__":
    sys.stdout.reconfigure(encoding="utf-8")
    sys.stderr.reconfigure(encoding="utf-8")

ROOT_DIR = Path(__file__).resolve().parents[3]
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))

from pulse.core.brain_utils import HIPPOCAMPUS_DIR, STATE_DIR, load_json_dict
from pulse.core.brain_io import save_json
from pulse.daemons.synthesis.skill_phylogeny_collectors import (
    COLLECTORS, HEBBIAN_FILE, _aggregate, _rank_items, _build_domain_stats,
)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [PHYLO] %(levelname)s %(message)s",
    datefmt="%H:%M:%S",
)
log = logging.getLogger(__name__)

DAG_OUTPUT = STATE_DIR / "skill_phylogeny.json"
CURRICULA_OUTPUT = HIPPOCAMPUS_DIR / "Knowledge" / "curricula.json"

_SYNTHESIS_PROMPT = """You are a curriculum designer for an AI agent swarm called PULSE.

Given the following ranked knowledge items for the "{domain}" domain, create an ORDERED learning curriculum.

## Ranked Items (highest importance first)
{items_text}

## Domain Statistics
- Total items analyzed: {total_items}
- Predecessor agents: {predecessors}
- Average success rate: {success_rate:.0%}

## Instructions
Create a JSON curriculum with:
1. A learning_path (3-7 ordered steps, each building on the previous)
2. critical_failures (1-3 specific failures new agents must avoid)
3. proven_strategies (1-3 concrete strategies that work)

Each learning_path step must have: order, name (short), reason (why this order), skills (list of skill names if any), golden_path (brief workflow if known), anti_pattern (specific thing to avoid, or null).

Output ONLY valid JSON:
{{
  "learning_path": [
    {{"order": 1, "name": "...", "reason": "...", "skills": [], "golden_path": "...", "anti_pattern": null}}
  ],
  "critical_failures": ["specific failure description"],
  "proven_strategies": ["specific strategy description"]
}}"""


def _synthesize_curriculum(domain: str, items: list[dict], stats: dict) -> dict | None:
    """LLM synthesis via dispatch(task_type='fast')."""
    try:
        from pulse.core.llm_router import dispatch, extract_json
    except ImportError:
        return None
    items_text = "\n".join(
        f"  {i+1}. [{it['type']}] {it['text']} (score: {it['score']})"
        for i, it in enumerate(items[:15])
    )
    prompt = _SYNTHESIS_PROMPT.format(
        domain=domain, items_text=items_text,
        total_items=stats.get("total_items", len(items)),
        predecessors=stats.get("predecessors", 0),
        success_rate=stats.get("avg_success_rate", 0),
    )
    try:
        parsed = extract_json(dispatch(prompt, task_type="fast", temperature=0.1))
        if parsed.get("learning_path"):
            return parsed
    except Exception as e:
        log.warning("LLM synthesis failed for %s: %s", domain, e)
    return None


def _fallback_curriculum(domain: str, items: list[dict], stats: dict) -> dict:
    """Rule-based fallback when LLM unavailable."""
    path, failures, strategies = [], [], []
    for idx, gp in enumerate([i for i in items if i["type"] == "golden_path"][:4], 1):
        path.append({"order": idx, "name": gp["text"][:60],
                      "reason": "proven golden path" if idx == 1 else "builds on previous",
                      "skills": [], "golden_path": gp["text"][:80], "anti_pattern": None})
    offset = len(path)
    for idx, s in enumerate([i for i in items if i["type"] == "schema"][:3], offset + 1):
        path.append({"order": idx, "name": s["text"][:60], "reason": "proven schema",
                      "skills": [], "golden_path": None, "anti_pattern": None})
    failures = [f["text"][:100] for f in items if f["type"] == "failure"][:3]
    strategies = [s["text"][:100] for s in items if s["type"] in ("lesson", "pattern")][:3]
    if not path and items:
        path.append({"order": 1, "name": items[0]["text"][:60], "reason": "highest-ranked",
                      "skills": [], "golden_path": None, "anti_pattern": None})
    return {"learning_path": path, "critical_failures": failures, "proven_strategies": strategies}


def _persist(dag: dict, curricula: dict) -> None:
    DAG_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
    CURRICULA_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
    save_json(DAG_OUTPUT, dag)
    save_json(CURRICULA_OUTPUT, curricula)
    log.info("Persisted DAG + curricula")


def run_once(verbose: bool = False, dry_run: bool = False) -> dict:
    """Full pipeline: COLLECT -> AGGREGATE -> RANK -> SYNTHESIZE -> PERSIST."""
    t0 = time.time()
    now = datetime.now(timezone.utc).isoformat()
    generation = load_json_dict(DAG_OUTPUT).get("generation", 0) + 1
    log.info("D15 Phylogenetic Skill Compiler — Generation %d", generation)

    log.info("Collecting from 8 data sources...")
    sources_read: dict[str, int] = {}
    collections = []
    for name, collector in COLLECTORS:
        try:
            coll = collector()
            count = sum(len(v) for v in coll.values())
            sources_read[name] = count
            collections.append(coll)
            if verbose:
                log.info("  %s: %d items across %d domains", name, count, len(coll))
        except Exception as e:
            log.warning("  %s: FAILED (%s)", name, e)
            sources_read[name] = 0
            collections.append({})

    total = sum(sources_read.values())
    log.info("Collected %d total items", total)
    if total == 0:
        empty = {"generation": generation, "compiled_at": now, "curricula": {}}
        if not dry_run:
            _persist({"generation": generation, "compiled_at": now, "domains": {},
                       "metadata": {"sources_read": sources_read, "elapsed_s": 0}}, empty)
        return empty

    domain_profiles = _aggregate(collections)
    log.info("Aggregating: %d domains", len(domain_profiles))
    hebbian_data = load_json_dict(HEBBIAN_FILE)
    dag_domains: dict[str, dict] = {}
    curricula_domains: dict[str, dict] = {}

    for domain, profile in sorted(domain_profiles.items()):
        ranked = _rank_items(profile)
        stats = _build_domain_stats(domain, ranked, hebbian_data)
        if verbose:
            log.info("  %s: %d items, %d preds, %.0f%%", domain, len(ranked),
                     stats["predecessors"], stats["avg_success_rate"] * 100)
        dag_domains[domain] = {
            "total_items": stats["total_items"], "predecessors": stats["predecessors"],
            "avg_success_rate": stats["avg_success_rate"],
            "items": [{"text": i["text"], "type": i["type"], "score": i["score"]} for i in ranked],
            "edges": [],
            "anti_patterns": [i["text"] for i in ranked if "failure" in i["type"] or "reflex" in i["type"]][:5],
        }
        if dry_run:
            cur = _fallback_curriculum(domain, ranked, stats)
        else:
            cur = _synthesize_curriculum(domain, ranked, stats)
            if not cur:
                cur = _fallback_curriculum(domain, ranked, stats)
        curricula_domains[domain] = {
            "domain": domain, "generation": generation,
            "stats": {"predecessors_analyzed": stats["predecessors"],
                      "avg_success_rate": stats["avg_success_rate"]},
            "learning_path": cur.get("learning_path", []),
            "critical_failures": cur.get("critical_failures", []),
            "proven_strategies": cur.get("proven_strategies", []),
        }

    elapsed = round(time.time() - t0, 1)
    dag = {"generation": generation, "compiled_at": now, "domains": dag_domains,
           "metadata": {"sources_read": sources_read, "elapsed_s": elapsed}}
    curricula = {"generation": generation, "compiled_at": now, "curricula": curricula_domains}
    if not dry_run:
        _persist(dag, curricula)
    log.info("Generation %d: %d domains, %.1fs", generation, len(curricula_domains), elapsed)
    return curricula


def show_status():
    """Print current phylogenetic compiler state."""
    dag = load_json_dict(DAG_OUTPUT)
    cur = load_json_dict(CURRICULA_OUTPUT)
    if not dag:
        print("  [D15] No phylogeny compiled yet. Run: python skill_phylogeny.py --once")
        return
    meta = dag.get("metadata", {})
    print(f"\n  [D15] Generation {dag.get('generation', 0)} | "
          f"{len(dag.get('domains', {}))} domains | {meta.get('elapsed_s', '?')}s")
    print(f"  Compiled: {dag.get('compiled_at', '?')}")
    for src, n in sorted(meta.get("sources_read", {}).items()):
        print(f"    {src}: {n}")
    for name, info in sorted(dag.get("domains", {}).items()):
        c = cur.get("curricula", {}).get(name, {})
        steps = len(c.get("learning_path", []))
        print(f"  {name}: {info.get('total_items', 0)} items, "
              f"{info.get('predecessors', 0)} preds, {steps} steps")
    print()


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="D15 Phylogenetic Skill Compiler")
    parser.add_argument("command", nargs="?", default=None, help="status")
    parser.add_argument("--once", action="store_true")
    parser.add_argument("-v", "--verbose", action="store_true")
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()
    if args.command == "status":
        show_status()
    elif args.once:
        run_once(verbose=args.verbose, dry_run=args.dry_run)
    else:
        parser.print_help()
