<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   First-run setup. Detects the environment, generates the SSH keypair, populates the host
//   conf from discovered services, writes the initial master.conf and host*.conf, and — on a
//   partner server — pulls master.conf down from HOST1.
//
// OPERATIONAL MODEL
//   Two entirely different journeys share this file because they write the same files. HOST1
//   authors master.conf from the wizard's answers; every other host receives it over SCP and
//   never edits it. Which one runs is decided by the action, not by a mode setting.
//
//   Ordering is the substance of the save path. master.conf is written first because
//   host*.conf is generated from a template that needs the host slot; the SSH keypair is
//   generated before the API key because the key path is written into the conf that the API
//   key provisioning then reads back. Reordering these silently produces a half-configured
//   host.
//
//   Storage mode is decided here and acted on later. The response reports whether the chosen
//   mode implies a relocation, and the UI hands that to storage.php — this endpoint never
//   moves anything itself.
//
//   The host conf is only ever created, never overwritten. Every path checks file_exists()
//   first, so re-running the wizard on a configured host cannot destroy credentials that were
//   filled in afterwards.
//
// DESIGN PRINCIPLES
//   Detection is advisory; the wizard's answer wins.
//       Boot transport is auto-detected, but an explicit storage_mode parameter overrides it.
//       The detection is a good default, not a verdict — someone deliberately choosing appdata
//       on an internal boot device has a reason.
//
//   The partner path resolves HOST1's layout rather than assuming it.
//       Before the SCP, HOST1's own varaverk.cfg is read over SSH to find its SCRIPTS_DIR,
//       because the two hosts can legitimately be in different storage modes. Falling back to
//       the default path when that read fails is the right degradation — it is correct in the
//       common case.
//
//   The template is filled by substitution, not by generation.
//       HOSTN/hostn placeholders are replaced and two specific keys rewritten. Everything else
//       in host.conf.template — comments, structure, ordering, the keys not yet filled in —
//       arrives intact, which is what makes the generated conf readable and diffable.
//
//   Failures are named at the step that failed.
//       Missing SSH key, unresolvable Tailscale name, failed SCP and missing master.conf each
//       return their own message with the remedy in it. This is the one screen where the user
//       has no context yet, so a generic error is worth nothing.
//
// OPERATIONAL SAFEGUARDS
//   The host slot is pattern-matched on both paths.
//       ^host\d+$ before it is used to compose a conf filename or upper-cased into variable
//       names. That check is what stops 'unknown' — vv_detect_host()'s failure value — from
//       producing an unknown.conf.
//
//   Every script run is externally time-boxed.
//       `timeout` wraps ssh_setup.sh, conf_populate.sh, the remote cfg read and the SCP.
//       set_time_limit() does not cover exec() time on Linux, so without this an unreachable
//       HOST1 would hold a php-fpm worker open for as long as SSH kept trying. The SCP also
//       runs BatchMode=yes so it can never sit waiting for a password.
//
//   Missing scripts are reported, not executed.
//       file_exists() precedes every exec(), so a partial deploy returns a named error rather
//       than a shell failure that looks like a failed setup.
//
//   Conf values are escaped before substitution.
//       addslashes() on every hostname written into master.conf, so a pasted value containing
//       a quote cannot terminate the assignment and corrupt the keys after it.
//
//   Conf writes are atomic.
//       vv_write_conf_raw() (tmp + rename) for both files, and the master.conf write is
//       checked before the host conf is generated from it.
//
//   The setup state is read-modify-written.
//       vv_setup_state_write() replaces the file wholesale, so the save path reads the
//       existing state first. Passing only the one new key would erase the partnership phase
//       markers on a host that had already been onboarded and re-ran the wizard.
//
//   The host conf is never overwritten.
//       Both creation paths are guarded by file_exists(), so re-running setup on a configured
//       host is a no-op for that file rather than a credential wipe.
//
//   The SCP target is the local conf path, composed here — no part of the request names a
//   destination file.
//
//   Every action that changes anything is POST only, which is what places them behind
//   Unraid's CSRF guard.
//       The platform prepend validates the token on every POST and inspects no GET at all.
//       Only `detect` remains GET-reachable, and it is a pure read. This endpoint writes
//       credentials and runs setup scripts as root — that is its function, and it is why the
//       method boundary is the one that matters here.
//
// REQUEST
//   GET  ?action=detect          hostname, Unraid version, boot transport, suggested mode
//   POST action=ssh_generate     generate the local keypair, return the public key
//                                (POST, so Unraid's CSRF guard applies)
//   POST action=populate         run conf_populate.sh --no-push
//   POST action=pull             partner: SCP master.conf from HOST1, create host conf
//        [my_slot] [my_hostname] [host1_hostname]
//   POST (action=save, default)  HOST1: write master.conf and host conf
//        host1  [host2] [my_slot] [my_hostname] [storage_mode=internal|flash]
//
// RESPONSE
//   detect        {"ok":true,"hostname","unraid_ver","transport","boot_device","mode",
//                  "scripts_dir"}
//   ssh_generate  {"ok":bool,"pubkey":"…","error":…}
//   populate      {"ok":bool,"lines":[…]}
//   pull          {"ok":true,"host_id","conf_file","api_key":…,"redirect":"…"}
//   save          {"ok":true,"host_id","api_key":…,"needs_migration":bool,
//                  "migrate_to":"internal|flash"|null,"redirect":"…"}
//   {"ok":false,"error":…} naming the specific step that failed
//
// DEPENDS ON
//   include/config.php              vv_get_hostname(), vv_detect_host(), vv_read_conf_raw(),
//                                   vv_write_conf_raw(), vv_setup_state_read/_write(),
//                                   vv_auto_create_api_key(), CONF_DIR, DEPLOY_DIR
//   Partnership/ssh_setup.sh        keypair generation
//   Deployment/conf_populate.sh     service discovery
//   Deployment/host.conf.template   source of the generated host conf
//   api/storage.php                 performs the migration this endpoint only recommends
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
    ? trim($_GET['action'] ?? '')
    : trim($_POST['action'] ?? 'save');

