<?php

namespace Modules\PortalApi\Services;

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

/**
 * Canonical, safe rebuild of the pet-owner PWA.
 *
 * This is the ONE true way to (re)deploy the PWA. It always builds the
 * committed source in  base_path('pwa-owner')  into  public_path('app')  —
 * removing the long-standing footgun where the app "reverted to old" because
 * someone ran `npm run build` from a stale copy / wrong directory, or vite's
 * relative outDir wrote to the wrong public/app.
 *
 * Safety: the build goes to a temp dir first and is only swapped into the live
 * public/app on success, so a failed build can never leave a broken/empty PWA.
 *
 * Used by:
 *   - artisan  portal:build-pwa   (reliable from a shell where node is on PATH)
 *   - the "Rebuild app now" button on the VetClinic → Portal Brand settings page
 */
class PwaBuilder
{
    /**
     * @return array{ok:bool, output:string, duration:?float, hint:?string}
     */
    public static function build(): array
    {
        $source = base_path('pwa-owner');
        $final  = public_path('app');
        $tmp    = public_path('app__build_tmp');
        $backup = public_path('app__prev');

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

        // --- Resolve the local vite binary -----------------------------------
        $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-owner/.");
        }

        // Clean any leftover temp from a previous interrupted build.
        if (is_dir($tmp)) {
            File::deleteDirectory($tmp);
        }

        // --- Run the build into the TEMP dir (never the live dir) -------------
        // We call vite directly (not `npm run build`) on purpose: the package
        // script also runs `vue-tsc --noEmit`, which can fail/stall on a type
        // error and block a deploy. The deploy only needs the bundle.
        // base path comes from pwa-owner/.env.local (VITE_BASE_URL).
        $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 portal:build-pwa` from a terminal instead (where node/npm work), or add Node to the service PATH.'
                : 'Build failed — see output below.';
            return ['ok' => false, 'output' => $output, 'duration' => $duration, 'hint' => $hint];
        }

        // Sanity: a real build must have produced an index.html.
        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.'];
        }

        // --- Swap temp → live, keeping a backup so we never lose the old one --
        try {
            if (is_dir($backup)) {
                File::deleteDirectory($backup);
            }
            if (is_dir($final)) {
                rename($final, $backup);     // move old live out of the way
            }
            rename($tmp, $final);            // promote the fresh build
            if (is_dir($backup)) {
                File::deleteDirectory($backup);
            }
        } catch (\Throwable $e) {
            // Best-effort restore of the previous live build.
            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/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')      // Windows cmd
            || str_contains($o, 'command not found')        // *nix
            || str_contains($o, 'node')  && str_contains($o, 'not found');
    }

    private static function fail(string $hint): array
    {
        return ['ok' => false, 'output' => '', 'duration' => null, 'hint' => $hint];
    }
}
