<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\ContractCreateResult;
use App\Models\ContractDeleteResult;
use App\Models\ContractGetResult;
use App\Models\ContractPublicSignResult;
use App\Models\ContractPublicViewResult;
use App\Models\ContractSendResult;
use App\Models\ContractUpdateResult;
use ApiException;

/**
 * Contract Module — Create, Get, Edit, Send, Delete, View & Sign contract (public) APIs.
 */
final class ContractService
{
    private const CREATE_PATH = '/karvipartner/contracts';
    private const GET_PATH = '/karvipartner/contracts/{id}';
    private const UPDATE_PATH = '/karvipartner/contracts/{id}';
    private const SEND_PATH = '/karvipartner/contracts/{id}/send';
    private const DELETE_PATH = '/karvipartner/contracts/{id}';
    private const VIEW_PUBLIC_PATH = '/karvipartner/contracts/sign/{token}';

    /** @var list<string> */
    private const EDIT_API_FIELDS = [
        'restaurant_name',
        'brand',
        'owner_name',
        'company_phone',
        'street_address',
        'zip_city',
        'email',
        'contract_url',
        'sign_url',
        'raw_data',
        'contract_status',
        'customer_ip',
        'terms_accepted',
        'terms_accepted_at',
        'sent_at',
        'viewed_at',
        'signed_at',
        'signer_device_type',
        'signer_browser',
        'signer_os',
    ];

    public function __construct(
        private readonly ApiService $api,
    ) {
    }

    /**
     * POST http://karviapi1.de/karvipartner/contracts
     * Uses session Bearer token + X-Request-Datetime via ApiService.
     *
     * @param array<string,mixed> $payload Exact API body (type, restaurant_name, …, raw_data)
     */
    public function createContract(array $payload): ContractCreateResult
    {
        if (!function_exists('auth_api_token') || auth_api_token() === null) {
            return ContractCreateResult::fail(
                'Not authenticated. Please log in again.',
                null,
                401,
                'invalid_token'
            );
        }

        if (!function_exists('api_auth_enabled') || !api_auth_enabled()) {
            return ContractCreateResult::fail(
                'Partner API is not configured.',
                null,
                503,
                'api_disabled'
            );
        }

        $path = (string)(function_exists('api_config') ? (api_config()['endpoints']['contract_create'] ?? self::CREATE_PATH) : self::CREATE_PATH);

        try {
            $response = $this->api->post($path, $payload, true);
        } catch (\Throwable $e) {
            $result = $this->mapTransportError($e);
            $this->logCreateContract($payload, ['http_status' => 0, 'success' => false, 'message' => $e->getMessage()], $result);

            return $result;
        }

        $result = $this->parseCreateResponse($response);
        $this->logCreateContract($payload, $response, $result);

        return $result;
    }

    /**
     * GET http://karviapi1.de/karvipartner/contracts/{contract_id}
     * Uses session Bearer token + X-Request-Datetime via ApiService.
     */
    public function getContract(int $contractId): ContractGetResult
    {
        if ($contractId <= 0) {
            return ContractGetResult::fail(
                'Invalid contract ID.',
                null,
                400,
                'invalid_id'
            );
        }

        if (!function_exists('auth_api_token') || auth_api_token() === null) {
            return ContractGetResult::fail(
                'Not authenticated. Please log in again.',
                null,
                401,
                'invalid_token'
            );
        }

        if (!function_exists('api_auth_enabled') || !api_auth_enabled()) {
            return ContractGetResult::fail(
                'Partner API is not configured.',
                null,
                503,
                'api_disabled'
            );
        }

        $endpoint = (string)(function_exists('api_config')
            ? (api_config()['endpoints']['contract_get'] ?? self::GET_PATH)
            : self::GET_PATH);
        $path = str_replace('{id}', (string)$contractId, $endpoint);

        try {
            $response = $this->api->get($path, true);
        } catch (\Throwable $e) {
            return $this->mapGetTransportError($e);
        }

        $result = $this->parseGetResponse($response, $contractId);
        $this->logGetContract($contractId, $response, $result);

        return $result;
    }

    /**
     * POST http://karviapi1.de/karvipartner/contracts/{contract_id}/send
     * No request body. Uses session Bearer token + X-Request-Datetime via ApiService.
     */
    public function sendContract(int $contractId): ContractSendResult
    {
        if ($contractId <= 0) {
            return ContractSendResult::fail(
                'Invalid contract ID.',
                null,
                400,
                'invalid_id'
            );
        }

        if (!function_exists('auth_api_token') || auth_api_token() === null) {
            return ContractSendResult::fail(
                'Not authenticated. Please log in again.',
                null,
                401,
                'invalid_token'
            );
        }

        if (!function_exists('api_auth_enabled') || !api_auth_enabled()) {
            return ContractSendResult::fail(
                'Partner API is not configured.',
                null,
                503,
                'api_disabled'
            );
        }

        $endpoint = (string)(function_exists('api_config')
            ? (api_config()['endpoints']['contract_send'] ?? self::SEND_PATH)
            : self::SEND_PATH);
        $path = str_replace('{id}', (string)$contractId, $endpoint);

        try {
            $response = $this->api->request('POST', $path, null, true);
        } catch (\Throwable $e) {
            $result = $this->mapSendTransportError($e);
            $this->logSendContract($contractId, ['http_status' => 0, 'success' => false, 'message' => $e->getMessage()], $result);

            return $result;
        }

        $result = $this->parseSendResponse($response);
        $this->logSendContract($contractId, $response, $result);

        if (!$result->success) {
            return $result;
        }

        $refreshed = $this->getContract($contractId);
        $contract = is_array($refreshed->contract) ? $refreshed->contract : [];
        $contract['sign_url'] = $result->signUrl !== '' ? $result->signUrl : ($contract['sign_url'] ?? '');
        $contract['sign_token'] = $result->signToken !== '' ? $result->signToken : ($contract['sign_token'] ?? '');
        $contract['partner_api_send_response'] = $result->apiResponse;

        if ($contract !== []) {
            $this->persistRefreshedContract($contract);
        }

        return $result;
    }

