<?php

declare(strict_types=1);

require_once __DIR__ . '/api_client.php';
require_once __DIR__ . '/local_store.php';
require_once __DIR__ . '/contract_admin.php';
require_once __DIR__ . '/contract_form.php';

/**
 * API service layer with local JSON fallback when external API is not configured.
 */

/** @return list<string> */
function api_contract_types(): array
{
    return ['partner', 'website', 'pos'];
}

function api_normalize_contract_type(?string $type): string
{
    $type = strtolower(trim((string)$type));

    return in_array($type, api_contract_types(), true) ? $type : 'partner';
}

function api_contract_type_from_record(array $contract): string
{
    if (!empty($contract['type'])) {
        return api_normalize_contract_type((string)$contract['type']);
    }

    $mode = strtolower(trim((string)($contract['invoice_mode'] ?? '')));
    if (in_array($mode, api_contract_types(), true)) {
        return $mode;
    }

    if (function_exists('invoice_contract_is') && invoice_contract_is($contract)) {
        $line = trim((string)($contract['contract_line'] ?? ''));
        if ($line === 'Karvi Website') {
            return 'website';
        }
        if (in_array($line, ['Karvi POS', 'TSE Lieferando'], true)) {
            return 'pos';
        }
    }

    return 'partner';
}

function api_contract_matches_type(array $contract, ?string $type): bool
{
    if ($type === null || $type === '') {
        return true;
    }

    return api_contract_type_from_record($contract) === api_normalize_contract_type($type);
}

function api_map_partner_contract_status(string $status, ?array $contract = null): string
{
    if (is_array($contract)) {
        $signedAt = trim((string)($contract['signed_at'] ?? ''));
        if ($signedAt !== '') {
            return 'signed';
        }

        $localStatus = strtolower(trim((string)($contract['status'] ?? '')));
        if ($localStatus === 'signed') {
            return 'signed';
        }
        if ($localStatus === 'signing') {
            return 'signing';
        }
    }

    return match (strtolower(trim($status))) {
        'signed', 'complete', 'completed', 'registered' => 'signed',
        'signing', 'viewed', 'in_progress' => 'signing',
        default => 'pending',
    };
}

function api_expand_partner_contract_fields(array $contract): array
{
    if (!empty($contract['raw_data']) && is_array($contract['raw_data'])) {
        $contract['api_raw_data'] = $contract['raw_data'];
        $contract = array_merge($contract['raw_data'], $contract);
    }

    return $contract;
}

function api_normalize_partner_contract(array $contract): array
{
    $contract = api_expand_partner_contract_fields($contract);

    $normalized = [
        'id' => (int)($contract['id'] ?? 0),
        'token' => (string)($contract['sign_token'] ?? $contract['token'] ?? ''),
        'restaurant_name' => (string)($contract['restaurant_name'] ?? ''),
        'partner_name' => (string)($contract['restaurant_name'] ?? ''),
        'partner_email' => (string)($contract['email'] ?? ''),
        'partner_type' => (string)($contract['brand'] ?? ''),
        'partnership_type' => (string)($contract['brand'] ?? ''),
        'status' => api_map_partner_contract_status(
            (string)($contract['contract_status'] ?? 'draft'),
            $contract
        ),
        'contract_status' => (string)($contract['contract_status'] ?? ''),
        'created_at' => (string)($contract['created_at'] ?? ''),
        'signed_at' => $contract['signed_at'] ?? null,
        'sign_url' => api_partner_sign_url((string)($contract['sign_token'] ?? $contract['token'] ?? ''), $contract),
        'sign_token' => (string)($contract['sign_token'] ?? $contract['token'] ?? ''),
        'customer_url' => contract_public_sign_path((string)($contract['sign_token'] ?? '')),
        'owner_name' => (string)($contract['owner_name'] ?? ''),
        'signed_pdf_path' => (string)($contract['signed_pdf_path'] ?? ''),
        'type' => api_contract_type_from_record($contract),
    ];

    return api_apply_local_contract_overrides($normalized);
}

