#!/usr/bin/env php
<?php

declare(strict_types=1);

// Quick manual test script: searches grants directly on SWDA
// (POST /tpg/grants/search, v2.0) and prints the JSON response. Read-only.
//
// Usage: php scripts/grants_search [options]
//
//   --from=Y-m-d            meta.lastUpdateDateFrom   (default: 30 days ago, SGT)
//   --to=Y-m-d              meta.lastUpdateDateTo     (default: today, SGT)
//   --enrolment-ref=ENR-..  grants.enrolment.referenceNumber
//   --course-run-id=ID      grants.course.run.id
//   --course-ref=TGS-..     grants.course.referenceNumber
//   --trainee-id=ID         grants.trainee.id
//   --employer-uen=UEN      grants.employer.uen
//   --sort-field=FIELD      updatedOn | createdOn     (default: updatedOn)
//   --sort-order=ORDER      asc | desc                (default: asc)
//   --page=N                0-based page number       (default: 0)
//   --page-size=N           1-100                     (default: 100)
//   --all                   walk every page from --page until SWDA runs out
//   --raw                   print only the decrypted response JSON (pipe to jq)
//   --help                  show this text
//
// The training partner UEN/code and the `Uen` header come from config('swda.*').

error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);

require __DIR__.'/../vendor/autoload.php';

$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

const MAX_PAGES = 200;

$longOptions = [
    'from:', 'to:', 'enrolment-ref:', 'course-run-id:', 'course-ref:', 'trainee-id:',
    'employer-uen:', 'sort-field:', 'sort-order:', 'page:', 'page-size:', 'all', 'raw', 'help',
];
$opts = getopt('', $longOptions, $restIndex);

if (isset($opts['help']) || $restIndex < count($argv)) {
    $source = (string) file_get_contents(__FILE__);
    preg_match_all('/^\/\/ ?(.*)$/m', $source, $matches);
    $end = count($matches[1]);

    foreach ($matches[1] as $index => $line) {
        if (str_starts_with($line, 'The training partner UEN')) {
            $end = $index + 1;
            break;
        }
    }

    fwrite(isset($opts['help']) ? STDOUT : STDERR, implode("\n", array_slice($matches[1], 0, $end))."\n");
    exit(isset($opts['help']) ? 0 : 1);
}

function fail(string $message): never
{
    fwrite(STDERR, "Error: {$message}\n");
    exit(1);
}

function date_option(array $opts, string $name, DateTimeImmutable $default): string
{
    $value = $opts[$name] ?? $default->format('Y-m-d');
    $parsed = DateTimeImmutable::createFromFormat('!Y-m-d', (string) $value);

    if (! $parsed || $parsed->format('Y-m-d') !== $value) {
        fail("--{$name} must be a valid date in Y-m-d format, got '{$value}'.");
    }

    return $value;
}

function enum_option(array $opts, string $name, array $allowed, string $default): string
{
    $value = (string) ($opts[$name] ?? $default);

    if (! in_array($value, $allowed, true)) {
        fail("--{$name} must be one of: ".implode(', ', $allowed).". Got '{$value}'.");
    }

    return $value;
}

function int_option(array $opts, string $name, int $min, int $max, int $default): int
{
    $value = $opts[$name] ?? (string) $default;

    if (! preg_match('/^\d+$/', (string) $value) || (int) $value < $min || (int) $value > $max) {
        fail("--{$name} must be an integer between {$min} and {$max}, got '{$value}'.");
    }

    return (int) $value;
}

$raw = isset($opts['raw']);
$all = isset($opts['all']);

$now = new DateTimeImmutable('now', new DateTimeZone('Asia/Singapore'));
$from = date_option($opts, 'from', $now->modify('-30 days'));
$to = date_option($opts, 'to', $now);

if ($from > $to) {
    fail("--from ({$from}) is after --to ({$to}).");
}

$sortField = enum_option($opts, 'sort-field', ['updatedOn', 'createdOn'], 'updatedOn');
$sortOrder = enum_option($opts, 'sort-order', ['asc', 'desc'], 'asc');
$page = int_option($opts, 'page', 0, PHP_INT_MAX, 0);
$pageSize = int_option($opts, 'page-size', 1, 100, 100);

$uen = (string) config('swda.training_provider_uen');
$code = (string) config('swda.training_provider_code');

if ($uen === '') {
    fail('swda.training_provider_uen is not configured; the Uen header is mandatory.');
}

$filters = [
    'course.run.id' => $opts['course-run-id'] ?? null,
    'course.referenceNumber' => $opts['course-ref'] ?? null,
    'trainee.id' => $opts['trainee-id'] ?? null,
    'employer.uen' => $opts['employer-uen'] ?? null,
    'enrolment.referenceNumber' => $opts['enrolment-ref'] ?? null,
];

function build_payload(string $from, string $to, array $filters, string $uen, string $code, string $sortField, string $sortOrder, int $page, int $pageSize): array
{
    $grants = [];

    foreach ($filters as $path => $value) {
        if ($value !== null && $value !== '') {
            data_set($grants, $path, (string) $value);
        }
    }

    // Search Grants rejects a request whose trainingPartner.uen does not match the Uen header.
    $grants['trainingPartner'] = ['uen' => $uen, 'code' => $code];

    return [
        'meta' => ['lastUpdateDateFrom' => $from, 'lastUpdateDateTo' => $to],
        'grants' => $grants,
        'sortBy' => ['field' => $sortField, 'order' => $sortOrder],
        'parameters' => ['page' => $page, 'pageSize' => $pageSize],
    ];
}