// ── GET: detect environment ────────────────────────────────────────────────────────────────────
if ($action === 'detect') {
    $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
    $bootDisk  = $bootPart
        ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '')
        : '';
    $transport = $bootDisk
        ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: ''))
        : 'unknown';

    $isUsb = ($transport === 'usb');

    preg_match('/version="([^"]+)"/', @file_get_contents('/etc/unraid-version') ?: '', $vm);

    echo json_encode([
        'ok'           => true,
        'hostname'     => vv_get_hostname(),
        'unraid_ver'   => $vm[1] ?? 'unknown',
        'transport'    => $transport,
        'boot_device'  => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
        'mode'         => $isUsb ? 'flash' : 'internal',
        'scripts_dir'  => SCRIPTS_DIR,
    ]);
    exit;
}

// ── GET/POST: generate local SSH keypair ──────────────────────────────────────────────────────
if ($action === 'ssh_generate') {
    // POST only. This generates a keypair, and Unraid's CSRF prepend validates POSTs while
    // ignoring GETs entirely — over GET it would run with no token check at all.
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        http_response_code(405);
        echo json_encode(['ok' => false, 'error' => 'POST only']);
        exit;
    }
    $script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']);
        exit;
    }
    // set_time_limit() does not cover exec() time on Linux — every script run below is
    // wrapped in `timeout` so a stalled child cannot hold a php-fpm worker open.
    exec('timeout 120 bash ' . escapeshellarg($script) . ' --local-only 2>&1', $out, $rc);
    // Derive pubkey path from hostname
    $hostname  = vv_get_hostname();
    $shortName = strtolower(preg_replace('/^unraid-/i', '', $hostname));
    $pubPath   = '/root/.ssh/' . $shortName . '_rsync_automation.pub';
    $pubKey    = trim(@file_get_contents($pubPath) ?: '');
    echo json_encode([
        'ok'     => $rc === 0 && !empty($pubKey),
        'pubkey' => $pubKey,
        'error'  => ($rc !== 0) ? implode(' ', array_slice(array_filter(array_map('trim', $out)), -3)) : null,
    ]);
    exit;
}

