#!/usr/bin/python3
"""Sites-overview pixel-map data endpoint.

Computes every pod's per-position color grid in ONE pass over the inventory +
live miner status, and returns colors only (no serials/temps/tooltips). Drives
the pixel-per-miner blocks on the status page sites overview.

Reuses the exact color logic from apps/miners/miner_grid.py, so the overview and
the click-into-pod detail view can never drift apart.

Kept OFF the websocket / listen_miner_data hot path deliberately (same
separation-of-concerns reasoning as flow_api / gen_ingest): overview display
grids are a page concern and must not be able to stall the load-bearing miner
listener. The page polls this ~every 60s.

Response:
  {
    "updated_at": "07/22 07:45",
    "pods": { "GN 1": {"rows": ["gg.yr...", ...]}, ... }
  }
Grid chars: g=full hash, y=low hash, r=zero hash, b=sleeping, w=offline,
p=dead board (pull), .=not installed. Error variants collapse to their base
color (a 1px cell can't carry the diagonal error stripe the modal shows).
"""

import os
import sys
import json
import time
from datetime import datetime

sys.path.insert(0, '/opt/ngon/apps')
from managers.config_manager import config
from miners.miner_grid import (
    load_inventory, load_miners, load_sleeping_miners, get_miner_color,
)

# Short-TTL cache on the ramdisk so concurrent dashboards share one computation
# (each open page polls ~60s; the TTL keeps N viewers from each doing the work).
CACHE_FILE = '/opt/ngon/data/live/overview_grid_cache.json'
CACHE_TTL = 20  # seconds

# CGI color name -> compact single char. Error variants fold to the base color.
COLOR_CHAR = {
    'green': 'g', 'green-error': 'g',
    'yellow': 'y', 'yellow-error': 'y',
    'red': 'r', 'red-error': 'r',
    'blue': 'b', 'blue-error': 'b',
    'purple': 'p', 'purple-error': 'p',
    'white': 'w',
    'empty': '.',
}


def build_pod_color_grid(pod_key, inventory, miners, sleeping_miners, specs):
    """Return {'rows': [str, ...]} of compact color chars for one pod, laid out
    on the real physical grid (row = y, position = x). None if the pod has no
    inventory/live footprint."""
    all_rows, all_pos = set(), set()
    if pod_key in inventory:
        for r in inventory[pod_key]:
            all_rows.add(r)
            all_pos.update(inventory[pod_key][r].keys())
    if pod_key in miners:
        for r in miners[pod_key]:
            all_rows.add(r)
            all_pos.update(miners[pod_key][r].keys())

    if not all_rows or not all_pos:
        return None

    min_row, max_row = min(all_rows), max(all_rows)
    min_pos, max_pos = min(all_pos), max(all_pos)

    rows = []
    for row in range(min_row, max_row + 1):
        chars = []
        for pos in range(min_pos, max_pos + 1):
            color = get_miner_color(pod_key, row, pos, inventory, miners,
                                    sleeping_miners, specs)
            chars.append(COLOR_CHAR.get(color, '.'))
        rows.append(''.join(chars))
    return {'rows': rows}


def compute_all_grids():
    inventory, mac_to_location = load_inventory()
    miners = load_miners(mac_to_location)
    sleeping_miners = load_sleeping_miners()
    try:
        specs = config.get_miner_type_specs()
    except Exception:
        specs = {}

    pods = {}
    for pod_key in config.get_pod_miner_counts().keys():
        grid = build_pod_color_grid(pod_key, inventory, miners, sleeping_miners, specs)
        if grid is not None:
            pods[pod_key] = grid

    return {'updated_at': datetime.now().strftime('%m/%d %H:%M'), 'pods': pods}


def get_cached_or_compute():
    try:
        age = time.time() - os.path.getmtime(CACHE_FILE)
        if age < CACHE_TTL:
            with open(CACHE_FILE, 'r') as f:
                return f.read()
    except (OSError, FileNotFoundError):
        pass

    payload = json.dumps(compute_all_grids())
    try:
        # Atomic-ish write so a concurrent reader never sees a half-written file.
        tmp = CACHE_FILE + '.tmp'
        with open(tmp, 'w') as f:
            f.write(payload)
        os.replace(tmp, CACHE_FILE)
    except OSError as e:
        print(f"WARNING: could not write overview grid cache: {e}", file=sys.stderr)
    return payload


print("Content-Type: application/json\n")
try:
    print(get_cached_or_compute())
except Exception as e:
    print(json.dumps({'error': str(e), 'pods': {}}))
