/* Minimal phpcore execution envelope: ping, relay, load and invoke. */

function phpcore_id() {
    return md5(__FILE__ . php_uname());
}

function phpcore_dir($create = false) {
    $dir = rtrim((string)sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR
        . '.{{CACHE_NAMESPACE}}';
    if ($create && !is_dir($dir) && !@mkdir($dir, 0700, true) && !is_dir($dir)) {
        throw new RuntimeException();
    }
    if ($create && !is_writable($dir)) throw new RuntimeException();
    return $dir;
}

function phpcore_path($params, $create = false) {
    $key = isset($params['componentKey']) ? (string)$params['componentKey'] : '';
    if (!preg_match('/^[a-f0-9]{80}$/', $key)) throw new InvalidArgumentException();
    $name = substr(md5('{{CACHE_NAMESPACE}}|' . substr($key, 0, 16)), 0, 10)
        . substr(md5('{{CACHE_NAMESPACE}}|' . $key), 0, 30);
    return phpcore_dir($create) . DIRECTORY_SEPARATOR . $name . '.{{CACHE_SUFFIX}}';
}

function phpcore_sweep($keep = null) {
    $dir = phpcore_dir();
    if (!is_dir($dir)) return;
    $now = time();
    foreach ((array)glob($dir . DIRECTORY_SEPARATOR . '.{{CACHE_TEMP_PREFIX}}*') as $temporary) {
        if ($now - (int)@filemtime($temporary) > 300) @unlink($temporary);
    }
    $files = [];
    foreach ((array)glob($dir . DIRECTORY_SEPARATOR . '*.{{CACHE_SUFFIX}}') as $path) {
        $modified = (int)@filemtime($path);
        if ($path !== $keep && $modified > 0 && $now - $modified > 604800) {
            @unlink($path); continue;
        }
        $files[] = $path;
    }
    usort($files, static function ($left, $right) { return (int)@filemtime($left) - (int)@filemtime($right); });
    while (count($files) > 48) {
        $path = array_shift($files);
        if ($path === $keep) $files[] = $path; else @unlink($path);
    }
}

function phpcore_open($params) {
    $path = phpcore_path($params);
    if (!is_file($path)) return null;
    $component = include $path;
    if (!is_array($component) || !isset($component['id'], $component['handle'])
        || $component['id'] !== $params['componentName'] || !is_callable($component['handle'])) {
        throw new RuntimeException();
    }
    $modified = (int)@filemtime($path);
    if ($modified > 0 && time() - $modified > 300) @touch($path);
    return $component;
}

function phpcore_test() {
    phpcore_sweep();
    $components = [];
    foreach ((array)glob(phpcore_dir() . DIRECTORY_SEPARATOR . '*.{{CACHE_SUFFIX}}') as $path) {
        $component = @include $path;
        if (is_array($component) && isset($component['id'], $component['handle'])
            && is_string($component['id']) && is_callable($component['handle'])) {
            $components[$component['id']] = true;
        }
    }
    $components = array_keys($components); sort($components);
    return ['code' => 200, 'hostId' => phpcore_id(), 'components' => $components];
}

function phpcore_load($params) {
    $path = phpcore_path($params, true);
    if (phpcore_open($params) !== null) return ['code' => 200, 'cached' => true];
    $source = isset($params['source']) ? (string)$params['source'] : '';
    if (strpos(ltrim($source), '<?php') !== 0) {
        throw new InvalidArgumentException();
    }
    $temporary = tempnam(phpcore_dir(true), '.{{CACHE_TEMP_PREFIX}}');
    if ($temporary === false) throw new RuntimeException();
    try {
        if (file_put_contents($temporary, $source, LOCK_EX) !== strlen($source)) {
            throw new RuntimeException();
        }
        @chmod($temporary, 0600);
        $component = include $temporary;
        if (!is_array($component) || !isset($component['id'], $component['handle'])
            || $component['id'] !== $params['componentName'] || !is_callable($component['handle'])) {
            throw new RuntimeException();
        }
        if (!@rename($temporary, $path)) throw new RuntimeException();
        $temporary = null;
        $key = (string)$params['componentKey'];
        $family = substr(md5('{{CACHE_NAMESPACE}}|' . substr($key, 0, 16)), 0, 10);
        foreach ((array)glob(phpcore_dir() . DIRECTORY_SEPARATOR . $family . '*.{{CACHE_SUFFIX}}') as $old) {
            if ($old !== $path) @unlink($old);
        }
        phpcore_sweep($path);
        return ['code' => 200, 'cached' => false];
    } finally {
        if ($temporary !== null) @unlink($temporary);
    }
}

function phpcore_invoke($params) {
    $component = phpcore_open($params);
    if ($component === null) return ['code' => 424];
    $action = isset($params['action']) ? (string)$params['action'] : '';
    $result = call_user_func($component['handle'], $action, $params);
    if (!is_array($result)) $result = ['result' => $result];
    if (!isset($result['code'])) $result['code'] = 200;
    return $result;
}

function phpcore_forward($params) {
    $url = isset($params['url']) ? (string)$params['url'] : '';
    $body = isset($params['body']) && is_string($params['body']) ? $params['body'] : '';
    if ($url === '') throw new InvalidArgumentException();
    $headers = isset($params['headers']) && is_array($params['headers']) ? $params['headers'] : [];
    $lines = [];
    foreach ($headers as $name => $value) {
        $lower = strtolower((string)$name);
        if (!preg_match('/^[A-Za-z0-9-]+$/', (string)$name)
            || strpos((string)$value, "\r") !== false || strpos((string)$value, "\n") !== false
            || in_array($lower, ['host', 'content-length', 'connection', 'content-encoding', 'transfer-encoding'], true)) continue;
        $lines[] = $name . ': ' . $value;
    }
    if (function_exists('curl_init')) {
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HTTPHEADER, $lines);
        curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 3); curl_setopt($curl, CURLOPT_TIMEOUT, 0);
        $response = curl_exec($curl); curl_close($curl);
        if ($response === false) throw new RuntimeException();
    } else {
        $context = stream_context_create(['http' => ['method' => 'POST', 'header' => implode("\r\n", $lines),
            'content' => $body, 'ignore_errors' => true, 'timeout' => 30]]);
        $response = @file_get_contents($url, false, $context);
        if ($response === false) throw new RuntimeException();
    }
    return ['code' => 200, 'url' => $url, 'body' => leo_binary($response)];
}

