"""Core Engine metrics collector — unified health & growth data for L3 self-governing.

Aggregates data from:
- proactive_state.json (learning, effectiveness, work distribution)
- MEMORY.md (section sizes, entry ages, freshness)
- DailyActivity/ (coverage, distillation health)
- context_health findings (DDD staleness, git health)
- Hook execution stats (from hook_stats.json)

All reads are filesystem-only. No LLM, no network.
Budget: <500ms for full collect.

Key public symbols:

- ``collect_engine_metrics()`` — Returns full metrics dict for API/dashboard.
- ``collect_memory_effectiveness()`` — MEMORY.md section analysis.
- ``collect_ddd_change_suggestions()`` — Granular code→doc update suggestions.
"""

from __future__ import annotations

import json
import logging
import re
import subprocess
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any, Optional

logger = logging.getLogger(__name__)

_GIT_TIMEOUT = 5


# ---------------------------------------------------------------------------
# Memory effectiveness — Sprint 6
# ---------------------------------------------------------------------------

def collect_memory_effectiveness(ws_path: Path) -> dict[str, Any]:
    """Analyze MEMORY.md for section health, entry freshness, and staleness.

    Returns:
        Dict with section_sizes, entry_ages, freshness_score, stale_entries.
    """
    memory_path = ws_path / ".context" / "MEMORY.md"
    if not memory_path.exists():
        return {"status": "missing", "sections": {}}

    try:
        content = memory_path.read_text(encoding="utf-8")
    except OSError:
        return {"status": "unreadable", "sections": {}}

    sections: dict[str, dict[str, Any]] = {}
    current_section = None
    current_entries: list[str] = []
    total_entries = 0
    dated_entries = 0
    stale_entries: list[dict] = []
    recent_entries = 0
    cutoff_14d = (date.today() - timedelta(days=14)).isoformat()
    cutoff_30d = (date.today() - timedelta(days=30)).isoformat()

    # Date pattern at start of entry: "- 2026-03-25:" or "- **2026-03-25**"
    date_pattern = re.compile(r"^-\s+\**(\d{4}-\d{2}-\d{2})\**[:\s]")

    for line in content.splitlines():
        if line.startswith("## ") and not line.startswith("## _"):
            # Save previous section
            if current_section:
                sections[current_section] = _analyze_section(
                    current_entries, cutoff_14d, cutoff_30d
                )
            current_section = line.lstrip("# ").strip()
            current_entries = []
        elif line.startswith("- ") and current_section:
            current_entries.append(line)
            total_entries += 1
            m = date_pattern.match(line)
            if m:
                dated_entries += 1
                entry_date = m.group(1)
                if entry_date >= cutoff_14d:
                    recent_entries += 1
                elif entry_date < cutoff_30d:
                    stale_entries.append({
                        "section": current_section,
                        "date": entry_date,
                        "preview": line[2:80].strip(),
                    })

    # Save last section
    if current_section:
        sections[current_section] = _analyze_section(
            current_entries, cutoff_14d, cutoff_30d
        )

    # Freshness score: 0-100 based on ratio of recent/total dated entries
    if dated_entries > 0:
        freshness_score = round(recent_entries / dated_entries * 100)
    else:
        freshness_score = 0

    return {
        "status": "ok",
        "total_entries": total_entries,
        "dated_entries": dated_entries,
        "recent_entries_14d": recent_entries,
        "stale_entries_30d": len(stale_entries),
        "freshness_score": freshness_score,
        "sections": sections,
        "stale_samples": stale_entries[:5],  # Top 5 for dashboard
        "size_bytes": memory_path.stat().st_size,
        "last_modified": date.fromtimestamp(memory_path.stat().st_mtime).isoformat(),
    }


