<?php

declare(strict_types=1);

namespace PpPhp\Modules\Projects\Controllers;

use PpPhp\Core\Build\BuildProtocol;
use PpPhp\Core\Build\BuildStaging;
use PpPhp\Core\Licensing\LicenseKeys;
use PpPhp\Core\Licensing\LicenseServer;
use PpPhp\Core\Update\UpdateProtocol;

/**
 * The BUILD AUTHORITY endpoint — the server side of server-side builds.
 *
 * A licensed {@see \PpPhp\Core\Build\BuildClient} posts a project's design here;
 * this authenticates the licence exactly as the template feed does, runs the real
 * generator on the design ({@see BuildStaging}), signs the result with the
 * server's Ed25519 key, and returns it. The generator never leaves this machine.
 *
 * Server-only: excluded from client bundles (serverOnlyFiles) and its route
 * dropped from client manifests (serverOnlyRoutePrefixes '/api/build') — a
 * customer copy can post a build, never answer one.
 *
 * SCAFFOLD — auth + generate + sign is complete; the production surface (per-
 * licence build metering / quota, a queue instead of an inline build) is flagged.
 */
class BuildApiController
{
    /** @return array<string,mixed> */
    private function input(): array
    {
        $raw = file_get_contents('php://input');
        $d = json_decode((string) $raw, true);
        return is_array($d) ? $d : $_POST;
    }

    private function json(array $payload, int $status = 200): never
    {
        http_response_code($status);
        header('Content-Type: application/json');
        echo json_encode($payload);
        exit;
    }

    /** POST /api/build/generate — build a caller's design, return a signed result. */
    public function generate(): never
    {
        @set_time_limit(0);
        $in = $this->input();

        $keys = new LicenseKeys();
        if (!$keys->exists()) {
            $this->json(['ok' => true, 'configured' => false, 'message' => 'This server is not configured to run builds.']);
        }
        if (!(new LicenseServer($keys))->isEntitled((string) ($in['instanceId'] ?? ''), (string) ($in['secret'] ?? ''))) {
            $this->json(['ok' => true, 'configured' => true, 'licensed' => false,
                'message' => 'A genuine, active licence is required to build. Check your licence status, then try again.']);
        }

        $instanceId = (string) ($in['instanceId'] ?? '');

        // Abuse guard: attribute this build to the instance and enforce the owner's
        // optional daily cap. Fail-open — a metering glitch never blocks a build.
        $quota = \PpPhp\Core\Build\BuildMeter::allow($instanceId);
        if (!$quota['ok']) {
            $this->json(['ok' => true, 'configured' => true, 'licensed' => true,
                'message' => $quota['message']]);
        }

        $design  = is_array($in['design'] ?? null) ? $in['design'] : [];
        $custom  = is_array($in['custom'] ?? null) ? $in['custom'] : [];
        $options = is_array($in['options'] ?? null) ? $in['options'] : [];
        // What the client already holds (for a delta reply): {path: sha256}. Kept
        // out of the cache key on purpose — see BuildStaging::run.
        $have    = is_array($in['have'] ?? null) ? $in['have'] : [];

        // Per-licence watermark: a short fingerprint DERIVED from the instance id
        // (never the id itself) that the generator stamps into the output, so a
        // leaked build traces back to the licence it was built under. We can recompute
        // this for any known instance to match a build found in the wild.
        $options['wm'] = ($instanceId !== '' && \PpPhp\Core\Build\BuildMeter::watermarkEnabled())
            ? substr(hash('sha256', 'ppb|' . $instanceId), 0, 16) : '';

        // This server must actually carry the code generator to build. If it does
        // not (a broken/partial install, or a copy that was never provisioned as a
        // build server), say so CLEANLY as an unsigned, retryable "not configured"
        // answer — the client then fails over to a healthy build server instead of
        // receiving a signed "Class BuildWriter not found" it cannot recover from.
        if (!class_exists(\PpPhp\Core\Build\BuildWriter::class)
            || !class_exists(\PpPhp\Core\Build\ComponentRenderer::class)) {
            $this->json(['ok' => true, 'configured' => false, 'buildable' => false, 'licensed' => true,
                'message' => 'This server is not set up to run builds (its code generator is not installed). '
                    . 'Trying another build server.']);
        }

        $__t0 = microtime(true);
        try {
            $out = BuildStaging::run($design, $custom, $options, $have);
        } catch (\Throwable $e) {
            $this->json(['ok' => false, 'configured' => true, 'licensed' => true,
                'message' => 'The build could not be completed on the server: ' . $e->getMessage()], 500);
        }
        $__durationMs = (int) round((microtime(true) - $__t0) * 1000);

        // Sign the whole result so the client can prove it came from us, unaltered.
        // `assets` is the list of static vendor files omitted from the ZIP for the
        // client to fill from its own public/assets — it rides inside the signature
        // so a man-in-the-middle cannot make the client copy in unexpected paths.
        // `manifest` is the {path: sha256} integrity map of the WHOLE finished app
        // (packed files + re-filled static assets). Signed, so the client can prove
        // the materialised app is byte-exact and reject any corruption.
        $token = UpdateProtocol::sign([
            'success'  => (bool) $out['success'],
            'message'  => (string) $out['message'],
            'zip'      => (string) ($out['zip'] ?? ''),
            'files'    => (int) ($out['filesGenerated'] ?? 0),
            'assets'   => array_values($out['assetsOmitted'] ?? []),
            'manifest' => is_array($out['manifest'] ?? null) ? $out['manifest'] : [],
            // Delta reply: only changed files are in `zip`; `deleted` names files the
            // client must remove. Absent/false => `zip` is the whole app.
            'delta'    => (bool) ($out['delta'] ?? false),
            'deleted'  => array_values($out['deleted'] ?? []),
            'cached'   => (bool) ($out['cached'] ?? false),
            'iat'      => time(),
        ], $keys->secretKey(), BuildProtocol::PREFIX);

        // Attribute the completed build to this instance (rollups + durable event).
        if (!empty($out['success'])) {
            \PpPhp\Core\Build\BuildMeter::record(
                $instanceId,
                strlen((string) ($out['zip'] ?? '')),
                (bool) ($out['cached'] ?? false),
                [
                    'kind'       => !empty($out['delta']) ? 'delta' : 'full',
                    'files'      => (int) ($out['filesGenerated'] ?? 0),
                    // A cache hit isn't a real generator run — don't skew build-time stats.
                    'durationMs' => empty($out['cached']) ? $__durationMs : 0,
                    'ok'         => true,
                ]
            );
        }

        $this->json(['ok' => true, 'configured' => true, 'licensed' => true, 'build' => $token]);
    }
}
