#!/usr/bin/env python3
"""
GENESIS EVOLUTION ENGINE  (GEN-028)
=====================================
Wires failure detection into the Genesis evolution cycle.

Every failure that flows through `on_failure_detected()` is:
  1. Logged to logs/failures.jsonl  (durable audit trail)
  2. Diagnosed — lesson extracted from the error context
  3. Axiom generated — a reusable rule to prevent recurrence
  4. Written to KNOWLEDGE_GRAPH/axioms/evolution_axioms.jsonl

This engine is the bridge between the failure detector and the living
knowledge base.  It implements RULE 14: "Failure Is Our Greatest Ally."

Relationship to existing engines:
  - evolution_engine.py       — deep 4-step diagnosis (Diagnose/Root-Cause/Pre-mortem/Evolve)
  - genesis_evolution_protocol.py — cross-session learning protocol (EvolutionProtocol class)
  - THIS FILE                 — lightweight hook-based failure wiring for GEN-028

Usage:
    from core.genesis_evolution_engine import on_failure_detected, record_lesson, get_evolution_stats

    on_failure_detected({
        "task_id": "RAI-009",
        "error": "Telnyx 422 — assistant clone failed",
        "context": "POST /ai/assistants/{id}/clone returned 422 Unprocessable"
    })

    # CLI:
    python3 genesis_evolution_engine.py --stats
    python3 genesis_evolution_engine.py --failure '{"task_id":"X","error":"timeout"}'
"""

import json
import sys
import uuid
import argparse
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

# All on E: drive (RULE 6)
GENESIS_ROOT = Path("/mnt/e/genesis-system")
LOGS_DIR = GENESIS_ROOT / "logs"
KG_AXIOMS_DIR = GENESIS_ROOT / "KNOWLEDGE_GRAPH" / "axioms"

FAILURES_LOG_PATH = LOGS_DIR / "failures.jsonl"
EVOLUTION_AXIOMS_PATH = KG_AXIOMS_DIR / "evolution_axioms.jsonl"


def _ensure_dirs():
    LOGS_DIR.mkdir(parents=True, exist_ok=True)
    KG_AXIOMS_DIR.mkdir(parents=True, exist_ok=True)


def _extract_lesson(failure_event: dict) -> str:
    """
    Derive a human-readable lesson from a failure event.

    Uses structured heuristics to extract meaning without requiring an LLM
    call at log time — keeping this path fast and side-effect free.
    """
    error = str(failure_event.get("error", ""))
    task_id = str(failure_event.get("task_id", "unknown"))
    context = str(failure_event.get("context", ""))

    # Build lesson from available signals
    parts = [f"Task {task_id} failed"]

    if error:
        # Truncate long errors to a readable length
        short_error = error[:120].rstrip()
        parts.append(f"with error: {short_error}")

    if context:
        short_ctx = context[:120].rstrip()
        parts.append(f"Context: {short_ctx}")

    # Derive a prescriptive lesson
    error_lower = error.lower()
    if "timeout" in error_lower or "timed out" in error_lower:
        parts.append("Lesson: Implement retry with exponential backoff for this operation.")
    elif "401" in error_lower or "403" in error_lower or "unauthorized" in error_lower or "forbidden" in error_lower:
        parts.append("Lesson: Validate API credentials and token scopes before execution.")
    elif "422" in error_lower or "unprocessable" in error_lower:
        parts.append("Lesson: Validate request payload schema against API spec before sending.")
    elif "rate limit" in error_lower or "429" in error_lower or "too many" in error_lower:
        parts.append("Lesson: Apply rate limiting / back-pressure before hitting external APIs.")
    elif "import" in error_lower or "module" in error_lower or "no module" in error_lower:
        parts.append("Lesson: Verify all dependencies are installed and importable in the target env.")
    elif "connection" in error_lower or "refused" in error_lower or "unreachable" in error_lower:
        parts.append("Lesson: Add health-check / circuit-breaker before dependent service calls.")
    elif "json" in error_lower or "decode" in error_lower or "parse" in error_lower:
        parts.append("Lesson: Always validate and sanitise external data before deserialising.")
    elif "key" in error_lower or "keyerror" in error_lower:
        parts.append("Lesson: Use .get() with defaults; never assume external dict keys exist.")
    elif "none" in error_lower or "nonetype" in error_lower or "attribute" in error_lower:
        parts.append("Lesson: Guard against None values before attribute access or iteration.")
    else:
        parts.append("Lesson: Add specific error handling and logging for this failure path.")

    return "  ".join(parts)


