#!/usr/bin/env python3
"""Vettability — the single source of truth for "is this property worth a viewing?"

One predicate, owned here, instead of three diverging copies (the old JS isVetted in
shortlist.html, compute_data_confidence, and _has_complete_new_system_data). The scorer
calls this at score time (fresh cp_score in hand) and writes `vetted` + `vetted_blockers`
into each shortlist entry; the page renders the flag rather than re-deriving it over
stale enriched_data.json.

A property is VETTED when it is substantively evaluated AND top-tier:
  - confirmed live within the last 7 days (availability recheck)
  - not removed by a hard gate
  - has a photo (you can look at it)
  - has been analyzed — vision character_score OR ≥4 GPT/Claude criteria
  - has a verified land_size_m2 (so the 3,000m² gate decides on real data)
  - CP score ≥ the top-tier floor (worth a viewing, not just worth tracking)

`vetted_blockers()` returns the reasons a property is NOT vetted (empty list = vetted),
so the gap is legible — both to the page and to a future "ensure-vettable" enrichment step.
"""
import datetime

VETTED_MIN_CP = 1.5           # low floor — the verification bars below do the gating, not the score
CHECKED_LIVE_WINDOW_DAYS = 7  # rural FR listings don't churn fast; a week-old check is credible

# NOTE (2026-06-08): "vetted" = a verified, land-backed HOMESTEAD ready for the weekly
# review — recently-live + photo + analysed + a real land size. Land IS required (it's the
# Cyber-Prairie keystone); the ~11 land-LESS village/town houses in the shortlist are
# deliberately excluded here (different model). That caps the set at the ~23 candidates
# that actually have land — accepted as the working set. The CP floor is low (1.5) on
# purpose: the bars (land/photo/analysis/recency) gate; the Paradiso Score just RANKS.


def _checked_recently(p, now):
    raw = p.get('availability_checked_at')
    if not raw:
        return False
    try:
        t = datetime.datetime.fromisoformat(str(raw).replace('Z', ''))
    except ValueError:
        return False
    return (now - t).days <= CHECKED_LIVE_WINDOW_DAYS


def vetted_blockers(p, cp_score, gated, now=None, min_cp=VETTED_MIN_CP):
    """Reasons this property is not vetted (empty = vetted). cp_score + gated come from
    the scorer (fresh), the rest from the property's enriched fields."""
    now = now or datetime.datetime.now()
    blockers = []
    if p.get('status') == 'Removed':
        blockers.append('removed')
    if not _checked_recently(p, now):
        blockers.append(f'availability not confirmed within {CHECKED_LIVE_WINDOW_DAYS}d')
    if gated:
        blockers.append('failed a hard gate')
    if not ((p.get('photo_urls') or []) or p.get('thumbnail')):
        blockers.append('no photo')
    criteria_count = len(p.get('criteria') or {})
    if p.get('character_score') is None and criteria_count < 4:
        blockers.append('not analyzed (no character score, fewer than 4 criteria)')
    if not isinstance(p.get('land_size_m2'), (int, float)):
        blockers.append('no verified land size')
    if (cp_score or 0) < min_cp:
        blockers.append(f'CP score below {min_cp} floor')
    return blockers


def is_vetted(p, cp_score, gated, now=None, min_cp=VETTED_MIN_CP):
    return not vetted_blockers(p, cp_score, gated, now, min_cp)
