#!/bin/bash
#
# rNitro installer — hardened
#
# v3.1.3 — refresh rate changed to 900ms; added a ₿ symbol to the left of
# the menu bar CPU readout (history buffer resized to 67 points to keep a
# ~60-second graph window at the new tick rate)
#
# v3.1.2b — stats now refresh every 500ms instead of every 1s, for a
# snappier-feeling menu bar, overlay HUD, and history graph (graph buffer
# doubled to 120 points so it still spans 60 seconds)
#
# v3.1.2a — version bump
#
# v3.1.2 — menu bar now shows "CPU: X%  Temp: Y°" instead of just a
# percentage, so usage and temperature are both visible at a glance
#
# v3.1.1b — Patched major bug
#
# v3.1.1a — version bump + added update checker (compares against
# version.json on rnitro.netlify.app at every launch; alerts and opens the
# site in the default browser if a newer version is published)
#
# v2.1.0 changelog:
#   - Added in-game overlay HUD (CPU%, GPU%, temp, RAM) — toggle with ⌥⇧O,
#     stays on top of fullscreen games, click-through, no input stolen
#   - Added "Launch App with FPS HUD" — runs a Metal-based game with Apple's
#     native Metal HUD enabled for real, engine-reported FPS/frame time
#   - Added real GPU usage (read from IOKit accelerator stats) and real RAM
#     usage (read from host VM statistics) — no synthetic estimates
#
# v2.0.1a changelog:
#   - Fixed minor temperature bug (gauge no longer sits flat; now varies
#     smoothly with CPU usage within each macOS thermal-state band)
#
set -Eeuo pipefail
IFS=$'\n\t'
umask 077

echo "🚀 rNitro Installer"
echo "-------------------"

# ── Security: refuse to run via a pipe (curl|bash) ───────────────────────────
# $0 is unreliable when the script is streamed into bash rather than saved to
# disk first. Force the user to download and run it as a real file so the
# integrity check below actually means something.
if [[ ! -f "$0" ]]; then
  echo "❌ This script must be saved to disk and run directly (e.g. \`bash install-rNitro.sh\`)."
  echo "   Do not run it via 'curl ... | bash'."
  exit 1
fi

# ── Security: macOS only ─────────────────────────────────────────────────────
if [[ "$(uname)" != "Darwin" ]]; then
  echo "❌ rNitro is macOS only. Aborting."
  exit 1
fi

# ── Security: must be Apple Silicon ──────────────────────────────────────────
if [[ "$(uname -m)" != "arm64" ]]; then
  echo "❌ rNitro requires Apple Silicon (M1/M2/M3). Aborting."
  exit 1
fi

# ── Security: must not be run as root ────────────────────────────────────────
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
  echo "❌ Do not run this installer as root or with sudo. Aborting."
  exit 1
fi

# ── Security: required tools must exist before we trust/use them ────────────
for bin in shasum xcode-select swiftc swift codesign open mktemp sips iconutil; do
  if ! command -v "$bin" >/dev/null 2>&1; then
    echo "❌ Required tool '$bin' not found on this system. Aborting."
    exit 1
  fi
done

# ── Security: HOME must be a sane, existing directory ────────────────────────
if [[ -z "${HOME:-}" || ! -d "$HOME" ]]; then
  echo "❌ \$HOME is not set to a valid directory. Aborting."
  exit 1
fi

# ── Security: verify script integrity (SHA-256) ───────────────────────────────
# This hash is the canonical checksum published at https://rnitro.netlify.app/
# If this check fails, your copy of the installer has been modified.
#
# Note on how this hash is computed: a script can't embed the hash of its own
# unmodified bytes (changing the EXPECTED_HASH value changes the hash). To
# break that circularity, the EXPECTED_HASH line itself is masked out before
# hashing — the published hash on the site is generated the same way, so it
# stays stable regardless of what value is plugged in here.
EXPECTED_HASH="e87270327b0adb0723a080e2e63e00ab44495a6c51aa0e88180bcfaaa7b7d595"
ACTUAL_HASH="$(sed 's/^EXPECTED_HASH=.*/EXPECTED_HASH="MASKED"/' "$0" | shasum -a 256 | awk '{print $1}')"
if [[ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]]; then
  echo "❌ Integrity check failed. This file may have been tampered with."
  echo "   Expected: $EXPECTED_HASH"
  echo "   Got:      $ACTUAL_HASH"
  echo "   Download a fresh copy from https://rnitro.netlify.app/"
  exit 1
fi
echo "✅ Integrity check passed."

# ── Security: verify Xcode CLT is present (don't install unknown toolchains) ─
if ! xcode-select -p &>/dev/null; then
  echo "❌ Xcode Command Line Tools not found."
  echo "   Run: xcode-select --install"
  echo "   Then re-run this installer."
  exit 1
fi

echo "✅ All checks passed."
echo ""

# ── Security: build in a private, randomized temp dir instead of a
#    predictable path under Downloads. Using mktemp avoids symlink/race
#    attacks where another local user could pre-create or swap the directory.
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/rnitro-build.XXXXXXXX")"
APP_DEST="$HOME/Applications/rNitro.app"

# ── Security: always clean up the build dir, even on failure/interrupt ───────
cleanup() { rm -rf -- "$WORK_DIR"; }
trap cleanup EXIT INT TERM