function api_apply_local_contract_overrides(array $contract): array
{
    $token = trim((string)($contract['token'] ?? $contract['sign_token'] ?? ''));
    if ($token === '') {
        return $contract;
    }

    $local = local_store_find($token);
    if ($local === null) {
        return $contract;
    }

    $merged = array_merge($local, $contract);
    $merged['id'] = (int)($contract['id'] ?? $local['id'] ?? 0);
    $merged['token'] = $token;
    $merged['sign_token'] = trim((string)($local['sign_token'] ?? $token));

    if (($local['status'] ?? '') === 'signed') {
        $merged['status'] = 'signed';
        $merged['contract_status'] = 'signed';
        $merged['signed_at'] = $merged['signed_at'] ?? $local['signed_at'] ?? null;
    } elseif (($local['status'] ?? '') === 'signing' && ($merged['status'] ?? '') === 'pending') {
        $merged['status'] = 'signing';
    }

    if (!empty($local['signed_pdf_path'])) {
        $merged['signed_pdf_path'] = (string)$local['signed_pdf_path'];
    }

    if (!empty($local['pdf_path']) && empty($merged['pdf_path'])) {
        $merged['pdf_path'] = (string)$local['pdf_path'];
    }

    return $merged;
}

function api_ensure_local_contract_record(array $contract, string $token): array
{
    $token = trim($token);
    if ($token === '') {
        return $contract;
    }

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

    $existing = local_store_find($token);
    if ($existing !== null) {
        return array_merge($existing, $contract);
    }

    if (empty($contract['status'])) {
        $contract['status'] = 'pending';
    }

    return local_store_upsert($contract);
}

function api_fetch_partner_contracts(?string $type = null): array
{
    $path = (string)(api_config()['endpoints']['contracts_list'] ?? '/karvipartner/contracts');
    if ($type !== null && $type !== '') {
        $path .= '?type=' . rawurlencode(api_normalize_contract_type($type));
    }

    require_once __DIR__ . '/mvc.php';
    $response = \App\Services\ApiService::instance()->request('GET', $path, null, true);

    if (!$response['success'] || !is_array($response['data'])) {
        throw new ApiException((string)($response['message'] ?? 'Could not fetch contracts.'), max(400, (int)$response['http_status']));
    }

    $list = api_extract_list($response['data']);
    $normalized = array_map('api_normalize_partner_contract', $list);

    if ($type !== null && $type !== '') {
        $normalized = array_values(array_filter(
            $normalized,
            static fn(array $contract): bool => api_contract_matches_type($contract, $type)
        ));
    }

    return $normalized;
}

function api_partner_sign_url(string $signToken, ?array $contract = null): string
{
    if ($contract !== null) {
        $fromApi = trim((string)($contract['sign_url'] ?? ''));
        if ($fromApi !== '') {
            return $fromApi;
        }
    }

    $signToken = trim($signToken);
    if ($signToken === '') {
        return '';
    }

    $config = api_config();
    $baseUrl = rtrim((string)($config['base_url'] ?? ''), '/');

    return $baseUrl . '/karvipartner/contracts/sign/' . $signToken;
}

function api_partner_create_contract_url(?string $clientRef = null): string
{
    $clientRef = trim((string)$clientRef);
    if ($clientRef !== '') {
        return app_absolute_url('/contracts/sign/' . rawurlencode($clientRef));
    }

    return app_absolute_url('/contracts/sign');
}

