"""Enforces `customers.retention_preference` (PRD §8 / the public Privacy
Policy's "retained for a default of 30 days... after which they're
deleted"). Runs as a second loop in the same `backend-worker` process as
the job orchestrator (see worker_loop.py's `__main__`) — a separate active
sweep, not a lazy check-on-access like services/quotas.py's period rollover,
because deletion has to happen even if nobody ever looks at that dump again.

Only the raw dump file is deleted. tier_reports/escalations are untouched.
The `dumps` row itself is never hard-deleted — it's scrubbed to an
anonymous metadata shell (status=EXPIRED, storage_uri and customer_id both
cleared) so it can't be tied back to a customer, while `sha256`,
`size_bytes`, `dump_type`, and `uploaded_at` survive for internal aggregate/
KB use. The sweep always proceeds on schedule regardless of any unresolved
Escalation on the dump's job — escalation content isn't touched by this at
all, so there's no investigation to lose by deleting just the file.
"""
import asyncio
import logging
from datetime import datetime, timedelta, timezone

from sqlalchemy import select

from ..config import get_settings
from ..db import get_session_factory
from ..models import AnalysisJob, Customer, Dump, DumpStatus, TierReport
from ..security.audit import write_audit
from ..storage import get_storage
from .. import telemetry
from ..telemetry import configure_telemetry
from ..triage.transcripts import is_stored_ref, transcript_prefix

logger = logging.getLogger("triage.retention_sweep")


async def sweep_expired_dumps(session, *, dry_run: bool) -> int:
    now = datetime.now(timezone.utc)
    result = await session.execute(
        select(Dump, Customer.retention_preference)
        .join(Customer, Dump.customer_id == Customer.id)
        .where(Dump.status != DumpStatus.EXPIRED)
    )
    swept = 0
    for dump, retention_days in result.all():
        uploaded_at = dump.uploaded_at
        if uploaded_at.tzinfo is None:
            uploaded_at = uploaded_at.replace(tzinfo=timezone.utc)
        cutoff = uploaded_at + timedelta(days=retention_days)
        if now < cutoff:
            continue

        swept += 1
        customer_id_for_log = dump.customer_id
        if dry_run:
            logger.info(
                "[dry-run] would expire dump %s (customer %s, uploaded %s, retention %sd)",
                dump.id, customer_id_for_log, dump.uploaded_at, retention_days,
            )
            await write_audit(session, actor="retention_sweep", action="dump_retention_dry_run", dump_id=dump.id)
            continue

        await get_storage().delete(dump.storage_uri)
        # Transcripts are debugger output from this dump — redacted, but
        # redaction is best-effort, so they follow the dump's retention
        # rather than outliving it. Deleted by prefix, not by following
        # TierReport.transcript_ref: a tier run that stored its transcript
        # and then failed (LLM timeout, lost lease, SIGTERM) never wrote the
        # report row that would point at it. The structured reports stay.
        await get_storage().delete_prefix(transcript_prefix(dump.id))
        reports = (
            await session.execute(
                select(TierReport).join(AnalysisJob, TierReport.job_id == AnalysisJob.id).where(AnalysisJob.dump_id == dump.id)
            )
        ).scalars().all()
        for report in reports:
            if is_stored_ref(report.transcript_ref):
                report.transcript_ref = "expired"
        dump.status = DumpStatus.EXPIRED
        dump.storage_uri = None
        dump.customer_id = None
        dump.expired_at = now
        telemetry.dumps_expired.add(1)
        await write_audit(session, actor="retention_sweep", action="dump_expired", dump_id=dump.id)
        logger.info("expired dump %s (was customer %s, uploaded %s)", dump.id, customer_id_for_log, dump.uploaded_at)

    return swept


async def run_retention_loop() -> None:
    configure_telemetry(service_name="dump-triage-worker")
    settings = get_settings()
    session_factory = get_session_factory()
    logger.info(
        "retention sweep loop starting, interval %.1fs, dry_run=%s",
        settings.retention_sweep_interval_seconds, settings.retention_sweep_dry_run,
    )
    while True:
        try:
            async with session_factory() as session:
                swept = await sweep_expired_dumps(session, dry_run=settings.retention_sweep_dry_run)
            if swept:
                verb = "would expire" if settings.retention_sweep_dry_run else "expired"
                logger.info("retention sweep %s %d dump(s)", verb, swept)
        except Exception:
            # Same reasoning as worker_loop.run_forever: one bad iteration
            # must not kill retention enforcement for good.
            logger.exception("retention sweep iteration failed, will retry")
        await asyncio.sleep(settings.retention_sweep_interval_seconds)