// ── POST: run conf_populate.sh ─────────────────────────────────────────────────────────────────
if ($action === 'populate') {
    $script = DEPLOY_DIR . '/conf_populate.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'conf_populate.sh not found']);
        exit;
    }
    exec('timeout 180 bash ' . escapeshellarg($script) . ' --no-push 2>&1', $out, $rc);
    $lines = array_values(array_filter(array_map('trim', $out)));
    echo json_encode(['ok' => $rc === 0, 'lines' => array_slice($lines, 0, 20)]);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
    exit;
}

$sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';

// ── Pull master.conf from HOST1 via SSH (wizard or checklist) ────────────────────────────────
if ($action === 'pull') {
    $mySlot        = trim($_POST['my_slot']        ?? '') ?: strtolower(vv_detect_host());
    $myHostname    = trim($_POST['my_hostname']     ?? '') ?: vv_get_hostname();
    $host1Hostname = trim($_POST['host1_hostname']  ?? '');
    if (!$host1Hostname) {
        $masterRaw = vv_read_conf_raw('master.conf');
        preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $masterRaw, $_mh);
        $host1Hostname = trim($_mh[1] ?? '');
    }
    if (!$host1Hostname) {
        echo json_encode(['ok' => false, 'error' => 'HOST1 hostname not set — fill in master.conf first']);
        exit;
    }
    if (!preg_match('/^host\d+$/', $mySlot)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
        exit;
    }

    $hostId    = strtoupper($mySlot);
    $hostIdLow = strtolower($mySlot);

    // Derive SSH key path from this server's hostname
    $sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname ?: vv_get_hostname()));
    $sshKey   = '/root/.ssh/' . $sshOwner . '_rsync_automation';

    if (!file_exists($sshKey)) {
        echo json_encode(['ok' => false, 'error' =>
            "SSH key not found at $sshKey — run Partnership/ssh_setup.sh first"]);
        exit;
    }

    // Resolve HOST1 Tailscale IP
    $ip = trim(shell_exec('tailscale ip -4 ' . escapeshellarg($host1Hostname) . ' 2>/dev/null') ?: '');
    if (!$ip) {
        echo json_encode(['ok' => false, 'error' =>
            "Cannot resolve Tailscale IP for $host1Hostname — is Tailscale running on both servers?"]);
        exit;
    }

    // Get HOST1's SCRIPTS_DIR from their varaverk.cfg
    $sshBase   = 'ssh -i ' . escapeshellarg($sshKey)
               . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
    $remoteCfg = trim(shell_exec('timeout 30 ' . $sshBase . ' "grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null"') ?: '');
    preg_match('/SCRIPTS_DIR\s*=\s*["\']?([^"\']+)["\']?/', $remoteCfg, $sm);
    $remoteConf = rtrim($sm[1] ?? '/boot/config/plugins/varaverk', '/') . '/Configurations';

    // SCP master.conf from HOST1
    $localMaster = CONF_DIR . '/master.conf';
    $src  = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf');
    $cmd  = 'timeout 60 scp -i ' . escapeshellarg($sshKey)
          . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o BatchMode=yes'
          . ' ' . $src . ' ' . escapeshellarg($localMaster) . ' 2>&1';
    exec($cmd, $out, $rc);
    if ($rc !== 0) {
        echo json_encode(['ok' => false, 'error' =>
            'SCP failed: ' . implode('; ', $out) .
            ' — ensure your SSH key is authorised on HOST1 (run Partnership/ssh_setup.sh)']);
        exit;
    }

    // Create host conf from template if it doesn't exist
    $confFile = $hostIdLow . '.conf';
    if (!file_exists(CONF_DIR . '/' . $confFile)) {
        $template = @file_get_contents(DEPLOY_DIR . '/host.conf.template') ?: '';
        if ($template) {
            $bootPart2  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
            $bootDisk2  = $bootPart2 ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart2) . ' 2>/dev/null') ?: '') : '';
            $transport2 = $bootDisk2 ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk2) . ' 2>/dev/null') ?: '')) : '';
            $storageInternal2 = ($transport2 !== 'usb') ? 'true' : 'false';
            $conf = str_replace('HOSTN', $hostId,    $template);
            $conf = str_replace('hostn', $hostIdLow, $conf);
            $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
                '${1}"' . $sshKey . '"', $conf);
            $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
                '${1}' . $storageInternal2, $conf);
            vv_write_conf_raw($confFile, $conf);
        }
    }

    if (file_exists($sshScript)) {
        exec('timeout 120 bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
    }
    $apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

    $state = vv_setup_state_read();
    $state['master_conf_pulled'] = 'true';
    vv_setup_state_write($state);

    echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
                      'api_key'  => $apiKeyResult,
                      'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
    exit;
}

