<?php

namespace Modules\VetStaffPortal\Services;

use Illuminate\Support\Facades\File;
use Symfony\Component\Process\Process;

/**
 * Canonical, safe rebuild of the staff PWA. Builds the committed source in
 * base_path('pwa-staff') into public_path('staff-app') using a temp-then-swap
 * so a failed build can never leave a broken/empty app. Mirrors PortalApi's
 * PwaBuilder.
 */
class StaffPwaBuilder
{
    /** @return array{ok:bool, output:string, duration:?float, hint:?string} */
    public static function build(): array
    {
        $source = base_path('pwa-staff');
        $final  = public_path('staff-app');
        $tmp    = public_path('staff-app__build_tmp');
        $backup = public_path('staff-app__prev');

        if (! is_dir($source) || ! is_file($source . DIRECTORY_SEPARATOR . 'package.json')) {
            return self::fail("PWA source not found at {$source}. Expected the 'pwa-staff' Vite project there.");
        }
        if (! is_dir($source . DIRECTORY_SEPARATOR . 'node_modules')) {
            return self::fail("Dependencies missing. Run `npm install` inside pwa-staff/ once, then rebuild.");
        }

        $isWin   = strncasecmp(PHP_OS, 'WIN', 3) === 0;
        $viteBin = $source . DIRECTORY_SEPARATOR . 'node_modules' . DIRECTORY_SEPARATOR
                 . '.bin' . DIRECTORY_SEPARATOR . ($isWin ? 'vite.cmd' : 'vite');
        if (! is_file($viteBin)) {
            return self::fail("vite binary not found at {$viteBin}. Run `npm install` inside pwa-staff/.");
        }

        if (is_dir($tmp)) {
            File::deleteDirectory($tmp);
        }

        // Call vite directly (skip vue-tsc) — the deploy only needs the bundle.
        $cmd = sprintf('%s build --outDir %s --emptyOutDir', self::quote($viteBin), self::quote($tmp));

        $process = Process::fromShellCommandline($cmd, $source, null, null, 600);
        $start   = microtime(true);
        try {
            $process->run();
        } catch (\Throwable $e) {
            self::cleanup($tmp);
            return self::fail('Could not start the build process: ' . $e->getMessage());
        }
        $duration = round(microtime(true) - $start, 1);
        $output   = self::tail($process->getOutput() . "\n" . $process->getErrorOutput());

        if (! $process->isSuccessful()) {
            self::cleanup($tmp);
            $hint = self::looksLikeMissingNode($output)
                ? 'The web server could not find Node.js on its PATH. Run `php artisan staff-portal:build-pwa` from a terminal where node/npm work.'
                : 'Build failed — see output below.';
            return ['ok' => false, 'output' => $output, 'duration' => $duration, 'hint' => $hint];
        }

        if (! is_file($tmp . DIRECTORY_SEPARATOR . 'index.html')) {
            self::cleanup($tmp);
            return ['ok' => false, 'output' => $output, 'duration' => $duration,
                    'hint' => 'Build reported success but produced no index.html.'];
        }

        try {
            if (is_dir($backup)) File::deleteDirectory($backup);
            if (is_dir($final))  rename($final, $backup);
            rename($tmp, $final);
            if (is_dir($backup)) File::deleteDirectory($backup);
        } catch (\Throwable $e) {
            if (! is_dir($final) && is_dir($backup)) @rename($backup, $final);
            self::cleanup($tmp);
            return ['ok' => false, 'output' => $output, 'duration' => $duration,
                    'hint' => 'Build succeeded but swapping into public/staff-app failed: ' . $e->getMessage()];
        }

        return ['ok' => true, 'output' => $output, 'duration' => $duration, 'hint' => null];
    }

    private static function quote(string $path): string { return '"' . str_replace('"', '', $path) . '"'; }
    private static function cleanup(string $dir): void { if (is_dir($dir)) File::deleteDirectory($dir); }
    private static function tail(string $text, int $max = 4000): string { $text = trim($text); return strlen($text) > $max ? '…' . substr($text, -$max) : $text; }
    private static function looksLikeMissingNode(string $output): bool {
        $o = strtolower($output);
        return str_contains($o, 'is not recognized') || str_contains($o, 'command not found') || (str_contains($o, 'node') && str_contains($o, 'not found'));
    }
    private static function fail(string $hint): array { return ['ok' => false, 'output' => '', 'duration' => null, 'hint' => $hint]; }
}
