"""ICERTI-STORAGE-01: 원장을 보존하고 보호 해제된 Event 미디어만 정리한다."""
import fcntl
import json
import os
from pathlib import Path
import shutil
import time

from app.event_artifact_contract import RESERVATION, relative_directory


def _safe(root, relative):
    path = root / relative
    if Path(relative).is_absolute() or '..' in Path(relative).parts or path.resolve() != path:
        raise ValueError('Unsafe retention path')
    if root not in path.parents or root.resolve() != root:
        raise ValueError('Retention path outside root')
    return path


def _size(root, path):
    if not path.exists():
        return 0
    device = root.stat().st_dev
    size = 0
    for parent, dirs, files in os.walk(path, followlinks=False):
        for name in dirs+files:
            p = Path(parent)/name
            if p.is_symlink() or p.stat().st_dev != device:
                raise ValueError('Retention symlink or mount boundary')
        size += sum((Path(parent)/n).stat().st_size for n in files)
    return size


def _remove(store, row):
    root = store.config.root
    expected = relative_directory(row['payload'])
    if row['relative_path'] != expected or row['payload']['eventId'] != row['event_id']:
        raise ValueError('Retention ownership mismatch')
    final = _safe(root, expected)
    tomb = _safe(root, '.retiring/'+row['event_id'])
    if final.exists() and tomb.exists():
        raise ValueError('Ambiguous retention directories')
    source = final if final.exists() else tomb
    size = _size(root, source)
    stage = _safe(root, '.staging/'+row['event_id'])
    if stage.exists():
        stage_size = _size(root, stage)
        owned = False
        for marker in ('failure.json', 'job.json'):
            p = stage/marker
            if p.is_file():
                event = json.loads(p.read_text()).get('event', {})
                owned = all(event.get(k) == row['payload'].get(k) for k in
                            ('eventId','cameraId','runId','runGeneration')) and event.get('eventId') == row['event_id']
                if owned: break
        if not owned:
            raise ValueError('Unverified Artifact staging ownership')
        size += stage_size
    if source.exists():
        if not shutil.rmtree.avoids_symlink_attacks:
            raise ValueError('Safe recursive deletion unavailable')
        if final.exists():
            tomb.parent.mkdir(mode=0o700, exist_ok=True)
            _safe(root, str(tomb.relative_to(root)))
            final.rename(tomb)
        shutil.rmtree(tomb)
    if stage.exists():
        if not shutil.rmtree.avoids_symlink_attacks:
            raise ValueError('Safe recursive deletion unavailable')
        shutil.rmtree(stage)
    # A concurrent scan may already account for removed bytes. Never subtract twice.
    with store.transaction() as cur:
        cur.execute('SELECT observed_at FROM contextra_artifact_storage WHERE id=1 FOR UPDATE')
        observed = cur.fetchone()['observed_at']
        cur.execute("""UPDATE contextra_event_artifacts SET media_retention_state='DELETED',
            media_retention_at=%s,media_retention_error=NULL,stored_bytes=0
            WHERE event_id=%s AND media_retention_state='DELETING'""", (time.time(),row['event_id']))
        if cur.rowcount != 1:
            raise RuntimeError('Stale retention completion')
        cur.execute('''UPDATE contextra_artifact_storage SET observed_bytes=GREATEST(0,observed_bytes-%s),
            accounting_revision=accounting_revision+1 WHERE id=1''',
            (size if observed == row['observed_at'] else 0,))


def maintain(store, *, max_jobs=64, budget_seconds=1, max_bytes=256*1024**2):
    if not store.config.retention_enabled:
        return dict(deleted=0, blocked=0)
    root = store.config.root
    _safe(root, '.retention.lock')
    fd = os.open(root/'.retention.lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
    counts = dict(deleted=0, blocked=0, reclaimed_bytes=0)
    try:
        try:
            fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            return dict(counts, busy=True)
        until = time.monotonic()+budget_seconds
        # Decide pressure briefly; the indexed candidate search never holds storage's
        # global admission lock. The retention flock owns selection/deletion ordering.
        with store.transaction() as cur:
            cur.execute('SELECT * FROM contextra_artifact_storage WHERE id=1 FOR UPDATE')
            storage = cur.fetchone()
            accounted = storage['observed_bytes']+storage['reserved_bytes']+RESERVATION
            target = storage['retention_target']
            if accounted >= store.config.cleanup_start and not target:
                target = store.config.cleanup_target or store.config.cap*9//10
            free = shutil.disk_usage(root).free-storage['reserved_bytes']-RESERVATION-store.config.external_reserve
            needed = (bool(target) and accounted>target) or free<store.config.low_free
            cur.execute('UPDATE contextra_artifact_storage SET retention_target=%s WHERE id=1', (target if needed else 0,))
        for _ in range(max_jobs):
            if time.monotonic() >= until or counts['reclaimed_bytes'] >= max_bytes:
                break
            with store.transaction() as cur:
                cur.execute('SELECT * FROM contextra_artifact_storage WHERE id=1')
                storage = cur.fetchone()
                accounted = storage['observed_bytes']+storage['reserved_bytes']+RESERVATION
                target = storage['retention_target']
                free = shutil.disk_usage(root).free-storage['reserved_bytes']-RESERVATION-store.config.external_reserve
                needed = (bool(target) and accounted>target) or free<store.config.low_free
                cur.execute("""SELECT * FROM contextra_event_artifacts WHERE reservation=0
                    AND claim_token IS NULL AND media_retention_state='DELETING'
                    ORDER BY confirmed_at,event_id LIMIT 1 FOR UPDATE SKIP LOCKED""")
                row = cur.fetchone()
                if row is None and needed:
                    cur.execute("""SELECT * FROM contextra_event_artifacts
                        WHERE reservation=0 AND claim_token IS NULL AND stored_bytes>0
                        AND artifact_state IN ('READY','FAILED','EXPIRED')
                        AND delivery_state IN ('SENT','FAILED','EXPIRED','SKIPPED','BLOCKED_TIME')
                        AND media_retention_state='PRESENT'
                        ORDER BY confirmed_at,event_id LIMIT 1 FOR UPDATE SKIP LOCKED""")
                    row = cur.fetchone()
                if not row:
                    break
                row = dict(row, observed_at=storage['observed_at'])
                cur.execute("""UPDATE contextra_event_artifacts SET media_retention_state='DELETING',
                    media_retention_at=%s WHERE event_id=%s""", (time.time(),row['event_id']))
            try:
                _remove(store, row)
                counts['deleted'] += 1
                counts['reclaimed_bytes'] += row['stored_bytes']
            except (OSError, ValueError) as error:
                with store.transaction() as cur:
                    cur.execute("""UPDATE contextra_event_artifacts SET media_retention_state='BLOCKED',
                        media_retention_error=%s WHERE event_id=%s AND media_retention_state='DELETING'""",
                        (type(error).__name__,row['event_id']))
                counts['blocked'] += 1
    finally:
        os.close(fd)
    return counts
