#!/usr/bin/python3
"""Recent sleep/wake miner actions for one generator group.

Powers the "Recent miner actions" section of the status-page control modal.
Sleep/wake history is the first thing we reach for when diagnosing a power
problem (see the 2026-07-17 GN 2 incident: the tell was that AW wakes simply
STOPPED at 03:06 and didn't resume for ~3 h), but until now the modal only
showed the single most recent action, so spotting a gap meant grepping
powercontrol.log by hand on the server.

powercontrol.log is the authoritative record — it carries manual operator
actions as well as AW/AS, which control_state.last_action does not.
"""

import os
import sys
import cgi
import json

sys.path.insert(0, '/opt/ngon/apps')
from managers.config_manager import config

POWERCONTROL_LOG = '/opt/ngon/logs/powercontrol.log'
# logrotate runs daily w/ delaycompress, so .1 is still plain text. Reading it as
# a fallback keeps the list useful just after midnight, when today's log is nearly
# empty. .2+ are gzipped and deliberately not read (this is a "recent" view).
POWERCONTROL_LOG_PREV = POWERCONTROL_LOG + '.1'

# Only sleep/wake. fix_power/fix_pools/reboot (user MM) are excluded on purpose:
# fix_power alone outnumbers stop_mining ~2:1 and would bury the sleep/wake story.
SLEEP_WAKE_ACTIONS = {'resume_mining': 'wake', 'stop_mining': 'sleep'}

# Cap how much of the log we read. Bounded so a pathological log (logrotate lets
# a file reach 100M before a size-triggered rotation) can't blow up the request.
MAX_TAIL_BYTES = 1_000_000
DEFAULT_LIMIT = 12
MAX_LIMIT = 50


def _tail_lines(path, max_bytes=MAX_TAIL_BYTES):
    """Last <=max_bytes of `path` as lines, oldest first. [] if unreadable."""
    try:
        size = os.path.getsize(path)
        with open(path, 'r', errors='replace') as f:
            if size > max_bytes:
                f.seek(size - max_bytes)
                f.readline()          # drop the partial line we landed in
            return f.read().splitlines()
    except Exception:
        return []


def _parse(line):
    """'ts,pod,count,action,user,reason' -> dict, or None if not a sleep/wake row."""
    parts = line.split(',', 5)
    if len(parts) < 5:
        return None
    ts, pod, count, action, user = (p.strip() for p in parts[:5])
    if action not in SLEEP_WAKE_ACTIONS:
        return None
    try:
        count = int(count)
    except ValueError:
        return None
    return {
        'ts': ts,
        'pod': pod,
        'count': count,
        'action': SLEEP_WAKE_ACTIONS[action],
        'user': user,
        # Reason is free text and may itself contain commas — split(',', 5) keeps
        # the remainder intact.
        'reason': parts[5].strip() if len(parts) > 5 else '',
    }


def _group_pods(site, group):
    cfg = config._master_config or {}
    grp = (cfg.get('sites', {}).get(site, {})
              .get('generator_groups', {}).get(group, {}))
    return set((grp.get('pods') or {}).keys())


def main():
    print("Content-Type: application/json\n")
    form = cgi.FieldStorage()
    site = (form.getfirst('site') or '').strip()
    group = (form.getfirst('group') or '').strip()
    try:
        limit = max(1, min(MAX_LIMIT, int(form.getfirst('limit') or DEFAULT_LIMIT)))
    except ValueError:
        limit = DEFAULT_LIMIT

    if not site or not group:
        print(json.dumps({'success': False, 'error': 'site and group are required'}))
        return

    pods = _group_pods(site, group)
    if not pods:
        print(json.dumps({'success': False,
                          'error': f'No pods found for {site} / {group}'}))
        return

    actions = []
    for path in (POWERCONTROL_LOG, POWERCONTROL_LOG_PREV):
        for line in reversed(_tail_lines(path)):     # newest first
            line = line.strip()
            if not line:
                continue
            row = _parse(line)
            if row and row['pod'] in pods:
                actions.append(row)
                if len(actions) >= limit:
                    break
        if len(actions) >= limit:
            break

    print(json.dumps({'success': True, 'actions': actions,
                      'pods': sorted(pods)}))


if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        # CGI must always emit a parseable body; the modal shows `error` inline.
        print(json.dumps({'success': False, 'error': f'{type(e).__name__}: {e}'}))