# Restrict the build dir to the current user only.
chmod 700 "$WORK_DIR"

# ── Write main.swift ──────────────────────────────────────────────────────────
cat > "$WORK_DIR/main.swift" << 'SWIFTEOF'
import Cocoa
import SwiftUI
import IOKit
import Combine

// ── Update check ────────────────────────────────────────────────────────────
// This build's version (kept in sync with CFBundleShortVersionString below).
// Compared against https://rnitro.netlify.app/version.json on every launch.
let CURRENT_VERSION = "3.1.3"
let UPDATE_CHECK_URL = URL(string: "https://rnitro.netlify.app/version.json")!
let UPDATE_PAGE_URL  = URL(string: "https://rnitro.netlify.app")!

struct VersionInfo: Decodable { let latest: String }

enum UpdateChecker {
    // Runs once per launch. If the site's published "latest" version differs
    // from this build's CURRENT_VERSION, shows a blocking alert and opens the
    // download page in the user's default browser.
    static func checkOnLaunch() {
        var req = URLRequest(url: UPDATE_CHECK_URL)
        req.cachePolicy = .reloadIgnoringLocalCacheData
        req.timeoutInterval = 8
        URLSession.shared.dataTask(with: req) { data, _, error in
            guard error == nil, let data = data,
                  let info = try? JSONDecoder().decode(VersionInfo.self, from: data) else { return }
            guard info.latest != CURRENT_VERSION else { return }
            DispatchQueue.main.async {
                NSApp.activate(ignoringOtherApps: true)
                let alert = NSAlert()
                alert.messageText = "rNitro Update Available"
                alert.informativeText = "You're running v\(CURRENT_VERSION). The latest version is v\(info.latest). Opening rnitro.netlify.app so you can update."
                alert.alertStyle = .warning
                alert.addButton(withTitle: "OK")
                alert.runModal()
                NSWorkspace.shared.open(UPDATE_PAGE_URL)
            }
        }.resume()
    }
}

struct CoreInfo: Identifiable {
    let id: Int
    var usage: Double
    var clockMHz: Double
}

class CPUMonitor: ObservableObject {
    static let shared = CPUMonitor()

    @Published var totalUsage: Double = 0
    @Published var temperature: Double = 0
    @Published var thermalState: ProcessInfo.ThermalState = .nominal
    @Published var baseClock: Double = 0
    @Published var boostClock: Double = 0
    @Published var cores: [CoreInfo] = []
    @Published var usageHistory: [Double] = Array(repeating: 0, count: 67) // ~60s at 900ms/tick
    @Published var cpuName: String = "Apple CPU"
    @Published var physicalCores: Int = 0
    @Published var logicalCores: Int = 0
    @Published var memoryUsedGB: Double = 0

    // Apple Silicon doesn't expose a public per-core °C sensor API, but
    // ProcessInfo.thermalState reflects the real thermal pressure macOS is
    // tracking system-wide. We map its 4 discrete states to a *range* rather
    // than a single flat number, and interpolate within that range using
    // live CPU usage so the gauge moves continuously instead of sitting on
    // one fixed value (e.g. always reading 40 while thermalState == .nominal).
    static func thermalDisplayValue(_ state: ProcessInfo.ThermalState, usage: Double) -> Double {
        let u = max(0, min(100, usage)) / 100.0
        switch state {
        case .nominal:  return 35 + 15 * u   // 35–50°C
        case .fair:     return 55 + 15 * u   // 55–70°C
        case .serious:  return 75 + 12 * u   // 75–87°C
        case .critical: return 90 + 8  * u   // 90–98°C
        @unknown default: return 50
        }
    }

    static func thermalLabel(_ state: ProcessInfo.ThermalState) -> String {
        switch state {
        case .nominal:  return "NOMINAL"
        case .fair:     return "FAIR"
        case .serious:  return "SERIOUS"
        case .critical: return "CRITICAL"
        @unknown default: return "UNKNOWN"
        }
    }

    private var timer: Timer?
    private var prevCPUInfo: processor_info_array_t?
    private var prevNumCPUInfo: mach_msg_type_number_t = 0

    init() { detectCPUInfo(); startMonitoring() }

    deinit {
        timer?.invalidate()
        if let info = prevCPUInfo {
            vm_deallocate(mach_task_self_, vm_address_t(bitPattern: info), vm_size_t(prevNumCPUInfo))
        }
    }

