<?php

declare(strict_types=1);

namespace PpPhp\Modules\Licensing\Controllers;

use PpPhp\Core\Config;
use PpPhp\Core\Licensing\LicenseClient;
use PpPhp\Core\Licensing\LicenseException;
use PpPhp\Core\Licensing\LicenseGate;
use PpPhp\Core\Licensing\LicenseProtocol;
use PpPhp\Core\Licensing\LicenseTrust;
use PpPhp\Core\Security;

/**
 * THIS installation's licence page — activate, see status, release the seat.
 *
 * Administrator-level, not owner-level: the person running a hosted copy has to
 * be able to enter the key they were sold without the vendor's account. (Issuing
 * licences is the other controller, and that one is owner-only.)
 *
 * Reachable even while the console is locked — LicenseGate allows /licensing
 * through, because a page that says "you are not licensed" is useless if it is
 * behind the lock it is explaining.
 */
class LicenseController
{
    public function __construct(private Config $config) {}

    private function gate(): void
    {
        Security::RequireAuth();
        if (!Security::IsAdmin()) {
            redirect('/projects');
        }
    }

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

    private function checkToken(array $input): void
    {
        if (!Security::CSRF()->Check((string) ($input['_token'] ?? ''))) {
            $this->json(['success' => false, 'message' => 'Invalid security token. Reload the page and try again.'], 400);
        }
    }

    private function input(): array
    {
        $data = json_decode((string) file_get_contents('php://input'), true);
        return is_array($data) ? $data : $_POST;
    }

    public function index(): string
    {
        $this->gate();

        $client   = new LicenseClient();
        $status   = $client->status();
        $enforced = LicenseGate::enforced();
        $stamped  = LicenseTrust::isStamped();
        $locked   = LicenseGate::locked();
        // A backup/secondary server is failover infrastructure — it needs no
        // licence of its own, so the page explains that instead of prompting to
        // activate. (enforced() is already false on a backup for the same reason.)
        // ServerRole is STRIPPED from client bundles, so guard it — a customer
        // copy is never a backup, and the page must render, not 500.
        $hasRole     = class_exists(\PpPhp\Core\Licensing\ServerRole::class);
        $isBackup    = $hasRole && \PpPhp\Core\Licensing\ServerRole::isBackup();
        $roleMainUrl = $hasRole ? \PpPhp\Core\Licensing\ServerRole::mainUrl() : '';
        // Does THIS install hold a signing key? If so it is the licence authority
        // (main), not a customer copy — the page says so instead of prompting to
        // activate a licence against itself.
        $isAuthority = LicenseGate::isAuthority();

        $baseUrl   = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');
        $user      = Security::CurrentUserData();
        $isOwner   = Security::IsOwner();

        ob_start();
        require __DIR__ . '/../Views/client.php';
        return (string) ob_get_clean();
    }

    /** Redeem an activation code. */
    public function activate(): void
    {
        $this->gate();
        $in = $this->input();
        $this->checkToken($in);

        try {
            $result = (new LicenseClient())->activate(
                (string) ($in['code'] ?? ''),
                (string) ($in['email'] ?? ''),
                (string) ($in['label'] ?? '')
            );
        } catch (LicenseException $e) {
            $this->json(['success' => false, 'message' => $e->getMessage()], 400);
        } catch (\Throwable $e) {
            $this->json(['success' => false, 'message' => 'Activation failed: ' . $e->getMessage()], 500);
        }

        $claims = $result['claims'];
        $this->json([
            'success' => true,
            'message' => !empty($result['reused'])
                ? 'Re-activated — this machine already had a seat, so none was used up.'
                : 'Activated. This installation is licensed to ' . ($claims['holder'] ?: 'you') . '.',
            'status'  => (new LicenseClient())->status(),
        ]);
    }

