#!/usr/bin/python3

import os
import sys
import json
import csv
from datetime import datetime

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

print("Content-Type: application/json\n")

# Shared position-grid color logic — single source of truth in
# apps/miners/miner_grid.py, also used by status_overview_grid_cgi.py so the
# sites-overview pixel map can never drift from this click-into-pod view.
from miners.miner_grid import (
    load_inventory, load_miners, load_sleeping_miners, get_miner_color,
)


def get_ever_hashed_macs(macs):
    """Return the subset of `macs` that have EVER recorded hs_rt > 0, via the
    maintained miner_lifecycle.ever_hashed flag (instant indexed lookup — the
    flag is set monotonically by get_hashrate_and_status.py on each snapshot).

    MACs are normalized to uppercase (how the DB stores them). On ANY DB error
    we fail safe by returning the full input set (treat all as having hashed),
    so a DB hiccup never floods the problem list with false "never hashed" flags."""
    macs = {m.upper() for m in macs if m}
    if not macs:
        return set()
    try:
        import sqlite3
        conn = sqlite3.connect(
            'file:/opt/ngon/data/miner_history.db?mode=ro', uri=True, timeout=5)
        cur = conn.cursor()
        ever = set()
        mac_list = list(macs)
        for i in range(0, len(mac_list), 500):
            chunk = mac_list[i:i + 500]
            ph = ','.join('?' * len(chunk))
            cur.execute(
                f"SELECT mac FROM miner_lifecycle WHERE ever_hashed = 1 AND mac IN ({ph})",
                chunk)
            ever.update(r[0].upper() for r in cur.fetchall() if r[0])
        conn.close()
        return ever
    except Exception as e:
        print(f"WARNING: ever-hashed lookup failed ({e}); treating all as hashed",
              file=sys.stderr)
        return set(macs)

def generate_pod_grid_data(pod_key, inventory, miners, sleeping_miners, miner_type_specs, ever_hashed_macs=None):
    """Generate data structure for a single pod's miner grid"""
    # Find all rows and positions for this pod
    all_rows = set()
    all_positions = set()
    

    if pod_key in inventory:
        for row_num in inventory[pod_key]:
            all_rows.add(row_num)
            for pos_num in inventory[pod_key][row_num]:
                all_positions.add(pos_num)

    if pod_key in miners:
        for row_num in miners[pod_key]:
            all_rows.add(row_num)
            for pos_num in miners[pod_key][row_num]:
                all_positions.add(pos_num)

    if not all_rows or not all_positions:
        return {'rows': [], 'message': 'No data'}

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

    grid_data = {
        'rows': [],
        'min_row': min_row,
        'max_row': max_row,
        'min_pos': min_pos,
        'max_pos': max_pos
    }

    for row in range(min_row, max_row + 1):
        row_data = {'row_num': row, 'positions': []}
        
        for pos in range(min_pos, max_pos + 1):
            color = get_miner_color(pod_key, row, pos, inventory, miners, sleeping_miners, miner_type_specs)
            
            position_data = {
                'pos_num': pos,
                'color': color,
                'row': row,
                'pos': pos,
            }

            # Add detailed miner info if available
            if (pod_key in miners and row in miners[pod_key] and pos in miners[pod_key][row]):
                miner = miners[pod_key][row][pos]
                mac = miner.get('mac', '').lower()
                is_sleeping = mac in sleeping_miners
                
                # Get serial from inventory if available
                serial = ''
                if (pod_key in inventory and row in inventory[pod_key] and pos in inventory[pod_key][row]):
                    serial = inventory[pod_key][row][pos]['serial']
                
                # "Never hashed" = no hs_rt>0 ever recorded for this MAC. A miner
                # that is hashing RIGHT NOW counts as having hashed even if the
                # 30-min history snapshot hasn't captured it yet.
                never_hashed = bool(ever_hashed_macs is not None and mac
                                    and mac.upper() not in ever_hashed_macs)
                if never_hashed and (miner.get('hashrate') or 0) > 0:
                    never_hashed = False

                position_data.update({
                    'miner_type': miner['miner_type'],
                    'hashrate': miner['hashrate'],
                    'power': miner['power'],
                    'error_codes': miner['error_codes'],
                    'temp': miner['temp'],
                    'fan_speed': miner['fan_speed'],
                    'uptime': miner['uptime'],
                    'mac': mac,
                    'serial': serial,
                    'is_sleeping': is_sleeping,
                    'never_hashed': never_hashed,
                    'status': 'online'
                })
            elif (pod_key in inventory and row in inventory[pod_key] and pos in inventory[pod_key][row]):
                inventory_data = inventory[pod_key][row][pos]
                omac = inventory_data['mac'] or ''
                # Offline + never-hashed still flags as Never Hashed (it wins).
                never_hashed = bool(ever_hashed_macs is not None and inventory_data['installed']
                                    and omac and omac.upper() not in ever_hashed_macs)
                position_data.update({
                    'serial': inventory_data['serial'],
                    'mac': inventory_data['mac'],
                    'never_hashed': never_hashed,
                    'status': 'offline' if inventory_data['installed'] else 'empty'
                })
            else:
                position_data['status'] = 'empty'
                
            row_data['positions'].append(position_data)
        
        grid_data['rows'].append(row_data)

    return grid_data