    /**
     * DELETE http://karviapi1.de/karvipartner/contracts/{contract_id}
     * No request body. Uses session Bearer token + X-Request-Datetime via ApiService.
     */
    public function deleteContract(int $contractId): ContractDeleteResult
    {
        if ($contractId <= 0) {
            return ContractDeleteResult::fail(
                'Invalid contract ID.',
                null,
                400,
                'invalid_id'
            );
        }

        if (!function_exists('auth_api_token') || auth_api_token() === null) {
            return ContractDeleteResult::fail(
                'Not authenticated. Please log in again.',
                null,
                401,
                'invalid_token'
            );
        }

        if (!function_exists('api_auth_enabled') || !api_auth_enabled()) {
            return ContractDeleteResult::fail(
                'Partner API is not configured.',
                null,
                503,
                'api_disabled'
            );
        }

        $endpoint = (string)(function_exists('api_config')
            ? (api_config()['endpoints']['contract_delete'] ?? self::DELETE_PATH)
            : self::DELETE_PATH);
        $path = str_replace('{id}', (string)$contractId, $endpoint);

        try {
            $response = $this->api->delete($path, true);
        } catch (\Throwable $e) {
            return $this->mapDeleteTransportError($e);
        }

        $result = $this->parseDeleteResponse($response, $contractId);
        $this->logDeleteContract($contractId, $response, $result);

        return $result;
    }

    /**
     * GET http://karviapi1.de/karvipartner/contracts/sign/{sign_token}
     * View contract (public) — no Authorization header, no request body.
     */
    public function viewPublicContract(string $signToken): ContractPublicViewResult
    {
        $signToken = $this->normalizeSignToken($signToken);
        if ($signToken === null) {
            return ContractPublicViewResult::fail(
                'Invalid sign token.',
                null,
                400,
                'invalid_token'
            );
        }

        if (!function_exists('api_auth_enabled') || !api_auth_enabled()) {
            return ContractPublicViewResult::fail(
                'Partner API is not configured.',
                null,
                503,
                'api_disabled'
            );
        }

        $endpoint = (string)(function_exists('api_config')
            ? (api_config()['endpoints']['contract_view_public'] ?? self::VIEW_PUBLIC_PATH)
            : self::VIEW_PUBLIC_PATH);
        $path = str_replace('{token}', rawurlencode($signToken), $endpoint);

        try {
            $response = $this->api->request('GET', $path, null, false);
        } catch (\Throwable $e) {
            return $this->mapPublicViewTransportError($e);
        }

        $result = $this->parsePublicViewResponse($response, $signToken);
        $this->logPublicViewContract($signToken, $response, $result);

        return $result;
    }

    /**
     * POST http://karviapi1.de/karvipartner/contracts/sign/{sign_token}
     * Sign contract (public) — no Authorization header.
     *
     * @param array<string,mixed> $data Body with terms_accepted and raw_data
     */
    public function signContractPublic(string $signToken, array $data): ContractPublicSignResult
    {
        $signToken = $this->normalizeSignToken($signToken);
        if ($signToken === null) {
            return ContractPublicSignResult::fail(
                'Invalid sign token.',
                null,
                400,
                'invalid_token'
            );
        }

        if (!function_exists('api_customer_api_enabled') || !api_customer_api_enabled()) {
            return ContractPublicSignResult::fail(
                'Partner API is not configured.',
                null,
                503,
                'api_disabled'
            );
        }

        $payload = $this->normalizePublicSignPayload($data);

        $endpoint = (string)(function_exists('api_config')
            ? (api_config()['endpoints']['contract_sign_public'] ?? api_config()['endpoints']['contract_sign'] ?? self::VIEW_PUBLIC_PATH)
            : self::VIEW_PUBLIC_PATH);
        $path = str_replace('{token}', rawurlencode($signToken), $endpoint);

        try {
            $response = $this->api->request('POST', $path, $payload, false);
        } catch (\Throwable $e) {
            return $this->mapPublicSignTransportError($e);
        }

        $result = $this->parsePublicSignResponse($response);
        $this->logPublicSignContract($signToken, $payload, $response, $result);

        if ($result->success && is_array($result->contract)) {
            $merged = $result->contract;
            $merged['sign_token'] = $signToken;
            $merged['token'] = $signToken;
            $this->persistRefreshedContract($merged);
        }

        return $result;
    }

    /**
     * Build Sign contract (public) request body from form POST + client context.
     *
     * @param array<string,mixed> $post
     * @return array{terms_accepted:bool,raw_data:array<string,mixed>}
     */
    public function buildPublicSignPayloadFromPost(array $post): array
    {
        $rawData = [
            'signed_from' => trim((string)($post['signed_from'] ?? 'customer page')) ?: 'customer page',
            'request_datetime' => ApiService::requestDatetime(),
        ];

        foreach (['user_agent', 'browser', 'device_type', 'os', 'screen_resolution', 'timezone'] as $key) {
            $value = trim((string)($post[$key] ?? ''));
            if ($value !== '') {
                $rawData[$key] = $value;
            }
        }

        if (($rawData['user_agent'] ?? '') === '' && !empty($_SERVER['HTTP_USER_AGENT'])) {
            $rawData['user_agent'] = trim((string)$_SERVER['HTTP_USER_AGENT']);
        }

        if (!empty($_SERVER['REMOTE_ADDR'])) {
            $rawData['customer_ip'] = trim((string)$_SERVER['REMOTE_ADDR']);
        }

        $json = trim((string)($post['raw_data_json'] ?? ''));
        if ($json !== '') {
            $decoded = json_decode($json, true);
            if (!is_array($decoded)) {
                throw new ApiException('raw_data must be valid JSON.', 422);
            }
            $rawData = array_replace_recursive($rawData, $decoded);
        }

        if (isset($post['raw_data']) && is_array($post['raw_data'])) {
            $rawData = array_replace_recursive($rawData, $post['raw_data']);
        }

        return $this->normalizePublicSignPayload([
            'terms_accepted' => !empty($post['terms_accepted']),
            'raw_data' => $rawData,
        ]);
    }