function api_build_partner_create_payload(array $contract): array
{
    $snapshot = contract_apply_derived_fields(contract_extract_full_snapshot($contract));

    $clientRef = trim((string)($contract['token'] ?? $snapshot['token'] ?? ''));
    if ($clientRef === '') {
        $clientRef = generate_token();
    }

    $ownerName = trim((string)($snapshot['owner_name'] ?? ''));
    if ($ownerName === '' && !empty($snapshot['owner_names']) && is_array($snapshot['owner_names'])) {
        $ownerName = contract_owner_name_display($snapshot['owner_names']);
    }

    $contractType = api_contract_type_from_record(array_merge($contract, $snapshot));

    $payload = [
        'type' => $contractType,
        'restaurant_name' => trim((string)($snapshot['restaurant_name'] ?? '')),
        'brand' => trim((string)($snapshot['brand'] ?? '')),
        'owner_name' => $ownerName,
        'company_phone' => trim((string)($snapshot['company_phone'] ?? '')),
        'street_address' => trim((string)($snapshot['street'] ?? '')),
        'zip_city' => trim((string)($snapshot['postal_city'] ?? '')),
        'email' => trim((string)($snapshot['email'] ?? '')),
        'contract_url' => trim((string)($snapshot['contract_url'] ?? '')) ?: api_partner_create_contract_url($clientRef),
    ];

    $mappedSources = [
        'type',
        'restaurant_name',
        'brand',
        'owner_name',
        'company_phone',
        'street',
        'postal_city',
        'email',
        'contract_url',
    ];

    $rawData = [];
    foreach ($snapshot as $key => $value) {
        if (is_object($value)) {
            continue;
        }

        if (in_array($key, $mappedSources, true) || in_array($key, contract_duplicate_runtime_keys(), true)) {
            continue;
        }

        $rawData[$key] = $value;
    }

    foreach (contract_all_field_names() as $name) {
        if (array_key_exists($name, $snapshot)) {
            $rawData[$name] = $snapshot[$name];
        }
    }

    $rawData['brands'] = contract_brands_from_record($snapshot);
    $rawData['owner_names'] = is_array($snapshot['owner_names'] ?? null) ? $snapshot['owner_names'] : [];
    $rawData['number_of_owners'] = (string)contract_normalize_owner_count($snapshot['number_of_owners'] ?? 1);
    $rawData['owner_name'] = $ownerName;
    $rawData['brand'] = (string)($snapshot['brand'] ?? '');
    $rawData['partner_name'] = (string)($snapshot['partner_name'] ?? '');
    $rawData['partner_email'] = (string)($snapshot['partner_email'] ?? $snapshot['email'] ?? '');
    $rawData['partner_phone'] = (string)($snapshot['partner_phone'] ?? '');
    $rawData['partner_address'] = (string)($snapshot['partner_address'] ?? '');
    $rawData['business_name'] = (string)($snapshot['business_name'] ?? '');
    $rawData['partnership_type'] = (string)($snapshot['partnership_type'] ?? $snapshot['brand'] ?? '');
    $rawData['type'] = $contractType;
    $rawData['client_ref'] = $clientRef;

    if (!empty($snapshot['duplicated_from_token'])) {
        $rawData['duplicated_from_token'] = $snapshot['duplicated_from_token'];
        $rawData['duplicated_from_id'] = $snapshot['duplicated_from_id'] ?? null;
    }

    $payload['raw_data'] = $rawData;

    return $payload;
}

function api_merge_partner_contract_into_local(array $formData, array $apiContract): array
{
    $signToken = trim((string)($apiContract['sign_token'] ?? ''));
    if ($signToken === '') {
        throw new ApiException('API did not return sign_token for the new contract.', 502, $apiContract);
    }

    $merged = $formData;
    $merged['token'] = $signToken;
    $merged['sign_token'] = $signToken;
    $merged['id'] = (int)($apiContract['id'] ?? $formData['id'] ?? 0);
    $merged['status'] = api_map_partner_contract_status((string)($apiContract['contract_status'] ?? 'draft'));
    $merged['contract_status'] = (string)($apiContract['contract_status'] ?? 'draft');
    $merged['sign_url'] = api_partner_sign_url($signToken, $apiContract);
    $merged['contract_url'] = (string)($apiContract['contract_url'] ?? '');
    $merged['customer_url'] = contract_customer_path($signToken);
    $merged['partner_api_id'] = $apiContract['id'] ?? null;
    $merged['partner_api_raw'] = $apiContract;

    if (!empty($apiContract['created_at'])) {
        $merged['created_at'] = (string)$apiContract['created_at'];
    }

    if (!empty($apiContract['updated_at'])) {
        $merged['updated_at'] = (string)$apiContract['updated_at'];
        $merged['modified_at'] = (string)$apiContract['updated_at'];
    }

    if (isset($apiContract['raw_data']) && is_array($apiContract['raw_data'])) {
        $merged['api_raw_data'] = $apiContract['raw_data'];
    }

    return $merged;
}