def _analyze_section(
    entries: list[str],
    cutoff_14d: str,
    cutoff_30d: str,
) -> dict[str, Any]:
    """Analyze a single MEMORY.md section."""
    date_pattern = re.compile(r"^-\s+\**(\d{4}-\d{2}-\d{2})\**[:\s]")
    count = len(entries)
    dates: list[str] = []
    for e in entries:
        m = date_pattern.match(e)
        if m:
            dates.append(m.group(1))

    recent = sum(1 for d in dates if d >= cutoff_14d)
    stale = sum(1 for d in dates if d < cutoff_30d)
    oldest = min(dates) if dates else None
    newest = max(dates) if dates else None

    return {
        "count": count,
        "dated": len(dates),
        "recent_14d": recent,
        "stale_30d": stale,
        "oldest_date": oldest,
        "newest_date": newest,
    }


# ---------------------------------------------------------------------------
# DDD change suggestions — Sprint 7
# ---------------------------------------------------------------------------

def collect_ddd_change_suggestions(ws_path: Path) -> list[dict[str, str]]:
    """Detect code changes that should trigger DDD doc updates.

    Checks recent commits (7 days) for structural patterns:
    - New Python modules/packages created
    - New frontend components
    - Dependency changes (pyproject.toml, package.json)
    - New API routes
    - Architecture-level file moves/deletes

    Returns list of {project, doc, reason, files} suggestions.
    """
    suggestions: list[dict[str, str]] = []
    swarmai_root = _find_swarmai_root(ws_path)
    if not swarmai_root:
        return suggestions

    # Get files changed in last 7 days
    try:
        result = subprocess.run(
            ["git", "log", "--name-status", "--since=7 days ago",
             "--pretty=format:", "--diff-filter=ADRM"],
            cwd=str(swarmai_root), capture_output=True, text=True,
            timeout=_GIT_TIMEOUT,
        )
        if result.returncode != 0:
            return suggestions
    except (subprocess.TimeoutExpired, OSError):
        return suggestions

    changes: dict[str, list[str]] = {"A": [], "D": [], "R": [], "M": []}
    for line in result.stdout.strip().splitlines():
        parts = line.split("\t")
        if len(parts) >= 2:
            status = parts[0][0]  # A, D, R, M
            filepath = parts[-1]
            if status in changes:
                changes[status].append(filepath)

    # Pattern 1: New backend modules → TECH.md
    new_backend = [f for f in changes["A"] if f.startswith("backend/") and f.endswith(".py")]
    new_packages = [f for f in new_backend if f.endswith("__init__.py")]
    if new_packages:
        suggestions.append({
            "project": "SwarmAI",
            "doc": "TECH.md",
            "reason": f"New package(s): {', '.join(p.rsplit('/', 2)[-2] for p in new_packages[:3])}",
            "files": ", ".join(new_packages[:5]),
            "section": "Architecture",
        })
    elif len(new_backend) >= 3:
        suggestions.append({
            "project": "SwarmAI",
            "doc": "TECH.md",
            "reason": f"{len(new_backend)} new backend files added",
            "files": ", ".join(new_backend[:5]),
            "section": "Architecture",
        })

    # Pattern 2: New frontend components → TECH.md
    new_frontend = [f for f in changes["A"]
                    if f.startswith("desktop/src/") and f.endswith((".tsx", ".ts"))]
    new_components = [f for f in new_frontend if "/components/" in f]
    if len(new_components) >= 2:
        suggestions.append({
            "project": "SwarmAI",
            "doc": "TECH.md",
            "reason": f"{len(new_components)} new UI components",
            "files": ", ".join(new_components[:5]),
            "section": "Frontend Architecture",
        })

    # Pattern 3: Dependency changes → TECH.md
    dep_files = [f for f in changes["M"]
                 if f in ("pyproject.toml", "desktop/package.json", "Cargo.toml")]
    if dep_files:
        suggestions.append({
            "project": "SwarmAI",
            "doc": "TECH.md",
            "reason": f"Dependency changes: {', '.join(dep_files)}",
            "files": ", ".join(dep_files),
            "section": "Tech Stack",
        })

    # Pattern 4: New API routes → TECH.md
    new_routers = [f for f in changes["A"]
                   if f.startswith("backend/routers/") and f.endswith(".py")]
    if new_routers:
        suggestions.append({
            "project": "SwarmAI",
            "doc": "TECH.md",
            "reason": f"New API route(s): {', '.join(f.split('/')[-1] for f in new_routers)}",
            "files": ", ".join(new_routers),
            "section": "API Surface",
        })

    # Pattern 5: Deleted modules (significant) → TECH.md + IMPROVEMENT.md
    deleted_backend = [f for f in changes["D"]
                       if f.startswith("backend/") and f.endswith(".py")
                       and "__pycache__" not in f]
    if len(deleted_backend) >= 3:
        suggestions.append({
            "project": "SwarmAI",
            "doc": "IMPROVEMENT.md",
            "reason": f"{len(deleted_backend)} backend files removed (migration?)",
            "files": ", ".join(deleted_backend[:5]),
            "section": "What Worked / What Failed",
        })

    # Pattern 6: New hooks → TECH.md
    new_hooks = [f for f in changes["A"]
                 if f.startswith("backend/hooks/") and f.endswith(".py")]
    if new_hooks:
        suggestions.append({
            "project": "SwarmAI",
            "doc": "TECH.md",
            "reason": f"New hook(s): {', '.join(f.split('/')[-1].replace('.py','') for f in new_hooks)}",
            "files": ", ".join(new_hooks),
            "section": "Hook Pipeline",
        })

    return suggestions


