#!/bin/bash
set -e
say(){ printf "\n\033[1;34m➤ %s\033[0m\n" "$1"; }
ok(){ printf "\033[1;32m  ✓ %s\033[0m\n" "$1"; }
HS_VER="1.1.1"
HS_URL="https://github.com/Hammerspoon/hammerspoon/releases/download/${HS_VER}/Hammerspoon-${HS_VER}.zip"
APPDIR="/Applications"
HSDIR="$HOME/.hammerspoon"
say "Step 1/3 — Hammerspoon"
if [ -d "$APPDIR/Hammerspoon.app" ]; then
  ok "Hammerspoon already installed."
else
  TMP="$(mktemp -d)"; echo "  Downloading Hammerspoon ${HS_VER}…"
  curl -L --fail --progress-bar "$HS_URL" -o "$TMP/hs.zip"
  ditto -x -k "$TMP/hs.zip" "$TMP/u"
  rm -rf "$APPDIR/Hammerspoon.app" 2>/dev/null || true
  ditto "$TMP/u/Hammerspoon.app" "$APPDIR/Hammerspoon.app"
  xattr -dr com.apple.quarantine "$APPDIR/Hammerspoon.app" 2>/dev/null || true
  rm -rf "$TMP"; ok "Hammerspoon installed."
fi
say "Step 2/3 — Control panel config"
mkdir -p "$HSDIR"
[ -f "$HSDIR/init.lua" ] && cp "$HSDIR/init.lua" "$HSDIR/init.lua.backup-$(date +%s)" && ok "Backed up existing init.lua."
cat > "$HSDIR/init.lua" <<'LUA_EOF'
-- ============================================================================
--  iPhone Mirroring Control Panel  +  Security Layer   (Hammerspoon config)
--
--  A floating, always-on-top button panel + global hotkeys to drive the macOS
--  "iPhone Mirroring" window, protected by:
--    1) PIN gate        - nothing works until you enter your PIN (masked, hashed)
--    2) Auto-lock       - re-locks itself after a period of no use
--    3) Confirm actions - optional yes/no prompt before it does anything
--    4) Tamper check    - warns you if this file was changed unexpectedly
--
--  WHAT THIS FILE CAN TOUCH (full transparency):
--    - Focuses / moves the iPhone Mirroring window and sends keystrokes to it
--    - Reads & writes ONE small file for your hashed PIN (~/.hammerspoon/.iphone_panel_pin)
--    - Reads its own file to compute a tamper hash
--    - NO network, NO other files, NO data collection.
--
--  EDIT ME: the APPS list and the SECURITY SETTINGS block below are all you
--  normally need to touch.
-- ============================================================================

----------------------------------------------------------------------
-- 1) YOUR APPS  --  `hotkey` is the number used with Cmd+Alt (⌘⌥).
----------------------------------------------------------------------
local APPS = {
  { name = "WhatsApp",   hotkey = "1" },
  { name = "Instagram",  hotkey = "2" },
  { name = "Messages",   hotkey = "3" },
  { name = "Phone",      hotkey = "4" },
  { name = "Photos",     hotkey = "5" },
  { name = "Settings",   hotkey = "6" },
}

----------------------------------------------------------------------
-- 2) SECURITY SETTINGS  --  turn features on/off and tune them here
----------------------------------------------------------------------
local REQUIRE_PIN     = true   -- lock everything behind a PIN
local IDLE_MINUTES    = 5      -- auto-lock after this many minutes of no use
local CONFIRM_ACTIONS = true   -- ask "Run: X?" before each action
local DOUBLE_CONFIRM  = true   -- ask a SECOND time before actually running
local SPEAK           = true   -- speak "<action> done" aloud after each action
local SHOW_ON_LAUNCH  = false  -- false = stay hidden until you open the app / press hotkey
local TAMPER_CHECK    = true   -- warn if this file changed unexpectedly
local SALT            = "iphone-panel-v1"  -- hashing salt (change = resets PIN)

