"""Build a ranked Vault digest for consolidation personal-noticing."""

from __future__ import annotations

import logging
import re
from datetime import datetime, timedelta, timezone
from typing import Any

from utils.redaction import cap_str, redact_text

_log = logging.getLogger(__name__)

_UNTITLED = "(untitled)"
_MIN_EXCERPT_CHARS = 40
_EXCERPT_CAP = 400
_MEANINGFUL_TEXT_RE = re.compile(r"[A-Za-z0-9]{3,}")


def _parse_iso_ts(raw: str | None) -> datetime | None:
    text = (raw or "").strip()
    if not text:
        return None
    try:
        if text.endswith("Z"):
            text = text[:-1] + "+00:00"
        dt = datetime.fromisoformat(text)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt
    except Exception:
        return None


def _is_untitled(title: str | None) -> bool:
    t = (title or "").strip().lower()
    return not t or t == _UNTITLED.lower() or t == "untitled"


def _meaningful_body_chars(body: str) -> int:
    text = re.sub(r"\s+", " ", (body or "").strip())
    if len(text) < _MIN_EXCERPT_CHARS:
        return 0
    if not _MEANINGFUL_TEXT_RE.search(text):
        return 0
    return len(text)


def _excerpt_from_body(body: str) -> str:
    cleaned = redact_text((body or "").strip())
    if _meaningful_body_chars(cleaned) < _MIN_EXCERPT_CHARS:
        return ""
    return cap_str(cleaned, _EXCERPT_CAP)


def _sort_key_for_note(meta: dict[str, Any], *, window_start: datetime | None) -> tuple[int, int, float]:
    """Higher sort key = earlier in list (discussed > titled > recency)."""
    discussed_at = _parse_iso_ts(str(meta.get("discussed_at") or ""))
    discussed_boost = 0
    if discussed_at and window_start and discussed_at >= window_start:
        discussed_boost = 1
    untitled_penalty = 1 if _is_untitled(str(meta.get("title") or "")) else 0
    updated = _parse_iso_ts(str(meta.get("updated_at") or ""))
    updated_ts = updated.timestamp() if updated else 0.0
    return (discussed_boost, -untitled_penalty, updated_ts)


def _format_note_line(title: str, *, updated_at: str, discussed_at: str, excerpt: str) -> str:
    parts: list[str] = []
    if updated_at:
        parts.append(f"updated {updated_at[:10]}")
    if discussed_at:
        parts.append("discussed in chat")
    meta = ", ".join(parts) if parts else "recent"
    line = f"- [{title}] ({meta})"
    if excerpt:
        line += f": {excerpt}"
    return line


def shrink_vault_digest(text: str, *, max_chars: int) -> str:
    """Trim vault digest from the bottom until within max_chars."""
    block = (text or "").strip()
    if not block or len(block) <= max_chars:
        return block
    lines = block.splitlines()
    header = lines[0] if lines else ""
    body_lines = lines[1:]
    while body_lines and len("\n".join([header, *body_lines])) > max_chars:
        body_lines.pop()
    if not body_lines:
        return cap_str(header, max_chars)
    out = "\n".join([header, *body_lines]).strip()
    if len(out) > max_chars:
        return out[:max_chars].rstrip()
    return out


def build_vault_noticing_digest(
    *,
    lookback_days: int = 7,
    max_chars: int = 4096,
    max_notes: int = 8,
    max_graph_pairs: int = 12,
) -> str:
    """Return a markdown section summarizing recent Vault activity, or '' if empty."""
    import chat_storage as cs

    since_hours = max(1.0, float(lookback_days) * 24.0)
    window_start = datetime.now(timezone.utc) - timedelta(hours=since_hours)

    recent = cs.list_recent_vault_notes(limit=max(max_notes * 3, 24), since_hours=since_hours)
    note_rows = list(recent.get("notes") or [])
    seen_ids = {str(row.get("id") or "").strip() for row in note_rows if row.get("id")}
    discussed = cs.list_vault_notes_discussed_since(
        since_hours=since_hours,
        limit=max(max_notes * 2, 12),
    )
    for row in discussed.get("notes") or []:
        note_id = str(row.get("id") or "").strip()
        if note_id and note_id not in seen_ids:
            note_rows.append(row)
            seen_ids.add(note_id)

    ranked: list[dict[str, Any]] = []
    for row in note_rows:
        note_id = str(row.get("id") or "").strip()
        if not note_id:
            continue
        try:
            capture = cs.get_capture(note_id) or {}
        except Exception:
            _log.debug("vault digest get_capture failed for %s", note_id[:8], exc_info=True)
            capture = {}
        metadata = capture.get("metadata") or {}
        discussed_at = str(
            row.get("discussed_at") or metadata.get("discussed_at") or ""
        )
        ranked.append(
            {
                "id": note_id,
                "title": str(row.get("title") or capture.get("title") or _UNTITLED),
                "updated_at": str(row.get("updated_at") or capture.get("updated_at") or ""),
                "discussed_at": discussed_at,
                "body": str(capture.get("body") or ""),
            }
        )

    ranked.sort(key=lambda m: _sort_key_for_note(m, window_start=window_start), reverse=True)

    lines: list[str] = ["## Vault activity (user notebook; synthesize with memory)"]
    note_lines_added = 0
    for meta in ranked:
        if note_lines_added >= max_notes:
            break
        excerpt = _excerpt_from_body(meta.get("body") or "")
        if not excerpt:
            continue
        lines.append(
            _format_note_line(
                redact_text(str(meta.get("title") or _UNTITLED)),
                updated_at=str(meta.get("updated_at") or ""),
                discussed_at=str(meta.get("discussed_at") or ""),
                excerpt=excerpt,
            )
        )
        note_lines_added += 1

    pairs_out = cs.list_recent_vault_graph_pairs(since_hours=since_hours, limit=max_graph_pairs)
    for pair in pairs_out.get("pairs") or []:
        title_a = redact_text(str(pair.get("title_a") or _UNTITLED).strip()) or _UNTITLED
        title_b = redact_text(str(pair.get("title_b") or _UNTITLED).strip()) or _UNTITLED
        kind = str(pair.get("kind") or "linked").strip()
        lines.append(f'- Graph: "{title_a}" ↔ "{title_b}" ({kind})')

    if len(lines) <= 1:
        return ""
    digest = shrink_vault_digest("\n".join(lines), max_chars=max_chars)
    return digest