def _find_swarmai_root(ws_path: Path) -> Optional[Path]:
    """Find the SwarmAI codebase root.

    Resolution order:
    1. SWARMAI_ROOT env var (explicit override)
    2. Relative to this file (works in dev and PyInstaller)
    3. Sibling of workspace parent (legacy layout)
    """
    import os

    # Env var — explicit override for any deployment
    env_root = os.environ.get("SWARMAI_ROOT")
    if env_root:
        p = Path(env_root)
        if (p / "backend").is_dir():
            return p

    # Relative to this source file: engine_metrics.py → core/ → backend/ → swarmai/
    source_root = Path(__file__).resolve().parents[2]
    if (source_root / "backend").is_dir():
        return source_root

    # Sibling of workspace parent
    sibling = ws_path.parent / "swarmai"
    if (sibling / "backend").is_dir():
        return sibling

    return None


# ---------------------------------------------------------------------------
# Hook execution stats
# ---------------------------------------------------------------------------

def collect_hook_stats(ws_path: Path) -> dict[str, Any]:
    """Read hook execution stats from hook_stats.json if available."""
    stats_path = ws_path / "hook_stats.json"
    if not stats_path.exists():
        return {"available": False}

    try:
        data = json.loads(stats_path.read_text(encoding="utf-8"))
        hooks = data.get("hooks", data)  # Support both nested and flat format
        total_runs = sum(h.get("total_runs", 0) for h in hooks.values())
        total_errors = sum(h.get("total_errors", 0) for h in hooks.values())
        return {
            "available": True,
            "updated_at": data.get("updated_at"),
            "hook_count": len(hooks),
            "total_runs": total_runs,
            "total_errors": total_errors,
            "error_rate": round(total_errors / max(total_runs, 1) * 100, 1),
            "hooks": hooks,
        }
    except (json.JSONDecodeError, OSError):
        return {"available": False}


# ---------------------------------------------------------------------------
# Unified metrics collector — Sprint 8
# ---------------------------------------------------------------------------