def load_all_miners_by_pod():
    """Load all miners grouped by pod field from miner_status.csv.
    Returns dict of pod_name -> list of miner dicts (includes miners not in inventory)."""
    pods = {}
    try:
        with open('/opt/ngon/data/live/miner_status.csv', 'r') as f:
            reader = csv.DictReader(f)
            for row in reader:
                pod = row.get('pod', '').strip()
                if not pod:
                    continue
                if pod not in pods:
                    pods[pod] = []
                pods[pod].append({
                    'mac': row.get('mac', '').strip().lower(),
                    'hashrate': float(row.get('hs_rt', 0) or 0),
                    'power': float(row.get('power', 0) or 0),
                    'miner_type': row.get('miner_type', ''),
                    'error_codes': row.get('error_codes', '').strip(),
                    'mining_state': row.get('mining_state', '').strip().lower(),
                })
    except Exception as e:
        print(f"Error loading all miners by pod: {e}", file=sys.stderr)
    return pods

def calculate_pod_hashrate(pod_key, inventory, miners, all_miners_by_pod=None):
    """Calculate total hashrate for a pod.
    Uses all_miners_by_pod (from pod field) for accurate totals that include
    miners not in inventory. Falls back to inventory-matched miners only."""
    if all_miners_by_pod and pod_key in all_miners_by_pod:
        return sum(m['hashrate'] for m in all_miners_by_pod[pod_key])
    # Fallback to inventory-matched miners
    total = 0
    if pod_key in miners:
        for row_num in miners[pod_key]:
            for pos_num in miners[pod_key][row_num]:
                total += miners[pod_key][row_num][pos_num]['hashrate']
    return total

def count_unpositioned_miners(pod_key, miners, all_miners_by_pod):
    """Count miners in a pod that aren't in the inventory grid."""
    if not all_miners_by_pod or pod_key not in all_miners_by_pod:
        return 0, 0.0
    # Get MACs that are positioned in the grid
    positioned_macs = set()
    if pod_key in miners:
        for row_num in miners[pod_key]:
            for pos_num in miners[pod_key][row_num]:
                mac = miners[pod_key][row_num][pos_num].get('mac', '').lower()
                if mac:
                    positioned_macs.add(mac)
    # Count unpositioned
    count = 0
    hashrate = 0.0
    for m in all_miners_by_pod[pod_key]:
        if m['mac'] and m['mac'] not in positioned_macs:
            count += 1
            hashrate += m['hashrate']
    return count, hashrate

def load_live_gen_index():
    """{unit_id: gen} from status_api, for gens the Mesa feed doesn't carry.

    status_api is the authority the rest of the status page renders from, so
    asking it here keeps this modal agreeing with the generator columns instead
    of quietly disagreeing. Best-effort by design: on any failure we return {}
    and resolve_generator() still emits the gen, just without a run status. A
    generator that is missing is a bug; one showing an unknown state is not.
    """
    try:
        import urllib.request
        with urllib.request.urlopen("http://localhost:5050/api/status", timeout=2) as r:
            data = json.loads(r.read().decode())
    except Exception:
        return {}
    out = {}
    for site in (data.get("sites") or {}).values():
        for group in (site.get("generator_groups") or {}).values():
            for gen_id, gen in (group.get("generators") or {}).items():
                out[gen_id] = gen
    return out