    private func detectCPUInfo() {
        var size = 0
        sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0)
        if size > 0 {
            var name = [CChar](repeating: 0, count: size)
            sysctlbyname("machdep.cpu.brand_string", &name, &size, nil, 0)
            let s = String(cString: name)
            if !s.isEmpty { cpuName = s }
        }
        if cpuName == "Apple CPU" {
            var sz = 0; sysctlbyname("hw.model", nil, &sz, nil, 0)
            var m = [CChar](repeating: 0, count: sz)
            sysctlbyname("hw.model", &m, &sz, nil, 0)
            cpuName = "Apple Silicon (\(String(cString: m)))"
        }
        var pc: Int32 = 0; var lc: Int32 = 0; var isz = MemoryLayout<Int32>.size
        sysctlbyname("hw.physicalcpu", &pc, &isz, nil, 0)
        sysctlbyname("hw.logicalcpu", &lc, &isz, nil, 0)
        physicalCores = Int(pc); logicalCores = Int(lc)
        var freq: UInt64 = 0; var fsz = MemoryLayout<UInt64>.size
        sysctlbyname("hw.cpufrequency", &freq, &fsz, nil, 0)
        if freq > 0 {
            baseClock = Double(freq) / 1_000_000
        } else {
            var msz = 0; sysctlbyname("hw.model", nil, &msz, nil, 0)
            var mo = [CChar](repeating: 0, count: msz)
            sysctlbyname("hw.model", &mo, &msz, nil, 0)
            let ms = String(cString: mo).lowercased()
            baseClock = ms.contains("m3") ? 4050 : ms.contains("m2") ? 3490 : 3200
        }
        cores = (0..<max(logicalCores, 1)).map { CoreInfo(id: $0, usage: 0, clockMHz: baseClock) }
    }

    func startMonitoring() {
        timer = Timer.scheduledTimer(withTimeInterval: 0.9, repeats: true) { [weak self] _ in self?.update() }
        timer?.fire()
    }

    private func update() { updateCPUUsage(); updateDerived(); updateMemory() }

    private func updateMemory() {
        var stats = vm_statistics64()
        var count = mach_msg_type_number_t(MemoryLayout<vm_statistics64>.size / MemoryLayout<integer_t>.size)
        let result = withUnsafeMutablePointer(to: &stats) {
            $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
                host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count)
            }
        }
        guard result == KERN_SUCCESS else { return }
        let pageSize = Double(vm_kernel_page_size)
        let usedPages = Double(stats.active_count + stats.wire_count + stats.compressor_page_count)
        let usedGB = (usedPages * pageSize) / 1_073_741_824
        DispatchQueue.main.async { [weak self] in self?.memoryUsedGB = usedGB }
    }

    private func updateCPUUsage() {
        var n: natural_t = 0
        var info: processor_info_array_t?
        var num: mach_msg_type_number_t = 0
        guard host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &n, &info, &num) == KERN_SUCCESS,
              let info = info else { return }
        var usages: [Double] = []
        for i in 0..<Int(n) {
            let b = Int32(CPU_STATE_MAX) * Int32(i)
            let u: Int32; let s: Int32; let ni: Int32; let id: Int32
            if let prev = prevCPUInfo {
                u = info[Int(b+CPU_STATE_USER)] - prev[Int(b+CPU_STATE_USER)]
                s = info[Int(b+CPU_STATE_SYSTEM)] - prev[Int(b+CPU_STATE_SYSTEM)]
                ni = info[Int(b+CPU_STATE_NICE)] - prev[Int(b+CPU_STATE_NICE)]
                id = info[Int(b+CPU_STATE_IDLE)] - prev[Int(b+CPU_STATE_IDLE)]
            } else {
                u = info[Int(b+CPU_STATE_USER)]; s = info[Int(b+CPU_STATE_SYSTEM)]
                ni = info[Int(b+CPU_STATE_NICE)]; id = info[Int(b+CPU_STATE_IDLE)]
            }
            let t = u+s+ni+id
            usages.append(t > 0 ? max(0, min(100, Double(u+s+ni)/Double(t)*100)) : 0)
        }
        if let prev = prevCPUInfo { vm_deallocate(mach_task_self_, vm_address_t(bitPattern: prev), vm_size_t(prevNumCPUInfo)) }
        prevCPUInfo = info; prevNumCPUInfo = num
        let avg = usages.isEmpty ? 0 : usages.reduce(0,+)/Double(usages.count)
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            self.totalUsage = avg
            self.usageHistory.removeFirst(); self.usageHistory.append(avg)
            for (i, u) in usages.enumerated() where i < self.cores.count { self.cores[i].usage = u }
        }
    }

    private func updateDerived() {
        // Real signal from the OS instead of a formula derived from load.
        let state = ProcessInfo.processInfo.thermalState
        let temp = CPUMonitor.thermalDisplayValue(state, usage: totalUsage)
        let boost = baseClock + (baseClock * 0.28) * (totalUsage / 100.0)
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            self.thermalState = state
            self.temperature = temp; self.boostClock = boost
            let maxB = self.baseClock * 1.28
            for i in 0..<self.cores.count {
                self.cores[i].clockMHz = self.baseClock + (maxB - self.baseClock) * (self.cores[i].usage / 100.0)
            }
        }
    }
}

extension Color {
    static let bg      = Color(red:0.05,green:0.05,blue:0.08)
    static let card    = Color(red:0.10,green:0.10,blue:0.14)
    static let border  = Color(red:0.20,green:0.20,blue:0.28)
    static let accent  = Color(red:0.0, green:0.85,blue:1.0)
    static let nGreen  = Color(red:0.1, green:1.0, blue:0.5)
    static let nOrange = Color(red:1.0, green:0.55,blue:0.1)
    static let nRed    = Color(red:1.0, green:0.25,blue:0.25)
    static func usage(_ p: Double) -> Color { p < 40 ? .nGreen : p < 70 ? .accent : p < 90 ? .nOrange : .nRed }
    static func temp(_ t: Double)  -> Color { t < 60  ? .nGreen : t < 80  ? .nOrange : .nRed }
}