function api_create_partner_contract(array $contract): array
{
    require_once __DIR__ . '/mvc.php';

    $result = contract_service()->createContractFromRecord($contract);

    if (!$result->success) {
        throw new ApiException($result->message, max(400, $result->httpStatus), $result->apiResponse);
    }

    return $result->contract ?? $contract;
}

function api_partner_contract_id(array $contract): int
{
    return (int)($contract['partner_api_id'] ?? $contract['id'] ?? 0);
}

function api_build_partner_update_payload(array $contract, array $updates): array
{
    $rawData = [];
    if (is_array($contract['api_raw_data'] ?? null)) {
        $rawData = $contract['api_raw_data'];
    } elseif (is_array($contract['partner_api_raw']['raw_data'] ?? null)) {
        $rawData = $contract['partner_api_raw']['raw_data'];
    }

    foreach ($contract as $key => $value) {
        if (is_scalar($value) || is_array($value)) {
            $rawData[$key] = $value;
        }
    }

    $rawData = array_merge($rawData, $updates);

    return ['raw_data' => $rawData];
}

function api_get_partner_contract(int $id): ?array
{
    if ($id <= 0 || !api_can_use_partner_api()) {
        return null;
    }

    require_once __DIR__ . '/mvc.php';

    $result = contract_service()->getContract($id);

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

    return api_normalize_partner_contract($result->contract);
}

function api_all_contracts(): array
{
    if (api_can_use_partner_api()) {
        try {
            return api_fetch_partner_contracts();
        } catch (Throwable) {
            // Fall back to local JSON store below.
        }
    }

    if (!api_is_configured()) {
        return local_store_all_contracts();
    }

    try {
        $response = api_resolve_bearer_token() !== null
            ? api_get('contracts_list')
            : api_public_get('contracts_list');
        $list = api_extract_list($response);
        if ($list !== []) {
            return array_map(
                static fn(array $contract): array => !empty($contract['sign_token']) || !empty($contract['contract_status'])
                    ? api_normalize_partner_contract($contract)
                    : $contract,
                $list
            );
        }
    } catch (Throwable) {
        // Fall back to local JSON store.
    }

    return local_store_all_contracts();
}

function api_find_partner_contract_by_token(string $token): ?array
{
    $token = trim($token);
    if ($token === '') {
        return null;
    }

    try {
        $contracts = api_fetch_partner_contracts();
    } catch (Throwable) {
        return null;
    }

    foreach ($contracts as $contract) {
        if (local_store_contract_matches($contract, $token)) {
            return $contract;
        }

        if ((string)($contract['id'] ?? '') === $token) {
            return $contract;
        }
    }

    return null;
}

function api_merge_contract_with_local(array $contract): array
{
    $local = null;

    foreach (contract_all_tokens($contract) as $candidate) {
        $local = local_store_find($candidate);
        if ($local !== null) {
            break;
        }
    }

    $contractId = (int)($contract['id'] ?? 0);
    if ($local === null && $contractId > 0) {
        $local = local_store_find_by_id($contractId);
    }

    if ($local === null) {
        return $contract;
    }

    $token = local_store_resolve_token($local, local_store_resolve_token($contract));

    return array_merge($contract, $local, [
        'id' => (int)($contract['id'] ?? $local['id'] ?? 0),
        'token' => $token,
        'sign_token' => trim((string)($local['sign_token'] ?? $contract['sign_token'] ?? $token)),
        'signed_pdf_path' => (string)($local['signed_pdf_path'] ?? $contract['signed_pdf_path'] ?? ''),
        'pdf_path' => (string)($local['pdf_path'] ?? $contract['pdf_path'] ?? ''),
    ]);
}