    /**
     * PUT http://karviapi1.de/karvipartner/contracts/{contract_id}
     * Uses session Bearer token + X-Request-Datetime via ApiService.
     *
     * @param array<string,mixed> $data Exact Edit contract API body
     */
    public function updateContract(int $contractId, array $data): ContractUpdateResult
    {
        if ($contractId <= 0) {
            return ContractUpdateResult::fail(
                'Invalid contract ID.',
                null,
                400,
                'invalid_id'
            );
        }

        if (!function_exists('auth_api_token') || auth_api_token() === null) {
            return ContractUpdateResult::fail(
                'Not authenticated. Please log in again.',
                null,
                401,
                'invalid_token'
            );
        }

        if (!function_exists('api_auth_enabled') || !api_auth_enabled()) {
            return ContractUpdateResult::fail(
                'Partner API is not configured.',
                null,
                503,
                'api_disabled'
            );
        }

        $endpoint = (string)(function_exists('api_config')
            ? (api_config()['endpoints']['contract_update'] ?? self::UPDATE_PATH)
            : self::UPDATE_PATH);
        $path = str_replace('{id}', (string)$contractId, $endpoint);

        $payload = $this->normalizeEditPayload($data);

        try {
            $response = $this->api->put($path, $payload, true);
        } catch (\Throwable $e) {
            return $this->mapUpdateTransportError($e);
        }

        $result = $this->parseUpdateResponse($response);
        $this->logUpdateContract($contractId, $payload, $response, $result);

        if (!$result->success) {
            return $result;
        }

        $refreshed = $this->getContract($contractId);
        if ($refreshed->success && is_array($refreshed->contract)) {
            $this->persistRefreshedContract($refreshed->contract);

            return ContractUpdateResult::ok(
                $result->message,
                $result->apiResponse ?? [],
                $result->contract ?? [],
                $refreshed->contract,
                $result->httpStatus
            );
        }

        return $result;
    }

    /**
     * Build Edit contract payload from admin form POST.
     *
     * @param array<string,mixed> $post
     * @param array<string,mixed> $existing
     * @return array<string,mixed>
     */
    public function buildEditPayloadFromPost(array $post, array $existing = []): array
    {
        $payload = $this->buildEditPayloadFromExisting($existing, []);

        foreach (self::EDIT_API_FIELDS as $field) {
            if ($field === 'owner_name') {
                $payload['owner_name'] = $this->parseOwnerNameInput($post, $existing);
                continue;
            }

            if ($field === 'raw_data') {
                $rawData = $this->parseRawDataInput($post);
                if ($rawData !== [] || trim((string)($post['raw_data_json'] ?? '')) !== '') {
                    $payload['raw_data'] = $rawData;
                }
                continue;
            }

            if ($field === 'terms_accepted') {
                if (array_key_exists('terms_accepted', $post)) {
                    $payload['terms_accepted'] = $this->parseBooleanInput($post['terms_accepted']);
                }
                continue;
            }

            if (array_key_exists($field, $post)) {
                $payload[$field] = $this->parseNullableScalar($post[$field]);
            }
        }

        return $this->normalizeEditPayload($payload);
    }

    /**
     * Merge partial updates into a full Edit contract payload.
     *
     * @param array<string,mixed> $existing
     * @param array<string,mixed> $updates
     * @return array<string,mixed>
     */
    public function buildEditPayloadFromExisting(array $existing, array $updates): array
    {
        $payload = [];

        foreach (self::EDIT_API_FIELDS as $field) {
            if ($field === 'raw_data') {
                $raw = $existing['raw_data'] ?? ($existing['api_raw_data'] ?? []);
                $payload['raw_data'] = is_array($raw) ? $raw : [];
                continue;
            }

            if (array_key_exists($field, $existing)) {
                $payload[$field] = $existing[$field];
            } else {
                $payload[$field] = $this->defaultEditFieldValue($field);
            }
        }

        foreach ($updates as $key => $value) {
            if ($key === 'pdf_viewed_at') {
                $payload['viewed_at'] = $value;
                continue;
            }

            if (in_array($key, self::EDIT_API_FIELDS, true)) {
                $payload[$key] = $value;
                continue;
            }

            $rawData = is_array($payload['raw_data'] ?? null) ? $payload['raw_data'] : [];
            $rawData[$key] = $value;
            $payload['raw_data'] = $rawData;
        }

        return $this->normalizeEditPayload($payload);
    }

    /**
     * Build local record + call Edit contract API, persist merged result.
     *
     * @param array<string,mixed> $post
     */
    public function updateContractFromPost(int $contractId, array $post): ContractUpdateResult
    {
        $existing = [];
        $current = $this->getContract($contractId);
        if ($current->success && is_array($current->contract)) {
            $existing = $current->contract;
        }

        try {
            $payload = $this->buildEditPayloadFromPost($post, $existing);
        } catch (ApiException $e) {
            return ContractUpdateResult::fail($e->getMessage(), $e->response, max(400, $e->statusCode), 'validation_error');
        }

        return $this->updateContract($contractId, $payload);
    }

    /**
     * Build local record + call Create contract API, persist merged result.
     *
     * @param array<string,mixed> $localContract
     */
    public function createContractFromRecord(array $localContract): ContractCreateResult
    {
        require_once dirname(__DIR__) . '/includes/api_service.php';

        $payload = api_build_partner_create_payload($localContract);
        $payload = $this->mergeRawDataOverride($payload, $localContract);

        $result = $this->createContract($payload);

        if (!$result->success || !is_array($result->apiContract)) {
            return $result;
        }

        $merged = api_merge_partner_contract_into_local($localContract, $result->apiContract);
        $merged['partner_api_create_response'] = $result->apiResponse;

        if (function_exists('contract_persist_local_contracts') && contract_persist_local_contracts()) {
            require_once dirname(__DIR__) . '/includes/local_store.php';
            $merged = local_store_save($merged);
        }

        return ContractCreateResult::ok(
            $result->message,
            $result->apiResponse ?? [],
            $result->apiContract,
            $merged,
            $result->httpStatus
        );
    }