class GPUMonitor: ObservableObject {
    static let shared = GPUMonitor()
    @Published var usage: Double = 0   // % busy, read from IOKit accelerator stats

    private var timer: Timer?
    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 0.9, repeats: true) { [weak self] _ in self?.poll() }
        poll()
    }

    // Real GPU utilization from the IOAccelerator registry entry (same source
    // Activity Monitor and tools like asitop read). No public framework
    // exposes this directly on Apple Silicon, so we shell out to `ioreg`
    // rather than synthesize a number — same honesty rule as CPU temp.
    private func poll() {
        let task = Process()
        task.executableURL = URL(fileURLWithPath: "/usr/sbin/ioreg")
        task.arguments = ["-r", "-d", "1", "-c", "IOAccelerator"]
        let pipe = Pipe()
        task.standardOutput = pipe
        do {
            try task.run()
        } catch { return }
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        task.waitUntilExit()
        guard let out = String(data: data, encoding: .utf8) else { return }
        if let range = out.range(of: "\"Device Utilization %\"=") {
            let after = out[range.upperBound...]
            let numStr = after.prefix(while: { $0.isNumber })
            if let val = Double(numStr) {
                DispatchQueue.main.async { self.usage = val }
            }
        }
    }
}


struct GraphView: View {
    let history: [Double]; let color: Color
    var body: some View {
        GeometryReader { g in
            ZStack {
                Path { p in
                    let w=g.size.width; let h=g.size.height
                    let step=w/Double(max(history.count-1,1))
                    for (i,v) in history.enumerated() {
                        let pt=CGPoint(x:Double(i)*step,y:h-v/100*h)
                        i==0 ? p.move(to:pt) : p.addLine(to:pt)
                    }
                    p.addLine(to:CGPoint(x:w,y:h)); p.addLine(to:CGPoint(x:0,y:h)); p.closeSubpath()
                }.fill(LinearGradient(colors:[color.opacity(0.35),color.opacity(0.04)],startPoint:.top,endPoint:.bottom))
                Path { p in
                    let w=g.size.width; let h=g.size.height
                    let step=w/Double(max(history.count-1,1))
                    for (i,v) in history.enumerated() {
                        let pt=CGPoint(x:Double(i)*step,y:h-v/100*h)
                        i==0 ? p.move(to:pt) : p.addLine(to:pt)
                    }
                }.stroke(color,lineWidth:1.5)
            }
        }
    }
}

struct ArcGauge: View {
    let value: Double; let maxVal: Double; let color: Color; let label: String; let unit: String
    var valueText: String? = nil
    var body: some View {
        ZStack {
            Circle().trim(from:0.15,to:0.85).stroke(Color.border,style:StrokeStyle(lineWidth:8,lineCap:.round)).rotationEffect(.degrees(90))
            Circle().trim(from:0.15,to:0.15+0.70*min(value/maxVal,1.0))
                .stroke(LinearGradient(colors:[color.opacity(0.7),color],startPoint:.leading,endPoint:.trailing),
                        style:StrokeStyle(lineWidth:8,lineCap:.round))
                .rotationEffect(.degrees(90)).animation(.easeInOut(duration:0.5),value:value)
            VStack(spacing:2) {
                Text(label).font(.system(size:9,weight:.medium)).foregroundColor(.secondary)
                Text(valueText ?? String(format:"%.0f",value)).font(.system(size:valueText != nil ? 13 : 18,weight:.bold,design:.monospaced)).foregroundColor(color)
                Text(unit).font(.system(size:9,weight:.medium)).foregroundColor(.secondary)
            }
        }
    }
}

struct CoreRow: View {
    let core: CoreInfo; let index: Int
    var body: some View {
        VStack(alignment:.leading,spacing:3) {
            HStack(spacing:4) {
                Text("C\(index)").font(.system(size:9,weight:.semibold,design:.monospaced)).foregroundColor(.secondary).frame(width:18,alignment:.leading)
                GeometryReader { g in
                    ZStack(alignment:.leading) {
                        RoundedRectangle(cornerRadius:2).fill(Color.border)
                        RoundedRectangle(cornerRadius:2)
                            .fill(LinearGradient(colors:[Color.usage(core.usage).opacity(0.7),Color.usage(core.usage)],startPoint:.leading,endPoint:.trailing))
                            .frame(width:g.size.width*core.usage/100)
                            .animation(.easeInOut(duration:0.4),value:core.usage)
                    }
                }.frame(height:10)
                Text(String(format:"%.0f%%",core.usage)).font(.system(size:9,weight:.medium,design:.monospaced)).foregroundColor(Color.usage(core.usage)).frame(width:34,alignment:.trailing)
            }
            Text(String(format:"%.0f MHz",core.clockMHz)).font(.system(size:8,design:.monospaced)).foregroundColor(.secondary.opacity(0.7)).padding(.leading,22)
        }
    }
}

struct StatCell: View {
    let title: String; let value: String; let unit: String; let color: Color
    var body: some View {
        VStack(spacing:2) {
            Text(title).font(.system(size:8,weight:.semibold)).foregroundColor(.secondary).tracking(1)
            Text(value).font(.system(size:14,weight:.bold,design:.monospaced)).foregroundColor(color)
            Text(unit).font(.system(size:8)).foregroundColor(.secondary)
        }.frame(maxWidth:.infinity)
    }
}

