#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Gate Feedback — Core I/O.

record_gate_failure  — persist a gate failure entry and return corrective dict.
get_active_feedback  — return non-expired entries for agent briefings.
decay_old_feedback   — expire stale entries (called by orchestrator scheduler).
"""
from __future__ import annotations

import hashlib
import logging
import os
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path

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

from pulse.core.brain_utils import (
    load_json_dict,
    save_json,
    STATE_DIR,
    now_iso as _now_iso,
)
from pulse.core.brain_io import _acquire
from pulse.daemons.neural.gate_feedback.templates import (
    _classify_failure,
    _extract_top_files,
    _generate_anti_pattern,
    _generate_corrective,
)

log = logging.getLogger("pulse.gate_feedback")

FEEDBACK_FILE = STATE_DIR / "gate_feedback.json"
MAX_ENTRIES = 20
DEFAULT_EXPIRY_HOURS = 48


def _feedback_id(task_id: str, failure_type: str) -> str:
    """Stable ID from task_id + failure_type."""
    raw = f"{task_id}|{failure_type}"
    return f"gf_{hashlib.md5(raw.encode()).hexdigest()[:12]}"


def record_gate_failure(
    task_id: str,
    task_title: str,
    task_description: str,
    task_domain: str,
    gate_failures: list[str],
    post_snapshot: dict[str, tuple[int, int]] | None = None,
) -> dict | None:
    """Record a gate failure, generate anti-pattern, return corrective task dict.

    Called by loop.py on gate fail. Returns corrective task dict or None.
    """
    failure_type, detail = _classify_failure(gate_failures)
    top_files = _extract_top_files(post_snapshot)
    anti_pattern = _generate_anti_pattern(
        failure_type, detail, top_files, task_title)
    corrective = _generate_corrective(
        failure_type, top_files, task_title, task_description)
    now = _now_iso()

    entry = {
        "id": _feedback_id(task_id, failure_type),
        "task_id": task_id,
        "task_title": task_title[:100],
        "domain": task_domain,
        "failure_type": failure_type,
        "failure_detail": detail[:300],
        "top_changed_files": top_files,
        "anti_pattern": anti_pattern[:300],
        "corrective_constraint": corrective[:300],
        "timestamp": now,
        "expires_at": (
            datetime.now(timezone.utc)
            + timedelta(hours=DEFAULT_EXPIRY_HOURS)
        ).isoformat(),
        "status": "active",
    }

    try:
        with _acquire(FEEDBACK_FILE):
            data = load_json_dict(FEEDBACK_FILE)
            entries = data.get("entries", [])

            # Dedup: replace existing entry for same task+type
            entries = [e for e in entries if e.get("id") != entry["id"]]
            entries.insert(0, entry)

            # Cap at MAX_ENTRIES
            entries = entries[:MAX_ENTRIES]

            save_json(FEEDBACK_FILE, {
                "entries": entries,
                "last_updated": now,
            })
    except Exception as e:
        log.error("Failed to write gate feedback: %s", e)
        return None

    log.info("[D5] Recorded: %s — %s", failure_type, anti_pattern[:80])

    return {
        "anti_pattern": anti_pattern,
        "corrective_constraint": corrective,
        "failure_type": failure_type,
    }


def get_active_feedback(max_items: int = 5) -> list[dict]:
    """Return active (non-expired) feedback entries sorted by recency.

    Used by boot loaders to inject gate feedback into agent briefings.
    """
    if not FEEDBACK_FILE.exists():
        return []
    try:
        data = load_json_dict(FEEDBACK_FILE)
        entries = data.get("entries", [])
        now = datetime.now(timezone.utc)
        active = []
        for e in entries:
            if e.get("status") != "active":
                continue
            try:
                expires = datetime.fromisoformat(e.get("expires_at", ""))
                if expires.tzinfo is None:
                    expires = expires.replace(tzinfo=timezone.utc)
                if expires < now:
                    continue
            except (ValueError, TypeError):
                pass
            active.append(e)
        return active[:max_items]
    except Exception:
        return []


def decay_old_feedback(max_age_hours: int = DEFAULT_EXPIRY_HOURS) -> int:
    """Expire entries older than threshold. Returns count of expired entries.

    Called by orchestrator scheduler with --decay flag.
    """
    if not FEEDBACK_FILE.exists():
        return 0
    try:
        with _acquire(FEEDBACK_FILE):
            data = load_json_dict(FEEDBACK_FILE)
            entries = data.get("entries", [])
            now = datetime.now(timezone.utc)
            expired_count = 0

            for e in entries:
                if e.get("status") != "active":
                    continue
                try:
                    expires = datetime.fromisoformat(e.get("expires_at", ""))
                    if expires.tzinfo is None:
                        expires = expires.replace(tzinfo=timezone.utc)
                    if expires < now:
                        e["status"] = "expired"
                        expired_count += 1
                except (ValueError, TypeError):
                    pass

            entries = [e for e in entries if e.get("status") == "active"]
            save_json(FEEDBACK_FILE, {
                "entries": entries,
                "last_updated": _now_iso(),
            })

        if expired_count:
            log.info("[D5] Decayed %d expired feedback entries", expired_count)
        return expired_count
    except Exception as e:
        log.error("[D5] Decay error: %s", e)
        return 0