def _generate_axiom(failure_event: dict, lesson: str) -> str:
    """
    Synthesise a concise, reusable axiom from the lesson.

    Axioms are written as imperative rules — short, memorable, actionable.
    """
    error = str(failure_event.get("error", "")).lower()

    if "timeout" in error or "timed out" in error:
        return "ALWAYS retry time-sensitive operations with exponential backoff (base 1s, max 30s, jitter)."
    elif "401" in error or "403" in error or "unauthorized" in error or "forbidden" in error:
        return "ALWAYS validate API credentials and required scopes at task start, not mid-execution."
    elif "422" in error or "unprocessable" in error:
        return "ALWAYS schema-validate request payloads against the API contract before sending."
    elif "rate limit" in error or "429" in error or "too many" in error:
        return "ALWAYS implement back-pressure and rate limiting before calling external APIs at scale."
    elif "import" in error or "module" in error or "no module" in error:
        return "ALWAYS verify import availability in a try/except at module load, fail fast with clear message."
    elif "connection" in error or "refused" in error or "unreachable" in error:
        return "ALWAYS wrap external service calls in a circuit breaker; degrade gracefully on failure."
    elif "json" in error or "decode" in error or "parse" in error:
        return "ALWAYS wrap JSON parsing in try/except and log raw input on failure for post-mortem."
    elif "keyerror" in error or "key" in error:
        return "ALWAYS use dict.get(key, default) and assert required keys at input boundaries."
    elif "none" in error or "nonetype" in error or "attribute" in error:
        return "ALWAYS guard against None before attribute access; use Optional typing and early returns."
    else:
        task_id = failure_event.get("task_id", "unknown")
        return f"ALWAYS add explicit error handling for the failure mode observed in task {task_id}."


def on_failure_detected(failure_event: dict) -> dict:
    """
    Primary hook: called whenever a failure is detected anywhere in Genesis.

    Performs:
      1. Assigns a unique failure_id
      2. Extracts a lesson from the failure context
      3. Generates a reusable axiom
      4. Persists to logs/failures.jsonl
      5. Writes axiom to KNOWLEDGE_GRAPH/axioms/evolution_axioms.jsonl

    Args:
        failure_event: dict with at minimum {"task_id": str, "error": str}
                       Optional keys: "context", "component", "severity"

    Returns:
        The full enriched failure record that was persisted.
    """
    _ensure_dirs()

    failure_id = f"FAIL-{uuid.uuid4().hex[:8].upper()}"
    lesson = _extract_lesson(failure_event)
    axiom = _generate_axiom(failure_event, lesson)
    now = datetime.now(timezone.utc).isoformat()

    failure_record = {
        "failure_id": failure_id,
        "task_id": failure_event.get("task_id", "unknown"),
        "error": str(failure_event.get("error", "")),
        "context": str(failure_event.get("context", "")),
        "component": str(failure_event.get("component", "")),
        "severity": failure_event.get("severity", "medium"),
        "lesson": lesson,
        "axiom_generated": axiom,
        "timestamp": now
    }

    # 1. Persist to failures log
    with open(FAILURES_LOG_PATH, "a", encoding="utf-8") as f:
        f.write(json.dumps(failure_record) + "\n")

    # 2. Persist axiom to KG
    axiom_record = {
        "axiom_id": f"AX-{uuid.uuid4().hex[:8].upper()}",
        "source_failure_id": failure_id,
        "source_task_id": failure_event.get("task_id", "unknown"),
        "axiom": axiom,
        "lesson": lesson,
        "generated_at": now
    }
    with open(EVOLUTION_AXIOMS_PATH, "a", encoding="utf-8") as f:
        f.write(json.dumps(axiom_record) + "\n")

    return failure_record


def record_lesson(lesson: str, source: str = "manual") -> dict:
    """
    Directly append a lesson (and derived axiom) to the evolution axioms log.
    Use this when a lesson is known upfront without a specific failure event.

    Args:
        lesson: The lesson string to record.
        source: Where this lesson came from (e.g. "manual", "review", task ID).

    Returns:
        The axiom record that was appended.
    """
    _ensure_dirs()

    axiom_record = {
        "axiom_id": f"AX-{uuid.uuid4().hex[:8].upper()}",
        "source_failure_id": None,
        "source_task_id": source,
        "axiom": lesson,
        "lesson": lesson,
        "generated_at": datetime.now(timezone.utc).isoformat()
    }

    with open(EVOLUTION_AXIOMS_PATH, "a", encoding="utf-8") as f:
        f.write(json.dumps(axiom_record) + "\n")

    return axiom_record


