<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Docker page backend. Owns Varaverk's container-folder grouping, builds the container
//   inventory the page renders, and keeps the folder map in sync with host*.conf so the
//   onboarding scripts see the same grouping.
//
// DESIGN PRINCIPLES
//   Varaverk owns its own folder store.
//       docker_folders.json is the primary record and has no external dependency. The
//       folder.view3 plugin is synced to only when it is actually installed, so Varaverk's
//       grouping survives that plugin being absent, removed, or reset.
//
//   The conf map is the interface to the scripts.
//       HOST*_DOCKER_FOLDER_MAP is written so onboarding and partnership scripts can act on
//       the same grouping the UI shows, without parsing a UI-owned JSON file.
//
//   Inventory is built from docker inspect in one pass.
//       vv_dk_inspect_all() collects everything once rather than per-container, because this
//       runs on every page load.
//
// OPERATIONAL SAFEGUARDS
//   JSON writes are atomic.
//       Written to .vv.tmp then rename()d into place, so a reader or a concurrent write never
//       observes a truncated folder store — losing it would scatter every container back to
//       ungrouped.
//
//   The optional folder.view3 sync is best-effort and never fatal.
//       Its write is suppressed and its failure ignored. A second plugin's file must not be
//       able to fail a Varaverk operation.
//
//   A missing store reads as empty, not as an error.
//       First run and a deleted file behave identically — no folders yet, page renders.
//
// EXPORTS
//   Store      vv_dk_read_json(), vv_dk_write_json(), vv_dk_gen_id()
//   Conf map   vv_dk_read_conf_map(), vv_dk_write_conf_map(),
//              vv_dk_sync_conf_to_json(), vv_dk_sync_json_to_conf()
//   Folders    vv_dk_create_folder(), vv_dk_rename_folder(), vv_dk_delete_folder(),
//              vv_dk_move_container()
//   Inventory  vv_dk_all(), vv_dk_inspect_all(), vv_dk_webui(), vv_dk_icon()
//
// CONFIGURATION
//   VV_DOCKER_JSON            /boot/config/plugins/varaverk/docker_folders.json — primary
//   VV_FV3_JSON               folder.view3's docker.json — synced only if present
//   HOST*_DOCKER_FOLDER_MAP   conf mirror consumed by the onboarding scripts
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Docker tab — folder management and container inventory
// Primary store: /boot/config/plugins/varaverk/docker_folders.json (Varaverk-owned, no external deps)
// Optional sync: /boot/config/plugins/folder.view3/docker.json (only if folder.view3 is installed)
// Reads/writes HOST*_DOCKER_FOLDER_MAP in host*.conf (used by onboard scripts)

require_once __DIR__ . '/config.php';

define('VV_DOCKER_JSON',    SCRIPTS_DIR . '/docker_folders.json');
define('VV_FV3_JSON',       '/boot/config/plugins/folder.view3/docker.json');

// ── JSON helpers ──────────────────────────────────────────────────────────────

function vv_dk_read_json(): array {
    // Bootstrap: if our file doesn't exist yet but folder.view3's does, import it once
    if (!file_exists(VV_DOCKER_JSON) && file_exists(VV_FV3_JSON)) {
        $imported = json_decode(file_get_contents(VV_FV3_JSON), true) ?: [];
        vv_dk_write_json($imported);
        return $imported;
    }
    if (!file_exists(VV_DOCKER_JSON)) return [];
    return json_decode(file_get_contents(VV_DOCKER_JSON), true) ?: [];
}

function vv_dk_write_json(array $data): bool {
    $path = VV_DOCKER_JSON;
    @mkdir(dirname($path), 0755, true);
    $tmp = $path . '.vv.tmp';
    if (file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_SLASHES)) === false) return false;
    if (!rename($tmp, $path)) return false;

    // Mirror to folder.view3 if installed — keeps both in sync for users who want both UIs
    if (file_exists(dirname(VV_FV3_JSON))) {
        $t = VV_FV3_JSON . '.vv.tmp';
        @file_put_contents($t, json_encode($data, JSON_UNESCAPED_SLASHES));
        @rename($t, VV_FV3_JSON);
    }
    return true;
}

function vv_dk_gen_id(): string {
    return substr(str_replace(['+', '/', '='], ['', '', ''], base64_encode(random_bytes(15))), 0, 20);
}

// ── Conf helpers ──────────────────────────────────────────────────────────────