    /**
     * Build exact API request body from form POST (Create contract fields).
     *
     * @return array<string,mixed>
     */
    public function buildPayloadFromPost(array $post): array
    {
        require_once dirname(__DIR__) . '/includes/api_service.php';

        $type = api_normalize_contract_type((string)($post['type'] ?? 'partner'));

        $payload = [
            'type' => $type,
            'restaurant_name' => trim((string)($post['restaurant_name'] ?? '')),
            'brand' => trim((string)($post['brand'] ?? '')),
            'owner_name' => trim((string)($post['owner_name'] ?? '')),
            'company_phone' => trim((string)($post['company_phone'] ?? '')),
            'street_address' => trim((string)($post['street_address'] ?? $post['street'] ?? '')),
            'zip_city' => trim((string)($post['zip_city'] ?? $post['postal_city'] ?? '')),
            'email' => trim((string)($post['email'] ?? '')),
            'contract_url' => trim((string)($post['contract_url'] ?? '')),
        ];

        if ($payload['contract_url'] === '' && function_exists('api_partner_create_contract_url')) {
            $payload['contract_url'] = api_partner_create_contract_url();
        }

        $rawData = $this->parseRawDataInput($post);
        if ($rawData !== []) {
            $payload['raw_data'] = $rawData;
        }

        return $payload;
    }

    /** @param array<string,mixed> $post */
    private function parseRawDataInput(array $post): array
    {
        if (isset($post['raw_data']) && is_array($post['raw_data'])) {
            return $post['raw_data'];
        }

        $json = trim((string)($post['raw_data_json'] ?? ''));
        if ($json === '') {
            return [];
        }

        $decoded = json_decode($json, true);
        if (!is_array($decoded)) {
            throw new ApiException('raw_data must be valid JSON.', 422);
        }

        return $decoded;
    }

    /** @param array<string,mixed> $payload @param array<string,mixed> $localContract */
    private function mergeRawDataOverride(array $payload, array $localContract): array
    {
        $override = $localContract['raw_data_override'] ?? null;
        if (!is_array($override) || $override === []) {
            return $payload;
        }

        $existing = is_array($payload['raw_data'] ?? null) ? $payload['raw_data'] : [];
        $payload['raw_data'] = array_replace_recursive($existing, $override);

        return $payload;
    }

    /** @param array<string,mixed> $response */
    private function parseCreateResponse(array $response): ContractCreateResult
    {
        $httpStatus = (int)($response['http_status'] ?? 0);
        $data = is_array($response['data'] ?? null) ? $response['data'] : null;

        if (!$response['success'] || !is_array($data)) {
            return $this->mapHttpError($httpStatus, (string)($response['message'] ?? 'Contract could not be created.'), $data);
        }

        if (empty($data['success'])) {
            return $this->mapHttpError(
                $httpStatus >= 400 ? $httpStatus : 422,
                (string)($data['message'] ?? 'Contract could not be created.'),
                $data
            );
        }

        $contract = null;
        if (isset($data['contract']) && is_array($data['contract'])) {
            $contract = $data['contract'];
        } elseif (!empty($data['sign_token']) || !empty($data['id'])) {
            $contract = $data;
        }

        if ($contract === null || trim((string)($contract['sign_token'] ?? '')) === '') {
            return ContractCreateResult::fail(
                'API did not return contract sign_token.',
                $data,
                502,
                'invalid_response'
            );
        }

        return ContractCreateResult::ok(
            (string)($data['message'] ?? 'Contract created.'),
            $data,
            $contract,
            null,
            $httpStatus > 0 ? $httpStatus : 200
        );
    }

    private function mapTransportError(\Throwable $e): ContractCreateResult
    {
        if ($e instanceof ApiException) {
            return $this->mapHttpError($e->statusCode, $e->getMessage(), $e->response);
        }

        $message = trim($e->getMessage());
        if (str_contains(strtolower($message), 'timed out') || str_contains(strtolower($message), 'timeout')) {
            return ContractCreateResult::fail('API request timed out. Please try again.', null, 504, 'timeout');
        }

        return ContractCreateResult::fail(
            $message !== '' ? $message : 'Could not connect to the Partner API.',
            null,
            502,
            'connection_error'
        );
    }

    /** @param array<string,mixed>|null $data */
    private function mapHttpError(int $httpStatus, string $message, ?array $data): ContractCreateResult
    {
        $errorCode = match (true) {
            $httpStatus === 401 => 'invalid_token',
            $httpStatus === 422, $httpStatus === 400 => 'validation_error',
            $httpStatus === 504, str_contains(strtolower($message), 'timeout') => 'timeout',
            $httpStatus >= 500 => 'server_error',
            default => 'api_error',
        };

        if ($httpStatus === 401) {
            $message = 'Invalid or expired session token. Please log in again.';
        }

        return ContractCreateResult::fail($message, $data, max(400, $httpStatus), $errorCode);
    }

    /** @param array<string,mixed> $payload @param array<string,mixed> $response */
    private function logCreateContract(array $payload, array $response, ContractCreateResult $result): void
    {
        if (!function_exists('app_log_dir')) {
            return;
        }

        $safePayload = $payload;
        if (isset($safePayload['raw_data']) && is_array($safePayload['raw_data'])) {
            $safePayload['raw_data'] = '[array:' . count($safePayload['raw_data']) . ']';
        }

        $entry = [
            'ts' => gmdate('c'),
            'api' => 'Create contract',
            'http_status' => (int)($response['http_status'] ?? 0),
            'success' => $result->success,
            'error_code' => $result->errorCode,
            'contract_id' => $result->id(),
            'request' => $safePayload,
            'response' => $result->apiResponse,
        ];

        $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($line === false) {
            return;
        }

        @file_put_contents(app_log_dir() . '/api.log', $line . PHP_EOL, FILE_APPEND | LOCK_EX);
    }

    /** @param array<string,mixed> $response */
    private function parseGetResponse(array $response, int $contractId): ContractGetResult
    {
        $httpStatus = (int)($response['http_status'] ?? 0);
        $data = is_array($response['data'] ?? null) ? $response['data'] : null;

        if (!$response['success'] || !is_array($data)) {
            return $this->mapGetHttpError(
                $httpStatus,
                (string)($response['message'] ?? 'Could not load contract.'),
                $data
            );
        }

        if (empty($data['success'])) {
            return $this->mapGetHttpError(
                $httpStatus >= 400 ? $httpStatus : 404,
                (string)($data['message'] ?? 'Contract not found.'),
                $data
            );
        }

        $contract = null;
        if (isset($data['contract']) && is_array($data['contract'])) {
            $contract = $data['contract'];
        } elseif (!empty($data['id'])) {
            $contract = $data;
        }

        if ($contract === null || (int)($contract['id'] ?? 0) <= 0) {
            return ContractGetResult::fail(
                'API did not return contract data.',
                $data,
                502,
                'invalid_response'
            );
        }

        return ContractGetResult::ok(
            (string)($data['message'] ?? 'OK'),
            $data,
            $contract,
            $httpStatus > 0 ? $httpStatus : 200
        );
    }

