#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Decision Consolidation Stage — dedup, prune, decay, archive.

Manages Hippocampus/Decisions/auto_decisions.jsonl quality lifecycle.
Called as a stage from consolidation_daemon.py.

Dependencies: brain_utils, brain_io (internal)
"""

import json
import os
from datetime import datetime, timedelta, timezone

from pulse.core.brain_utils import (
    DECISIONS_JSONL,
    read_jsonl_entries,
    now_iso as _now_iso,
)
from pulse.core.brain_io import _acquire

# Tuning constants
_MIN_CONFIDENCE = 0.3
_DECAY_RATE_PER_WEEK = 0.02
_DECAY_GRACE_DAYS = 14
_SUPERSEDED_TTL_DAYS = 30
_ARCHIVE_AGE_DAYS = 180
_ARCHIVE_SCOPES = {"tooling", "infrastructure"}


def _parse_ts(ts_str: str) -> datetime | None:
    """Parse ISO timestamp, return None on failure."""
    if not ts_str:
        return None
    try:
        return datetime.fromisoformat(ts_str)
    except (ValueError, TypeError):
        return None


def stage_decision_consolidation(verbose: bool = False) -> dict:
    """Consolidate decisions JSONL: dedup, prune, decay, archive.

    P48 Fix 2F: Reads from SQLite decisions table first, falls back to DECISIONS_JSONL.
    Returns stats dict: {deduped, pruned, quality_gated, decayed, archived, total}.
    """
    stats = {"deduped": 0, "pruned": 0, "quality_gated": 0,
             "decayed": 0, "archived": 0, "total": 0}

    # P48 Fix 2F: Read from SQLite first (decisions table), fall back to DECISIONS_JSONL
    entries = []
    _from_sqlite = False
    try:
        from pulse.core.brain_db import get_conn
        conn = get_conn()
        tbl_check = conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='decisions'"
        ).fetchone()
        if tbl_check:
            rows = conn.execute("SELECT * FROM decisions").fetchall()
            if rows:
                # Get column names from cursor description
                cursor = conn.execute("SELECT * FROM decisions LIMIT 0")
                col_names = [d[0] for d in (cursor.description or [])]
                if col_names:
                    entries = [
                        dict(row)
                        for row in rows
                    ]
                    _from_sqlite = True
    except Exception:
        pass

    if not _from_sqlite:
        if not DECISIONS_JSONL.exists():
            if verbose:
                print("  [DECISIONS] No decisions file — skipped")
            return stats
        with _acquire(DECISIONS_JSONL):
            entries = read_jsonl_entries(DECISIONS_JSONL)
        if not entries:
            if verbose:
                print("  [DECISIONS] Empty — skipped")
            return stats

    if not entries:
        if verbose:
            print("  [DECISIONS] Empty — skipped")
        return stats

    now = datetime.now(timezone.utc)

    # --- 1. Dedup: keep first occurrence per id ---
    seen_ids: dict[str, int] = {}
    deduped: list[dict] = []
    for entry in entries:
        eid = entry.get("id", "")
        if eid and eid in seen_ids:
            stats["deduped"] += 1
            continue
        if eid:
            seen_ids[eid] = len(deduped)
        deduped.append(entry)

    # --- 2. Prune superseded older than TTL ---
    surviving: list[dict] = []
    for entry in deduped:
        if entry.get("status") == "superseded":
            ts = _parse_ts(entry.get("superseded_at", "")
                           or entry.get("timestamp", ""))
            if ts and (now - ts).days > _SUPERSEDED_TTL_DAYS:
                stats["pruned"] += 1
                continue
        surviving.append(entry)

    # --- 3. Quality gate: drop low-confidence active decisions ---
    gated: list[dict] = []
    for entry in surviving:
        conf = entry.get("confidence", 0.5)
        if entry.get("status", "active") == "active" and conf < _MIN_CONFIDENCE:
            stats["quality_gated"] += 1
            continue
        gated.append(entry)

    # --- 4. Temporal decay: reduce confidence for old decisions ---
    for entry in gated:
        if entry.get("status") != "active":
            continue
        ts = _parse_ts(entry.get("timestamp", ""))
        if not ts:
            continue
        age_days = (now - ts).days
        if age_days <= _DECAY_GRACE_DAYS:
            continue
        weeks_past_grace = (age_days - _DECAY_GRACE_DAYS) / 7.0
        decay = round(weeks_past_grace * _DECAY_RATE_PER_WEEK, 4)
        if decay > 0:
            old_conf = entry.get("confidence", 0.5)
            entry["confidence"] = round(max(0.0, old_conf - decay), 4)
            stats["decayed"] += 1

    # --- 5. Staleness archive: old tooling/infra decisions ---
    for entry in gated:
        if entry.get("status") != "active":
            continue
        ts = _parse_ts(entry.get("timestamp", ""))
        if not ts:
            continue
        scope = entry.get("scope", "general")
        if (now - ts).days > _ARCHIVE_AGE_DAYS and scope in _ARCHIVE_SCOPES:
            entry["status"] = "archived"
            entry["archived_at"] = _now_iso()
            stats["archived"] += 1

    stats["total"] = len(gated)

    # Primary: sync to SQLite
    try:
        from pulse.core.brain_db import bulk_upsert
        ok = bulk_upsert("decisions", gated)
        if ok > 0:
            if verbose:
                print(f"  [DECISIONS] {stats['deduped']} deduped, {stats['pruned']} pruned, "
                      f"{stats['quality_gated']} quality-gated, {stats['decayed']} decayed, "
                      f"{stats['archived']} archived ({stats['total']} remaining)")
            return stats  # Skip JSONL write
    except Exception:
        pass

    # Fallback: atomic JSONL rewrite
    DECISIONS_JSONL.parent.mkdir(parents=True, exist_ok=True)
    tmp = DECISIONS_JSONL.with_suffix(".jsonl.tmp")
    with open(tmp, "w", encoding="utf-8") as f:
        for item in gated:
            f.write(json.dumps(item, ensure_ascii=False,
                               separators=(",", ":")) + "\n")
    os.replace(str(tmp), str(DECISIONS_JSONL))

    if verbose:
        print(f"  [DECISIONS] {stats['deduped']} deduped, {stats['pruned']} pruned, "
              f"{stats['quality_gated']} quality-gated, {stats['decayed']} decayed, "
              f"{stats['archived']} archived ({stats['total']} remaining)")
    return stats
