#!/usr/bin/env bash
set -euo pipefail

# ODS Hardware Classifier — Two-pass GPU matching
# Pass 1: Match known_gpus by device_id then name_patterns (gpu-database.json)
# Pass 2: Fall back to heuristic_classes (threshold-based, same as old hardware-classes.json)
#
# Accepts both old args (--platform-id, --gpu-vendor) and new args (--device-id, --gpu-name, --ram-mb)
# Output contract: HW_CLASS_ID, HW_CLASS_LABEL, HW_REC_BACKEND, HW_REC_TIER,
#                  HW_REC_COMPOSE_OVERLAYS, HW_BANDWIDTH_GBPS, HW_MEMORY_SOURCE, HW_GPU_LABEL

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
GPU_DB="${ROOT_DIR}/config/gpu-database.json"
ENV_MODE="false"
PLATFORM_ID="${PLATFORM_ID:-unknown}"
GPU_VENDOR="${GPU_VENDOR:-unknown}"
MEMORY_TYPE="${MEMORY_TYPE:-unknown}"
VRAM_MB="${VRAM_MB:-0}"
DEVICE_ID=""
GPU_NAME=""
CPU_NAME=""
RAM_MB="0"

while [[ $# -gt 0 ]]; do
    case "$1" in
        --platform-id) PLATFORM_ID="${2:-$PLATFORM_ID}"; shift 2 ;;
        --gpu-vendor)  GPU_VENDOR="${2:-$GPU_VENDOR}"; shift 2 ;;
        --memory-type) MEMORY_TYPE="${2:-$MEMORY_TYPE}"; shift 2 ;;
        --vram-mb)     VRAM_MB="${2:-$VRAM_MB}"; shift 2 ;;
        --device-id)   DEVICE_ID="${2:-}"; shift 2 ;;
        --gpu-name)    GPU_NAME="${2:-}"; shift 2 ;;
        --cpu-name)    CPU_NAME="${2:-}"; shift 2 ;;
        --ram-mb)      RAM_MB="${2:-0}"; shift 2 ;;
        --env)         ENV_MODE="true"; shift ;;
        --db)          GPU_DB="${2:-$GPU_DB}"; shift 2 ;;
        *)
            echo "Unknown argument: $1" >&2
            exit 1
            ;;
    esac
done

if [[ ! -f "$GPU_DB" ]]; then
    echo "ERROR: GPU database not found: $GPU_DB" >&2
    exit 1
fi

PYTHON_CMD="python3"
if [[ -f "$ROOT_DIR/lib/python-cmd.sh" ]]; then
    . "$ROOT_DIR/lib/python-cmd.sh"
    PYTHON_CMD="$(ods_detect_python_cmd)"
elif command -v python >/dev/null 2>&1; then
    PYTHON_CMD="python"
fi

"$PYTHON_CMD" - "$GPU_DB" "$ENV_MODE" "$PLATFORM_ID" "$GPU_VENDOR" "$MEMORY_TYPE" "$VRAM_MB" "$DEVICE_ID" "$GPU_NAME" "$CPU_NAME" "$RAM_MB" <<'PY'
import json
import sys

db_path = sys.argv[1]
env_mode = sys.argv[2] == "true"
platform_id = sys.argv[3]
gpu_vendor = sys.argv[4]
memory_type = sys.argv[5]
vram_mb = int(float(sys.argv[6] or 0))
device_id = sys.argv[7]
gpu_name = sys.argv[8]
cpu_name = sys.argv[9]
ram_mb = int(float(sys.argv[10] or 0))

with open(db_path, "r", encoding="utf-8") as f:
    db = json.load(f)

# --- Compose overlay mapping (backend → default overlays) ---
# CPU backend uses cpu overlay: CPU-only llama.cpp image, no GPU reservation
OVERLAY_MAP = {
    "amd":    ["docker-compose.base.yml", "docker-compose.amd.yml"],
    "nvidia": ["docker-compose.base.yml", "docker-compose.nvidia.yml"],
    "apple":  ["docker-compose.base.yml", "docker-compose.apple.yml"],
    "cpu":    ["docker-compose.base.yml", "docker-compose.cpu.yml"],
}

# --- Pass 1: Match known_gpus by device_id then name_patterns ---
selected = None
best_name_len = 0          # longest matching pattern wins (prevents "XT" matching "XTX")
best_id_vram_diff = None   # closest VRAM wins for device_id-only fallback
combined_name = f"{gpu_name} {cpu_name}".strip().lower()

for entry in db.get("known_gpus", []):
    match = entry.get("match", {})

    # Try device_id match (exact, most reliable)
    dev_ids = [d.lower() for d in match.get("device_ids", [])]
    id_matched = device_id.lower() in dev_ids if device_id else False

    # Try name_patterns match (case-insensitive substring against gpu_name + cpu_name)
    patterns = match.get("name_patterns", [])
    matched_patterns = [p for p in patterns if p.lower() in combined_name] if combined_name and patterns else []
    name_matched = len(matched_patterns) > 0
    match_len = max((len(p) for p in matched_patterns), default=0)

    if id_matched and name_matched:
        # Both match — prefer longest pattern to avoid "XT" matching "XTX"
        if match_len > best_name_len:
            selected = entry
            best_name_len = match_len
    elif id_matched and best_name_len == 0:
        # Device ID matched but name didn't — use VRAM proximity as tiebreaker
        entry_vram = entry.get("specs", {}).get("memory_mb", 0)
        if vram_mb > 0:
            diff = abs(entry_vram - vram_mb)
        else:
            # No VRAM info: prefer smallest card (under-provision is safe,
            # over-provision crashes the model loader)
            diff = entry_vram if entry_vram > 0 else float("inf")
        if best_id_vram_diff is None or diff < best_id_vram_diff:
            selected = entry
            best_id_vram_diff = diff
    elif name_matched and not selected:
        selected = entry
        best_name_len = match_len