function api_find_contract(string $token): ?array
{
    $token = trim($token);
    if ($token === '') {
        return null;
    }

    $local = local_store_find($token);
    if ($local === null && ctype_digit($token)) {
        $local = local_store_find_by_id((int)$token);
    }

    $remote = null;
    if (api_can_use_partner_api()) {
        $remote = api_find_partner_contract_by_token($token);
        if ($remote === null && ctype_digit($token)) {
            $remote = api_get_partner_contract((int)$token);
        }
    }

    if ($remote !== null && $local !== null) {
        return api_merge_contract_with_local(array_merge($remote, $local));
    }

    if ($remote !== null) {
        return api_merge_contract_with_local($remote);
    }

    return $local;
}

function api_partner_sign_enabled(): bool
{
    return api_feature_enabled('sign_submit') && api_customer_api_enabled();
}

function api_legacy_sign_enabled(): bool
{
    return api_feature_enabled('sign_submit') && api_is_configured();
}

function api_filter_sign_raw_data(array $rawData): array
{
    $filtered = [];

    foreach ($rawData as $key => $value) {
        if ($value === null) {
            continue;
        }

        $filtered[$key] = $value;
    }

    return $filtered;
}

function api_contract_customer_raw_snapshot(array $contract): array
{
    $snapshot = [];

    foreach ($contract as $key => $value) {
        if (is_object($value)) {
            continue;
        }

        $snapshot[$key] = $value;
    }

    return $snapshot;
}

function api_map_signature_for_raw_data(array $signature): array
{
    $image = basename((string)($signature['signature_image'] ?? ''));
    $mapped = $signature;
    $mapped['signature_filename'] = $image;
    $mapped['signature_path'] = $image !== '' ? uploads_relative_path('signatures', $image) : '';

    return api_filter_sign_raw_data($mapped);
}

function api_build_partner_sign_raw_data(
    array $contract,
    string $signToken,
    array $signaturePayload,
    string $signatureFilename,
    bool $isFinal,
    string $contractPdfFilename = '',
    string $signedPdfFilename = '',
    array $customerContext = []
): array {
    $contractToken = trim((string)($contract['token'] ?? $contract['sign_token'] ?? $signToken));
    $termsAcceptedAt = trim((string)($signaturePayload['terms_accepted_at'] ?? $signaturePayload['signed_at'] ?? ''));

    $signatures = api_get_signatures_for_token($contractToken);
    $mappedSignatures = array_map('api_map_signature_for_raw_data', $signatures);

    $rawData = array_merge(
        api_contract_customer_raw_snapshot($contract),
        [
            'signed_from' => 'customer page',
            'request_datetime' => api_request_datetime(),
            'terms_accepted' => true,
            'terms_accepted_at' => $termsAcceptedAt !== '' ? $termsAcceptedAt : date('Y-m-d H:i:s'),
            'agreed' => true,
            'sign_token' => $signToken,
            'contract_token' => $contractToken,
            'contract_id' => $contract['id'] ?? ($signaturePayload['contract_id'] ?? null),
            'owner_index' => $signaturePayload['owner_index'] ?? null,
            'owner_count' => $signaturePayload['owner_count'] ?? null,
            'is_final' => $isFinal,
            'signer_name' => $signaturePayload['signer_name'] ?? '',
            'signature_method' => $signaturePayload['signature_method'] ?? '',
            'signed_at' => $signaturePayload['signed_at'] ?? '',
            'ip_address' => $signaturePayload['ip_address'] ?? '',
            'user_agent' => $signaturePayload['user_agent'] ?? '',
            'screen_resolution' => $signaturePayload['screen_resolution'] ?? '',
            'timezone' => $signaturePayload['timezone'] ?? '',
            'server_data' => $signaturePayload['server_data'] ?? [],
            'signature_image' => $signaturePayload['signature_image'] ?? $signatureFilename,
            'signature_filename' => $signatureFilename,
            'signature_path' => uploads_relative_path('signatures', $signatureFilename),
            'signed_pdf_path' => $signaturePayload['signed_pdf_path'] ?? ($isFinal ? $signedPdfFilename : ''),
            'signed_pdf_url' => $signaturePayload['signed_pdf_url'] ?? '',
            'pdf_viewed_at' => $contract['pdf_viewed_at'] ?? null,
            'link_first_access' => $contract['link_first_access'] ?? null,
            'link_last_access' => $contract['link_last_access'] ?? null,
            'link_access_log' => $contract['link_access_log'] ?? [],
            'customer_location' => $contract['customer_location'] ?? null,
            'signatures' => $mappedSignatures,
            'signatures_completed' => count($mappedSignatures),
            'customer_context' => $customerContext,
        ],
        $signaturePayload
    );

    foreach (location_audit_fields(contract_link_location($contract)) as $key => $value) {
        $rawData[$key] = $value;
    }

    if ($contractPdfFilename !== '') {
        $rawData['contract_pdf_filename'] = $contractPdfFilename;
        $rawData['contract_pdf_path'] = uploads_relative_path('contracts', $contractPdfFilename);
    }

    if ($isFinal && $signedPdfFilename !== '') {
        $rawData['signed_pdf_filename'] = $signedPdfFilename;
        $rawData['signed_pdf_path'] = uploads_relative_path('signed', $signedPdfFilename);
    }

    if (!empty($contract['api_raw_data']) && is_array($contract['api_raw_data'])) {
        $rawData['contract_api_raw_data'] = $contract['api_raw_data'];
    }

    if (!empty($contract['partner_api_raw']) && is_array($contract['partner_api_raw'])) {
        $rawData['partner_api_raw'] = $contract['partner_api_raw'];
    }

    return api_filter_sign_raw_data($rawData);
}