def collect_engine_metrics(ws_path_str: str) -> dict[str, Any]:
    """Collect all Core Engine metrics for the dashboard API.

    Aggregates: learning state, memory effectiveness, DDD health,
    hook stats, context health findings.
    """
    ws_path = Path(ws_path_str)
    metrics: dict[str, Any] = {
        "collected_at": datetime.now().isoformat(timespec="seconds"),
        "engine_level": _compute_engine_level(ws_path),
    }

    # 1. Proactive learning state
    try:
        from core.proactive_learning import load_learning_state
        state = load_learning_state(ws_path)
        metrics["learning"] = {
            "work_type_distribution": state.work_type_distribution,
            "effectiveness": state.effectiveness,
            "total_observations": len(state.observations),
            "item_history_size": len(state.item_history),
            "preferred_work_type": state.preferred_work_type(),
            "learning_summary": state.learning_summary(),
        }
    except Exception as exc:
        logger.debug("Failed to load learning state: %s", exc)
        metrics["learning"] = {"error": str(exc)}

    # 2. Memory effectiveness
    try:
        metrics["memory"] = collect_memory_effectiveness(ws_path)
    except Exception as exc:
        logger.debug("Failed to collect memory effectiveness: %s", exc)
        metrics["memory"] = {"status": "error", "error": str(exc)}

    # 3. DDD change suggestions
    try:
        metrics["ddd_suggestions"] = collect_ddd_change_suggestions(ws_path)
    except Exception as exc:
        logger.debug("Failed to collect DDD suggestions: %s", exc)
        metrics["ddd_suggestions"] = []

    # 4. DDD staleness (quick check)
    try:
        metrics["ddd_health"] = _collect_ddd_health(ws_path)
    except Exception as exc:
        metrics["ddd_health"] = {"error": str(exc)}

    # 5. Context health findings (from last deep check)
    try:
        findings_path = ws_path / "Services" / "swarm-jobs" / "health_findings.json"
        if findings_path.exists():
            metrics["context_health"] = json.loads(
                findings_path.read_text(encoding="utf-8")
            )
        else:
            metrics["context_health"] = {"findings": [], "last_check": None}
    except (json.JSONDecodeError, OSError):
        metrics["context_health"] = {"findings": [], "last_check": None}

    # 6. Hook stats
    metrics["hooks"] = collect_hook_stats(ws_path)

    # 7. Session stats (from DailyActivity)
    try:
        metrics["sessions"] = _collect_session_stats(ws_path)
    except Exception as exc:
        metrics["sessions"] = {"error": str(exc)}

    return metrics


def _compute_engine_level(ws_path: Path) -> dict[str, Any]:
    """Compute current Core Engine growth level and progress."""
    # L0-L2: DONE (hardcoded, verified in prior sessions)
    # L3: Check which features are active
    l3_features = {
        "proactive_gap_detection": False,
        "stale_correction_detection": False,
        "session_type_detection": False,
        "memory_effectiveness_tracking": False,
        "ddd_auto_update_suggestions": False,
        "growth_metrics_dashboard": False,
    }

    # Check proactive gap detection — look for the capability_gaps function
    try:
        from core.proactive_intelligence import build_session_briefing_data
        l3_features["proactive_gap_detection"] = True
    except ImportError:
        pass

    # Check if session-type detection exists (channel sessions get lighter prompts)
    try:
        from core.prompt_builder import PromptBuilder
        # Feature detection via attribute check — works in dev and .pyc bundles
        # PromptBuilder.build_system_prompt must accept is_channel kwarg
        import inspect
        sig = inspect.signature(PromptBuilder.build_system_prompt)
        if "is_channel" in sig.parameters:
            l3_features["session_type_detection"] = True
    except (ImportError, Exception):
        pass

    # Stale correction detection — check if distillation does git cross-ref
    try:
        from hooks.distillation_hook import _IMPLEMENTATION_KEYWORDS
        l3_features["stale_correction_detection"] = True
    except ImportError:
        pass

    # Memory effectiveness — this module existing means it's active
    l3_features["memory_effectiveness_tracking"] = True

    # DDD auto-update suggestions — this module existing means it's active
    l3_features["ddd_auto_update_suggestions"] = True

    # Growth metrics dashboard — if this function is being called, it's active
    l3_features["growth_metrics_dashboard"] = True

    done = sum(1 for v in l3_features.values() if v)
    total = len(l3_features)

    return {
        "current": "L3" if done >= 4 else "L2",
        "l3_progress": f"{done}/{total}",
        "l3_features": l3_features,
        "levels": {
            "L0_reactive": "complete",
            "L1_self_maintaining": "complete",
            "L2_self_improving": "complete",
            "L3_self_governing": "in_progress" if done < total else "complete",
            "L4_autonomous": "future",
        },
    }