def get_evolution_stats() -> dict:
    """
    Count lessons learned and axioms generated from failures.

    Returns:
        {
            total_failures_logged:   int,
            total_axioms_generated:  int,
            axioms_from_failures:    int,
            axioms_from_manual:      int,
            severity_breakdown:      dict,
            most_recent_failure:     dict | None
        }
    """
    _ensure_dirs()

    # Count failures
    failures = []
    if FAILURES_LOG_PATH.exists():
        with open(FAILURES_LOG_PATH, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line:
                    try:
                        failures.append(json.loads(line))
                    except json.JSONDecodeError:
                        continue

    # Count axioms
    axioms = []
    if EVOLUTION_AXIOMS_PATH.exists():
        with open(EVOLUTION_AXIOMS_PATH, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line:
                    try:
                        axioms.append(json.loads(line))
                    except json.JSONDecodeError:
                        continue

    severity_breakdown: dict = {}
    for f_rec in failures:
        sev = f_rec.get("severity", "medium")
        severity_breakdown[sev] = severity_breakdown.get(sev, 0) + 1

    axioms_from_failures = sum(1 for a in axioms if a.get("source_failure_id") is not None)
    axioms_from_manual = sum(1 for a in axioms if a.get("source_failure_id") is None)

    most_recent = failures[-1] if failures else None

    return {
        "total_failures_logged": len(failures),
        "total_axioms_generated": len(axioms),
        "axioms_from_failures": axioms_from_failures,
        "axioms_from_manual": axioms_from_manual,
        "severity_breakdown": severity_breakdown,
        "most_recent_failure": most_recent
    }


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def _print_stats(stats: dict):
    print("\n=== GENESIS EVOLUTION ENGINE STATS ===")
    print(f"  Failures logged       : {stats['total_failures_logged']}")
    print(f"  Total axioms generated: {stats['total_axioms_generated']}")
    print(f"    - from failures     : {stats['axioms_from_failures']}")
    print(f"    - manual lessons    : {stats['axioms_from_manual']}")
    if stats["severity_breakdown"]:
        print(f"  Severity breakdown    : {stats['severity_breakdown']}")
    if stats["most_recent_failure"]:
        mrf = stats["most_recent_failure"]
        print(f"\n  Most recent failure   : [{mrf['failure_id']}] {mrf['task_id']}")
        print(f"    Error  : {str(mrf['error'])[:80]}")
        print(f"    Lesson : {str(mrf['lesson'])[:100]}")
        print(f"    Axiom  : {str(mrf['axiom_generated'])[:100]}")
        print(f"    Time   : {mrf['timestamp']}")
    print()


def main():
    parser = argparse.ArgumentParser(
        description="Genesis Evolution Engine — wire failures into the knowledge graph"
    )
    parser.add_argument("--stats", action="store_true", help="Print evolution stats")
    parser.add_argument(
        "--failure",
        metavar="JSON",
        help='Process a failure event. JSON string e.g. \'{"task_id":"X","error":"msg"}\''
    )
    parser.add_argument(
        "--lesson",
        metavar="LESSON",
        help='Record a manual lesson/axiom'
    )
    parser.add_argument(
        "--source",
        metavar="SOURCE",
        default="manual",
        help='Source label for --lesson (default: manual)'
    )

    args = parser.parse_args()

    if args.failure:
        try:
            event = json.loads(args.failure)
        except json.JSONDecodeError as e:
            print(f"ERROR: Could not parse failure JSON — {e}")
            sys.exit(1)
        record = on_failure_detected(event)
        print(f"Failure processed:\n{json.dumps(record, indent=2)}")

    if args.lesson:
        rec = record_lesson(args.lesson, source=args.source)
        print(f"Lesson recorded:\n{json.dumps(rec, indent=2)}")

    if args.stats or (not args.failure and not args.lesson):
        stats = get_evolution_stats()
        _print_stats(stats)


if __name__ == "__main__":
    main()