----------------------------------------------------------------------
-- 3) GENERAL SETTINGS
----------------------------------------------------------------------
local MIRROR_APP = "iPhone Mirroring"
local MOD        = { "cmd", "alt" }
local PANEL_W    = 200
local ROW_H      = 34
local TITLE_H    = 28
local START_POS  = { x = 60, y = 80 }

----------------------------------------------------------------------
-- 4) STATE + SECURITY HELPERS
----------------------------------------------------------------------
local unlocked     = not REQUIRE_PIN
local lastActivity = hs.timer.secondsSinceEpoch()
local pinFile      = hs.configdir .. "/.iphone_panel_pin"

local function touch() lastActivity = hs.timer.secondsSinceEpoch() end

local function hashPin(pin) return hs.hash.SHA256(SALT .. pin) end

local function loadStoredPin()
  local f = io.open(pinFile, "r"); if not f then return nil end
  local h = (f:read("a") or ""):gsub("%s", ""); f:close()
  if #h > 0 then return h end
  return nil
end

local function savePin(hash)
  local f = io.open(pinFile, "w")
  if f then f:write(hash); f:close() end
end

-- Masked text entry via a native macOS dialog (hidden answer). Returns the
-- typed string, or nil if the user cancelled.
local function askHidden(msg)
  local script = table.concat({
    "try",
    '  set r to display dialog "' .. msg .. '" default answer "" ' ..
      'with hidden answer buttons {"Cancel", "OK"} default button "OK" ' ..
      'with title "iPhone Control"',
    "  return text returned of r",
    "on error",
    '  return "__CANCEL__"',
    "end try",
  }, "\n")
  local ok, out = hs.osascript.applescript(script)
  if not ok or out == "__CANCEL__" then return nil end
  return out
end

-- forward declarations so the panel-title updater can be used everywhere
local updateTitle

local function lock()
  unlocked = false
  updateTitle()
end

local function unlock()
  unlocked = true
  touch()
  updateTitle()
end

local function promptUnlock()
  if not REQUIRE_PIN then unlocked = true; return end
  local stored = loadStoredPin()
  if not stored then
    local p1 = askHidden("Set a NEW PIN for the control panel:")
    if not p1 or #p1 == 0 then return end
    local p2 = askHidden("Confirm your new PIN:")
    if p2 ~= p1 then hs.alert.show("PINs didn't match ❌"); return end
    savePin(hashPin(p1))
    hs.alert.show("PIN set 🔐")
    unlock()
    return
  end
  local p = askHidden("Enter PIN to unlock:")
  if not p then return end
  if hashPin(p) == stored then
    unlock()
    hs.alert.show("Unlocked 🔓")
  else
    hs.alert.show("Wrong PIN ❌")
  end
end

-- Speak text aloud (strips emoji/symbols so it sounds clean)
local function speak(text)
  if not SPEAK then return end
  local clean = tostring(text):gsub("[^%w%s/]", ""):gsub("%s+", " ")
  hs.execute('say "' .. clean .. '" &')
end

-- Runs an action, enforcing lock + optional (double) confirmation + spoken result
local function runAction(label, fn)
  if not unlocked then
    promptUnlock()
    if not unlocked then return end
  end
  touch()
  if CONFIRM_ACTIONS then
    local b1 = hs.dialog.blockAlert("Run: " .. label .. " ?", "", "Yes", "Cancel")
    if b1 ~= "Yes" then return end
    if DOUBLE_CONFIRM then
      local b2 = hs.dialog.blockAlert("Confirm again", "Really run: " .. label .. " ?", "Confirm", "Cancel")
      if b2 ~= "Confirm" then return end
    end
  end
  fn()
  speak(label .. " done")
end