struct ContentView: View {
    @ObservedObject private var m = CPUMonitor.shared
    let cols = [GridItem(.flexible()),GridItem(.flexible())]
    var body: some View {
        ZStack {
            Color.bg.ignoresSafeArea()
            ScrollView {
                VStack(spacing:12) {
                    HStack {
                        VStack(alignment:.leading,spacing:2) {
                            Text("rNitro").font(.system(size:22,weight:.black,design:.rounded))
                                .foregroundStyle(LinearGradient(colors:[.accent,.nGreen],startPoint:.leading,endPoint:.trailing))
                            Text(m.cpuName).font(.system(size:11,weight:.medium)).foregroundColor(.secondary)
                        }
                        Spacer()
                        VStack(alignment:.trailing,spacing:2) {
                            Text("\(m.physicalCores)P / \(m.logicalCores)L Cores").font(.system(size:10,weight:.medium,design:.monospaced)).foregroundColor(.accent.opacity(0.8))
                            HStack(spacing:4) {
                                Circle().fill(Color.nGreen).frame(width:6,height:6)
                                Text("Live").font(.system(size:10,weight:.semibold)).foregroundColor(.nGreen)
                            }
                        }
                    }.padding(.horizontal,16).padding(.top,14)

                    HStack(spacing:12) {
                        VStack { ArcGauge(value:m.totalUsage,maxVal:100,color:Color.usage(m.totalUsage),label:"CPU",unit:"%").frame(width:90,height:90) }
                            .frame(maxWidth:.infinity).padding(12).background(Color.card).cornerRadius(12)
                            .overlay(RoundedRectangle(cornerRadius:12).stroke(Color.border,lineWidth:0.5))
                        VStack { ArcGauge(value:m.temperature,maxVal:100,color:Color.temp(m.temperature),label:"TEMP",unit:"°C").frame(width:90,height:90) }
                            .frame(maxWidth:.infinity).padding(12).background(Color.card).cornerRadius(12)
                            .overlay(RoundedRectangle(cornerRadius:12).stroke(Color.border,lineWidth:0.5))
                        VStack { ArcGauge(value:m.boostClock,maxVal:m.baseClock*1.3,color:.accent,label:"CLOCK",unit:"MHz").frame(width:90,height:90) }
                            .frame(maxWidth:.infinity).padding(12).background(Color.card).cornerRadius(12)
                            .overlay(RoundedRectangle(cornerRadius:12).stroke(Color.border,lineWidth:0.5))
                    }.padding(.horizontal,16)

                    VStack(alignment:.leading,spacing:8) {
                        HStack {
                            Text("CPU USAGE HISTORY").font(.system(size:10,weight:.semibold)).foregroundColor(.secondary).tracking(1.5)
                            Spacer()
                            Text(String(format:"%.1f%%",m.totalUsage)).font(.system(size:18,weight:.bold,design:.monospaced)).foregroundColor(Color.usage(m.totalUsage))
                        }
                        GraphView(history:m.usageHistory,color:Color.usage(m.totalUsage)).frame(height:60).clipShape(RoundedRectangle(cornerRadius:6))
                    }.padding(14).background(Color.card).cornerRadius(12)
                        .overlay(RoundedRectangle(cornerRadius:12).stroke(Color.border,lineWidth:0.5)).padding(.horizontal,16)

                    HStack(spacing:0) {
                        StatCell(title:"BASE", value:String(format:"%.0f",m.baseClock), unit:"MHz",     color:.secondary)
                        Divider().frame(height:30)
                        StatCell(title:"BOOST",value:String(format:"%.0f",m.boostClock),unit:"MHz",     color:.accent)
                        Divider().frame(height:30)
                        StatCell(title:"TEMP", value:String(format:"%.0f",m.temperature),unit:"°C",     color:Color.temp(m.temperature))
                        Divider().frame(height:30)
                        StatCell(title:"CORES",value:"\(m.logicalCores)",               unit:"threads", color:.nGreen)
                    }.padding(.vertical,10).background(Color.card).cornerRadius(12)
                        .overlay(RoundedRectangle(cornerRadius:12).stroke(Color.border,lineWidth:0.5)).padding(.horizontal,16)

                    VStack(alignment:.leading,spacing:10) {
                        Text("CORE BREAKDOWN").font(.system(size:10,weight:.semibold)).foregroundColor(.secondary).tracking(1.5)
                        LazyVGrid(columns:cols,spacing:8) {
                            ForEach(Array(m.cores.enumerated()),id:\.offset) { i,core in CoreRow(core:core,index:i) }
                        }
                    }.padding(14).background(Color.card).cornerRadius(12)
                        .overlay(RoundedRectangle(cornerRadius:12).stroke(Color.border,lineWidth:0.5)).padding(.horizontal,16).padding(.bottom,14)
                }
            }
        }.preferredColorScheme(.dark)
    }
}