    /**
     * Live reachability diagnostic — does the SAME server-side outbound call the
     * real activation makes, and reports the raw transport result so the admin can
     * see WHY "could not reach the licence service" happens (blocked outbound 443,
     * DNS, TLS trust, …). Read-only: it never activates or changes state.
     */
    public function diagnose(): void
    {
        $this->gate();
        $in = $this->input();
        $this->checkToken($in);

        $client = new LicenseClient();
        try {
            $test = $client->connectionTest(
                isset($in['code']) && trim((string) $in['code']) !== '' ? (string) $in['code'] : null
            );
        } catch (\Throwable $e) {
            $this->json(['success' => false, 'verdict' => 'error', 'message' => 'Diagnostic failed: ' . $e->getMessage()], 500);
        }

        $verdict = (string) ($test['verdict'] ?? 'unknown');
        $first   = $test['targets'][0] ?? [];
        $host    = ($first['url'] ?? '') ? parse_url((string) $first['url'], PHP_URL_HOST) : '';
        $eg      = $test['egress'] ?? [];
        $rip     = (string) ($eg['resolvedIp'] ?? '');
        $ripTxt  = $rip !== '' ? " (this host resolves {$host} → {$rip})" : '';
        $msgs = [
            'ok'          => 'The licence server is reachable from this host — activation should work. If it still fails, the activation code itself may be wrong or already used.',
            'blocked'     => 'This server cannot open ANY outbound HTTPS connection — it could not reach the licence server, and also could not reach neutral public servers (Cloudflare 1.1.1.1 / Google 8.8.8.8) on port 443. Outbound HTTPS is blocked for this whole host by its firewall — ask your hosting provider to allow outbound connections on port 443.',
            'authority-blocked' => 'Outbound HTTPS from this host WORKS (it reached neutral public servers on port 443) — but it cannot reach the licence server ' . ($host ?: '') . ' specifically' . $ripTxt . '. That points at the LICENCE SERVER side: its firewall is most likely blocking this client server\'s IP address, or the licence host resolves here to the wrong IP. Whitelist this client\'s outgoing IP on the licence server\'s firewall (CSF/DirectAdmin), and confirm ' . ($host ?: 'the licence host') . ' resolves to the correct server IP.',
            'timeout'     => 'The connection to the licence server timed out — outbound HTTPS is most likely filtered by the host. Ask your provider to allow outbound connections on port 443.',
            'dns'         => 'This server could not resolve the licence-server address (DNS)' . $ripTxt . '. Check that the host can resolve the licence domain to the correct server IP.',
            'tls'         => 'A secure (TLS) connection was made but the certificate could not be verified — this host likely has an out-of-date CA-certificate bundle. Ask your host to update ca-certificates.',
            'unreachable' => 'The licence server could not be contacted and no specific cause was reported — outbound HTTPS is most likely blocked on this host. Ask your provider to allow outbound port 443.',
            'no-target'   => 'No licence-server address is known yet. Paste the activation code first, then run the test — it will probe the address the code points to.',
            'unknown'     => 'The connection test was inconclusive.',
        ];
        $test['success'] = ($verdict === 'ok');
        $test['message'] = $msgs[$verdict] ?? $msgs['unknown'];
        $this->json($test);
    }

    /** Pull a fresh lease now rather than waiting for the daily rotation. */
    public function refresh(): void
    {
        $this->gate();
        $in = $this->input();
        $this->checkToken($in);

        $client = new LicenseClient();
        if (!$client->isActivated()) {
            $this->json(['success' => false, 'message' => 'This installation is not activated yet.'], 400);
        }
        $ok = $client->renew();
        $status = $client->status();
        $this->json([
            'success' => $ok,
            'message' => $ok
                ? 'Licence refreshed — valid for another ' . LicenseProtocol::human($status['expiresIn'] ?? 0) . '.'
                : ($status['lastError'] ?: 'Could not reach the licence server. Your licence keeps working until the grace period runs out.'),
            'status'  => $status,
        ]);
    }

    /** Release this seat so it can be used on another server. */
    public function deactivate(): void
    {
        $this->gate();
        $in = $this->input();
        $this->checkToken($in);

        try {
            (new LicenseClient())->deactivate();
        } catch (LicenseException $e) {
            $this->json(['success' => false, 'message' => $e->getMessage()], 400);
        }
        $this->json([
            'success' => true,
            'message' => 'Seat released. This installation is no longer activated.',
            'status'  => (new LicenseClient())->status(),
        ]);
    }
}