----------------------------------------------------------------------
-- 5) TAMPER CHECK  --  hash this file (excluding the stored-hash line)
----------------------------------------------------------------------
-- Stored expected hash. Leave "" the first time; the panel will show you the
-- value to paste here. After any INTENTIONAL edit, update this to the new
-- value shown in the Hammerspoon Console.
local EXPECTED_HASH = ""

local function computeSelfHash()
  local f = io.open(hs.configdir .. "/init.lua", "r")
  if not f then return nil end
  local data = f:read("a"); f:close()
  -- normalise the stored-hash line so the hash is stable regardless of its value
  data = data:gsub('EXPECTED_HASH%s*=%s*"[^"]*"', 'EXPECTED_HASH = ""')
  return hs.hash.SHA256(SALT .. data)
end

local function runTamperCheck()
  if not TAMPER_CHECK then return end
  local current = computeSelfHash()
  if not current then return end
  if EXPECTED_HASH == "" then
    print("[iPhone Panel] TAMPER CHECK not set yet.")
    print("[iPhone Panel] Paste this into EXPECTED_HASH, then Reload Config:")
    print("[iPhone Panel] " .. current)
    hs.alert.show("Tamper check: set EXPECTED_HASH (see Console) ⚠️", 4)
  elseif current ~= EXPECTED_HASH then
    hs.alert.show("⚠️ Config file changed since last verify!", 5)
    print("[iPhone Panel] WARNING: init.lua differs from EXPECTED_HASH.")
    print("[iPhone Panel] If YOU edited it, update EXPECTED_HASH to: " .. current)
  end
end

----------------------------------------------------------------------
-- 6) CORE ACTIONS (drive iPhone Mirroring)
----------------------------------------------------------------------
local function focusMirror()
  hs.application.launchOrFocus(MIRROR_APP)
  return hs.application.find(MIRROR_APP)
end

local function mirrorKey(key)
  focusMirror()
  hs.timer.doAfter(0.35, function() hs.eventtap.keyStroke({ "cmd" }, key, 0) end)
end

local function launchPhoneApp(appName)
  focusMirror()
  hs.timer.doAfter(0.45, function()
    hs.eventtap.keyStroke({ "cmd" }, "3", 0)
    hs.timer.doAfter(0.55, function()
      hs.eventtap.keyStrokes(appName)
      hs.timer.doAfter(0.45, function() hs.eventtap.keyStroke({}, "return", 0) end)
    end)
  end)
end

local function mirrorWindow()
  local app = hs.application.find(MIRROR_APP)
  if app then return app:mainWindow() end
  return nil
end

local function snapMirror(where)
  focusMirror()
  hs.timer.doAfter(0.3, function()
    local win = mirrorWindow(); if not win then return end
    local scr = win:screen():frame()
    local f = win:frame()
    if where == "left" then
      f.x, f.y = scr.x + 12, scr.y + 12
    elseif where == "right" then
      f.x, f.y = scr.x + scr.w - f.w - 12, scr.y + 12
    elseif where == "center" then
      f.x, f.y = scr.x + (scr.w - f.w) / 2, scr.y + (scr.h - f.h) / 2
    end
    win:setFrame(f)
  end)
end

----------------------------------------------------------------------
-- 7) BUTTON LIST  (bypass = true means the button ignores the lock,
--                  e.g. the Lock/Unlock toggle itself)
----------------------------------------------------------------------
local function toggleLock()
  if unlocked then lock(); hs.alert.show("Locked 🔒") else promptUnlock() end
end

local buttons = {
  { label = "🔐 Lock / Unlock", action = toggleLock,                       bypass = true },
  { label = "📱 Open Mirror",   action = function() focusMirror() end },
  { label = "🏠 Home",           action = function() mirrorKey("1") end },
  { label = "⧉ App Switcher",   action = function() mirrorKey("2") end },
  { label = "🔍 Search",         action = function() mirrorKey("3") end },
}

for _, app in ipairs(APPS) do
  table.insert(buttons, { label = "▶ " .. app.name, action = function() launchPhoneApp(app.name) end })
