#!/usr/bin/env php
<?php
/**
 * APIMarket developer CLI. Standalone: no application session or config required.
 * PHP 8.1+ with ext-curl. It never writes requests, credentials or responses to disk.
 */

const CLI_VERSION = '1.0.0';
const CLI_DEFAULT_TIMEOUT = 30;
const CLI_MAX_TIMEOUT = 120;
const CLI_DEFAULT_MAX_BYTES = 307200;

require_once dirname(__DIR__) . '/includes/ip_policy.php';

function cli_out(string $text = ''): void { fwrite(STDOUT, $text . PHP_EOL); }
function cli_err(string $text, int $code = 1): never { fwrite(STDERR, "api-tester: {$text}" . PHP_EOL); exit($code); }
function cli_usage(): never {
    cli_out(<<<'TXT'
APIMarket developer CLI 1.0.0

Usage:
  api-tester test <url> [options]          Send an HTTP request and show the response
  api-tester docs <url> [options]          Generate Markdown API documentation
  api-tester integration <url> [options]  Generate copy-ready integration snippets
  api-tester help                          Show this help

Request options:
  -X, --method METHOD       GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS
  -H, --header 'Name: value' Add a request header; may be repeated
  -d, --data BODY           Request body (also accepts @file for local input)
      --body BODY           Alias for --data
      --timeout SECONDS     Connect/request timeout, 1-120 (default: 30)
      --max-bytes BYTES     Response cap, 1 KB-10 MB (default: 307200)
      --json                Print machine-readable JSON for test
      --show-headers        Include response headers in normal test output
      --out FILE             Write docs/snippets to a file (explicit action)

Documentation options:
      --response TEXT       Captured response sample (or @file)
      --lang LANGUAGE       curl, python, javascript, php, or all (integration)

Examples:
  api-tester test 'https://your-marketplace.example/health.php' --show-headers
  api-tester docs 'https://your-marketplace.example/health.php' --out APIMarket-health.md
  api-tester integration 'https://your-marketplace.example/health.php' --lang python

Safety:
  Only absolute http:// and https:// URLs are accepted. Local, private, loopback,
  link-local and reserved destinations are blocked. Redirects are not followed.
  Secret-looking values are replaced with placeholders in generated snippets/docs.
TXT
    );
    exit(0);
}