// Read HOST*_DOCKER_FOLDER_MAP from raw conf text.
// Returns ['ContainerName' => 'Folder Name', ...]
function vv_dk_read_conf_map(string $raw, string $id): array {
    $varname = "{$id}_DOCKER_FOLDER_MAP";
    // Match: declare -A VARNAME=( ... ) — may span multiple lines
    if (!preg_match('/declare\s+-A\s+' . preg_quote($varname, '/') . '\s*=\s*\(\s*(.*?)\s*\)/s', $raw, $m))
        return [];
    preg_match_all('/\["([^"]+)"\]\s*=\s*"([^"]*)"/', $m[1], $pairs);
    $out = [];
    foreach ($pairs[1] as $i => $k) $out[$k] = $pairs[2][$i];
    return $out;
}

// Write HOST*_DOCKER_FOLDER_MAP back into raw conf text. Returns updated text.
function vv_dk_write_conf_map(string $raw, string $id, array $map): string {
    $varname = "{$id}_DOCKER_FOLDER_MAP";
    ksort($map);

    $lines = ["    declare -A {$varname}=("];
    foreach ($map as $container => $folder) {
        $lines[] = '        ["' . $container . '"]="' . $folder . '"';
    }
    $lines[] = '    )';
    $block = implode("\n", $lines);

    // Replace existing block
    $pattern = '/[ \t]*declare\s+-A\s+' . preg_quote($varname, '/') . '\s*=\s*\(.*?\)/s';
    if (preg_match($pattern, $raw)) {
        return preg_replace($pattern, $block, $raw);
    }

    // Append before the "End Of HOST*" comment if present, otherwise at end
    $endMarker = "# ── End Of {$id}";
    $altMarker = "# ──────────────────────── End Of {$id}";
    foreach ([$altMarker, $endMarker] as $marker) {
        $pos = strpos($raw, $marker);
        if ($pos !== false) {
            $header = "\n# ── Docker Folder Map — managed by Varaverk Docker tab ──────────────────────\n";
            return substr($raw, 0, $pos) . $header . $block . "\n\n" . substr($raw, $pos);
        }
    }
    return $raw . "\n# ── Docker Folder Map ────────────────────────────────────────────────────────\n" . $block . "\n";
}

// ── Container inventory (full inspect) ───────────────────────────────────────

function vv_dk_webui(string $name, array $portMap): string {
    $template = '/boot/config/plugins/dockerMan/templates-user/my-' . $name . '.xml';
    if (!file_exists($template)) return '';
    $xml = @file_get_contents($template) ?: '';
    if (!preg_match('/<WebUI>(.*?)<\/WebUI>/s', $xml, $m)) return '';
    $url = trim($m[1]);
    if (!$url) return '';
    $url = str_replace('[IP]', vv_local_ip(), $url);
    $url = preg_replace_callback('/\[PORT:(\d+)\]/', fn($pm) => $portMap[$pm[1]] ?? $pm[1], $url);
    return $url;
}

function vv_dk_icon(string $name): string {
    $template = '/boot/config/plugins/dockerMan/templates-user/my-' . $name . '.xml';
    if (!file_exists($template)) return '';
    $xml = @file_get_contents($template) ?: '';
    return preg_match('/<Icon>(.*?)<\/Icon>/s', $xml, $m) ? trim($m[1]) : '';
}

function vv_dk_inspect_all(): array {
    $ids = trim(shell_exec("docker ps -aq 2>/dev/null") ?: '');
    if (!$ids) return [];

    $idList = implode(' ', array_map('escapeshellarg', explode("\n", $ids)));
    $raw    = shell_exec("docker inspect $idList 2>/dev/null") ?: '[]';
    $data   = json_decode($raw, true) ?: [];

    $out = [];
    foreach ($data as $c) {
        $name    = ltrim($c['Name'] ?? '', '/');
        if (!$name) continue;
        $status  = $c['State']['Status'] ?? 'unknown';
        $running = $status === 'running';
        $image   = $c['Config']['Image'] ?? '';

        // Networks: name → IP
        $networks = [];
        foreach ($c['NetworkSettings']['Networks'] ?? [] as $netName => $net) {
            $ip = $net['IPAddress'] ?? '';
            if ($ip) $networks[$netName] = $ip;
        }

        // Port mappings: hostPort → containerPort/proto
        $ports    = [];
        $portMap  = [];  // containerPort → hostPort (for WebUI resolution)
        foreach ($c['HostConfig']['PortBindings'] ?? [] as $containerPort => $bindings) {
            foreach ($bindings ?? [] as $b) {
                $hp = $b['HostPort'] ?? '';
                if ($hp) {
                    $ports[] = $hp . '→' . $containerPort;
                    [$cp] = explode('/', $containerPort);
                    $portMap[$cp] = $hp;
                }
            }
        }

        // Bind mounts only
        $mounts = [];
        foreach ($c['Mounts'] ?? [] as $m) {
            if (($m['Type'] ?? '') === 'bind') {
                $mounts[] = ['src' => $m['Source'] ?? '', 'dst' => $m['Destination'] ?? ''];
            }
        }

        $webui = vv_dk_webui($name, $portMap);
        $icon  = vv_dk_icon($name);

        $out[$name] = [
            'name'     => $name,
            'running'  => $running,
            'status'   => $status,
            'image'    => $image,
            'networks' => $networks,
            'ports'    => $ports,
            'mounts'   => $mounts,
            'webui'    => $webui,
            'icon'     => $icon,
        ];
    }
    return $out;
}