end

table.insert(buttons, { label = "⯇ Snap Left",  action = function() snapMirror("left")   end })
table.insert(buttons, { label = "⯈ Snap Right", action = function() snapMirror("right")  end })
table.insert(buttons, { label = "◎ Center",     action = function() snapMirror("center") end })

----------------------------------------------------------------------
-- 8) DRAW THE FLOATING PANEL
----------------------------------------------------------------------
local panelH = TITLE_H + (#buttons * ROW_H) + 8
local panel = hs.canvas.new({ x = START_POS.x, y = START_POS.y, w = PANEL_W, h = panelH })
panel:level(hs.canvas.windowLevels.floating)
panel:behavior({ "canJoinAllSpaces", "stationary" })
panel:clickActivating(false)

panel:appendElements({  -- [1] background
  type = "rectangle", action = "fill", roundedRectRadii = { xRadius = 12, yRadius = 12 },
  fillColor = { red = 0.10, green = 0.11, blue = 0.13, alpha = 0.96 },
  frame = { x = 0, y = 0, w = PANEL_W, h = panelH },
})
panel:appendElements({  -- [2] title bar (drag handle)
  type = "rectangle", action = "fill", id = "titlebar",
  roundedRectRadii = { xRadius = 12, yRadius = 12 },
  fillColor = { red = 0.18, green = 0.20, blue = 0.24, alpha = 1.0 },
  frame = { x = 0, y = 0, w = PANEL_W, h = TITLE_H },
  trackMouseDown = true,
})
panel:appendElements({  -- [3] title text (updated by updateTitle)
  type = "text", text = "iPhone Control",
  textColor = { white = 0.9 }, textSize = 13, textAlignment = "center",
  frame = { x = 0, y = 6, w = PANEL_W, h = TITLE_H },
})
local TITLE_IDX = 3

for i, btn in ipairs(buttons) do
  local y = TITLE_H + (i - 1) * ROW_H + 4
  panel:appendElements({
    type = "rectangle", action = "fill", id = "btn" .. i,
    roundedRectRadii = { xRadius = 7, yRadius = 7 },
    fillColor = { red = 0.22, green = 0.24, blue = 0.29, alpha = 1.0 },
    frame = { x = 8, y = y, w = PANEL_W - 16, h = ROW_H - 6 },
    trackMouseUp = true,
  })
  panel:appendElements({
    type = "text", text = btn.label,
    textColor = { white = 0.95 }, textSize = 13,
    frame = { x = 20, y = y + 5, w = PANEL_W - 24, h = ROW_H - 6 },
  })
end

-- now that `panel` exists, define the title updater
updateTitle = function()
  local state = unlocked and "🔓 Unlocked" or "🔒 Locked"
  panel[TITLE_IDX].text = "iPhone Control  " .. state
end
updateTitle()

----------------------------------------------------------------------
-- 9) MOUSE: click buttons + drag panel by title bar
----------------------------------------------------------------------
local drag = {}
panel:mouseCallback(function(_, evType, elemId)
  if elemId == "titlebar" and evType == "mouseDown" then
    local topLeft = panel:topLeft()
    local m = hs.mouse.absolutePosition()
    drag.dx, drag.dy = m.x - topLeft.x, m.y - topLeft.y
    drag.tap = hs.eventtap.new({ hs.eventtap.event.types.mouseMoved,
                                 hs.eventtap.event.types.leftMouseUp }, function(e)
      if e:getType() == hs.eventtap.event.types.leftMouseUp then
        if drag.tap then drag.tap:stop() end
        return false
      end
      local m2 = hs.mouse.absolutePosition()
      panel:topLeft({ x = m2.x - drag.dx, y = m2.y - drag.dy })
      return false
    end)
    drag.tap:start()
  elseif type(elemId) == "string" and elemId:match("^btn%d+$") and evType == "mouseUp" then
    local idx = tonumber(elemId:match("%d+"))
    local btn = buttons[idx]
    if not btn then return end
    if btn.bypass then btn.action() else runAction(btn.label, btn.action) end
  end
end)