    private function mapGetTransportError(\Throwable $e): ContractGetResult
    {
        if ($e instanceof ApiException) {
            return $this->mapGetHttpError($e->statusCode, $e->getMessage(), $e->response);
        }

        $message = trim($e->getMessage());
        if (str_contains(strtolower($message), 'timed out') || str_contains(strtolower($message), 'timeout')) {
            return ContractGetResult::fail('API request timed out. Please try again.', null, 504, 'timeout');
        }

        return ContractGetResult::fail(
            $message !== '' ? $message : 'Could not connect to the Partner API.',
            null,
            502,
            'connection_error'
        );
    }

    /** @param array<string,mixed>|null $data */
    private function mapGetHttpError(int $httpStatus, string $message, ?array $data): ContractGetResult
    {
        $errorCode = match (true) {
            $httpStatus === 401 => 'invalid_token',
            $httpStatus === 404 => 'not_found',
            $httpStatus === 422, $httpStatus === 400 => 'validation_error',
            $httpStatus === 504, str_contains(strtolower($message), 'timeout') => 'timeout',
            $httpStatus >= 500 => 'server_error',
            default => 'api_error',
        };

        if ($httpStatus === 401) {
            $message = 'Invalid or expired session token. Please log in again.';
        } elseif ($httpStatus === 404) {
            $message = 'Contract not found.';
        }

        return ContractGetResult::fail($message, $data, max(400, $httpStatus), $errorCode);
    }

    /** @param array<string,mixed> $response */
    private function logGetContract(int $contractId, array $response, ContractGetResult $result): void
    {
        if (!function_exists('app_log_dir')) {
            return;
        }

        $entry = [
            'ts' => gmdate('c'),
            'api' => 'Get contract',
            'contract_id' => $contractId,
            'http_status' => (int)($response['http_status'] ?? 0),
            'success' => $result->success,
            'error_code' => $result->errorCode,
            'response' => $result->apiResponse,
        ];

        $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($line === false) {
            return;
        }

        @file_put_contents(app_log_dir() . '/api.log', $line . PHP_EOL, FILE_APPEND | LOCK_EX);
    }

    /** @param array<string,mixed> $data @return array<string,mixed> */
    private function normalizeEditPayload(array $data): array
    {
        $payload = [];

        foreach (self::EDIT_API_FIELDS as $field) {
            if (!array_key_exists($field, $data)) {
                continue;
            }

            $value = $data[$field];

            if ($field === 'owner_name') {
                $payload[$field] = $this->normalizeOwnerNameValue($value);
                continue;
            }

            if ($field === 'raw_data') {
                $payload[$field] = is_array($value) ? $value : [];
                continue;
            }

            if ($field === 'terms_accepted') {
                $payload[$field] = (bool)$value;
                continue;
            }

            $payload[$field] = $value;
        }

        return $payload;
    }

    private function normalizeOwnerNameValue(mixed $value): string|array
    {
        if (is_array($value)) {
            $names = [];
            foreach ($value as $name) {
                $name = trim((string)$name);
                if ($name !== '') {
                    $names[] = $name;
                }
            }

            return $names !== [] ? $names : '';
        }

        return trim((string)$value);
    }

    private function defaultEditFieldValue(string $field): mixed
    {
        return match ($field) {
            'raw_data' => [],
            'terms_accepted' => false,
            'customer_ip', 'terms_accepted_at', 'sent_at', 'viewed_at', 'signed_at',
            'signer_device_type', 'signer_browser', 'signer_os' => null,
            default => '',
        };
    }

    /** @param array<string,mixed> $post @param array<string,mixed> $existing */
    private function parseOwnerNameInput(array $post, array $existing): string|array
    {
        $mode = trim((string)($post['owner_name_mode'] ?? ''));

        if ($mode === 'json') {
            $json = trim((string)($post['owner_name_json'] ?? ''));
            if ($json === '') {
                return $this->normalizeOwnerNameValue($existing['owner_name'] ?? '');
            }

            $decoded = json_decode($json, true);
            if (!is_array($decoded)) {
                throw new ApiException('owner_name must be valid JSON (string or array of names).', 422);
            }

            if ($decoded !== [] && array_is_list($decoded)) {
                return $this->normalizeOwnerNameValue($decoded);
            }

            if (isset($decoded['owner_name'])) {
                return $this->normalizeOwnerNameValue($decoded['owner_name']);
            }

            throw new ApiException('owner_name JSON must be an array of names or contain owner_name.', 422);
        }

        if ($mode === 'array' || isset($post['owner_names']) && is_array($post['owner_names'])) {
            $names = [];
            foreach ($post['owner_names'] as $name) {
                $name = trim((string)$name);
                if ($name !== '') {
                    $names[] = $name;
                }
            }

            return $names !== [] ? $names : $this->normalizeOwnerNameValue($existing['owner_name'] ?? '');
        }

        $single = trim((string)($post['owner_name'] ?? ''));
        if ($single !== '') {
            return $single;
        }

        return $this->normalizeOwnerNameValue($existing['owner_name'] ?? '');
    }

    private function parseBooleanInput(mixed $value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        $normalized = strtolower(trim((string)$value));

        return in_array($normalized, ['1', 'true', 'yes', 'on'], true);
    }

    private function parseNullableScalar(mixed $value): mixed
    {
        if ($value === null) {
            return null;
        }

        if (is_string($value) && trim($value) === '') {
            return null;
        }

        return $value;
    }