// ── Main data builder ─────────────────────────────────────────────────────────

function vv_dk_all(): array {
    $jsonData   = vv_dk_read_json();
    $containers = vv_dk_inspect_all();
    $currentHost= vv_detect_host();
    $myId       = strtoupper($currentHost);
    $myRaw      = vv_read_conf_raw($currentHost . '.conf');
    $confMap    = vv_dk_read_conf_map($myRaw, $myId);

    $empty = ['name'=>'','running'=>false,'status'=>'','image'=>'','networks'=>[],'ports'=>[],'mounts'=>[],'webui'=>'','icon'=>''];

    // Build folder list enriched with live container data
    $assigned = [];
    $folders  = [];
    foreach ($jsonData as $fid => $f) {
        $ctrs = [];
        foreach ($f['containers'] ?? [] as $cname) {
            $assigned[$cname] = true;
            $ctrs[] = array_merge($empty, ['name' => $cname], $containers[$cname] ?? []);
        }
        usort($ctrs, fn($a, $b) => $b['running'] <=> $a['running'] ?: strcmp($a['name'], $b['name']));
        $running = count(array_filter($ctrs, fn($c) => $c['running']));
        $folders[] = [
            'id'         => $fid,
            'name'       => $f['name'] ?? 'Unnamed',
            'total'      => count($ctrs),
            'running'    => $running,
            'containers' => $ctrs,
        ];
    }
    usort($folders, fn($a, $b) => strcmp($a['name'], $b['name']));

    // Ungrouped: running but not in any folder
    $ungrouped = [];
    foreach ($containers as $name => $c) {
        if (isset($assigned[$name])) continue;
        $ungrouped[] = $c;
    }
    usort($ungrouped, fn($a, $b) => strcmp($a['name'], $b['name']));

    // Drift: containers in conf map but assigned to a different folder in json, or missing from json
    $drift = [];
    foreach ($confMap as $cname => $desiredFolder) {
        // Find where it actually is in json
        $actualFolder = null;
        foreach ($jsonData as $f) {
            if (in_array($cname, $f['containers'] ?? [], true)) {
                $actualFolder = $f['name'];
                break;
            }
        }
        if ($actualFolder !== $desiredFolder) {
            $drift[] = ['container' => $cname, 'conf' => $desiredFolder, 'actual' => $actualFolder];
        }
    }

    return [
        'ts'        => time(),
        'host'      => $myId,
        'folders'   => $folders,
        'ungrouped' => $ungrouped,
        'conf_map'  => $confMap,
        'drift'     => $drift,
        'fv3_synced'=> file_exists(dirname(VV_FV3_JSON)),
    ];
}

// ── Write actions ─────────────────────────────────────────────────────────────

// Move a container to a folder (or '' to ungroup). Writes json + conf.
function vv_dk_move_container(string $container, string $targetFolderId): array {
    if (!$container) return ['ok' => false, 'error' => 'No container'];
    $data = vv_dk_read_json();

    // Remove from all folders
    foreach ($data as &$f) {
        $f['containers'] = array_values(array_filter($f['containers'] ?? [], fn($c) => $c !== $container));
        if (isset($f['containerImages'][$container])) unset($f['containerImages'][$container]);
    }
    unset($f);

    // Add to target folder
    if ($targetFolderId && isset($data[$targetFolderId])) {
        if (!in_array($container, $data[$targetFolderId]['containers'] ?? [], true)) {
            $data[$targetFolderId]['containers'][] = $container;
        }
    }

    if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];

    // Update conf
    _vv_dk_sync_conf_from_json($data);
    return ['ok' => true];
}

// Create a new folder. Returns new folder id.
function vv_dk_create_folder(string $name): array {
    $name = trim($name);
    if (!$name) return ['ok' => false, 'error' => 'Name required'];
    $data = vv_dk_read_json();
    $id   = vv_dk_gen_id();
    $data[$id] = [
        'name'            => $name,
        'icon'            => '',
        'settings'        => [],
        'regex'           => '',
        'containers'      => [],
        'containerImages' => [],
        'hidden_preview'  => [],
        'actions'         => [],
    ];
    if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
    return ['ok' => true, 'id' => $id];
}