function phpcore_envelope_response($requestId, $result) {
    if (!is_array($result)) $result = ['code' => 500];
    $code = isset($result['code']) ? (int)$result['code'] : 500;
    unset($result['code']);
    $response = ['requestId' => (string)$requestId, 'code' => $code];
    if ($code >= 200 && $code < 300) {
        $response['data'] = $result;
    } else {
        $response['error'] = $result;
    }
    return $response;
}

function phpcore_run($request) {
    if (!is_array($request)) throw new InvalidArgumentException();
    $requestId = isset($request['requestId']) ? (string)$request['requestId'] : '';
    $operation = isset($request['operation']) ? (string)$request['operation'] : '';
    if ($requestId === '' || $operation === '') throw new InvalidArgumentException();
    $params = isset($request['params']) && is_array($request['params']) ? $request['params'] : [];
    if (isset($request['hostId'])) $params['hostId'] = $request['hostId'];
    if (isset($request['component'])) $params['componentName'] = $request['component'];
    if (isset($request['action'])) $params['action'] = $request['action'];
    if (($operation === 'COMPONENT_LOAD' || $operation === 'COMPONENT_INVOKE')
        && isset($params['hostId']) && $params['hostId'] !== ''
        && $params['hostId'] !== phpcore_id()) {
        $result = [
            'code' => 409,
            'errorCode' => 'HOST_ID_MISMATCH',
            'hostId' => phpcore_id(),
            'msg' => 'HostId does not match the current runtime instance'
        ];
    }
    elseif ($operation === 'PING') $result = phpcore_test();
    elseif ($operation === 'RELAY') $result = phpcore_forward($params);
    elseif ($operation === 'COMPONENT_LOAD') $result = phpcore_load($params);
    elseif ($operation === 'COMPONENT_INVOKE') $result = phpcore_invoke($params);
    else $result = ['code' => 404];
    return phpcore_envelope_response($requestId, $result);
}