    /** @param array<string,mixed> $response */
    private function parseUpdateResponse(array $response): ContractUpdateResult
    {
        $httpStatus = (int)($response['http_status'] ?? 0);
        $data = is_array($response['data'] ?? null) ? $response['data'] : null;

        if (!$response['success'] || !is_array($data)) {
            return $this->mapUpdateHttpError(
                $httpStatus,
                (string)($response['message'] ?? 'Contract could not be updated.'),
                $data
            );
        }

        if (empty($data['success'])) {
            return $this->mapUpdateHttpError(
                $httpStatus >= 400 ? $httpStatus : 422,
                (string)($data['message'] ?? 'Contract could not be updated.'),
                $data
            );
        }

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

        if ($contract === null) {
            return ContractUpdateResult::fail(
                'API did not return updated contract data.',
                $data,
                502,
                'invalid_response'
            );
        }

        return ContractUpdateResult::ok(
            (string)($data['message'] ?? 'Contract updated.'),
            $data,
            $contract,
            null,
            $httpStatus > 0 ? $httpStatus : 200
        );
    }

    private function mapUpdateTransportError(\Throwable $e): ContractUpdateResult
    {
        if ($e instanceof ApiException) {
            return $this->mapUpdateHttpError($e->statusCode, $e->getMessage(), $e->response);
        }

        $message = trim($e->getMessage());
        if (str_contains(strtolower($message), 'timed out') || str_contains(strtolower($message), 'timeout')) {
            return ContractUpdateResult::fail('API request timed out. Please try again.', null, 504, 'timeout');
        }

        return ContractUpdateResult::fail(
            $message !== '' ? $message : 'Could not connect to the Partner API.',
            null,
            502,
            'connection_error'
        );
    }

    /** @param array<string,mixed>|null $data */
    private function mapUpdateHttpError(int $httpStatus, string $message, ?array $data): ContractUpdateResult
    {
        $errorCode = match (true) {
            $httpStatus === 401 => 'invalid_token',
            $httpStatus === 404 => 'not_found',
            $httpStatus === 422, $httpStatus === 400 => 'validation_error',
            $httpStatus === 504, str_contains(strtolower($message), 'timeout') => 'timeout',
            $httpStatus >= 500 => 'server_error',
            default => 'api_error',
        };

        if ($httpStatus === 401) {
            $message = 'Invalid or expired session token. Please log in again.';
        } elseif ($httpStatus === 404) {
            $message = 'Contract not found.';
        }

        return ContractUpdateResult::fail($message, $data, max(400, $httpStatus), $errorCode);
    }

    /** @param array<string,mixed> $payload @param array<string,mixed> $response */
    private function logUpdateContract(int $contractId, array $payload, array $response, ContractUpdateResult $result): void
    {
        if (!function_exists('app_log_dir')) {
            return;
        }

        $safePayload = $payload;
        if (isset($safePayload['raw_data']) && is_array($safePayload['raw_data'])) {
            $safePayload['raw_data'] = '[array:' . count($safePayload['raw_data']) . ']';
        }

        $entry = [
            'ts' => gmdate('c'),
            'api' => 'Edit contract',
            'contract_id' => $contractId,
            'http_status' => (int)($response['http_status'] ?? 0),
            'success' => $result->success,
            'error_code' => $result->errorCode,
            'request' => $safePayload,
            'response' => $result->apiResponse,
        ];

        $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($line === false) {
            return;
        }

        @file_put_contents(app_log_dir() . '/api.log', $line . PHP_EOL, FILE_APPEND | LOCK_EX);
    }

    /** @param array<string,mixed> $response */
    private function parseSendResponse(array $response): ContractSendResult
    {
        $httpStatus = (int)($response['http_status'] ?? 0);
        $data = is_array($response['data'] ?? null) ? $response['data'] : null;

        if (!$response['success'] || !is_array($data)) {
            return $this->mapSendHttpError(
                $httpStatus,
                (string)($response['message'] ?? 'Contract could not be sent.'),
                $data
            );
        }

        if (array_key_exists('success', $data) && empty($data['success'])) {
            return $this->mapSendHttpError(
                $httpStatus >= 400 ? $httpStatus : 422,
                (string)($data['message'] ?? 'Contract could not be sent.'),
                $data
            );
        }

        $signUrl = trim((string)($data['sign_url'] ?? ''));
        $signToken = trim((string)($data['sign_token'] ?? ''));

        if ($signUrl === '' && $signToken === '') {
            return ContractSendResult::fail(
                'API did not return sign_url or sign_token.',
                $data,
                502,
                'invalid_response'
            );
        }

        return ContractSendResult::ok(
            (string)($data['message'] ?? 'Contract sent successfully.'),
            $signUrl,
            $signToken,
            $data,
            $httpStatus > 0 ? $httpStatus : 200
        );
    }

    private function mapSendTransportError(\Throwable $e): ContractSendResult
    {
        if ($e instanceof ApiException) {
            return $this->mapSendHttpError($e->statusCode, $e->getMessage(), $e->response);
        }

        $message = trim($e->getMessage());
        if (str_contains(strtolower($message), 'timed out') || str_contains(strtolower($message), 'timeout')) {
            return ContractSendResult::fail('API request timed out. Please try again.', null, 504, 'timeout');
        }

        return ContractSendResult::fail(
            $message !== '' ? $message : 'Could not connect to the Partner API.',
            null,
            502,
            'connection_error'
        );
    }

    /** @param array<string,mixed>|null $data */
    private function mapSendHttpError(int $httpStatus, string $message, ?array $data): ContractSendResult
    {
        $errorCode = match (true) {
            $httpStatus === 401 => 'invalid_token',
            $httpStatus === 404 => 'not_found',
            $httpStatus === 422, $httpStatus === 400 => 'validation_error',
            $httpStatus === 504, str_contains(strtolower($message), 'timeout') => 'timeout',
            $httpStatus >= 500 => 'server_error',
            default => 'api_error',
        };

        if ($httpStatus === 401) {
            $message = 'Invalid or expired session token. Please log in again.';
        } elseif ($httpStatus === 404) {
            $message = 'Contract not found.';
        }

        return ContractSendResult::fail($message, $data, max(400, $httpStatus), $errorCode);
    }

    /** @param array<string,mixed> $response */
    private function logSendContract(int $contractId, array $response, ContractSendResult $result): void
    {
        if (!function_exists('app_log_dir')) {
            return;
        }

        $entry = [
            'ts' => gmdate('c'),
            'api' => 'Send contract',
            'contract_id' => $contractId,
            'http_status' => (int)($response['http_status'] ?? 0),
            'success' => $result->success,
            'error_code' => $result->errorCode,
            'request_body' => null,
            'response' => $result->apiResponse,
        ];

        $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($line === false) {
            return;
        }

        @file_put_contents(app_log_dir() . '/api.log', $line . PHP_EOL, FILE_APPEND | LOCK_EX);
    }