function api_build_partner_sign_request_body(array $rawData, bool $termsAccepted = true): array
{
    return [
        'terms_accepted' => $termsAccepted,
        'raw_data' => $rawData,
    ];
}

function api_partner_sign_debug_snapshot(string $signToken, array $requestBody): array
{
    $signToken = trim($signToken);
    $requestJson = api_encode_json($requestBody);
    $termsAccepted = $requestBody['terms_accepted'] ?? null;

    return [
        'endpoint_key' => 'contract_sign',
        'url' => api_build_url('contract_sign', ['token' => $signToken]),
        'method' => 'POST',
        'sign_token' => $signToken,
        'request_body' => $requestBody,
        'request_json' => $requestJson,
        'terms_accepted' => $termsAccepted,
        'terms_accepted_type' => gettype($termsAccepted),
        'raw_data_keys' => array_keys(is_array($requestBody['raw_data'] ?? null) ? $requestBody['raw_data'] : []),
        'top_level_keys' => array_keys($requestBody),
    ];
}

function api_submit_partner_contract_sign(string $signToken, array $rawData, bool $termsAccepted = true): array
{
    require_once __DIR__ . '/mvc.php';

    $mergedRaw = array_merge([
        'signed_from' => 'customer page',
        'request_datetime' => api_request_datetime(),
    ], $rawData);

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

    $result = contract_service()->signContractPublic($signToken, [
        'terms_accepted' => $termsAccepted,
        'raw_data' => $mergedRaw,
    ]);

    if (!$result->success) {
        throw new ApiException(
            $result->message,
            max(400, $result->httpStatus),
            $result->apiResponse
        );
    }

    return is_array($result->apiResponse) ? $result->apiResponse : ['success' => true, 'message' => $result->message];
}

function api_save_contract(array $contract): array
{
    if (api_can_use_partner_api()) {
        require_once __DIR__ . '/mvc.php';
        $result = contract_service()->createContractFromRecord($contract);

        if (!$result->success) {
            throw new ApiException($result->message, max(400, $result->httpStatus), $result->apiResponse);
        }

        return $result->contract ?? $contract;
    }

    return local_store_save($contract);
}