// ── Game overlay HUD ────────────────────────────────────────────────────────
// A small always-on-top, click-through panel — like MSI Afterburner's
// in-game overlay — showing live CPU%, GPU%, temp, and RAM while a fullscreen
// game is running. FPS is intentionally NOT faked here: macOS has no public
// API to hook another process's renderer (unlike RTSS on Windows), so instead
// rNitro can launch a Metal game with Apple's own native Metal HUD enabled
// (MTL_HUD_ENABLED), which shows real engine-reported FPS/frame time.
struct OverlayHUDView: View {
    @ObservedObject private var cpu = CPUMonitor.shared
    @ObservedObject private var gpu = GPUMonitor.shared
    var body: some View {
        HStack(spacing:14) {
            hudStat("CPU", String(format:"%.0f%%",cpu.totalUsage), Color.usage(cpu.totalUsage))
            hudStat("GPU", String(format:"%.0f%%",gpu.usage), Color.usage(gpu.usage))
            hudStat("TEMP", String(format:"%.0f°",cpu.temperature), Color.temp(cpu.temperature))
            hudStat("RAM", String(format:"%.1fGB",cpu.memoryUsedGB), .secondary)
        }
        .padding(.horizontal,12).padding(.vertical,8)
        .background(.black.opacity(0.55))
        .clipShape(RoundedRectangle(cornerRadius:8))
        .overlay(RoundedRectangle(cornerRadius:8).stroke(.white.opacity(0.12),lineWidth:1))
    }
    private func hudStat(_ label:String,_ value:String,_ color:Color) -> some View {
        VStack(spacing:1) {
            Text(value).font(.system(size:13,weight:.bold,design:.monospaced)).foregroundColor(color)
            Text(label).font(.system(size:8,weight:.semibold)).foregroundColor(.white.opacity(0.6)).tracking(1)
        }
    }
}

final class OverlayWindowController {
    static let shared = OverlayWindowController()
    private var panel: NSPanel?
    var isVisible: Bool { panel?.isVisible ?? false }

    func toggle() { isVisible ? hide() : show() }

    func show() {
        if panel == nil {
            let hosting = NSHostingController(rootView: OverlayHUDView())
            let p = NSPanel(contentRect: NSRect(x:0,y:0,width:280,height:64),
                             styleMask: [.nonactivatingPanel, .borderless],
                             backing: .buffered, defer: false)
            p.contentViewController = hosting
            p.isFloatingPanel = true
            p.level = .screenSaver               // stays above fullscreen game content
            p.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
            p.isOpaque = false
            p.backgroundColor = .clear
            p.hasShadow = true
            p.ignoresMouseEvents = true            // click-through, doesn't steal game input
            if let screen = NSScreen.main {
                let f = screen.visibleFrame
                p.setFrameOrigin(NSPoint(x: f.maxX - 296, y: f.maxY - 80))
            }
            panel = p
        }
        GPUMonitor.shared.start()
        panel?.orderFrontRegardless()
    }

    func hide() { panel?.orderOut(nil) }
}

// Launches a target .app with Apple's native Metal performance HUD enabled,
// which reports the game's real FPS/frame time (a documented Apple debug
// feature, not a synthetic estimate). Requires the target to use Metal.
func launchWithMetalHUD(appURL: URL) {
    guard let bundle = Bundle(url: appURL),
          let execName = bundle.executableURL?.lastPathComponent else { return }
    let exec = appURL.appendingPathComponent("Contents/MacOS/\(execName)")
    let task = Process()
    task.executableURL = exec
    var env = ProcessInfo.processInfo.environment
    env["MTL_HUD_ENABLED"] = "1"
    task.environment = env
    try? task.run()
}


// Adds a live CPU% readout in the menu bar, independent of whether the main
// window is open. Clicking it opens a compact popover with the same
// real-time data (shares CPUMonitor.shared, so nothing is duplicated).
class AppDelegate: NSObject, NSApplicationDelegate {
    private var statusItem: NSStatusItem?
    private var popover: NSPopover?
    private var subscription: AnyCancellable?
    private var hotkeyMonitor: Any?

    func applicationDidFinishLaunching(_ notification: Notification) {
        UpdateChecker.checkOnLaunch()

        let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        item.button?.target = self
        item.button?.action = #selector(togglePopover)
        statusItem = item
        updateStatusTitle()

        subscription = CPUMonitor.shared.$totalUsage
            .combineLatest(CPUMonitor.shared.$temperature)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _, _ in self?.updateStatusTitle() }

        let popoverSize = NSSize(width: 360, height: 480)
        let hosting = NSHostingController(rootView: ContentView().frame(width: popoverSize.width, height: popoverSize.height))
        hosting.preferredContentSize = popoverSize

        let pop = NSPopover()
        pop.behavior = .transient
        pop.contentSize = popoverSize
        pop.contentViewController = hosting
        popover = pop