// ── Default action: save (HOST1 first-run wizard) ────────────────────────────────────────────
$host1      = trim($_POST['host1']      ?? '');
$host2      = trim($_POST['host2']      ?? '');
$mySlot     = trim($_POST['my_slot']    ?? 'host1');
$myHostname = trim($_POST['my_hostname'] ?? '');

if (empty($host1)) {
    echo json_encode(['ok' => false, 'error' => 'HOST1 hostname is required']);
    exit;
}
if (!preg_match('/^host\d+$/', $mySlot)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
    exit;
}

// Write HOST1 / HOST2 into master.conf
$master = vv_read_conf_raw('master.conf');
if ($master === '') {
    echo json_encode(['ok' => false, 'error' => 'master.conf not found — check SCRIPTS_DIR in varaverk.cfg']);
    exit;
}

$master = preg_replace('/^(\s*HOST1\s*=\s*).*$/m', '${1}"' . addslashes($host1) . '"', $master);
$master = preg_replace('/^(\s*HOST2\s*=\s*).*$/m', '${1}"' . addslashes($host2) . '"', $master);

$slotNum = (int) preg_replace('/\D/', '', $mySlot);
if ($slotNum > 2 && !empty($myHostname)) {
    $hostKey = 'HOST' . $slotNum;
    if (!preg_match('/^\s*' . $hostKey . '\s*=/m', $master)) {
        $master = preg_replace('/^(\s*HOST2\s*=.*$)/m',
            '$1' . "\n    {$hostKey}=\"" . addslashes($myHostname) . '"', $master);
    } else {
        $master = preg_replace('/^(\s*' . $hostKey . '\s*=\s*).*$/m',
            '${1}"' . addslashes($myHostname) . '"', $master);
    }
}

if (!vv_write_conf_raw('master.conf', $master)) {
    echo json_encode(['ok' => false, 'error' => 'Failed to write master.conf']);
    exit;
}

// Create host*.conf from template
$hostId    = strtoupper($mySlot);
$hostIdLow = strtolower($mySlot);
$confFile  = $hostIdLow . '.conf';

// Storage mode: use wizard selection, fall back to auto-detect from boot transport
$smParam = trim($_POST['storage_mode'] ?? '');
if ($smParam === 'flash') {
    $storageInternal = 'false';
} elseif ($smParam === 'internal') {
    $storageInternal = 'true';
} else {
    $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
    $bootDisk  = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
    $transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
    $storageInternal = ($transport !== 'usb') ? 'true' : 'false';
}

if (!file_exists(CONF_DIR . '/' . $confFile)) {
    $template = @file_get_contents(DEPLOY_DIR . '/host.conf.template') ?: '';
    if ($template) {
        $sshOwner   = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
        $sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';

        $conf = str_replace('HOSTN', $hostId,    $template);
        $conf = str_replace('hostn', $hostIdLow, $conf);
        $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
            '${1}"' . $sshKeyPath . '"', $conf);
        $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
            '${1}' . $storageInternal, $conf);
        if (!vv_write_conf_raw($confFile, $conf)) {
            echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
            exit;
        }
    }
}

// Write setup state file — lets partner servers know HOST1 is configured.
// Read-modify-write: vv_setup_state_write() replaces the file wholesale, and re-running the
// wizard must not erase onboarding progress recorded by the partnership phases.
$state = vv_setup_state_read();
$state['host1_hostname'] = $host1;
vv_setup_state_write($state);

// Auto-generate SSH keypair (local only — remote copy happens during onboarding)
if (file_exists($sshScript)) {
    exec('timeout 120 bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
}

// Auto-create Unraid API key and write into the fresh conf
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

$targetDir      = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk';
$needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir);

echo json_encode([
    'ok'             => true,
    'host_id'        => $hostId,
    'api_key'        => $apiKeyResult,
    'needs_migration'=> $needsMigration,
    'migrate_to'     => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null,
    'redirect'       => '?tab=scheduler&vv_setup=master.conf',
]);