# --- Pass 2: Heuristic fallback (threshold-based, top-down) ---
if not selected:
    for entry in db.get("heuristic_classes", []):
        match = entry.get("match", {})

        # Check vendor
        m_vendor = match.get("vendor", "")
        if m_vendor and m_vendor != gpu_vendor:
            continue

        # Check memory_type
        m_memtype = match.get("memory_type", "")
        if m_memtype and m_memtype != memory_type:
            continue

        # Check min_vram_mb
        min_vram = match.get("min_vram_mb", -1)
        if min_vram >= 0 and vram_mb < min_vram:
            continue

        # Check max_vram_mb. This lets the database model "too small for
        # GPU inference" classes explicitly instead of catching them in broad
        # vendor fallbacks.
        max_vram = match.get("max_vram_mb", -1)
        if max_vram >= 0 and vram_mb > max_vram:
            continue

        # Check min_ram_mb (for unified memory classes)
        min_ram = match.get("min_ram_mb", -1)
        if min_ram >= 0 and ram_mb < min_ram:
            continue

        selected = entry
        break

# --- Bandwidth lookup ---
bandwidth = 0
if selected and "specs" in selected:
    bandwidth = selected["specs"].get("bandwidth_gbps", 0)

if bandwidth == 0 and gpu_name:
    # Search bandwidth table by substring match
    vendor_bw = db.get("known_gpu_bandwidth", {}).get(gpu_vendor, {})
    for bw_name, bw_val in vendor_bw.items():
        if bw_name.lower() in gpu_name.lower() or bw_name.lower() in cpu_name.lower():
            bandwidth = bw_val
            break

if bandwidth == 0:
    # Fall back to default bandwidth
    backend_key_map = {"nvidia": "cuda", "amd": "rocm", "apple": "metal"}
    bk = backend_key_map.get(gpu_vendor, "cpu_x86")
    bandwidth = db.get("defaults", {}).get("bandwidth_gbps", {}).get(bk, 0)

# --- Build result ---
if selected:
    # Known GPU entry
    if "specs" in selected:
        class_id = selected.get("id", "unknown")
        label = selected["specs"].get("label", selected.get("id", "Unknown"))
        rec = selected.get("recommended", {})
        backend = rec.get("backend", "cpu")
        tier = rec.get("tier", "T1")
        memory_source = selected["specs"].get("memory_source", "vram")
    else:
        # Heuristic class entry
        class_id = selected.get("id", "unknown")
        label = selected.get("id", "Unknown").replace("_", " ").title()
        rec = selected.get("recommended", {})
        backend = rec.get("backend", "cpu")
        tier = rec.get("tier", "T1")
        m_memtype = selected.get("match", {}).get("memory_type", "")
        memory_source = "ram" if backend == "cpu" or m_memtype == "unified" else "vram"
else:
    class_id = "unknown"
    label = "Unknown"
    backend = "cpu"
    tier = "T1"
    memory_source = "vram"

overlays = OVERLAY_MAP.get(backend, ["docker-compose.base.yml"])
# Darwin hosts running the apple backend use the canonical macOS overlay
# (installers/macos/docker-compose.macos.yml). The OVERLAY_MAP entry for
# "apple" still lists docker-compose.apple.yml so Linux hosts selecting
# --gpu-backend apple (Lemonade) continue to get the top-level overlay.
if backend == "apple" and platform_id == "macos":
    overlays = ["docker-compose.base.yml", "installers/macos/docker-compose.macos.yml"]
gpu_label = selected["specs"].get("label", "") if selected and "specs" in selected else ""

# --- Output ---
def out(key, value):
    safe = str(value).replace("\\", "\\\\").replace('"', '\\"')
    print(f'{key}="{safe}"')

if env_mode:
    out("HW_CLASS_ID", class_id)
    out("HW_CLASS_LABEL", label)
    out("HW_REC_BACKEND", backend)
    out("HW_REC_TIER", tier)
    out("HW_REC_COMPOSE_OVERLAYS", ",".join(overlays))
    out("HW_BANDWIDTH_GBPS", bandwidth)
    out("HW_MEMORY_SOURCE", memory_source)
    out("HW_GPU_LABEL", gpu_label)
else:
    result = {
        "id": class_id,
        "label": label,
        "recommended": {
            "backend": backend,
            "tier": tier,
            "compose_overlays": overlays,
        },
        "bandwidth_gbps": bandwidth,
        "memory_source": memory_source,
        "gpu_label": gpu_label,
    }
    print(json.dumps(result, indent=2))
PY