    /** @param array<string,mixed> $response */
    private function parseDeleteResponse(array $response, int $contractId): ContractDeleteResult
    {
        $httpStatus = (int)($response['http_status'] ?? 0);
        $data = is_array($response['data'] ?? null) ? $response['data'] : null;

        if (!$response['success'] || !is_array($data)) {
            return $this->mapDeleteHttpError(
                $httpStatus,
                (string)($response['message'] ?? 'Contract could not be deleted.'),
                $data
            );
        }

        if (array_key_exists('success', $data) && empty($data['success'])) {
            return $this->mapDeleteHttpError(
                $httpStatus >= 400 ? $httpStatus : 422,
                (string)($data['message'] ?? 'Contract could not be deleted.'),
                $data
            );
        }

        $deletedId = isset($data['deleted_id']) ? (int)$data['deleted_id'] : $contractId;
        if ($deletedId <= 0) {
            $deletedId = $contractId;
        }

        return ContractDeleteResult::ok(
            (string)($data['message'] ?? 'Contract deleted.'),
            $deletedId,
            $data,
            $httpStatus > 0 ? $httpStatus : 200
        );
    }

    private function mapDeleteTransportError(\Throwable $e): ContractDeleteResult
    {
        if ($e instanceof ApiException) {
            return $this->mapDeleteHttpError($e->statusCode, $e->getMessage(), $e->response);
        }

        $message = trim($e->getMessage());
        if (str_contains(strtolower($message), 'timed out') || str_contains(strtolower($message), 'timeout')) {
            return ContractDeleteResult::fail('API request timed out. Please try again.', null, 504, 'timeout');
        }

        return ContractDeleteResult::fail(
            $message !== '' ? $message : 'Could not connect to the Partner API.',
            null,
            502,
            'connection_error'
        );
    }

    /** @param array<string,mixed>|null $data */
    private function mapDeleteHttpError(int $httpStatus, string $message, ?array $data): ContractDeleteResult
    {
        $errorCode = match (true) {
            $httpStatus === 401 => 'invalid_token',
            $httpStatus === 404 => 'not_found',
            $httpStatus === 422, $httpStatus === 400 => 'validation_error',
            $httpStatus === 504, str_contains(strtolower($message), 'timeout') => 'timeout',
            $httpStatus >= 500 => 'server_error',
            default => 'api_error',
        };

        if ($httpStatus === 401) {
            $message = 'Invalid or expired session token. Please log in again.';
        } elseif ($httpStatus === 404) {
            $message = 'Contract not found.';
        } elseif ($httpStatus === 400 && $errorCode === 'validation_error') {
            $message = 'Invalid contract ID.';
        }

        return ContractDeleteResult::fail($message, $data, max(400, $httpStatus), $errorCode);
    }

    /** @param array<string,mixed> $response */
    private function logDeleteContract(int $contractId, array $response, ContractDeleteResult $result): void
    {
        if (!function_exists('app_log_dir')) {
            return;
        }

        $entry = [
            'ts' => gmdate('c'),
            'api' => 'Delete contract',
            'contract_id' => $contractId,
            'http_status' => (int)($response['http_status'] ?? 0),
            'success' => $result->success,
            'error_code' => $result->errorCode,
            'deleted_id' => $result->deletedId,
            'request_body' => null,
            'response' => $result->apiResponse,
        ];

        $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($line === false) {
            return;
        }

        @file_put_contents(app_log_dir() . '/api.log', $line . PHP_EOL, FILE_APPEND | LOCK_EX);
    }

    /** @param array<string,mixed> $contract */
    private function persistRefreshedContract(array $contract): void
    {
        if (!function_exists('contract_persist_local_contracts') || !contract_persist_local_contracts()) {
            return;
        }

        require_once dirname(__DIR__) . '/includes/api_service.php';
        require_once dirname(__DIR__) . '/includes/local_store.php';

        $signToken = trim((string)($contract['sign_token'] ?? ''));
        $local = $signToken !== '' ? local_store_find($signToken) : null;

        if ($local === null) {
            $local = [
                'token' => $signToken,
                'sign_token' => $signToken,
                'id' => (int)($contract['id'] ?? 0),
            ];
        }

        $merged = array_merge($local, $contract);
        $merged['partner_api_id'] = $contract['id'] ?? null;
        $merged['partner_api_raw'] = $contract;
        if (isset($contract['raw_data']) && is_array($contract['raw_data'])) {
            $merged['api_raw_data'] = $contract['raw_data'];
        }

        local_store_save($merged);
    }

    private function normalizeSignToken(string $signToken): ?string
    {
        $signToken = trim($signToken);
        if ($signToken === '' || !preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $signToken)) {
            return null;
        }

