"""Read aggregate Linux /proc counters. No workload, privileged changes, or process details."""
import argparse
import csv
import datetime
import json
import os
import sys
import time
import uuid
from pathlib import Path


def counters(text):
    result = {}
    for line in text.splitlines():
        parts = line.replace(':', '').split()
        if len(parts) >= 2:
            try:
                result[parts[0]] = int(parts[1])
            except ValueError:
                pass
    return result


def pressure(text):
    result = {}
    for line in text.splitlines():
        parts = line.split()
        values = dict(part.split('=', 1) for part in parts[1:] if '=' in part)
        if parts and 'total' in values:
            result[parts[0]] = int(values['total'])
    return result


def rate(previous, current, elapsed):
    if previous is None or current is None or current < previous or elapsed <= 0:
        return None
    return (current - previous) / elapsed


def read_sample(proc=Path('/proc')):
    mem = counters((proc / 'meminfo').read_text())
    vm = counters((proc / 'vmstat').read_text())
    try:
        psi = pressure((proc / 'pressure/memory').read_text())
    except (OSError, ValueError):
        psi = {}
    def mib(key):
        return mem[key] / 1024 if key in mem else None
    total, free = mib('SwapTotal'), mib('SwapFree')
    return {'at_utc': datetime.datetime.now(datetime.timezone.utc).isoformat(),
            'mem_total_mib': mib('MemTotal'), 'mem_free_mib': mib('MemFree'),
            'mem_available_mib': mib('MemAvailable'),
            'swap_used_mib': total - free if total is not None and free is not None else None,
            'pswpin_pages': vm.get('pswpin'), 'pswpout_pages': vm.get('pswpout'),
            'psi_some_total_us': psi.get('some'), 'psi_full_total_us': psi.get('full')}


def collect(out, count, interval):
    if sys.platform != 'linux':
        raise RuntimeError('This collector requires Linux /proc. Do not interpret Windows counters as Linux.')
    page_size = os.sysconf('SC_PAGE_SIZE')
    rows = []
    previous, previous_clock = None, None
    for index in range(count):
        if index:
            time.sleep(interval)
        row = read_sample()
        clock = time.monotonic()
        elapsed = clock - previous_clock if previous_clock is not None else None
        row.update(interval_seconds=elapsed, swap_in_mib_per_second=None, swap_out_mib_per_second=None,
                   psi_some_stall_percent=None, psi_full_stall_percent=None)
        if previous is not None:
            for name, field in [('in', 'pswpin_pages'), ('out', 'pswpout_pages')]:
                value = rate(previous[field], row[field], elapsed)
                row['swap_' + name + '_mib_per_second'] = None if value is None else value * page_size / (1024 * 1024)
            for name in ['some', 'full']:
                field = 'psi_' + name + '_total_us'
                value = rate(previous[field], row[field], elapsed)
                row['psi_' + name + '_stall_percent'] = None if value is None else value / 10000
        rows.append(row)
        previous, previous_clock = row, clock
    folder = out / ('memory-sample-' + uuid.uuid4().hex[:12])
    folder.mkdir(parents=True, exist_ok=False)
    def values(field):
        return [row[field] for row in rows if row[field] is not None]
    def bounds(field):
        actual = values(field)
        return {'min': min(actual), 'max': max(actual)} if actual else None
    summary = {'started_at_utc': rows[0]['at_utc'], 'finished_at_utc': rows[-1]['at_utc'],
               'samples': len(rows), 'intervals': len(rows) - 1,
               'elapsed_seconds': sum(values('interval_seconds')), 'page_size_bytes': page_size,
               'mem_total_mib': rows[0]['mem_total_mib'], 'mem_free_mib': bounds('mem_free_mib'),
               'mem_available_mib': bounds('mem_available_mib'), 'swap_used_mib': bounds('swap_used_mib'),
               'swap_in_mib_per_second': bounds('swap_in_mib_per_second'),
               'swap_out_mib_per_second': bounds('swap_out_mib_per_second'),
               'psi_some_stall_percent': bounds('psi_some_stall_percent'),
               'psi_full_stall_percent': bounds('psi_full_stall_percent'),
               'scope': 'One Linux /proc view, aggregate counters only; no induced load, process identification, latency measurement, peak-capacity or recovery test.',
               'units': 'meminfo kB interpreted as KiB; pswpin/out pages converted using runtime page size; PSI cumulative microsecond deltas divided by actual monotonic interval. First interval is null, not zero.'}
    (folder / 'samples.json').write_text(json.dumps(rows, indent=2) + '\n', encoding='utf-8')
    (folder / 'summary.json').write_text(json.dumps(summary, indent=2) + '\n', encoding='utf-8')
    with (folder / 'samples.csv').open('w', encoding='utf-8', newline='') as file:
        writer = csv.DictWriter(file, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)
    print(json.dumps({'output': str(folder), 'summary': summary}, indent=2), flush=True)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--out', type=Path, required=True)
    parser.add_argument('--samples', type=int, default=13)
    parser.add_argument('--interval', type=float, default=5)
    args = parser.parse_args()
    if not 2 <= args.samples <= 61 or not 1 <= args.interval <= 60 or (args.samples - 1) * args.interval > 300:
        parser.error('Use 2–61 samples, intervals 1–60 seconds, total wait at most 300 seconds.')
    collect(args.out, args.samples, args.interval)


if __name__ == '__main__':
    main()