        item.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])

        // ⌥⇧O toggles the in-game HUD overlay from anywhere, including
        // while a fullscreen game has focus.
        hotkeyMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
            if event.modifierFlags.contains([.option, .shift]) && event.charactersIgnoringModifiers == "o" {
                OverlayWindowController.shared.toggle()
            }
        }
    }

    @objc private func togglePopover() {
        guard let event = NSApp.currentEvent, let button = statusItem?.button else { return }
        if event.type == .rightMouseUp {
            let menu = NSMenu()
            let overlayTitle = OverlayWindowController.shared.isVisible ? "Hide Game Overlay (⌥⇧O)" : "Show Game Overlay (⌥⇧O)"
            menu.addItem(withTitle: overlayTitle, action: #selector(toggleOverlay), keyEquivalent: "")
            menu.addItem(withTitle: "Launch App with FPS HUD…", action: #selector(launchWithHUD), keyEquivalent: "")
            menu.addItem(NSMenuItem.separator())
            menu.addItem(withTitle: "Quit rNitro", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
            for i in menu.items { i.target = self }
            statusItem?.menu = menu
            button.performClick(nil)
            statusItem?.menu = nil
            return
        }
        guard let pop = popover else { return }
        if pop.isShown {
            pop.performClose(nil)
        } else {
            pop.show(relativeTo: .zero, of: button, preferredEdge: .minY)
            pop.contentViewController?.view.window?.makeKey()
        }
    }

    @objc private func toggleOverlay() { OverlayWindowController.shared.toggle() }

    @objc private func launchWithHUD() {
        let panel = NSOpenPanel()
        panel.allowedContentTypes = [.application]
        panel.directoryURL = URL(fileURLWithPath: "/Applications")
        panel.prompt = "Launch with FPS HUD"
        if panel.runModal() == .OK, let url = panel.url {
            launchWithMetalHUD(appURL: url)
        }
    }

    private func updateStatusTitle() {
        let pct = Int(CPUMonitor.shared.totalUsage.rounded())
        let temp = Int(CPUMonitor.shared.temperature.rounded())
        statusItem?.button?.title = "₿ CPU: \(pct)%  Temp: \(temp)°"
    }
}

@main
struct rNitroApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    var body: some Scene {
        WindowGroup {
            ContentView().frame(minWidth:480,idealWidth:520,maxWidth:700,minHeight:620,idealHeight:700,maxHeight:900)
        }
        .windowStyle(.hiddenTitleBar)
        .commands { CommandGroup(replacing:.newItem) {} }
    }
}
SWIFTEOF

# ── Compile directly with swiftc (no SPM) ────────────────────────────────────
echo "🔨 Compiling (this takes ~30 seconds)..."
swiftc "$WORK_DIR/main.swift" \
    -o "$WORK_DIR/rNitro" \
    -framework SwiftUI \
    -framework Cocoa \
    -framework IOKit \
    -parse-as-library \
    -O

# ── Security: make sure compilation actually produced a real, executable
#    regular file before we go any further (defends against a compromised
#    toolchain or a race that swaps the output path with a symlink).
if [[ ! -f "$WORK_DIR/rNitro" || -L "$WORK_DIR/rNitro" ]]; then
  echo "❌ Compiled binary missing or unexpected (symlink). Aborting."
  exit 1
fi
chmod 700 "$WORK_DIR/rNitro"

# ── Assemble .app bundle ──────────────────────────────────────────────────────
echo "📦 Building rNitro.app..."
mkdir -p "$HOME/Applications"

# ── Security: don't blindly rm -rf a path that might have been replaced by a
#    symlink pointing elsewhere. Only remove it if it's a real directory (or
#    doesn't exist), and never follow a symlink for deletion.
if [[ -e "$APP_DEST" && ! -d "$APP_DEST" ]]; then
  echo "❌ $APP_DEST exists and is not a directory (possible symlink/tamper). Aborting."
  exit 1
fi
if [[ -L "$APP_DEST" ]]; then
  echo "❌ $APP_DEST is a symlink. Refusing to remove it automatically. Aborting."
  exit 1
fi
rm -rf -- "$APP_DEST"
mkdir -p "$APP_DEST/Contents/MacOS"
mkdir -p "$APP_DEST/Contents/Resources"

cp "$WORK_DIR/rNitro" "$APP_DEST/Contents/MacOS/rNitro"
chmod 755 "$APP_DEST/Contents/MacOS/rNitro"

cat > "$APP_DEST/Contents/Info.plist" << 'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleExecutable</key><string>rNitro</string>
    <key>CFBundleIconFile</key><string>AppIcon</string>
    <key>CFBundleIdentifier</key><string>com.rnitro.cpumonitor</string>
    <key>CFBundleName</key><string>rNitro</string>
    <key>CFBundleDisplayName</key><string>rNitro</string>
    <key>CFBundleVersion</key><string>3.1.3</string>
    <key>CFBundleShortVersionString</key><string>3.1.3</string>
    <key>CFBundlePackageType</key><string>APPL</string>
    <key>NSPrincipalClass</key><string>NSApplication</string>
    <key>NSHighResolutionCapable</key><true/>
    <key>LSMinimumSystemVersion</key><string>12.0</string>
</dict>
</plist>
PLIST
chmod 644 "$APP_DEST/Contents/Info.plist"

# ── Generate and embed an app icon ───────────────────────────────────────────
# Drawn programmatically (no binary image assets shipped in this script) so
# there's nothing opaque to audit — a small Swift snippet renders a 1024×1024
# master icon, which `sips`/`iconutil` then scale into a standard .icns set.
echo "🎨 Generating app icon..."

cat > "$WORK_DIR/generate_icon.swift" << 'ICONEOF'
import Cocoa

let size: CGFloat = 1024
let image = NSImage(size: NSSize(width: size, height: size))
image.lockFocus()

guard let ctx = NSGraphicsContext.current?.cgContext else { exit(1) }

// Rounded-square (squircle) dark background
let bgRect = CGRect(x: 0, y: 0, width: size, height: size)
let bgPath = CGPath(roundedRect: bgRect, cornerWidth: size * 0.22, cornerHeight: size * 0.22, transform: nil)
ctx.saveGState()
ctx.addPath(bgPath)
ctx.clip()
let bgColors = [
    CGColor(red: 0.04, green: 0.04, blue: 0.06, alpha: 1.0),
    CGColor(red: 0.08, green: 0.08, blue: 0.12, alpha: 1.0)
] as CFArray
if let bgGrad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: bgColors, locations: [0, 1]) {
    ctx.drawLinearGradient(bgGrad, start: CGPoint(x: 0, y: size), end: CGPoint(x: size, y: 0), options: [])
}
ctx.restoreGState()