// Rename a folder. Updates json + conf (folder name strings in map).
function vv_dk_rename_folder(string $folderId, string $newName): array {
    $newName = trim($newName);
    if (!$newName || !$folderId) return ['ok' => false, 'error' => 'Missing params'];
    $data = vv_dk_read_json();
    if (!isset($data[$folderId])) return ['ok' => false, 'error' => 'Folder not found'];
    $oldName = $data[$folderId]['name'];
    $data[$folderId]['name'] = $newName;
    if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];

    // Update conf: replace old folder name with new name in the map
    $currentHost = vv_detect_host();
    $myId        = strtoupper($currentHost);
    $raw         = vv_read_conf_raw($currentHost . '.conf');
    $map         = vv_dk_read_conf_map($raw, $myId);
    foreach ($map as &$v) {
        if ($v === $oldName) $v = $newName;
    }
    unset($v);
    $updated = vv_dk_write_conf_map($raw, $myId, $map);
    vv_write_conf_raw($currentHost . '.conf', $updated);
    return ['ok' => true];
}

// Delete a folder (containers become ungrouped). Writes json + conf.
function vv_dk_delete_folder(string $folderId): array {
    if (!$folderId) return ['ok' => false, 'error' => 'No folder id'];
    $data = vv_dk_read_json();
    if (!isset($data[$folderId])) return ['ok' => false, 'error' => 'Folder not found'];
    $folderName = $data[$folderId]['name'];
    unset($data[$folderId]);
    if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];

    // Remove from conf map
    $currentHost = vv_detect_host();
    $myId        = strtoupper($currentHost);
    $raw         = vv_read_conf_raw($currentHost . '.conf');
    $map         = vv_dk_read_conf_map($raw, $myId);
    $map         = array_filter($map, fn($v) => $v !== $folderName);
    $updated     = vv_dk_write_conf_map($raw, $myId, $map);
    vv_write_conf_raw($currentHost . '.conf', $updated);
    return ['ok' => true];
}

// Sync conf desired state → json (apply conf map, fix drift)
function vv_dk_sync_conf_to_json(): array {
    $currentHost = vv_detect_host();
    $myId        = strtoupper($currentHost);
    $raw         = vv_read_conf_raw($currentHost . '.conf');
    $confMap     = vv_dk_read_conf_map($raw, $myId);
    $data        = vv_dk_read_json();

    // Build folder name → id map
    $nameToId = [];
    foreach ($data as $fid => $f) $nameToId[$f['name']] = $fid;

    foreach ($confMap as $container => $folderName) {
        // Create folder if missing
        if (!isset($nameToId[$folderName])) {
            $newId = vv_dk_gen_id();
            $data[$newId] = ['name' => $folderName, 'icon' => '', 'settings' => [],
                             'regex' => '', 'containers' => [], 'containerImages' => [],
                             'hidden_preview' => [], 'actions' => []];
            $nameToId[$folderName] = $newId;
        }
        $targetId = $nameToId[$folderName];

        // Remove from all folders
        foreach ($data as &$f) {
            $f['containers'] = array_values(array_filter($f['containers'] ?? [], fn($c) => $c !== $container));
        }
        unset($f);

        // Add to target
        if (!in_array($container, $data[$targetId]['containers'], true))
            $data[$targetId]['containers'][] = $container;
    }

    if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
    return ['ok' => true, 'applied' => count($confMap)];
}

// Sync json current state → conf (capture manual json edits)
function vv_dk_sync_json_to_conf(): array {
    $data        = vv_dk_read_json();
    $currentHost = vv_detect_host();
    $myId        = strtoupper($currentHost);
    $raw         = vv_read_conf_raw($currentHost . '.conf');
    _vv_dk_sync_conf_from_json($data, $raw, $currentHost, $myId);
    return ['ok' => true];
}

// Internal: rebuild conf map from current json state and write it
function _vv_dk_sync_conf_from_json(array $data, string $raw = '', string $host = '', string $id = ''): void {
    if (!$host) $host = vv_detect_host();
    if (!$id)   $id   = strtoupper($host);
    if (!$raw)  $raw  = vv_read_conf_raw($host . '.conf');

    $map = [];
    foreach ($data as $f) {
        $name = $f['name'] ?? '';
        foreach ($f['containers'] ?? [] as $c) $map[$c] = $name;
    }
    $updated = vv_dk_write_conf_map($raw, $id, $map);
    vv_write_conf_raw($host . '.conf', $updated);
}