$certPath = config('swda.client.cert_path');
$keyPath = config('swda.client.key_path');
$passphrase = config('swda.client.passphrase');

foreach (['cert' => $certPath, 'key' => $keyPath] as $label => $path) {
    if (! $path || ! is_readable($path)) {
        fail("SWDA client {$label} is missing or unreadable: ".($path ?: '(not set)'));
    }
}

$url = rtrim((string) config('swda.base_url'), '/').'/tpg/grants/search';
$crypto = new App\Services\Swda\SwdaCrypto(config('swda.encryption.key'), config('swda.encryption.iv'));

/**
 * @return array{0: int, 1: array<string, mixed>|null, 2: string, 3: string} status, decoded body, raw body, curl error
 */
function call_swda(string $url, array $payload, string $uen, App\Services\Swda\SwdaCrypto $crypto, string $certPath, string $keyPath, ?string $passphrase, ?string &$encryptedBody = null): array
{
    $encryptedBody = $crypto->encrypt($payload);

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_POSTFIELDS => $encryptedBody,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Accept: application/json',
            'Content-Type: application/json',
            // The gateway labels a plain base64 body as gzip; refuse compression
            // so nothing tries to inflate it.
            'Accept-Encoding: identity',
            'Uen: '.$uen,
        ],
        CURLOPT_SSLCERT => $certPath,
        CURLOPT_SSLKEY => $keyPath,
        CURLOPT_TIMEOUT => 60,
    ]);

    if ($passphrase) {
        curl_setopt($ch, CURLOPT_SSLKEYPASSWD, $passphrase);
    }

    $body = curl_exec($ch);
    $httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $curlError = curl_error($ch);
    curl_close($ch);

    $decoded = null;

    if ($curlError === '') {
        try {
            $decoded = $crypto->decrypt((string) $body);
        } catch (Throwable) {
            $decoded = null;
        }
    }

    return [$httpCode, $decoded, (string) $body, $curlError];
}

function pretty(mixed $value): string
{
    return json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)."\n";
}

// SWDA signals "past the last page" (and zero results) as a 404 / TGS-443.
function is_no_records(int $httpCode, ?array $decoded): bool
{
    return $httpCode === 404 && $decoded !== null && str_contains((string) json_encode($decoded), 'TGS-443');
}

$timestampSgt = $now->format('Y-m-d H:i:s').' SGT';

if (! $all) {
    $payload = build_payload($from, $to, $filters, $uen, $code, $sortField, $sortOrder, $page, $pageSize);
    [$httpCode, $decoded, $body, $curlError] = call_swda($url, $payload, $uen, $crypto, $certPath, $keyPath, $passphrase, $encrypted);

    if (! $raw) {
        echo "========================================\n";
        echo "[POST /tpg/grants/search]\n";
        echo "Timestamp: {$timestampSgt}\n";
        echo "URL: {$url}\n";
        echo "Header: Uen: {$uen}\n";
        echo "Request Body (plaintext):\n".pretty($payload);
        echo "Request Body (encrypted, base64):\n{$encrypted}\n";
        echo "========================================\n";
        echo "HTTP Status Code: {$httpCode}\n";
        echo "Full JSON Response:\n";
    }

    if ($curlError !== '') {
        fwrite(STDERR, pretty(['curl_error' => $curlError]));
        exit(1);
    }

    if ($decoded === null) {
        fwrite(STDERR, "Could not decode response as SWDA ciphertext.\n".$crypto->preview($body)."\n");
        exit(1);
    }

    echo pretty($decoded);
    exit($httpCode >= 200 && $httpCode < 300 ? 0 : 1);
}

// --all: walk pages until an empty page or TGS-443, then print one merged document.
$records = [];
$pages = 0;
$failed = false;

for ($i = 0; $i < MAX_PAGES; $i++, $page++) {
    $payload = build_payload($from, $to, $filters, $uen, $code, $sortField, $sortOrder, $page, $pageSize);
    [$httpCode, $decoded, $body, $curlError] = call_swda($url, $payload, $uen, $crypto, $certPath, $keyPath, $passphrase);

    if ($curlError !== '') {
        fwrite(STDERR, "Page {$page}: ".pretty(['curl_error' => $curlError]));
        $failed = true;
        break;
    }

    if (is_no_records($httpCode, $decoded)) {
        break;
    }

    if ($decoded === null || $httpCode < 200 || $httpCode >= 300) {
        fwrite(STDERR, "Page {$page}: HTTP {$httpCode}\n".($decoded === null ? $crypto->preview($body) : pretty($decoded))."\n");
        $failed = true;
        break;
    }

    $pages++;
    $data = $decoded['data'] ?? [];

    if (is_array($data) && isset($data['grants']) && is_array($data['grants'])) {
        $data = $data['grants'];
    }

    if (! is_array($data) || $data === []) {
        break;
    }

    array_push($records, ...array_values($data));

    if (! $raw) {
        fwrite(STDERR, "Page {$page}: ".count($data)." record(s)\n");
    }
}

if (! $raw) {
    fwrite(STDERR, "[POST /tpg/grants/search] {$timestampSgt} | {$url} | window {$from} to {$to} | {$pages} page(s), ".count($records)." record(s)\n");
}

echo pretty(['data' => $records]);
exit($failed ? 1 : 0);