function cli_parse(array $argv): array {
    if (count($argv) < 2 || in_array($argv[1], array('--help', '-h', 'help'), true)) cli_usage();
    $command = strtolower((string) $argv[1]);
    if (!in_array($command, array('test', 'docs', 'integration'), true)) cli_err('unknown command. Use "api-tester help".');
    $args = array('command' => $command, 'url' => '', 'method' => '', 'headers' => array(), 'body' => '', 'timeout' => CLI_DEFAULT_TIMEOUT, 'max_bytes' => CLI_DEFAULT_MAX_BYTES, 'json' => false, 'show_headers' => false, 'out' => '', 'response' => '', 'lang' => 'all');
    $i = 2;
    while ($i < count($argv)) {
        $a = (string) $argv[$i];
        $next = static function () use (&$i, $argv): string {
            $i++;
            if (!isset($argv[$i])) cli_err('an option value is missing. Use --help.');
            return (string) $argv[$i];
        };
        if ($a === '-X' || $a === '--method') $args['method'] = strtoupper($next());
        elseif ($a === '-H' || $a === '--header') $args['headers'][] = $next();
        elseif ($a === '-d' || $a === '--data' || $a === '--body') $args['body'] = cli_read_value($next());
        elseif ($a === '--timeout') $args['timeout'] = max(1, min(CLI_MAX_TIMEOUT, (int) $next()));
        elseif ($a === '--max-bytes') $args['max_bytes'] = max(1024, min(10 * 1024 * 1024, (int) $next()));
        elseif ($a === '--json') $args['json'] = true;
        elseif ($a === '--show-headers') $args['show_headers'] = true;
        elseif ($a === '--out') $args['out'] = $next();
        elseif ($a === '--response') $args['response'] = cli_read_value($next());
        elseif ($a === '--lang') $args['lang'] = strtolower($next());
        elseif (str_starts_with($a, '-')) cli_err("unknown option {$a}. Use --help.");
        elseif ($args['url'] === '') $args['url'] = $a;
        else cli_err('only one URL may be supplied.');
        $i++;
    }
    if ($args['url'] === '') cli_err('a URL is required. Use --help.');
    if ($args['method'] === '') $args['method'] = $command === 'test' ? 'GET' : 'GET';
    if (!in_array($args['method'], array('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'), true)) cli_err('unsupported HTTP method.');
    if (!in_array($args['lang'], array('curl', 'python', 'javascript', 'php', 'all'), true)) cli_err('unsupported --lang.');
    return $args;
}
function cli_read_value(string $value): string {
    if (strlen($value) > 1 && $value[0] === '@') {
        $path = substr($value, 1);
        if (!is_file($path) || !is_readable($path)) cli_err("cannot read {$path}");
        $value = (string) file_get_contents($path);
    }
    if (strlen($value) > 200000) cli_err('request body is larger than 200 KB.');
    return $value;
}
function cli_headers(array $raw): array {
    $out = array();
    foreach ($raw as $line) {
        $p = strpos($line, ':');
        if ($p === false || $p < 1) cli_err('headers must use Name: value format.');
        $name = trim(substr($line, 0, $p)); $value = trim(substr($line, $p + 1));
        if (!preg_match("/^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/", $name)) cli_err("invalid header name: {$name}");
        if ($value === '' || preg_match('/[\r\n]/', $value)) cli_err("invalid value for header {$name}");
        $lower = strtolower($name);
        if (in_array($lower, array('host', 'content-length', 'transfer-encoding', 'connection', 'cookie', 'set-cookie'), true) || str_starts_with($lower, 'proxy-') || str_starts_with($lower, 'x-forwarded-')) cli_err("header {$name} is not allowed");
        $out[$lower] = array('name' => $name, 'value' => substr($value, 0, 4000));
    }
    return array_values($out);
}
function cli_host_ips(string $host): array {
    $host = trim($host, '[]');
    $ips = array();
    if (filter_var($host, FILTER_VALIDATE_IP)) return array($host);
    $a = @gethostbynamel($host); if (is_array($a)) $ips = array_merge($ips, $a);
    $aaaa = @dns_get_record($host, DNS_AAAA); if (is_array($aaaa)) foreach ($aaaa as $r) if (!empty($r['ipv6'])) $ips[] = $r['ipv6'];
    return array_values(array_unique($ips));
}
function cli_url_ok(string $url): array {
    if (strlen($url) > 4096 || !preg_match('#^https?://#i', $url)) cli_err('URL must be an absolute http:// or https:// URL.');
    $p = parse_url($url); $host = strtolower((string) ($p['host'] ?? ''));
    $rawHost = trim($host, '[]');
    if ($rawHost === '' || isset($p['user']) || isset($p['pass']) || isset($p['fragment']) || substr($rawHost, -1) === '.') cli_err('URL must contain a public host and no embedded credentials or fragment.');
    if ($rawHost === 'localhost' || str_ends_with($rawHost, '.localhost') || str_ends_with($rawHost, '.local') || str_ends_with($rawHost, '.internal') || str_ends_with($rawHost, '.home.arpa')) cli_err('local/internal hostnames are blocked.');
    $ips = cli_host_ips($rawHost); if (!$ips) cli_err('the host could not be resolved.');
    foreach ($ips as $ip) {
        if (apim_ip_is_non_public((string) $ip)) cli_err('private, loopback, link-local, documentation, benchmarking or reserved destinations are blocked.');
    }
    return $p;
}
function cli_redact_name(string $name): bool { return (bool) preg_match('/(authorization|api[-_]?key|token|secret|password|passwd|credential|private[-_]?key|signature|cookie)/i', $name); }
function cli_redact_value(string $name, string $value): string {
    if (!cli_redact_name($name)) return $value;
    if (strcasecmp($name, 'authorization') === 0 || strcasecmp($name, 'proxy-authorization') === 0) {
        if (preg_match('/^\\s*bearer\\b/i', $value)) return 'Bearer YOUR_TOKEN';
        if (preg_match('/^\\s*basic\\b/i', $value)) return 'Basic YOUR_USERNAME:YOUR_PASSWORD';
    }
    return 'YOUR_API_KEY';
}
function cli_safe_body(string $body): string {
    if ($body === '') return '';
    $j = json_decode($body, true);
    if (is_array($j)) {
        $walk = function ($v, $key = '') use (&$walk) { if (is_array($v)) { foreach ($v as $k => $x) $v[$k] = $walk($x, (string) $k); return $v; } return cli_redact_name((string) $key) ? 'YOUR_API_KEY' : $v; };
        $j = $walk($j); return (string) json_encode($j, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    }
    return preg_replace_callback('/((?:api[_-]?key|token|secret|password|access[_-]?token)\s*[=:]\s*)([^&\s,}]+)/i', static function ($m) { return $m[1] . 'YOUR_API_KEY'; }, $body);
}
function cli_safe_url(string $url): string {
    return (string) preg_replace_callback('/([?&](?:api[_-]?key|token|secret|password|access[_-]?token|key)=)[^&#]*/i', static fn($m) => $m[1] . 'YOUR_API_KEY', $url);
}
function cli_request(array $a): array {
    $parts = cli_url_ok($a['url']); $headers = cli_headers($a['headers']); $wire = array(); foreach ($headers as $h) $wire[] = $h['name'] . ': ' . $h['value'];
    $host = strtolower(trim((string) ($parts['host'] ?? ''), '[]'));
    $scheme = strtolower((string) ($parts['scheme'] ?? 'https'));
    $port = isset($parts['port']) ? (int) $parts['port'] : ($scheme === 'https' ? 443 : 80);
    $resolve = array();
    foreach (cli_host_ips($host) as $ip) {
        $resolveIp = strpos((string) $ip, ':') !== false ? '[' . trim((string) $ip, '[]') . ']' : (string) $ip;
        $resolve[] = (strpos($host, ':') !== false ? '[' . $host . ']' : $host) . ':' . $port . ':' . $resolveIp;
    }
    $body = ''; $cap = $a['max_bytes']; $truncated = false; $status = 0; $responseHeaders = array(); $headerBlock = '';
    $ch = curl_init($a['url']); if (!$ch) cli_err('could not initialise cURL.');
    curl_setopt_array($ch, array(CURLOPT_CUSTOMREQUEST => $a['method'], CURLOPT_HTTPHEADER => $wire, CURLOPT_FOLLOWLOCATION => false, CURLOPT_RETURNTRANSFER => false, CURLOPT_HEADER => false, CURLOPT_CONNECTTIMEOUT => min(10, $a['timeout']), CURLOPT_TIMEOUT => $a['timeout'], CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_PROXY => '', CURLOPT_NOPROXY => '*', CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, CURLOPT_RESOLVE => $resolve, CURLOPT_USERAGENT => 'APIMarket-CLI/' . CLI_VERSION, CURLOPT_HEADERFUNCTION => static function ($ch, $line) use (&$headerBlock, &$responseHeaders, &$status) {
        $len = strlen($line); $headerBlock .= $line; $trim = trim($line); if (preg_match('#^HTTP/[^ ]+\s+(\d+)#i', $trim, $m)) { $status = (int) $m[1]; $responseHeaders = array(); }
        elseif (strpos($line, ':') !== false) { [$k, $v] = explode(':', $line, 2); $responseHeaders[strtolower(trim($k))] = trim($v); } return $len;
    }, CURLOPT_WRITEFUNCTION => static function ($ch, $chunk) use (&$body, $cap, &$truncated) { $remaining = $cap - strlen($body); if ($remaining <= 0) { $truncated = true; return strlen($chunk); } if (strlen($chunk) > $remaining) { $body .= substr($chunk, 0, $remaining); $truncated = true; } else $body .= $chunk; return strlen($chunk); }));
    if ($a['body'] !== '' && !in_array($a['method'], array('GET', 'HEAD', 'OPTIONS'), true)) curl_setopt($ch, CURLOPT_POSTFIELDS, $a['body']);
    $start = microtime(true); $ran = curl_exec($ch); $error = curl_error($ch); $errno = curl_errno($ch); $latency = (int) round((microtime(true) - $start) * 1000); curl_close($ch);
    if ($ran === false || $errno !== 0) {
        /* cURL diagnostics may echo a full URL; do not print a buyer/provider
           query credential or arbitrary request target to the terminal. */
        cli_err($errno === CURLE_OPERATION_TIMEDOUT ? 'request timed out.' : 'request failed.');
    }
    return array('ok' => true, 'status' => $status, 'latency_ms' => $latency,
        'url' => cli_safe_url($a['url']), 'method' => $a['method'], 'request_headers' => $headers,
        'request_body' => cli_safe_body($a['body']), 'response_headers' => $responseHeaders,
        'response_body' => cli_safe_body($body), 'truncated' => $truncated,
        'content_type' => $responseHeaders['content-type'] ?? '');
}
function cli_analysis(array $a, ?array $result = null): array {
    $p = parse_url($a['url']); $query = array(); parse_str((string) ($p['query'] ?? ''), $query); $params = array();
    foreach ($query as $name => $value) $params[] = array('name' => (string) $name, 'location' => 'query', 'example' => cli_redact_name((string) $name) ? 'YOUR_API_KEY' : (string) $value);
    foreach (cli_headers($a['headers']) as $h) if (cli_redact_name($h['name'])) $auth = array('needed' => true, 'mode' => 'header', 'name' => $h['name']);
    $auth = $auth ?? array('needed' => false, 'mode' => 'none', 'name' => '');
    $safeUrl = cli_safe_url($a['url']); $safeBody = cli_safe_body($a['body']);
    $lines = array(); $lines[] = '## Integration notes'; $lines[] = 'Use **' . $a['method'] . '** with the API endpoint below.'; $lines[] = ''; $lines[] = '**Endpoint:** `' . $safeUrl . '`';
    if ($auth['needed']) $lines[] = '**Authentication:** send the credential in the `' . $auth['name'] . '` header. Replace `YOUR_API_KEY` locally; never commit real credentials.'; else $lines[] = '**Authentication:** no credential header was detected. Confirm the API does not require one before production use.';
    if ($params) { $lines[] = ''; $lines[] = '**Parameters:**'; foreach ($params as $x) $lines[] = '- `' . $x['name'] . '` — ' . $x['location'] . ' parameter; example `' . $x['example'] . '`.'; }
    if ($a['body'] !== '') { $lines[] = ''; $lines[] = '**Request body:** provide the following payload (redacted values are placeholders):'; $lines[] = '```json'; $lines[] = $safeBody; $lines[] = '```'; }
    if ($result && $result['status'] >= 400) { $lines[] = ''; $lines[] = '**Common error:** this request returned HTTP ' . $result['status'] . '. Check the URL, required parameters, authentication and content type.'; }
    return array('method' => $a['method'], 'base' => ($p['scheme'] ?? 'https') . '://' . ($p['host'] ?? ''), 'path' => $p['path'] ?? '/', 'url' => $safeUrl, 'params' => $params, 'auth' => $auth, 'safe_body' => $safeBody, 'docs' => implode(PHP_EOL, $lines));
}
function cli_snippets(array $a): array {
    $h = cli_headers($a['headers']);
    $safeUrl = cli_safe_url($a['url']);
    $headers = array();
    foreach ($h as $x) {
        $headers[$x['name']] = cli_redact_value($x['name'], $x['value']);
    }
    $body = cli_safe_body($a['body']);
    $curl = 'curl -X ' . $a['method'] . ' ' . escapeshellarg($safeUrl);
    foreach ($headers as $name => $value) $curl .= ' -H ' . escapeshellarg($name . ': ' . $value);
    if ($body !== '') $curl .= ' --data-raw ' . escapeshellarg($body);

    $python = "import requests\n\nurl = " . var_export($safeUrl, true) . "\nheaders = {\n";
    foreach ($headers as $name => $value) $python .= '    ' . var_export($name, true) . ' : ' . var_export($value, true) . ",\n";
    $python .= "}\n";
    if ($body !== '') $python .= 'data = ' . var_export($body, true) . "\n";
    $python .= 'response = requests.request(' . var_export($a['method'], true) . ', url, headers=headers' . ($body !== '' ? ', data=data' : '') . ")\nprint(response.status_code)\nprint(response.text)";

    $js = "const response = await fetch(" . json_encode($safeUrl) . ", {\n  method: " . json_encode($a['method']) . ",\n  headers: " . json_encode((object) $headers, JSON_UNESCAPED_SLASHES);
    if ($body !== '') $js .= ",\n  body: " . json_encode($body, JSON_UNESCAPED_SLASHES);
    $js .= "\n});\nconsole.log(response.status, await response.text());";

    $phpHeaders = array();
    foreach ($headers as $name => $value) $phpHeaders[] = $name . ': ' . $value;
    $php = "<?php\n\$ch = curl_init(" . var_export($safeUrl, true) . ");\ncurl_setopt_array(\$ch, [\n    CURLOPT_CUSTOMREQUEST => " . var_export($a['method'], true) . ",\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => " . var_export($phpHeaders, true) . ($body !== '' ? ",\n    CURLOPT_POSTFIELDS => " . var_export($body, true) : '') . "\n]);\n\$response = curl_exec(\$ch); curl_close(\$ch); echo \$response;";
    return array('curl' => $curl, 'python' => $python, 'javascript' => $js, 'php' => $php);
}
function cli_docs(array $a, ?array $result): string {
    $x = cli_analysis($a, $result);
    $safeUrl = $x['url'];
    $snips = cli_snippets($a);
    $body = $x['safe_body'];
    $out = "# API documentation\n\n";
    $out .= "> Generated by APIMarket developer CLI. Review heuristic authentication and error guidance before publishing.\n\n";
    $out .= "## Endpoint\n\n";
    $out .= "- **Method:** `{$x['method']}`\n- **Base URL:** `{$x['base']}`\n- **Path:** `{$x['path']}`\n- **URL:** `{$safeUrl}`\n\n";
    $out .= "## Authentication\n\n";
    if ($x['auth']['needed']) {
        $out .= 'Send credentials in the `' . $x['auth']['name'] . "` header. The generated examples use `YOUR_API_KEY` as a placeholder. Detection is heuristic.\n\n";
    } else {
        $out .= "No authentication header was detected. The API may still require authentication; confirm this with its official documentation.\n\n";
    }
    $out .= "## Parameters\n\n";
    if ($x['params']) foreach ($x['params'] as $p) $out .= '- `' . $p['name'] . '` — ' . $p['location'] . '; example `' . $p['example'] . "`\n";
    else $out .= "No query parameters were detected in the supplied request.\n";
    $out .= "\n## Request example\n\n```bash\n" . $snips['curl'] . "\n```\n";
    if ($body !== '') $out .= "\n### Body\n\n```json\n" . $body . "\n```\n";
    if ($result && !empty($result['response_body'])) $out .= "\n## Response example\n\n```json\n" . cli_safe_body($result['response_body']) . "\n```\n";
    $out .= "\n## Common errors\n\n- **400:** validate required parameters and request body syntax.\n- **401/403:** verify the credential, scope and authentication header location.\n- **404:** check the base URL and endpoint path.\n- **429:** slow down and follow the API rate-limit guidance.\n- **5xx:** retry only when the API documents safe retry behavior.\n";
    $out .= "\n## Integration snippets\n\n### Python\n\n```python\n" . $snips['python'] . "\n```\n\n### JavaScript\n\n```javascript\n" . $snips['javascript'] . "\n```\n\n### PHP\n\n```php\n" . $snips['php'] . "\n```\n";
    return $out;
}

$args = cli_parse($argv);
$headers = cli_headers($args['headers']);
$result = null;
if ($args['command'] === 'test') $result = cli_request($args);
if ($args['command'] === 'docs' && $args['response'] !== '') {
    $result = array('status' => 200, 'response_body' => $args['response']);
}
if ($args['command'] === 'test') {
    if ($args['json']) { $safe = $result; $safe['request_headers'] = array_map(static fn($h) => array('name' => $h['name'], 'value' => cli_redact_value($h['name'], $h['value'])), $safe['request_headers']); cli_out((string) json_encode($safe, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); exit(0); }
    cli_out('HTTP ' . $result['status'] . ' · ' . $result['latency_ms'] . ' ms · ' . ($result['content_type'] ?: 'unknown content type')); if ($args['show_headers']) foreach ($result['response_headers'] as $k => $v) cli_out($k . ': ' . $v); cli_out(''); cli_out($result['response_body']); if ($result['truncated']) cli_err('response was capped at ' . $args['max_bytes'] . ' bytes.', 0); exit(0);
}
if ($args['command'] === 'integration') { $snips = cli_snippets($args); $chosen = $args['lang'] === 'all' ? $snips : array($args['lang'] => $snips[$args['lang']]); $text = ''; foreach ($chosen as $name => $value) $text .= strtoupper($name) . "\n" . str_repeat('-', strlen($name)) . "\n" . $value . "\n\n"; if ($args['out'] !== '') { if (@file_put_contents($args['out'], $text) === false) cli_err('could not write output file.'); cli_out('Wrote ' . $args['out']); } else cli_out(rtrim($text)); exit(0); }
$docs = cli_docs($args, $result); if ($args['out'] !== '') { if (@file_put_contents($args['out'], $docs) === false) cli_err('could not write output file.'); cli_out('Wrote ' . $args['out']); } else cli_out($docs);
