<?php
#version 103
namespace Keysoft\FileMaker;

class Client {
    private static ?Client $instance = null;
    private string $baseUrl;
    private string $username;
    private string $password;
    private bool $debug;

    public function __construct(string $baseUrl, string $username, string $password, bool $debug = false) {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->username = $username;
        $this->password = $password;
        $this->debug = $debug;
    }

    public static function getInstance(string $baseUrl = '', string $username = '', string $password = '', bool $debug = false): Client {
        if (self::$instance === null) {
            if (empty($baseUrl) || empty($username) || empty($password)) {
                throw new \InvalidArgumentException('Must provide baseUrl, username, and password for the first init.');
            }
            self::$instance = new self($baseUrl, $username, $password, $debug);
        }
        return self::$instance;
    }

    private function request($method, $endpoint, $data = null, $headers = []) {
        $url = $this->baseUrl . '/' . ltrim($endpoint, '/');

        $ch = curl_init($url);
        $defaultHeaders = [
            'Accept: application/json',
            'Content-Type: application/json',
        ];

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => strtoupper($method),
            CURLOPT_USERPWD => "{$this->username}:{$this->password}",
            CURLOPT_HTTPHEADER => array_merge($defaultHeaders, $headers),
            CURLOPT_SSL_VERIFYPEER => false,
        ]);

        if ($data !== null) {
            $jsonData = json_encode($data);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);

        curl_close($ch);

        if ($curlError) {
            return $this->error("cURL error: $curlError");
        }

        $decoded = json_decode($response, true);

        if ($this->debug) {
            echo "\nRequest: $method $url";
            if ($data) echo "\nPayload: " . json_encode($data);
            echo "<br/>Response ($httpCode): $response\n";
        }

        if ($httpCode >= 200 && $httpCode < 300) {
            return $this->success($decoded, $httpCode);
        } else {
            return $this->error("HTTP $httpCode", [
                'url' => $url,
                'raw_response' => $response,
                'decoded_response' => $decoded,
            ], $httpCode);
        }
    }

    public function get($resource, $query = [], $allPages = false) {
        $results = [];
        $query['$count'] = true;

        $resource = str_replace(' ', '%20', $resource);

        do {
            $queryString = $this->buildQuery($query);
            $res = $this->request('GET', "$resource?$queryString");

            if (!$res['success']) {
                return $res;
            }

            $data = $res['data'];
            $records = $data['value'] ?? [];
            $results = array_merge($results, $records);

            if ($allPages && isset($query['$top'])) {
                $query['$skip'] = ($query['$skip'] ?? 0) + count($records);
                if (count($records) < $query['$top']) {
                    break;
                }
            } else {
                break;
            }
        } while (true);

        return $this->success([
            'value' => $results,
            'count' => count($results)
        ]);
    }

    public function getById($resource, $key) {
        return $this->request('GET', "$resource($key)");
    }

    public function create($resource, $data) {
        return $this->request('POST', $resource, $data);
    }

    public function update($resource, $key, $data) {
        return $this->request('PATCH', "$resource($key)", $data);
    }

    public function delete($resource, $key) {
        return $this->request('DELETE', "$resource($key)");
    }

    public function exists($resource, $key) {
        $res = $this->getById($resource, $key);
        return $res['success'];
    }

    public function getCount($resource, $filter = null) {
        $query = [];
        if ($filter) {
            $query['$filter'] = $filter;
        }
        $queryString = $this->buildQuery($query);
        $endpoint = "$resource/\$count";
        if (!empty($queryString)) {
            $endpoint .= "?$queryString";
        }

        $res = $this->request('GET', $endpoint);
        if ($res['success']) {
            return (int)$res['data'];
        } else {
            return $this->error("Failed to get count", $res['data'], $res['status']);
        }
    }

    public function buildODataQuery(array $options = []): array {
        $query = [];

        if (isset($options['filter'])) {
            $query['$filter'] = $options['filter'];
        }

        if (isset($options['orderby'])) {
            $query['$orderby'] = $options['orderby'];
        }

        if (isset($options['select'])) {
            $query['$select'] = is_array($options['select']) ? implode(',', $options['select']) : $options['select'];
        }

        if (isset($options['expand'])) {
            $query['$expand'] = is_array($options['expand']) ? implode(',', $options['expand']) : $options['expand'];
        }

        if (isset($options['top'])) {
            $query['$top'] = (int)$options['top'];
        }

        if (isset($options['skip'])) {
            $query['$skip'] = (int)$options['skip'];
        }

        if (!empty($options['count']) || $options['count'] === true) {
            $query['$count'] = 'true';
        }

        return $query;
    }

    private function buildQuery($options = []) {
        $queryParts = [];

        foreach ($options as $key => $value) {
            if (is_bool($value)) {
                $value = $value ? 'true' : 'false';
            } elseif (is_array($value)) {
                $value = implode(',', $value);
            }

            // Use rawurlencode for all values except $orderby and $select (which must preserve commas and spaces)
            if (in_array($key, ['$orderby', '$select'])) {
                $encodedValue = str_replace(' ', '%20', $value); // encode spaces only
            } else {
                $encodedValue = rawurlencode($value);
            }

            $queryParts[] = "$key=$encodedValue";
        }

        return implode('&', $queryParts);
    }

    private function success($data, $status = 200) {
        return [
            'success' => true,
            'status' => $status,
            'data' => $data
        ];
    }

    private function error($message, $data = [], $status = 500) {
        return [
            'success' => false,
            'status' => $status,
            'error' => $message,
            'data' => $data
        ];
    }
}

// one instance