"""Which backup chains have stopped running.

Split from the route for the same reason retention.py is: the rule here is the
interesting part and it should be testable without an auth layer in the way.

THE RULE, and it is not the obvious one. Dedup means an unchanged configuration
writes no revision, so `collectedat` moves only when something CHANGES. A
machine whose settings have been stable for six months has a six-month-old
newest revision and is entirely healthy. Staleness is therefore measured from
`lastseenat` - when the config was last CONFIRMED current, recorded on every
matching post including the no-op that writes nothing else.

Keyed on the CHAIN (asset, kind, source PC), because that is the unit that fails
independently. A machine with two part markers can have one still reporting
while the other stopped, and a per-asset view would report the machine as fine.
"""

from datetime import datetime, timedelta

from shopdb.api import db, Asset

from ..models import BackupRevision

DEFAULTSTALEDAYS = 3


def staledays():
    """Configured threshold; 0 or negative disables the card entirely."""
    from shopdb.api import Setting

    setting = Setting.query.filter_by(key='backups_staledays').first()
    if setting and (setting.value or '').strip():
        try:
            return int(setting.value)
        except (TypeError, ValueError):
            pass
    return DEFAULTSTALEDAYS


def latestperchain():
    """Newest revision of every (asset, kind, source) chain."""
    latest = {}
    for revision in (BackupRevision.query
                     .order_by(BackupRevision.backuprevisionid.asc()).all()):
        latest[(revision.assetid, revision.backupkind,
                revision.sourcehostname)] = revision
    return latest


def stalechains(days=None, limit=50):
    """Chains whose last confirmed check is older than the threshold.

    Deliberately silent about assets that have NEVER been backed up. Whether one
    SHOULD be is a question only the manifest can answer - most machines carry
    no NTLARS at all - and guessing would list a hundred healthy assets, which
    is the noise that makes a board worth ignoring. That is the
    desired-versus-observed card, which needs the manifest resolver.
    """
    days = staledays() if days is None else days
    if days <= 0:
        return []

    now = datetime.utcnow()
    cutoff = now - timedelta(days=days)

    rows = []
    for revision in latestperchain().values():
        # NULL means never confirmed since the column existed, and it is NOT
        # substituted with collectedat. That substitution is what the first
        # version did, and on a real fleet it reported every machine whose
        # config had simply been stable for months as a stopped backup - the
        # whole board lit up and said the site was broken when it was fine.
        # A chain becomes measurable the first time its PC posts; until then
        # this card says nothing about it, which is the truth.
        seen = revision.lastseenat
        if seen is None or seen >= cutoff:
            continue
        asset = db.session.get(Asset, revision.assetid)
        rows.append({
            'assetid': revision.assetid,
            'assetnumber': asset.assetnumber if asset else str(revision.assetid),
            'backupkind': revision.backupkind,
            'sourcehostname': revision.sourcehostname,
            'lastseenat': seen.isoformat() + 'Z',
            'quietdays': (now - seen).days,
        })

    rows.sort(key=lambda r: -r['quietdays'])
    return rows[:limit]