if SHOW_ON_LAUNCH then panel:show() else panel:hide() end

-- Open-on-demand: the "iPhone Control" app opens hammerspoon://show
hs.urlevent.bind("show",   function() panel:show() end)
hs.urlevent.bind("hide",   function() panel:hide() end)
hs.urlevent.bind("toggle", function() if panel:isShowing() then panel:hide() else panel:show() end end)

----------------------------------------------------------------------
-- 10) AUTO-LOCK WHEN IDLE
----------------------------------------------------------------------
local idleTimer = hs.timer.new(20, function()
  if REQUIRE_PIN and unlocked
     and (hs.timer.secondsSinceEpoch() - lastActivity) > IDLE_MINUTES * 60 then
    lock()
    hs.alert.show("Locked (idle) 🔒")
  end
end)
idleTimer:start()

----------------------------------------------------------------------
-- 11) GLOBAL HOTKEYS  (⌘⌥)
----------------------------------------------------------------------
hs.hotkey.bind(MOD, "L", function() toggleLock() end)                              -- lock/unlock
hs.hotkey.bind(MOD, "M", function() runAction("Open Mirror", focusMirror) end)
hs.hotkey.bind(MOD, "H", function() runAction("Home", function() mirrorKey("1") end) end)
hs.hotkey.bind(MOD, "Left",  function() runAction("Snap Left",  function() snapMirror("left")  end) end)
hs.hotkey.bind(MOD, "Right", function() runAction("Snap Right", function() snapMirror("right") end) end)
hs.hotkey.bind(MOD, "P", function() if panel:isShowing() then panel:hide() else panel:show() end end)

for _, app in ipairs(APPS) do
  if app.hotkey then
    hs.hotkey.bind(MOD, app.hotkey, function()
      runAction(app.name, function() launchPhoneApp(app.name) end)
    end)
  end
end

----------------------------------------------------------------------
-- 12) STARTUP
----------------------------------------------------------------------
runTamperCheck()
hs.alert.show("iPhone Control Panel loaded " .. (REQUIRE_PIN and "🔒" or "✅"))

-- [diagnostic] end-to-end self test (safe to leave in)
hs.urlevent.bind("selftest", function()
  local ok = hs.accessibilityState()
  local f = io.open("/tmp/ip_selftest", "w")
  if f then f:write(ok and "ACCESSIBILITY_OK" or "ACCESSIBILITY_OFF"); f:close() end
  hs.alert.show(ok and "Self-test: Accessibility ON, sending Home" or "Self-test: Accessibility is OFF")
  if ok then
    hs.application.launchOrFocus("iPhone Mirroring")
    hs.timer.doAfter(0.7, function() hs.eventtap.keyStroke({"cmd"}, "1", 0) end)
  end
end)
LUA_EOF
ok "Config written to ~/.hammerspoon/init.lua"
say "Step 3/3 — App icon + launch"
LAUNCHER="$APPDIR/iPhone Control.app"
rm -rf "$LAUNCHER" 2>/dev/null || true
osacompile -o "$LAUNCHER" -e 'do shell script "open hammerspoon://show"' 2>/dev/null || osacompile -o "$LAUNCHER" -e 'tell application "Hammerspoon" to activate'
ok "Created 'iPhone Control' app in Applications."
open -a Hammerspoon || true
sleep 1
open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" || true
cat <<'DONE'

────────────────────────────────────────────
 ✅ Almost done — ONE manual step (Apple requires it):
 In the System Settings window that just opened
 (Privacy & Security ▸ Accessibility), turn ON "Hammerspoon".
 Then open "iPhone Control" from Applications and set your PIN.
────────────────────────────────────────────
DONE