function api_update_contract(string $token, array $updates): ?array
{
    $local = local_store_find($token);
    if ($local !== null) {
        $local = local_store_update($token, $updates) ?? $local;
    }

    $contract = is_array($local) ? $local : api_find_partner_contract_by_token($token);
    if (!is_array($contract)) {
        return $local;
    }

    $partnerId = api_partner_contract_id($contract);
    if ($partnerId <= 0 || !api_can_use_partner_api()) {
        return $local;
    }

    require_once __DIR__ . '/mvc.php';

    try {
        $service = contract_service();
        $current = $service->getContract($partnerId);
        $existing = ($current->success && is_array($current->contract)) ? $current->contract : $contract;
        $payload = $service->buildEditPayloadFromExisting($existing, $updates);
        $result = $service->updateContract($partnerId, $payload);

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

        $refreshed = $result->refreshedContract ?? $result->contract;
        if (!is_array($refreshed)) {
            return $local;
        }

        $merged = array_merge($local ?? [], api_normalize_partner_contract($refreshed));
        if (function_exists('contract_persist_local_contracts') && contract_persist_local_contracts()) {
            require_once __DIR__ . '/local_store.php';

            return local_store_save($merged);
        }

        return $merged;
    } catch (Throwable) {
        return $local;
    }
}

function api_update_partner_contract(int $contractId, array $data): array
{
    require_once __DIR__ . '/mvc.php';

    $result = contract_service()->updateContract($contractId, $data);

    if (!$result->success) {
        throw new ApiException($result->message, max(400, $result->httpStatus), $result->apiResponse);
    }

    return $result->refreshedContract ?? $result->contract ?? [];
}

function api_send_partner_contract(int $contractId): array
{
    require_once __DIR__ . '/mvc.php';

    $result = contract_service()->sendContract($contractId);

    if (!$result->success) {
        throw new ApiException($result->message, max(400, $result->httpStatus), $result->apiResponse);
    }

    return [
        'sign_url' => $result->signUrl,
        'sign_token' => $result->signToken,
        'message' => $result->message,
        'api_response' => $result->apiResponse,
    ];
}

function api_delete_partner_contract(int $contractId): array
{
    require_once __DIR__ . '/mvc.php';

    $result = contract_service()->deleteContract($contractId);

    if (!$result->success) {
        throw new ApiException($result->message, max(400, $result->httpStatus), $result->apiResponse);
    }

    return [
        'deleted_id' => $result->deletedId ?? $contractId,
        'message' => $result->message,
        'api_response' => $result->apiResponse,
    ];
}

function api_view_public_contract(string $signToken): array
{
    require_once __DIR__ . '/mvc.php';

    $result = contract_service()->viewPublicContract($signToken);

    if (!$result->success) {
        throw new ApiException($result->message, max(400, $result->httpStatus), $result->apiResponse);
    }

    return [
        'message' => $result->message,
        'contract' => $result->contract ?? [],
        'api_response' => $result->apiResponse,
    ];
}

function api_sign_contract_public(string $signToken, array $data): array
{
    require_once __DIR__ . '/mvc.php';

    $result = contract_service()->signContractPublic($signToken, $data);

    if (!$result->success) {
        throw new ApiException($result->message, max(400, $result->httpStatus), $result->apiResponse);
    }

    return [
        'message' => $result->message,
        'contract' => $result->contract,
        'api_response' => $result->apiResponse,
    ];
}

function api_save_signature(string $token, array $signature): array
{
    if (!api_is_configured()) {
        return local_store_save_signature($token, $signature);
    }

    $response = api_public_post('contract_sign', ['token' => $token], $signature);
    $saved = api_extract_item($response);

    if ($saved === null) {
        return $signature;
    }

    return $saved;
}

function api_get_signatures_for_contract(int $contractId): array
{
    if (!api_is_configured()) {
        $data = local_store_read();
        return array_values(array_filter(
            $data['signatures'],
            fn(array $item): bool => (int)($item['contract_id'] ?? 0) === $contractId
        ));
    }

    return [];
}

function api_get_signatures_for_token(string $token): array
{
    return local_store_owner_signatures($token);
}