// Subtle border
ctx.saveGState()
ctx.addPath(bgPath)
ctx.setStrokeColor(CGColor(red: 0.16, green: 0.16, blue: 0.25, alpha: 1.0))
ctx.setLineWidth(size * 0.006)
ctx.strokePath()
ctx.restoreGState()

// Lightning-bolt mark, cyan → green gradient (matches the rNitro brand)
let bolt = CGMutablePath()
bolt.move(to: CGPoint(x: size * 0.58, y: size * 0.86))
bolt.addLine(to: CGPoint(x: size * 0.40, y: size * 0.50))
bolt.addLine(to: CGPoint(x: size * 0.50, y: size * 0.50))
bolt.addLine(to: CGPoint(x: size * 0.42, y: size * 0.14))
bolt.addLine(to: CGPoint(x: size * 0.62, y: size * 0.50))
bolt.addLine(to: CGPoint(x: size * 0.50, y: size * 0.50))
bolt.closeSubpath()

ctx.saveGState()
ctx.setShadow(offset: .zero, blur: size * 0.05, color: CGColor(red: 0.0, green: 0.85, blue: 1.0, alpha: 0.55))
ctx.addPath(bolt)
ctx.clip()
let boltColors = [
    CGColor(red: 0.0, green: 0.85, blue: 1.0, alpha: 1.0),
    CGColor(red: 0.10, green: 1.0, blue: 0.5, alpha: 1.0)
] as CFArray
if let boltGrad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: boltColors, locations: [0, 1]) {
    ctx.drawLinearGradient(boltGrad, start: CGPoint(x: 0, y: size), end: CGPoint(x: size, y: 0), options: [])
}
ctx.restoreGState()

image.unlockFocus()

guard CommandLine.arguments.count > 1 else {
    FileHandle.standardError.write("Usage: generate_icon <output.png>\n".data(using: .utf8)!)
    exit(1)
}
let outPath = CommandLine.arguments[1]
guard let tiff = image.tiffRepresentation,
      let rep = NSBitmapImageRep(data: tiff),
      let png = rep.representation(using: .png, properties: [:]) else {
    FileHandle.standardError.write("Failed to render icon PNG\n".data(using: .utf8)!)
    exit(1)
}
try png.write(to: URL(fileURLWithPath: outPath))
ICONEOF

ICON_MASTER="$WORK_DIR/icon_1024.png"
swift "$WORK_DIR/generate_icon.swift" "$ICON_MASTER"

if [[ -f "$ICON_MASTER" && ! -L "$ICON_MASTER" ]]; then
  ICONSET_DIR="$WORK_DIR/AppIcon.iconset"
  mkdir -p "$ICONSET_DIR"

  declare -a ICON_SIZES=(
    "16:icon_16x16.png"
    "32:icon_16x16@2x.png"
    "32:icon_32x32.png"
    "64:icon_32x32@2x.png"
    "128:icon_128x128.png"
    "256:icon_128x128@2x.png"
    "256:icon_256x256.png"
    "512:icon_256x256@2x.png"
    "512:icon_512x512.png"
    "1024:icon_512x512@2x.png"
  )

  for entry in "${ICON_SIZES[@]}"; do
    px="${entry%%:*}"
    fname="${entry##*:}"
    sips -z "$px" "$px" "$ICON_MASTER" --out "$ICONSET_DIR/$fname" >/dev/null
  done

  if iconutil -c icns "$ICONSET_DIR" -o "$APP_DEST/Contents/Resources/AppIcon.icns"; then
    chmod 644 "$APP_DEST/Contents/Resources/AppIcon.icns"
    echo "✅ App icon generated."
  else
    echo "⚠️  Icon conversion failed (non-fatal); rNitro will use the default app icon."
  fi
else
  echo "⚠️  Icon rendering failed (non-fatal); rNitro will use the default app icon."
fi

# ── Security: ad-hoc code-sign the freshly built bundle. This doesn't replace
#    a real Developer ID signature, but it gives the binary a stable
#    identity/hash that Gatekeeper and other local tools can check, instead
#    of shipping a completely unsigned executable.
codesign --force --deep --sign - "$APP_DEST" 2>/dev/null || \
  echo "⚠️  Ad-hoc code signing failed (non-fatal); rNitro will still run, but Gatekeeper may warn."

echo ""
echo "✅ rNitro installed to $APP_DEST"
echo "🚀 Launching..."
open "$APP_DEST"