def _collect_ddd_health(ws_path: Path) -> dict[str, Any]:
    """DDD 5-dimensional health scoring — per-project, per-section.

    Uses ddd_health.compute_section_health() for full scoring when available,
    falls back to staleness-only scan on import error.
    """
    projects_dir = ws_path / "Projects"
    if not projects_dir.is_dir():
        return {"projects": []}

    try:
        from core.ddd_health import compute_section_health

        projects: list[dict] = []
        for project_dir in sorted(projects_dir.iterdir()):
            if not project_dir.is_dir() or project_dir.name.startswith("."):
                continue
            result = compute_section_health(project_dir)
            # Ensure all 4 standard docs appear with at least an "exists" key
            # (compute_section_health only includes docs with parseable sections)
            raw_docs = result.get("docs", {})
            docs_normalized: dict[str, dict] = {}
            for doc_name in ("PRODUCT.md", "TECH.md", "IMPROVEMENT.md", "PROJECT.md"):
                if doc_name in raw_docs:
                    docs_normalized[doc_name] = {**raw_docs[doc_name], "exists": True}
                else:
                    doc_path = project_dir / doc_name
                    docs_normalized[doc_name] = {"exists": doc_path.exists()}
            projects.append({
                "name": project_dir.name,
                "docs": docs_normalized,
                "computed_at": result.get("computed_at"),
            })
        return {"projects": projects, "scoring": "5-dimensional"}
    except ImportError:
        # Fallback: staleness-only (pre-T3 behavior)
        projects = []
        now = datetime.now()
        for project_dir in sorted(projects_dir.iterdir()):
            if not project_dir.is_dir() or project_dir.name.startswith("."):
                continue
            docs: dict[str, dict] = {}
            for doc_name in ("PRODUCT.md", "TECH.md", "IMPROVEMENT.md", "PROJECT.md"):
                doc_path = project_dir / doc_name
                if doc_path.exists():
                    mtime = datetime.fromtimestamp(doc_path.stat().st_mtime)
                    age_days = (now - mtime).days
                    docs[doc_name] = {"exists": True, "age_days": age_days, "stale": age_days > 14}
                else:
                    docs[doc_name] = {"exists": False}
            projects.append({"name": project_dir.name, "docs": docs})
        return {"projects": projects, "scoring": "staleness-only"}


_CHANNEL_PATTERN = re.compile(r"^\#\#\s+\d{2}:\d{2}\s+\|.*\[Channel:")

def _collect_session_stats(ws_path: Path) -> dict[str, Any]:
    """Session volume from DailyActivity files (last 7 days).

    Separates interactive sessions (user-initiated chat) from channel
    sessions (Slack bot pings) to avoid inflating the count.
    """
    da_dir = ws_path / "Knowledge" / "DailyActivity"
    if not da_dir.is_dir():
        return {"available": False}

    cutoff = date.today() - timedelta(days=7)
    total_sessions = 0
    interactive_sessions = 0
    channel_sessions = 0
    days_active = 0

    for f in sorted(da_dir.glob("*.md")):
        if not f.stem[:4].isdigit():
            continue
        try:
            file_date = date.fromisoformat(f.stem[:10])
        except ValueError:
            continue
        if file_date < cutoff:
            continue

        days_active += 1
        try:
            content = f.read_text(encoding="utf-8")
            for line in content.splitlines():
                if line.startswith("## ") and not line.startswith("## _"):
                    total_sessions += 1
                    if _CHANNEL_PATTERN.match(line):
                        channel_sessions += 1
                    else:
                        interactive_sessions += 1
        except (OSError, UnicodeDecodeError):
            continue

    return {
        "last_7d_sessions": total_sessions,
        "last_7d_interactive": interactive_sessions,
        "last_7d_channel": channel_sessions,
        "last_7d_active_days": days_active,
        "avg_interactive_per_day": round(
            interactive_sessions / max(days_active, 1), 1
        ),
    }