        return $signToken;
    }

    /** @param array<string,mixed> $response */
    private function parsePublicViewResponse(array $response, string $signToken): ContractPublicViewResult
    {
        $httpStatus = (int)($response['http_status'] ?? 0);
        $data = is_array($response['data'] ?? null) ? $response['data'] : null;

        if (!is_array($data)) {
            return $this->mapPublicViewHttpError(
                $httpStatus,
                (string)($response['message'] ?? 'Could not load contract.'),
                null
            );
        }

        $message = (string)($data['message'] ?? ($response['message'] ?? ''));

        if (!$response['success'] || (array_key_exists('success', $data) && empty($data['success']))) {
            return $this->mapPublicViewHttpError(
                $httpStatus >= 400 ? $httpStatus : 404,
                $message !== '' ? $message : 'Signing link not found.',
                $data
            );
        }

        $contract = null;
        if (isset($data['contract']) && is_array($data['contract'])) {
            $contract = $data['contract'];
        } elseif (!empty($data['restaurant_name']) || !empty($data['sign_token'])) {
            $contract = $data;
        }

        if ($contract === null) {
            return ContractPublicViewResult::fail(
                $message !== '' ? $message : 'API did not return contract data.',
                $data,
                502,
                'invalid_response'
            );
        }

        $contract['sign_token'] = trim((string)($contract['sign_token'] ?? $signToken));

        return ContractPublicViewResult::ok(
            $message !== '' ? $message : 'OK',
            $data,
            $contract,
            $httpStatus > 0 ? $httpStatus : 200
        );
    }

    private function mapPublicViewTransportError(\Throwable $e): ContractPublicViewResult
    {
        if ($e instanceof ApiException) {
            return $this->mapPublicViewHttpError($e->statusCode, $e->getMessage(), $e->response);
        }

        $message = trim($e->getMessage());
        if (str_contains(strtolower($message), 'timed out') || str_contains(strtolower($message), 'timeout')) {
            return ContractPublicViewResult::fail('API request timed out. Please try again.', null, 504, 'timeout');
        }

        return ContractPublicViewResult::fail(
            $message !== '' ? $message : 'Could not connect to the Partner API.',
            null,
            502,
            'connection_error'
        );
    }

    /** @param array<string,mixed>|null $data */
    private function mapPublicViewHttpError(int $httpStatus, string $message, ?array $data): ContractPublicViewResult
    {
        $lowerMessage = strtolower($message);

        $errorCode = match (true) {
            $httpStatus === 404, str_contains($lowerMessage, 'not found') => 'not_found',
            str_contains($lowerMessage, 'expired') => 'expired_token',
            str_contains($lowerMessage, 'invalid') => 'invalid_token',
            $httpStatus === 504, str_contains($lowerMessage, 'timeout') => 'timeout',
            $httpStatus >= 500 => 'server_error',
            default => 'api_error',
        };

        return ContractPublicViewResult::fail($message, $data, max(400, $httpStatus), $errorCode);
    }

    /** @param array<string,mixed> $response */
    private function logPublicViewContract(string $signToken, array $response, ContractPublicViewResult $result): void
    {
        if (!function_exists('app_log_dir')) {
            return;
        }

        $entry = [
            'ts' => gmdate('c'),
            'api' => 'View contract (public)',
            'sign_token' => substr($signToken, 0, 8) . '...',
            'http_status' => (int)($response['http_status'] ?? 0),
            'success' => $result->success,
            'error_code' => $result->errorCode,
            'message' => $result->message,
            'response' => $result->apiResponse,
        ];

        $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($line === false) {
            return;
        }

        @file_put_contents(app_log_dir() . '/api.log', $line . PHP_EOL, FILE_APPEND | LOCK_EX);
    }

    /** @param array<string,mixed> $data @return array{terms_accepted:bool,raw_data:array<string,mixed>} */
    private function normalizePublicSignPayload(array $data): array
    {
        $rawData = $data['raw_data'] ?? [];
        if (!is_array($rawData)) {
            $rawData = [];
        }

        return [
            'terms_accepted' => !empty($data['terms_accepted']),
            'raw_data' => $rawData,
        ];
    }

    /** @param array<string,mixed> $response */
    private function parsePublicSignResponse(array $response): ContractPublicSignResult
    {
        $httpStatus = (int)($response['http_status'] ?? 0);
        $data = is_array($response['data'] ?? null) ? $response['data'] : null;

        if (!is_array($data)) {
            return $this->mapPublicSignHttpError(
                $httpStatus,
                (string)($response['message'] ?? 'Contract could not be signed.'),
                null
            );
        }

        $message = (string)($data['message'] ?? ($response['message'] ?? ''));

        if (!$response['success'] || (array_key_exists('success', $data) && empty($data['success']))) {
            return $this->mapPublicSignHttpError(
                $httpStatus >= 400 ? $httpStatus : 422,
                $message !== '' ? $message : 'Signing link not found.',
                $data
            );
        }

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

        return ContractPublicSignResult::ok(
            $message !== '' ? $message : 'Contract signed successfully.',
            $data,
            $contract,
            $httpStatus > 0 ? $httpStatus : 200
        );
    }

    private function mapPublicSignTransportError(\Throwable $e): ContractPublicSignResult
    {
        if ($e instanceof ApiException) {
            return $this->mapPublicSignHttpError($e->statusCode, $e->getMessage(), $e->response);
        }

        $message = trim($e->getMessage());
        if (str_contains(strtolower($message), 'timed out') || str_contains(strtolower($message), 'timeout')) {
            return ContractPublicSignResult::fail('API request timed out. Please try again.', null, 504, 'timeout');
        }

        return ContractPublicSignResult::fail(
            $message !== '' ? $message : 'Could not connect to the Partner API.',
            null,
            502,
            'connection_error'
        );
    }

    /** @param array<string,mixed>|null $data */
    private function mapPublicSignHttpError(int $httpStatus, string $message, ?array $data): ContractPublicSignResult
    {
        $lowerMessage = strtolower($message);

        $errorCode = match (true) {
            $httpStatus === 404, str_contains($lowerMessage, 'not found') => 'not_found',
            str_contains($lowerMessage, 'expired') => 'expired_token',
            str_contains($lowerMessage, 'invalid') => 'invalid_token',
            $httpStatus === 504, str_contains(strtolower($message), 'timeout') => 'timeout',
            $httpStatus >= 500 => 'server_error',
            default => 'api_error',
        };

        return ContractPublicSignResult::fail($message, $data, max(400, $httpStatus), $errorCode);
    }

    /** @param array<string,mixed> $payload @param array<string,mixed> $response */
    private function logPublicSignContract(string $signToken, array $payload, array $response, ContractPublicSignResult $result): void
    {
        if (!function_exists('app_log_dir')) {
            return;
        }

        $safePayload = $payload;
        if (isset($safePayload['raw_data']) && is_array($safePayload['raw_data'])) {
            $safePayload['raw_data'] = '[array:' . count($safePayload['raw_data']) . ']';
        }

        $entry = [
            'ts' => gmdate('c'),
            'api' => 'Sign contract (public)',
            'sign_token' => substr($signToken, 0, 8) . '...',
            'http_status' => (int)($response['http_status'] ?? 0),
            'success' => $result->success,
            'error_code' => $result->errorCode,
            'message' => $result->message,
            'request' => $safePayload,
            'response' => $result->apiResponse,
        ];

        $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($line === false) {
            return;
        }

        @file_put_contents(app_log_dir() . '/api.log', $line . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
}