def resolve_generator(gen_id, mesa_by_unit, live_by_unit):
    """Always return a renderable generator entry for a configured gen.

    Prefers the full Mesa record when there is one. Otherwise synthesises the
    two fields the modal actually reads (unit_id, run_status) from status_api.
    Never returns None: if a gen is mapped to a pod in master_config it belongs
    on screen, and an unknown-state dot beats a missing generator.

    run_status here is the non-live branch of the frontend's
    genEffectiveRunStatus() -- status_api's `status`, i.e. what Mesa last said.
    The live-telemetry branch is deliberately NOT reimplemented: duplicating that
    freshness rule in a second place is how the two drift apart. A gen running on
    our own DSE feed with no Mesa record would read offline here; no gen is in
    that state today, and the modal's dot is binary anyway.
    """
    gen_data = mesa_by_unit.get(gen_id)
    if gen_data:
        return gen_data
    live = live_by_unit.get(gen_id) or {}
    return {
        "unit_id": gen_id,
        "run_status": live.get("status"),
        "no_provider_data": True,   # consumers can flag "not in the Mesa feed"
    }


def get_site_detail_data(site_name):
    """Get comprehensive site detail data for modal display"""
    try:
        # Load all data
        inventory, mac_to_location = load_inventory()
        miners = load_miners(mac_to_location)
        sleeping_miners = load_sleeping_miners()
        all_miners_by_pod = load_all_miners_by_pod()

        # Load miner type specs
        try:
            miner_type_specs = config.get_miner_type_specs()
        except:
            miner_type_specs = {}

        # Get site pods and organize data
        site_pods = []
        # Look for pods that start with site name or match exactly
        all_pods = config.get_pod_miner_counts().keys()
        site_pods = [pod for pod in all_pods if pod.startswith(site_name + " ") or pod == site_name]

        # One history lookup for the whole site: which of these MACs have ever hashed.
        # Drives the "Never Hashed" problem-list bucket (status_site_modal.js).
        site_macs = set()
        for pk in site_pods:
            if pk in inventory:
                for r in inventory[pk]:
                    for p in inventory[pk][r]:
                        m = inventory[pk][r][p].get('mac')
                        if m:
                            site_macs.add(m)
            if pk in miners:
                for r in miners[pk]:
                    for p in miners[pk][r]:
                        m = miners[pk][r][p].get('mac')
                        if m:
                            site_macs.add(m)
        ever_hashed_macs = get_ever_hashed_macs(site_macs)


        # Get generator groups and pod-to-generator mapping
        try:
            generator_groups = config.get_generator_groups()
            pod_to_gens = config.get_generator_to_pod_mapping()
        except:
            generator_groups = {}
            pod_to_gens = {}

        # Load generator data. This file is the raw MESA PROVIDER FEED, so it only
        # contains units Mesa knows about — 128 of the fleet's 166 at the time of
        # writing. Gens absent from it (a new site, a unit Mesa has not picked up,
        # anything parked out of service) used to be dropped silently by the
        # `if gen_data:` guards below, so a whole site's generators could vanish
        # from this modal while showing correctly everywhere else on the page.
        # See resolve_generator() for the fallback.
        generator_data = []
        generator_data_path = "/opt/ngon/data/live/generator_data.json"
        if os.path.exists(generator_data_path):
            with open(generator_data_path, 'r') as f:
                generator_data = json.load(f)
        mesa_by_unit = {g.get('unit_id'): g for g in generator_data if g.get('unit_id')}
        live_by_unit = load_live_gen_index()

        # Create site detail structure
        site_detail = {
            'site_name': site_name,
            'total_hashrate': 0,
            'pod_groups': [],
            'standalone_pods': [],
            'generators': {}
        }

        # Calculate total site hashrate (uses all miners by pod field for accuracy)
        for pod_key in site_pods:
            pod_hashrate = calculate_pod_hashrate(pod_key, inventory, miners, all_miners_by_pod)
            site_detail['total_hashrate'] += pod_hashrate
        
        # Organize pods by generator groups
        grouped_pods = set()
        
        # Process grouped pods
        for group_name, pod_names in generator_groups.items():
            # Handle both prefixed and unprefixed group names
            if group_name.startswith(site_name + "_"):  # e.g., "Alpha_Alpha 1-2"
                group_display_name = group_name.split("_", 1)[1]  # Extract "Alpha 1-2" from "Alpha_Alpha 1-2"
            elif group_name == site_name or (site_name in ["Out of Service", "Spares"] and group_name in ["Out of Service", "Available Spares"]):
                # Handle special sites without prefixes
                group_display_name = group_name
            else:
                # Skip groups that don't belong to this site
                continue
                
            # Find pods in this site that belong to this group
            pods_in_group = [pod for pod in pod_names if pod in site_pods]
                
            if pods_in_group:
                group_data = {
                    'group_name': group_display_name,
                    'pods': []
                }
                
                for pod_key in pods_in_group:
                    pod_hashrate = calculate_pod_hashrate(pod_key, inventory, miners, all_miners_by_pod)
                    pod_grid = generate_pod_grid_data(pod_key, inventory, miners, sleeping_miners, miner_type_specs, ever_hashed_macs)
                    unpos_count, unpos_hashrate = count_unpositioned_miners(pod_key, miners, all_miners_by_pod)

                    pod_entry = {
                        'pod_key': pod_key,
                        'hashrate': pod_hashrate,
                        'grid_data': pod_grid
                    }
                    if unpos_count > 0:
                        pod_entry['unpositioned_count'] = unpos_count
                        pod_entry['unpositioned_hashrate'] = unpos_hashrate
                    group_data['pods'].append(pod_entry)
                    
                    grouped_pods.add(pod_key)
                
                # Get generators for this group (maintain order from first pod)
                group_generators = []
                seen_gen_ids = set()
                
                # Use the first pod's generator order as the canonical order
                for pod_key in pods_in_group:
                    if pod_key in pod_to_gens:
                        # Process all generators from this pod in order
                        for gen_id in pod_to_gens[pod_key]:
                            if gen_id not in seen_gen_ids:
                                group_generators.append(
                                    resolve_generator(gen_id, mesa_by_unit, live_by_unit))
                                seen_gen_ids.add(gen_id)
                        # Once we have generators from the first pod, we have the order
                        # Additional pods in the group should have the same generators
                        break
                
                group_data['generators'] = group_generators
                site_detail['pod_groups'].append(group_data)
        
        # Process standalone pods
        for pod_key in site_pods:
            if pod_key not in grouped_pods:
                pod_hashrate = calculate_pod_hashrate(pod_key, inventory, miners, all_miners_by_pod)
                pod_grid = generate_pod_grid_data(pod_key, inventory, miners, sleeping_miners, miner_type_specs, ever_hashed_macs)
                unpos_count, unpos_hashrate = count_unpositioned_miners(pod_key, miners, all_miners_by_pod)

                # Get generators for this pod
                pod_generators = []
                if pod_key in pod_to_gens:
                    for gen_id in pod_to_gens[pod_key]:
                        pod_generators.append(
                            resolve_generator(gen_id, mesa_by_unit, live_by_unit))

                pod_entry = {
                    'pod_key': pod_key,
                    'hashrate': pod_hashrate,
                    'grid_data': pod_grid,
                    'generators': pod_generators
                }
                if unpos_count > 0:
                    pod_entry['unpositioned_count'] = unpos_count
                    pod_entry['unpositioned_hashrate'] = unpos_hashrate
                site_detail['standalone_pods'].append(pod_entry)
        
        # Add timestamp
        site_detail['updated_at'] = datetime.now().strftime('%m/%d %H:%M')
        
        return site_detail
        
    except Exception as e:
        return {
            'error': str(e),
            'site_name': site_name,
            'updated_at': datetime.now().strftime('%m/%d %H:%M:%S')
        }

# Main execution
try:
    # Get site name from query parameters
    import urllib.parse
    query_string = os.environ.get('QUERY_STRING', '')
    params = urllib.parse.parse_qs(query_string)
    site_name = params.get('site', [''])[0]
    
    if not site_name:
        data = {"error": "Missing site parameter"}
    else:
        data = get_site_detail_data(site_name)
    
    print(json.dumps(data, indent=2))
    
except Exception as e:
    print(json.dumps({"error": str(e)}, indent=2))