function api_get_signatures_for_contract_record(array $contract): array
{
    $seen = [];
    $results = [];

    $add = static function (array $signature) use (&$seen, &$results): void {
        $id = (int)($signature['id'] ?? 0);
        $image = (string)($signature['signature_image'] ?? '');
        $key = $id > 0 ? 'id:' . $id : 'img:' . $image;

        if ($image === '' || isset($seen[$key])) {
            return;
        }

        $seen[$key] = true;
        $results[] = $signature;
    };

    foreach (contract_all_tokens($contract) as $token) {
        foreach (local_store_owner_signatures($token) as $signature) {
            $add($signature);
        }
    }

    $contractId = (int)($contract['id'] ?? 0);
    if ($contractId > 0 && api_is_configured()) {
        foreach (api_get_signatures_for_contract($contractId) as $signature) {
            $add($signature);
        }
    }

    usort(
        $results,
        static fn(array $a, array $b): int => ((int)($a['owner_index'] ?? 0)) <=> ((int)($b['owner_index'] ?? 0))
    );

    return $results;
}

function api_record_pdf_viewed(string $token): ?array
{
    $viewedAt = date('Y-m-d H:i:s');
    $contract = local_store_find($token);

    if ($contract === null && api_is_configured() && api_feature_enabled('contract_get')) {
        try {
            $contract = api_find_contract($token);
        } catch (Throwable) {
            $contract = null;
        }
    }

    if ($contract === null) {
        return null;
    }

    if (!empty($contract['pdf_viewed_at'])) {
        return $contract;
    }

    if (local_store_find($token) !== null) {
        $contract = local_store_update($token, ['pdf_viewed_at' => $viewedAt]) ?? $contract;
    }

    if (api_is_configured() && api_feature_enabled('pdf_viewed')) {
        $updated = api_update_contract($token, ['pdf_viewed_at' => $viewedAt]);
        if ($updated !== null) {
            return $updated;
        }
    }

    return array_merge($contract, ['pdf_viewed_at' => $viewedAt]);
}

function api_record_link_access(string $token, array $access): ?array
{
    if (!api_is_configured() || !api_feature_enabled('link_access')) {
        return local_store_log_link_access($token, $access);
    }

    $contract = local_store_find($token);
    if ($contract === null) {
        try {
            $contract = api_find_contract($token);
        } catch (Throwable) {
            $contract = null;
        }
    }

    if ($contract === null) {
        return null;
    }

    if (local_store_find($token) !== null) {
        $contract = local_store_log_link_access($token, $access) ?? $contract;
    }

    $log = is_array($contract['link_access_log'] ?? null) ? $contract['link_access_log'] : [];
    $log[] = $access;

    $updates = [
        'link_access_log' => $log,
        'link_last_access' => $access,
    ];

    if (empty($contract['link_first_access'])) {
        $updates['link_first_access'] = $access;
    }

    return api_update_contract($token, $updates);
}

function api_delete_contract(string $token, ?int $contractId = null): bool
{
    $contract = local_store_find($token);
    if ($contract === null) {
        $contract = api_find_contract($token);
    }

    $signatures = api_get_signatures_for_token($token);
    $resolvedId = $contractId ?? (int)($contract['id'] ?? $contract['partner_api_id'] ?? 0);

    if (api_can_use_partner_api()) {
        if ($resolvedId <= 0) {
            throw new ApiException('Contract id is required for API delete.', 400);
        }

        require_once __DIR__ . '/mvc.php';
        $deleteResult = contract_service()->deleteContract($resolvedId);
        if (!$deleteResult->success) {
            throw new ApiException(
                $deleteResult->message,
                max(400, $deleteResult->httpStatus),
                $deleteResult->apiResponse
            );
        }
    } elseif (api_is_configured()) {
        try {
            api_request('DELETE', 'contract_delete', ['token' => $token]);
        } catch (Throwable $e) {
            throw new ApiException('Could not delete contract from API: ' . api_error_message($e), 502);
        }
    }

    if (local_store_find($token) !== null) {
        return local_store_delete_contract($token);
    }

    if ($contract !== null) {
        contract_delete_related_files($token, $contract, $signatures);
    }

    return $contract !== null || $resolvedId > 0;
}

function api_duplicate_contract(string $token, int $id = 0): ?array
{
    require_once __DIR__ . '/admin_download_resolve.php';

    $source = admin_resolve_download_contract($token, $id);
    if ($source === null) {
        $source = api_find_contract($token);
    }

    if ($source === null) {
        return null;
    }

    $duplicate = contract_prepare_duplicate($source);

    return api_save_contract($duplicate);
}
