<?php

declare(strict_types=1);

error_reporting(E_ERROR | E_PARSE);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/tmp/executa_scp_errors.log');

function scp_require_once_first_readable(array $candidates): void
{
    foreach ($candidates as $candidate) {
        if (is_string($candidate) && $candidate !== '' && is_readable($candidate)) {
            require_once $candidate;
            return;
        }
    }

    throw new RuntimeException('Dependency not found: ' . implode(' | ', $candidates));
}

/**
 * @return array<int, DateTimeImmutable>
 */
function scp_extract_dates_from_text(string $text): array
{
    if (trim($text) === '') {
        return [];
    }

    preg_match_all('/\b(\d{2}\/\d{2}\/\d{4})\b/u', $text, $matches);
    $dates = [];
    foreach ((array)($matches[1] ?? []) as $candidate) {
        $dt = DateTimeImmutable::createFromFormat('d/m/Y', (string)$candidate);
        if (!$dt instanceof DateTimeImmutable) {
            continue;
        }

        $year = (int)$dt->format('Y');
        if ($year < 2000 || $year > 2100) {
            continue;
        }

        $dates[] = $dt;
    }

    return $dates;
}

/**
 * @return array<int, DateTimeImmutable>
 */
function scp_extract_dates_from_dt_mov_column(string $text): array
{
    if (trim($text) === '') {
        return [];
    }

    $lines = preg_split('/\r\n|\r|\n/', $text) ?: [];
    $dates = [];
    $insideMovTable = false;

    foreach ($lines as $rawLine) {
        $line = trim((string)$rawLine);
        if ($line === '') {
            continue;
        }

        if (preg_match('/\bdt\.?\s*mov\.?\b/iu', $line) === 1) {
            $insideMovTable = true;
            continue;
        }

        if (!$insideMovTable) {
            continue;
        }

        if (preg_match('/^(\d{2}\/\d{2}\/\d{4})\b/u', $line, $m) !== 1) {
            continue;
        }

        $dt = DateTimeImmutable::createFromFormat('d/m/Y', (string)$m[1]);
        if (!$dt instanceof DateTimeImmutable) {
            continue;
        }

        $year = (int)$dt->format('Y');
        if ($year < 2000 || $year > 2100) {
            continue;
        }

        $dates[] = $dt;
    }

    return $dates;
}

function scp_detect_reference_date_from_pdfs(array $files): ?string
{
    if (!function_exists('saned_extract_pdf_text')) {
        return null;
    }

    $dtMovDates = [];
    $allDates = [];
    foreach ($files as $file) {
        $tmpName = (string)($file['tmp_name'] ?? '');
        if ($tmpName === '' || !is_readable($tmpName)) {
            continue;
        }

        $text = saned_extract_pdf_text($tmpName);
        if ($text === '') {
            continue;
        }

        $dtMovDates = array_merge($dtMovDates, scp_extract_dates_from_dt_mov_column($text));
        $allDates = array_merge($allDates, scp_extract_dates_from_text($text));
    }

    $preferredDates = $dtMovDates !== [] ? $dtMovDates : $allDates;

    if ($preferredDates === []) {
        return null;
    }

    usort($preferredDates, static function (DateTimeImmutable $a, DateTimeImmutable $b): int {
        return $a <=> $b;
    });

    return $preferredDates[0]->modify('-1 day')->format('d/m/Y');
}

function scp_include_once_first_readable(array $candidates): void
{
    foreach ($candidates as $candidate) {
        if (is_string($candidate) && $candidate !== '' && is_readable($candidate)) {
            include_once $candidate;
            return;
        }
    }
}

function scp_hydrate_cli_request_globals(): void
{
    global $argv;

    if (PHP_SAPI !== 'cli' || !is_array($argv)) {
        return;
    }

    // Accept first CLI argument as query-string style payload.
    if (!empty($argv[1]) && is_string($argv[1]) && strpos($argv[1], '=') !== false) {
        $parsed = [];
        parse_str($argv[1], $parsed);
        if (is_array($parsed) && $parsed !== []) {
            foreach ($parsed as $k => $v) {
                if (!array_key_exists((string)$k, $_GET)) {
                    $_GET[(string)$k] = $v;
                }
            }
        }
    }

    if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] === '') {
        $_SERVER['REQUEST_METHOD'] = 'GET';
    }
}

scp_hydrate_cli_request_globals();

scp_require_once_first_readable([
    dirname(__DIR__) . '/workers/run_scp_routes.php',
    dirname(__DIR__) . '/run_scp_routes.php',
    dirname(__DIR__, 2) . '/workers/run_scp_routes.php',
    dirname(__DIR__, 2) . '/run_scp_routes.php',
]);

scp_include_once_first_readable([
    dirname(__DIR__) . '/workers/_shared/upload_helper.php',
    dirname(__DIR__, 2) . '/_shared/upload_helper.php',
    dirname(__DIR__) . '/utilitarios/shared/upload_helper.php',
]);

function scp_is_truthy($value): bool
{
    if (is_bool($value)) {
        return $value;
    }
    if ($value === null) {
        return false;
    }

    $normalized = strtolower(trim((string)$value));
    return in_array($normalized, ['1', 'true', 'yes', 'y', 'on', 'debug'], true);
}

function scp_debug_add(string $message, array $context = []): void
{
    global $scpDebugEnabled, $scpDebugTrace, $scpDebugLog;

    if (!$scpDebugEnabled) {
        return;
    }

    $entry = [
        'ts' => date('c'),
        'message' => $message,
    ];
    if ($context !== []) {
        $entry['context'] = $context;
    }

    $scpDebugTrace['steps'][] = $entry;

    $line = '[' . date('c') . '] executa_scp_debug ' . $message;
    if ($context !== []) {
        $line .= ' | ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    @file_put_contents($scpDebugLog, $line . PHP_EOL, FILE_APPEND | LOCK_EX);
}

function scp_collect_debug_flag(array $payload): bool
{
    if (scp_is_truthy($payload['debug'] ?? null)) {
        return true;
    }

    if (scp_is_truthy($_GET['debug'] ?? null) || scp_is_truthy($_POST['debug'] ?? null)) {
        return true;
    }

    if (scp_is_truthy($_SERVER['HTTP_X_DEBUG'] ?? null)) {
        return true;
    }

    if (scp_is_truthy(getenv('WRAPPER_DEBUG') ?: null) || scp_is_truthy(getenv('DEBUG') ?: null)) {
        return true;
    }

    global $argv;
    if (PHP_SAPI === 'cli' && is_array($argv)) {
        foreach ($argv as $arg) {
            $argValue = strtolower(trim((string)$arg));
            if ($argValue === '--debug' || $argValue === '-d' || $argValue === 'debug=1' || $argValue === 'debug=true') {
                return true;
            }
        }
    }

    return false;
}

function scp_log(string $message): void
{
    $baseDir = dirname(__DIR__);
    $logDir = $baseDir . '/logs';
    if (!is_dir($logDir)) {
        @mkdir($logDir, 0775, true);
    }

    $line = '[' . date('c') . '] executa_scp ' . $message . PHP_EOL;
    @file_put_contents($logDir . '/executa_scp.log', $line, FILE_APPEND | LOCK_EX);
}

function scp_timeout_seconds(): int
{
    $timeout = (int)($_GET['timeoutSeconds'] ?? $_POST['timeoutSeconds'] ?? 180);
    if ($timeout < 30) {
        $timeout = 30;
    }
    if ($timeout > 1800) {
        $timeout = 1800;
    }
    return $timeout;
}

function scp_max_files(): int
{
    $maxFiles = (int)($_GET['maxFiles'] ?? $_POST['maxFiles'] ?? 25);
    if ($maxFiles < 1) {
        $maxFiles = 1;
    }
    if ($maxFiles > 200) {
        $maxFiles = 200;
    }
    return $maxFiles;
}

function scp_normalize_empresa_id($value, string $fallback = 'EMP00000010'): string
{
    $candidate = strtoupper(trim((string)$value));
    if ($candidate === '') {
        return $fallback;
    }

    if (preg_match('/^EMP\d{8}$/', $candidate) === 1) {
        return $candidate;
    }

    // Allow numeric shorthand values such as 10 => EMP00000010.
    if (ctype_digit($candidate)) {
        return 'EMP' . str_pad($candidate, 8, '0', STR_PAD_LEFT);
    }

    return $candidate;
}

function scp_resolve_empresa_id(array $payload, string $fallback = 'EMP00000010'): string
{
    $preferred = trim((string)($payload['empresaID'] ?? ''));
    if ($preferred !== '') {
        return scp_normalize_empresa_id($preferred, $fallback);
    }

    $legacy = trim((string)($payload['empresa'] ?? ''));
    if ($legacy !== '') {
        return scp_normalize_empresa_id($legacy, $fallback);
    }

    return $fallback;
}

function scp_lock_path(string $key): string
{
    return sys_get_temp_dir() . '/executa_scp_' . preg_replace('/[^a-z0-9_\-]/i', '_', strtolower($key)) . '.lock';
}

function scp_is_pid_running(int $pid): bool
{
    if ($pid <= 1) {
        return false;
    }

    if (function_exists('posix_kill')) {
        return @posix_kill($pid, 0);
    }

    $output = [];
    $rc = 1;
    @exec('ps -p ' . (int)$pid . ' -o pid=', $output, $rc);
    return $rc === 0 && !empty($output);
}

function scp_status_payload(string $routeKey): array
{
    $path = scp_lock_path($routeKey);
    $pid = 0;
    $running = false;

    if (is_file($path) && is_readable($path)) {
        $raw = trim((string)@file_get_contents($path));
        if ($raw !== '' && ctype_digit($raw)) {
            $pid = (int)$raw;
            $running = scp_is_pid_running($pid);
            if (!$running) {
                @unlink($path);
            }
        }
    }

    return [
        'route' => $routeKey,
        'running' => $running,
        'pid' => $running ? $pid : null,
        'lock_file' => $path,
        'checked_at' => date('c'),
    ];
}

function scp_stop_route(string $routeKey): array
{
    $status = scp_status_payload($routeKey);
    if (!(bool)($status['running'] ?? false)) {
        return [
            'status' => 'idle',
            'message' => 'Nenhuma execucao em andamento para esta rota.',
            'route' => $routeKey,
            'stopped' => false,
        ];
    }

    $pid = (int)($status['pid'] ?? 0);
    if ($pid <= 1) {
        return [
            'status' => 'error',
            'message' => 'PID invalido para encerramento.',
            'route' => $routeKey,
            'stopped' => false,
        ];
    }

    @exec('kill -TERM ' . $pid);
    usleep(400000);
    $stillRunning = scp_is_pid_running($pid);

    if ($stillRunning) {
        @exec('kill -KILL ' . $pid);
        usleep(200000);
        $stillRunning = scp_is_pid_running($pid);
    }

    if (!$stillRunning) {
        $lockPath = scp_lock_path($routeKey);
        if (is_file($lockPath)) {
            @unlink($lockPath);
        }
        scp_log('Execucao encerrada manualmente. rota=' . $routeKey . ' pid=' . $pid);

        return [
            'status' => 'stopped',
            'message' => 'Execucao encerrada com sucesso.',
            'route' => $routeKey,
            'stopped' => true,
            'pid' => $pid,
        ];
    }

    return [
        'status' => 'error',
        'message' => 'Nao foi possivel encerrar a execucao em andamento.',
        'route' => $routeKey,
        'stopped' => false,
        'pid' => $pid,
    ];
}

/**
 * @return array{handle:resource,path:string}|null
 */
function scp_acquire_lock(string $key)
{
    $lockPath = scp_lock_path($key);
    $handle = @fopen($lockPath, 'c+');
    if ($handle === false) {
        scp_log('Falha ao abrir lock file: ' . $lockPath);
        return null;
    }

    if (!@flock($handle, LOCK_EX | LOCK_NB)) {
        @fclose($handle);
        return null;
    }

    @ftruncate($handle, 0);
    @fwrite($handle, (string)getmypid());

    return ['handle' => $handle, 'path' => $lockPath];
}

/**
 * @param array{handle:resource,path:string}|null $lock
 */
function scp_release_lock($lock): void
{
    if (!is_array($lock) || !isset($lock['handle'])) {
        return;
    }

    $handle = $lock['handle'];
    if (!is_resource($handle)) {
        return;
    }

    // Prevent double-release during shutdown paths.
    $lock['handle'] = null;

    @flock($handle, LOCK_UN);
    @fclose($handle);
}

function scp_is_json_request(): bool
{
    if (isset($_GET['format']) && strtolower(trim((string)$_GET['format'])) === 'json') {
        return true;
    }

    $accept = strtolower((string)($_SERVER['HTTP_ACCEPT'] ?? ''));
    return str_contains($accept, 'application/json');
}

function scp_get_current_upload_pdf_url(): string
{
    $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    $host = (string)($_SERVER['HTTP_HOST'] ?? 'localhost');
    $scriptName = (string)($_SERVER['SCRIPT_NAME'] ?? '/scripts/wrappers/executa_scp.php');

    // Allow explicit override for environments where upload endpoint is custom.
    $forced = trim((string)($_GET['uploadUrl'] ?? $_POST['uploadUrl'] ?? ''));
    if ($forced !== '') {
        if (preg_match('#^https?://#i', $forced) === 1) {
            return $forced;
        }
        return $scheme . '://' . $host . (str_starts_with($forced, '/') ? $forced : '/' . $forced);
    }

    $scriptDir = rtrim(dirname($scriptName), '/\\');
    if ($scriptDir === '' || $scriptDir === '.') {
        $scriptDir = '/';
    }

    // When entrypoint is /executa_scp.php, upload endpoint usually lives at /upload_pdf.php.
    if ($scriptDir === '/') {
        return $scheme . '://' . $host . '/upload_pdf.php';
    }

    return $scheme . '://' . $host . $scriptDir . '/upload_pdf.php';
}

function scp_routes(): array
{
    return [
        'construdaher' => [
            'key' => 'construdaher',
            'name' => 'Construdaher',
            'upload_url' => scp_get_current_upload_pdf_url(),
            'headers' => [],
        ],
        'saned' => [
            'key' => 'saned',
            'name' => 'Saned',
            'upload_url' => scp_get_current_upload_pdf_url(),
            'headers' => [],
        ],
        'progredior' => [
            'key' => 'progredior',
            'name' => 'Progredior',
            'upload_url' => scp_get_current_upload_pdf_url(),
            'headers' => [],
        ],
        'pilao' => [
            'key' => 'pilao',
            'name' => 'Pilao',
            'upload_url' => scp_get_current_upload_pdf_url(),
            'headers' => [],
        ],
        'consitec' => [
            'key' => 'consitec',
            'name' => 'THE',
            'upload_url' => scp_get_current_upload_pdf_url(),
            'headers' => [],
        ],
    ];
}

function scp_normalize_route_key(string $routeKey): string
{
    $routeKey = strtolower(trim($routeKey));
    $aliases = [
        'the' => 'consitec',
    ];

    return $aliases[$routeKey] ?? $routeKey;
}

function scp_collect_uploaded_pdfs(): array
{
    if (!isset($_FILES['pdffiles'])) {
        return [];
    }

    $entry = $_FILES['pdffiles'];
    $files = [];

    if (is_array($entry['tmp_name'] ?? null)) {
        $count = count($entry['tmp_name']);
        for ($index = 0; $index < $count; $index++) {
            $tmp = (string)($entry['tmp_name'][$index] ?? '');
            $name = (string)($entry['name'][$index] ?? '');
            $type = (string)($entry['type'][$index] ?? 'application/pdf');
            $error = (int)($entry['error'][$index] ?? UPLOAD_ERR_NO_FILE);
            $size = (int)($entry['size'][$index] ?? 0);

            if ($error !== UPLOAD_ERR_OK || $tmp === '' || !is_uploaded_file($tmp)) {
                continue;
            }

            $files[] = [
                'tmp_name' => $tmp,
                'name' => $name,
                'type' => $type,
                'size' => $size,
            ];
        }

        return $files;
    }
    $tmp = (string)($entry['tmp_name'] ?? '');
    $type = (string)($entry['type'] ?? 'application/pdf');
    $error = (int)($entry['error'] ?? UPLOAD_ERR_NO_FILE);
    $size = (int)($entry['size'] ?? 0);

    if ($error === UPLOAD_ERR_OK && $tmp !== '' && is_uploaded_file($tmp)) {
        $files[] = [
            'tmp_name' => $tmp,
            'name' => $name,
            'type' => $type,
            'size' => $size,
        ];
    }

    return $files;
}

function scp_pdf_text_contains_any(string $text, array $needles): bool
{
    foreach ($needles as $needle) {
        $needle = trim((string)$needle);
        if ($needle !== '' && stripos($text, $needle) !== false) {
            return true;
        }
    }

    return false;
}

function scp_detect_pdf_route_from_text(string $text): ?string
{
    $text = trim($text);
    if ($text === '') {
        return null;
    }

    $signals = [
        'saned' => [
            'EXTRATO MENSAL',
            'SANED ENGENHARIA E EMPREENDIMENTOS',
            'EMPRESA 1 - SANED',
            'SCP GH',
        ],
        'construdaher' => [
            'RAZÃO ANALÍTICO FINANCEIRO CONSOLIDADO',
            'RAZAO ANALITICO FINANCEIRO CONSOLIDADO',
        ],
        'progredior' => [
            'CONTAS PAGAS (POR DATA)',
            'PROGREDIOR',
            'CONSTRUTORA PROGREDIOR LTDA',
        ],
        'pilao' => [
            'EXTRATO DA CONTA CORRENTE:013008382-9',
            'RELATÓRIO DE EXTRATO DE CONTA CORRENTE DE',
            '013008382-9',
            'PILAO',
            'PILÃO',
        ],
        'consitec' => [
            'PLANILHA DE CENTRO DE CUSTOS',
            'THE MONTAGENS INDUSTRIAIS',
            'GH ENGENHARIA',
        ],
    ];

    foreach ($signals as $routeKey => $needles) {
        if (scp_pdf_text_contains_any($text, $needles)) {
            return $routeKey;
        }
    }

    return null;
}

function scp_detect_pdf_route_from_name(string $fileName): ?string
{
    $normalized = strtolower(trim($fileName));
    if ($normalized === '') {
        return null;
    }

    $signals = [
        'construdaher' => ['construdaher'],
        'saned' => ['saned'],
        'progredior' => ['progredior'],
        'pilao' => ['pilao', 'pilão'],
        'consitec' => ['consitec', 'the', 'gh', 'centro_de_custos'],
    ];

    foreach ($signals as $routeKey => $needles) {
        foreach ($needles as $needle) {
            if ($needle !== '' && str_contains($normalized, $needle)) {
                return $routeKey;
            }
        }
    }

    return null;
}

function scp_validate_pdf_routes(array $files, string $expectedRouteKey): array
{
    $expectedRouteKey = scp_normalize_route_key($expectedRouteKey);
    $detectedRoutes = [];

    foreach ($files as $file) {
        $tmpName = (string)($file['tmp_name'] ?? '');
        $originalName = (string)($file['name'] ?? 'arquivo.pdf');
        if ($tmpName === '' || !is_readable($tmpName)) {
            return [
                'ok' => false,
                'message' => 'Arquivo PDF temporario indisponivel para validar a origem.',
                'file' => $originalName,
            ];
        }

        $text = '';
        if (function_exists('saned_extract_pdf_text')) {
            $text = saned_extract_pdf_text($tmpName);
        }

        $detectedRouteFromText = scp_detect_pdf_route_from_text($text);
        $detectedRouteFromName = scp_detect_pdf_route_from_name($originalName);

        if ($detectedRouteFromText !== null && $detectedRouteFromName !== null && $detectedRouteFromText !== $detectedRouteFromName) {
            return [
                'ok' => false,
                'message' => 'Origem do PDF divergente entre nome do arquivo e conteudo.',
                'file' => $originalName,
                'detected_route_text' => $detectedRouteFromText,
                'detected_route_name' => $detectedRouteFromName,
                'expected_route' => $expectedRouteKey,
            ];
        }

        $detectedRoute = scp_normalize_route_key((string)($detectedRouteFromText ?? $detectedRouteFromName ?? ''));
        if ($detectedRoute === '') {
            return [
                'ok' => false,
                'message' => 'Nao foi possivel identificar a origem do PDF.',
                'file' => $originalName,
            ];
        }

        $detectedRoutes[$originalName] = $detectedRoute;
        if ($detectedRoute !== $expectedRouteKey) {
            return [
                'ok' => false,
                'message' => 'PDF de outra rota detectado. A rota informada nao corresponde ao arquivo enviado.',
                'file' => $originalName,
                'detected_route_text' => $detectedRouteFromText,
                'detected_route_name' => $detectedRouteFromName,
                'detected_route' => $detectedRoute,
                'expected_route' => $expectedRouteKey,
            ];
        }
    }

    return [
        'ok' => true,
        'detected_routes' => $detectedRoutes,
    ];
}

function scp_json_exit(array $payload, int $status = 200): void
{
    global $scpDebugEnabled, $scpDebugTrace;

    $payload = scp_sanitize_difference_report_payload($payload);

    if (isset($payload['difference_report']) && is_array($payload['difference_report'])) {
        // Always rebuild from comparison payload so response does not inherit
        // short/legacy xml_content_atualizar emitted by upstream scripts.
        $rebuilt = scp_build_xml_content_atualizar_from_payload($payload);
        if (is_string($rebuilt) && $rebuilt !== '') {
            $payload['xml_content_atualizar'] = $rebuilt;
        }

        $xmlAtualizar = (string)($payload['xml_content_atualizar'] ?? '');

        if ($xmlAtualizar !== '') {
            $routeKey = (string)($payload['route'] ?? $payload['difference_report']['route'] ?? '');
            if ($routeKey !== '') {
                $snapshot = scp_write_xml_content_atualizar_file($routeKey, $xmlAtualizar);
                $payload['xml_content_atualizar_file'] = (string)($snapshot['latest_path'] ?? '');
                $payload['xml_content_atualizar_url'] = (string)($snapshot['latest_url'] ?? '');
                $payload['xml_content_atualizar_snapshot'] = $snapshot;
            }
        }
    }

    // Hide raw transformed XML from direct API responses after derivative
    // artifacts were built.
    unset($payload['xml_content']);

    $snapshotRoute = (string)($payload['route'] ?? $payload['difference_report']['route'] ?? 'pilao');
    $uploadsSnapshot = scp_write_response_snapshot_to_uploads($snapshotRoute, $payload);
    $payload['response_snapshot_uploads'] = $uploadsSnapshot;
    $payload['response_json_uploads_url'] = (string)($uploadsSnapshot['latest_url'] ?? $uploadsSnapshot['history_url'] ?? '');

    if ($scpDebugEnabled) {
        $scpDebugTrace['finished_at'] = date('c');
        $payload['debug_wrapper'] = $scpDebugTrace;
    }

    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE);
    exit($status >= 400 ? 1 : 0);
}

    // Hide raw transformed XML from direct API responses after derivative
    // artifacts were built.
    unset($payload['xml_content']);
function scp_get_request_payload(): array
{
    $payload = [];

    if (is_array($_GET)) {
        $payload = array_merge($payload, $_GET);
    }

    if (is_array($_POST)) {
        $payload = array_merge($payload, $_POST);
    }

    $rawInput = file_get_contents('php://input');
    if (is_string($rawInput) && trim($rawInput) !== '') {
        $decoded = json_decode($rawInput, true);
        if (is_array($decoded)) {
            $payload = array_merge($payload, $decoded);
        }
    }

    return $payload;
}

function scp_root_dir(): string
{
    return dirname(dirname(__DIR__));
}

function scp_route_file_stem(string $routeKey): string
{
    $stem = strtolower(trim($routeKey));
    $stem = preg_replace('/[^a-z0-9_\-]+/i', '_', $stem) ?? $stem;
    $stem = trim($stem, '_-');
    return $stem !== '' ? $stem : 'scp';
}

function scp_resolve_local_path(string $path): ?string
{
    $candidate = trim($path);
    if ($candidate === '') {
        return null;
    }

    if (is_file($candidate) && is_readable($candidate)) {
        return $candidate;
    }

    $base = scp_root_dir();
    $prefixed = $base . '/' . ltrim($candidate, '/');
    if (is_file($prefixed) && is_readable($prefixed)) {
        return $prefixed;
    }

    return null;
}

function scp_resolve_csv_path_from_value(string $value): ?string
{
    $value = trim($value);
    if ($value === '') {
        return null;
    }

    $local = scp_resolve_local_path($value);
    if ($local !== null && strtolower(pathinfo($local, PATHINFO_EXTENSION)) === 'csv') {
        return $local;
    }

    if (preg_match('#/uploads/([^\?]+)#i', $value, $m) === 1) {
        $fileName = basename(urldecode((string)$m[1]));
        if ($fileName !== '' && function_exists('upload_helper_get_upload_dir')) {
            $uploadDir = (string)upload_helper_get_upload_dir();
            $candidate = rtrim($uploadDir, '/\\') . '/' . $fileName;
            if (is_file($candidate) && is_readable($candidate)) {
                if (strtolower(pathinfo($candidate, PATHINFO_EXTENSION)) === 'csv') {
                    return $candidate;
                }
            }
        }
    }

    return null;
}

function scp_detect_latest_csv_file(): ?string
{
    $base = scp_root_dir();
    $dirs = [
        $base . '/output',
        $base . '/scripts/output',
        $base . '/xsl-out',
    ];

    $latestFile = null;
    $latestMtime = 0;

    foreach ($dirs as $dir) {
        if (!is_dir($dir)) {
            continue;
        }

        $files = @glob(rtrim($dir, '/\\') . '/*.csv');
        if (!is_array($files)) {
            continue;
        }

        foreach ($files as $file) {
            if (!is_file($file) || !is_readable($file)) {
                continue;
            }

            $mtime = (int)@filemtime($file);
            if ($mtime >= $latestMtime) {
                $latestMtime = $mtime;
                $latestFile = $file;
            }
        }
    }

    return $latestFile;
}

function scp_csv_has_data_rows(string $csvPath): bool
{
    if (!is_file($csvPath) || !is_readable($csvPath)) {
        return false;
    }

    $handle = @fopen($csvPath, 'rb');
    if ($handle === false) {
        return false;
    }

    $hasHeader = false;
    while (($line = fgets($handle)) !== false) {
        $trimmed = trim((string)$line);
        if ($trimmed === '') {
            continue;
        }

        if (!$hasHeader) {
            $hasHeader = true;
            continue;
        }

        fclose($handle);
        return true;
    }

    fclose($handle);
    return false;
}

/**
 * Preenche datas vazias no CSV com a ultima data valida anterior.
 *
 * @return array{status:string,updated_rows:int,message:string}
 */
function scp_fill_missing_csv_dates_from_previous(string $csvPath): array
{
    if (!is_file($csvPath) || !is_readable($csvPath)) {
        return [
            'status' => 'error',
            'updated_rows' => 0,
            'message' => 'CSV nao encontrado para preencher datas.',
        ];
    }

    $in = @fopen($csvPath, 'rb');
    if ($in === false) {
        return [
            'status' => 'error',
            'updated_rows' => 0,
            'message' => 'Falha ao abrir CSV para leitura.',
        ];
    }

    $tmpPath = $csvPath . '.tmp.' . bin2hex(random_bytes(4));
    $out = @fopen($tmpPath, 'wb');
    if ($out === false) {
        fclose($in);
        return [
            'status' => 'error',
            'updated_rows' => 0,
            'message' => 'Falha ao abrir arquivo temporario de escrita.',
        ];
    }

    $header = fgetcsv($in, 0, ';', '"');
    if (!is_array($header) || $header === []) {
        fclose($in);
        fclose($out);
        @unlink($tmpPath);
        return [
            'status' => 'error',
            'updated_rows' => 0,
            'message' => 'CSV sem cabecalho valido.',
        ];
    }

    $dataIndex = null;
    foreach ($header as $idx => $colName) {
        $normalized = strtolower(trim((string)$colName, " \t\n\r\0\x0B\""));
        if ($normalized === 'data') {
            $dataIndex = (int)$idx;
            break;
        }
    }

    if ($dataIndex === null) {
        fclose($in);
        fclose($out);
        @unlink($tmpPath);
        return [
            'status' => 'error',
            'updated_rows' => 0,
            'message' => 'Coluna Data nao encontrada no CSV.',
        ];
    }

    fputcsv($out, $header, ';', '"');

    $updatedRows = 0;
    $lastDate = '';

    while (($row = fgetcsv($in, 0, ';', '"')) !== false) {
        if (!is_array($row)) {
            continue;
        }

        if (!array_key_exists($dataIndex, $row)) {
            $row = array_pad($row, $dataIndex + 1, '');
        }

        $currentDate = trim((string)$row[$dataIndex]);
        if ($currentDate !== '') {
            $lastDate = $currentDate;
        } elseif ($lastDate !== '') {
            $row[$dataIndex] = $lastDate;
            $updatedRows++;
        }

        fputcsv($out, $row, ';', '"');
    }

    fclose($in);
    fclose($out);

    if (!@rename($tmpPath, $csvPath)) {
        @unlink($tmpPath);
        return [
            'status' => 'error',
            'updated_rows' => 0,
            'message' => 'Falha ao substituir CSV apos preencher datas.',
        ];
    }

    @chmod($csvPath, 0664);

    return [
        'status' => 'success',
        'updated_rows' => $updatedRows,
        'message' => 'Datas vazias preenchidas a partir da linha anterior.',
    ];
}

function scp_resolve_transform_script_path(): ?string
{
    $base = scp_root_dir();
    $candidates = [
        $base . '/scripts/transform_csv.php',
        $base . '/scripts/workers/transform_csv.php',
        dirname(__DIR__) . '/transform_csv.php',
        dirname(__DIR__) . '/workers/transform_csv.php',
    ];

    foreach ($candidates as $path) {
        if (is_file($path) && is_readable($path)) {
            return $path;
        }
    }

    return null;
}

function scp_resolve_php_cli_bin(): string
{
    $candidates = [
        trim((string)getenv('PHP_CLI_BIN')),
        '/usr/bin/php',
        '/usr/bin/php8.4',
        '/usr/bin/php8.3',
        '/usr/bin/php8.2',
        '/usr/bin/php8.1',
        '/usr/local/bin/php',
        '/opt/homebrew/bin/php',
        trim((string)@shell_exec('command -v php 2>/dev/null')),
        PHP_BINARY,
        'php',
    ];

    foreach ($candidates as $candidate) {
        if (!is_string($candidate) || $candidate === '') {
            continue;
        }

        $base = strtolower((string)basename($candidate));
        if (strpos($base, 'php-fpm') !== false || $base === 'php-cgi') {
            continue;
        }

        if ($candidate !== 'php' && !is_executable($candidate)) {
            continue;
        }

        $sapiProbe = trim((string)@shell_exec(escapeshellarg($candidate) . " -r 'echo PHP_SAPI;' 2>/dev/null"));
        if ($sapiProbe !== 'cli') {
            continue;
        }

        return $candidate;
    }

    throw new RuntimeException('php_cli_not_found');
}

function scp_is_filemaker_xml(string $xmlContent): bool
{
    $xmlContent = trim($xmlContent);
    if ($xmlContent === '') {
        return false;
    }

    libxml_use_internal_errors(true);
    $xml = simplexml_load_string($xmlContent);
    if (!$xml instanceof SimpleXMLElement) {
        return false;
    }

    $rootName = strtolower((string)$xml->getName());
    if ($rootName !== 'fmpxmlresult') {
        $fmpRoot = $xml->xpath('/*[local-name()="FMPXMLRESULT"]');
        if (!is_array($fmpRoot) || !isset($fmpRoot[0])) {
            return false;
        }
    }

    $requiredNodes = [
        '/*[local-name()="FMPXMLRESULT"]/*[local-name()="METADATA"]',
        '/*[local-name()="FMPXMLRESULT"]/*[local-name()="RESULTSET"]',
    ];

    foreach ($requiredNodes as $xpath) {
        $nodes = $xml->xpath($xpath);
        if (!is_array($nodes) || !isset($nodes[0])) {
            return false;
        }
    }

    return true;
}

function scp_transform_csv_to_filemaker(array $request, array $route, array $workerResult, array $files): array
{
    $scriptPath = scp_resolve_transform_script_path();
    if ($scriptPath === null) {
        return [
            'status' => 'error',
            'message' => 'Script transform_csv.php nao encontrado.',
            'xml_content' => null,
            'output_file' => null,
            'csv_input' => null,
            'child_output' => '',
        ];
    }

    $routeKey = (string)($route['key'] ?? 'scp');
    $routeStem = scp_route_file_stem($routeKey);
    $base = scp_root_dir();
    $outputDir = $base . '/output';
    if (!is_dir($outputDir)) {
        @mkdir($outputDir, 0775, true);
    }

    $namedCsvPath = rtrim($outputDir, '/\\') . '/' . $routeStem . '.csv';
    $namedXmlPath = rtrim($outputDir, '/\\') . '/' . $routeStem . '.xml';

    $resolvedCsv = null;

    $csvCandidates = [];
    foreach (['csvFile', 'csvPath', 'csvUrl', 'inputFile'] as $key) {
        if (!empty($request[$key]) && is_string($request[$key])) {
            $csvCandidates[] = (string)$request[$key];
        }
    }

    foreach (['csv_file', 'csv_path', 'csv_url'] as $key) {
        if (!empty($workerResult[$key]) && is_string($workerResult[$key])) {
            $csvCandidates[] = (string)$workerResult[$key];
        }
    }

    foreach ((array)($workerResult['results'] ?? []) as $item) {
        if (!is_array($item)) {
            continue;
        }
        foreach (['csv_file', 'csv_path', 'csv_url'] as $key) {
            if (!empty($item[$key]) && is_string($item[$key])) {
                $csvCandidates[] = (string)$item[$key];
            }
        }
    }

    if (!isset($resolvedCsv) || $resolvedCsv === null) {
        foreach ($csvCandidates as $candidate) {
            $resolvedCsv = scp_resolve_csv_path_from_value($candidate);
            if ($resolvedCsv !== null) {
                break;
            }
        }
    }

    if ($resolvedCsv === null) {
        $resolvedCsv = scp_detect_latest_csv_file();
    }

    if ($resolvedCsv === null) {
        return [
            'status' => 'error',
            'message' => 'CSV gerado nao encontrado para transformacao.',
            'xml_content' => null,
            'output_file' => null,
            'csv_input' => null,
            'child_output' => '',
        ];
    }

    $builderRows = (int)($workerResult['csv_builder']['rows'] ?? -1);
    if ($builderRows === 0 || !scp_csv_has_data_rows($resolvedCsv)) {
        return [
            'status' => 'error',
            'message' => 'CSV da rota foi gerado sem lancamentos. Transformacao abortada para evitar XML zerado.',
            'xml_content' => null,
            'output_file' => $namedXmlPath,
            'csv_input' => $resolvedCsv,
            'csv_output' => $namedCsvPath,
            'child_output' => 'empty_csv_rows',
        ];
    }

    $sourceCsvReal = realpath($resolvedCsv) ?: $resolvedCsv;
    $namedCsvReal = realpath($namedCsvPath) ?: $namedCsvPath;

    if ($sourceCsvReal !== $namedCsvReal) {
        if (!@copy($resolvedCsv, $namedCsvPath)) {
            return [
                'status' => 'error',
                'message' => 'Falha ao salvar CSV no output com nome da rota.',
                'xml_content' => null,
                'output_file' => null,
                'csv_input' => $resolvedCsv,
                'csv_output' => $namedCsvPath,
                'child_output' => '',
            ];
        }
        @chmod($namedCsvPath, 0664);
    }

    $transformInputCsv = is_file($namedCsvPath) ? $namedCsvPath : $resolvedCsv;

    if (in_array(strtolower($routeKey), ['consitec', 'the'], true)) {
        $fillResult = scp_fill_missing_csv_dates_from_previous($transformInputCsv);
        scp_debug_add('CSV fill de datas executado', [
            'route' => $routeKey,
            'status' => (string)($fillResult['status'] ?? 'unknown'),
            'updated_rows' => (int)($fillResult['updated_rows'] ?? 0),
            'message' => (string)($fillResult['message'] ?? ''),
            'csv_input' => $transformInputCsv,
        ]);
        if (($fillResult['status'] ?? 'error') === 'success') {
            $updated = (int)($fillResult['updated_rows'] ?? 0);
            if ($updated > 0) {
                scp_log('CSV pos-processado com datas herdadas da linha anterior. rota=' . $routeKey . ' atualizadas=' . $updated);
            }
        } else {
            scp_log('Falha no preenchimento de datas do CSV. rota=' . $routeKey . ' msg=' . (string)($fillResult['message'] ?? 'erro desconhecido'));
        }
    }

    $outputXml = $namedXmlPath;
    // Remove stale XML from previous runs so success checks are reliable.
    if (is_file($outputXml)) {
        @unlink($outputXml);
    }
    if (trim((string)($request['outputFile'] ?? '')) !== '') {
        // Mantem consistencia com a solicitacao: nome da rota prevalece no output.
        scp_log('Parametro outputFile ignorado para manter padrao de nome por rota. rota=' . $routeKey);
    }

    $routeCcustoDefaults = [
        'saned' => 'SAN',
        'pilao' => 'PILAO',
        'construdaher' => 'CDH',
        'progredior' => 'PRG',
        'consitec' => 'THE',
    ];
    $routeCcustoDefault = (string)($routeCcustoDefaults[strtolower($routeKey)] ?? 'A00');
    $ccustoID = (string)($request['ccustoID'] ?? $routeCcustoDefault);
    $contaID = (string)($request['contaID'] ?? 'CON99');
    $dataID = trim((string)($request['dataID'] ?? ''));
    if ($dataID === '') {
        $dataID = trim((string)($workerResult['csv_builder']['detected_data_id'] ?? ''));
    }
    if ($dataID === '') {
        $dataID = trim((string)(scp_detect_reference_date_from_pdfs($files) ?? ''));
        if ($dataID !== '') {
            scp_log('dataID inferido pela coluna Dt. Mov. (fallback). rota=' . $routeKey . ' dataID=' . $dataID);
        }
    }
    if ($dataID === '') {
        $dataID = (new DateTimeImmutable('today'))->modify('-1 day')->format('d/m/Y');
        scp_log('dataID nao detectado nos PDFs; aplicando fallback automatico. rota=' . $routeKey . ' dataID=' . $dataID);
    }
    $moedaID = (string)($request['moedaID'] ?? 'R$');
    $empresaID = scp_resolve_empresa_id($request);
    $routeProfileMap = [
        'construdaher' => 'csvConstrudaher',
        'saned' => 'csvSaned',
        'progredior' => 'csvProgredior',
        'pilao' => 'csvPilao',
        'consitec' => 'csvThe',
    ];
    $defaultConfigKey = (string)($routeProfileMap[strtolower($routeKey)] ?? 'csvPilao');
    $requestedConfigKey = trim((string)($request['transformConfigKey'] ?? $request['csvProfile'] ?? ''));
    $configKey = $defaultConfigKey;

    if ($requestedConfigKey !== '' && $requestedConfigKey !== $defaultConfigKey) {
        scp_log('Perfil de transformacao ignorado para manter a rota. route=' . $routeKey . ' requested=' . $requestedConfigKey . ' applied=' . $configKey);
    }

    try {
        $phpBin = scp_resolve_php_cli_bin();
    } catch (Throwable $exception) {
        return [
            'status' => 'error',
            'message' => 'PHP CLI nao encontrado para executar transform_csv.php.',
            'xml_content' => null,
            'output_file' => $outputXml,
            'csv_input' => $transformInputCsv,
            'csv_output' => $namedCsvPath,
            'child_output' => $exception->getMessage(),
        ];
    }
    $cmd = escapeshellarg($phpBin)
        . ' ' . escapeshellarg($scriptPath)
        . ' ' . escapeshellarg($transformInputCsv)
        . ' ' . escapeshellarg($ccustoID)
        . ' ' . escapeshellarg($contaID)
        . ' ' . escapeshellarg($dataID)
        . ' ' . escapeshellarg($moedaID)
        . ' ' . escapeshellarg($empresaID)
        . ' ' . escapeshellarg($outputXml)
        . ' ' . escapeshellarg($configKey);

    $output = [];
    $rc = 1;
    @exec($cmd . ' 2>&1', $output, $rc);
    $childOutput = implode("\n", $output);

    if ($rc !== 0) {
        $detail = '';
        $lines = array_values(array_filter(array_map('trim', $output), static function ($line): bool {
            return $line !== '';
        }));

        // Pick the last concrete error line from tool output.
        for ($i = count($lines) - 1; $i >= 0; $i--) {
            $line = $lines[$i];
            $lower = strtolower($line);
            if (strpos($lower, 'well-formed xml saved as csv.xml') !== false) {
                continue;
            }
            if ($lower === 'output:' || $lower === 'command:') {
                continue;
            }
            if (strpos($lower, 'error during transformation.') === 0) {
                continue;
            }
            if (strpos($lower, 'return code:') !== false) {
                continue;
            }
            if (strpos($lower, 'usage: php') === 0) {
                continue;
            }
            $detail = $line;
            break;
        }

        if ($detail === '') {
            // Fallback to first non-empty line if no concrete detail could be extracted.
            foreach ($lines as $line) {
                if ($line !== '') {
                    $detail = $line;
                    break;
                }
            }
        }

        $failureMessage = 'Falha ao transformar CSV em XML FileMaker.';
        if ($detail !== '') {
            $failureMessage .= ' Detalhe: ' . $detail;
        }
        return [
            'status' => 'error',
            'message' => $failureMessage,
            'xml_content' => null,
            'output_file' => $outputXml,
            'csv_input' => $transformInputCsv,
            'csv_output' => $namedCsvPath,
            'child_output' => $childOutput,
        ];
    }

    if (!is_file($outputXml) || !is_readable($outputXml)) {
        return [
            'status' => 'error',
            'message' => 'Transformacao concluida sem arquivo de XML final legivel.',
            'xml_content' => null,
            'output_file' => $outputXml,
            'csv_input' => $transformInputCsv,
            'csv_output' => $namedCsvPath,
            'child_output' => $childOutput,
        ];
    }

    $xmlContent = @file_get_contents($outputXml);
    if (!is_string($xmlContent)) {
        return [
            'status' => 'error',
            'message' => 'Falha ao ler XML final gerado.',
            'xml_content' => null,
            'output_file' => $outputXml,
            'csv_input' => $transformInputCsv,
            'csv_output' => $namedCsvPath,
            'child_output' => $childOutput,
        ];
    }

    if (!scp_is_filemaker_xml($xmlContent)) {
        return [
            'status' => 'error',
            'message' => 'XML final gerado fora do formato FileMaker (FMPXMLRESULT).',
            'xml_content' => null,
            'output_file' => $outputXml,
            'csv_input' => $transformInputCsv,
            'csv_output' => $namedCsvPath,
            'child_output' => $childOutput,
        ];
    }

    return [
        'status' => 'success',
        'message' => 'CSV transformado para XML FileMaker com sucesso.',
        'empresa_id' => $empresaID,
        'xml_content' => $xmlContent,
        'output_file' => $outputXml,
        'csv_input' => $transformInputCsv,
        'csv_output' => $namedCsvPath,
        'child_output' => $childOutput,
    ];
}

function scp_resolve_compare_script_path(): ?string
{
    $base = scp_root_dir();
    $candidates = [
        $base . '/scripts/utilitarios/compare_xml_input_output.php',
        $base . '/scripts/compare_xml_input_output.php',
        $base . '/compare_xml_input_output.php',
        dirname(__DIR__) . '/compare_xml_input_output.php',
        dirname(__DIR__) . '/utilitarios/compare_xml_input_output.php',
    ];

    foreach ($candidates as $path) {
        if (is_file($path) && is_readable($path)) {
            return $path;
        }
    }

    return null;
}

function scp_resolve_input_xml_path(string $routeKey): ?string
{
    $routeStem = scp_route_file_stem($routeKey);
    if ($routeStem === '') {
        return null;
    }

    $base = scp_root_dir();
    $uploadDirs = [];
    if (function_exists('upload_helper_get_upload_dir')) {
        $uploadDirs[] = rtrim((string)upload_helper_get_upload_dir(), '/\\');
    }

    $uploadDirs = array_merge($uploadDirs, [
        $base . '/uploads',
        $base . '/scripts/uploads',
        $base . '/scripts/utilitarios/uploads',
        dirname($base) . '/uploads',
        dirname($base) . '/scripts/uploads',
        '/var/www/alvaroguedes.lat/uploads',
        '/var/www/alvaroguedes.lat/scripts/uploads',
        '/var/www/alvaroguedes.lat/scripts/utilitarios/uploads',
        '/var/www/html/uploads',
        '/var/www/html/scripts/uploads',
        '/var/www/html/scripts/utilitarios/uploads',
    ]);

    $uploadDirs = array_values(array_unique(array_filter($uploadDirs, static function ($dir): bool {
        return is_string($dir) && trim($dir) !== '';
    })));

    $routeInputLower = strtolower($routeStem . '_input.xml');

    // 1) Prefer exact route_input.xml when available (case-insensitive).
    foreach ($uploadDirs as $dir) {
        $candidate = rtrim($dir, '/\\') . '/' . $routeStem . '_input.xml';
        if (is_file($candidate) && is_readable($candidate)) {
            return $candidate;
        }

        if (!is_dir($dir)) {
            continue;
        }

        $entries = @scandir($dir);
        if (!is_array($entries)) {
            continue;
        }

        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $entryPath = rtrim($dir, '/\\') . '/' . $entry;
            if (!is_file($entryPath) || !is_readable($entryPath)) {
                continue;
            }

            if (strtolower($entry) === $routeInputLower) {
                return $entryPath;
            }
        }
    }

    // 2) Then prefer newest file that references the route and ends with _input.xml (case-insensitive).
    $bestRouteFile = null;
    $bestRouteMtime = 0;
    $routeStemLower = strtolower($routeStem);
    foreach ($uploadDirs as $dir) {
        if (!is_dir($dir)) {
            continue;
        }

        $entries = @scandir($dir);
        if (!is_array($entries)) {
            continue;
        }

        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $entryLower = strtolower($entry);
            if (!str_contains($entryLower, $routeStemLower) || !str_ends_with($entryLower, '_input.xml')) {
                continue;
            }

            $match = rtrim($dir, '/\\') . '/' . $entry;
            if (!is_file($match) || !is_readable($match)) {
                continue;
            }

            $mtime = (int)@filemtime($match);
            if ($mtime >= $bestRouteMtime) {
                $bestRouteMtime = $mtime;
                $bestRouteFile = $match;
            }
        }
    }

    if (is_string($bestRouteFile) && $bestRouteFile !== '') {
        return $bestRouteFile;
    }

    // 3) Last resort: newest *_input.xml from known upload dirs (case-insensitive).
    $bestAnyFile = null;
    $bestAnyMtime = 0;
    foreach ($uploadDirs as $dir) {
        if (!is_dir($dir)) {
            continue;
        }

        $entries = @scandir($dir);
        if (!is_array($entries)) {
            continue;
        }

        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            if (!str_ends_with(strtolower($entry), '_input.xml')) {
                continue;
            }

            $match = rtrim($dir, '/\\') . '/' . $entry;
            if (!is_file($match) || !is_readable($match)) {
                continue;
            }

            $mtime = (int)@filemtime($match);
            if ($mtime >= $bestAnyMtime) {
                $bestAnyMtime = $mtime;
                $bestAnyFile = $match;
            }
        }
    }

    return is_string($bestAnyFile) && $bestAnyFile !== '' ? $bestAnyFile : null;
}

function scp_build_difference_report(array $route, array $transformResult): array
{
    $routeKey = (string)($route['key'] ?? '');
    $outputFile = trim((string)($transformResult['output_file'] ?? ''));
    $inputFile = scp_resolve_input_xml_path($routeKey);

    if ((string)($transformResult['status'] ?? '') !== 'success') {
        return [
            'status' => 'skipped',
            'message' => 'Relatorio de diferencas nao gerado porque a transformacao nao foi concluida com sucesso.',
            'route' => $routeKey,
            'input_file' => null,
            'output_file' => $outputFile !== '' ? $outputFile : null,
            'summary' => null,
            'faltando_na_scp [extra no filemaker]' => [],
            'extra_na_scp [faltando no filemaker]' => [],
        ];
    }

    if ($routeKey === '' || $outputFile === '' || !is_readable($outputFile)) {
        return [
            'status' => 'error',
            'message' => 'Relatorio de diferencas nao gerado por falta de parametros de comparacao (rota/output).',
            'route' => $routeKey,
            'input_file' => $inputFile,
            'output_file' => $outputFile !== '' ? $outputFile : null,
            'summary' => null,
            'faltando_na_scp [extra no filemaker]' => [],
            'extra_na_scp [faltando no filemaker]' => [],
        ];
    }

    if ($inputFile === null) {
        return [
            'status' => 'error',
            'message' => 'Relatorio de diferencas nao gerado: arquivo de entrada *_input.xml nao encontrado para a rota.',
            'route' => $routeKey,
            'input_file' => null,
            'output_file' => $outputFile,
            'summary' => null,
            'faltando_na_scp [extra no filemaker]' => [],
            'extra_na_scp [faltando no filemaker]' => [],
        ];
    }

    $compareScript = scp_resolve_compare_script_path();
    if ($compareScript === null) {
        return [
            'status' => 'error',
            'message' => 'Relatorio de diferencas nao gerado: compare_xml_input_output.php nao encontrado.',
            'route' => $routeKey,
            'input_file' => null,
            'output_file' => $outputFile,
            'summary' => null,
            'faltando_na_scp [extra no filemaker]' => [],
            'extra_na_scp [faltando no filemaker]' => [],
        ];
    }

    try {
        $phpBin = scp_resolve_php_cli_bin();
    } catch (Throwable $exception) {
        return [
            'status' => 'error',
            'message' => 'Relatorio de diferencas nao gerado: PHP CLI indisponivel.',
            'route' => $routeKey,
            'input_file' => null,
            'output_file' => $outputFile,
            'summary' => null,
            'faltando_na_scp [extra no filemaker]' => [],
            'extra_na_scp [faltando no filemaker]' => [],
            'detail' => $exception->getMessage(),
        ];
    }

    $cmd = escapeshellarg($phpBin)
        . ' ' . escapeshellarg($compareScript)
        . ' ' . escapeshellarg($inputFile)
        . ' ' . escapeshellarg($outputFile)
        . ' --limit=0 --json';

    $output = [];
    $rc = 1;
    @exec($cmd . ' 2>&1', $output, $rc);
    $raw = trim(implode("\n", $output));

    if ($rc !== 0 || $raw === '') {
        return [
            'status' => 'error',
            'message' => 'Relatorio de diferencas nao gerado: falha ao executar comparador.',
            'route' => $routeKey,
            'input_file' => $inputFile,
            'output_file' => $outputFile,
            'summary' => null,
            'faltando_na_scp [extra no filemaker]' => [],
            'extra_na_scp [faltando no filemaker]' => [],
            'detail' => $raw,
        ];
    }

    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        return [
            'status' => 'error',
            'message' => 'Relatorio de diferencas nao gerado: saida JSON invalida do comparador.',
            'route' => $routeKey,
            'input_file' => $inputFile,
            'output_file' => $outputFile,
            'summary' => null,
            'faltando_na_scp [extra no filemaker]' => [],
            'extra_na_scp [faltando no filemaker]' => [],
            'detail' => mb_substr($raw, 0, 1000),
        ];
    }

    $summaryRaw = is_array($decoded['summary'] ?? null) ? $decoded['summary'] : [];
    $summary = [
        'total_filemaker' => (int)($summaryRaw['total_filemaker'] ?? $summaryRaw['total_input_nf'] ?? 0),
        'total_scp' => (int)($summaryRaw['total_scp'] ?? $summaryRaw['total_output_nf'] ?? 0),
        'matched_equal' => (int)($summaryRaw['matched_equal'] ?? $summaryRaw['iguais'] ?? 0),
        'matched_with_differences' => (int)($summaryRaw['matched_with_differences'] ?? $summaryRaw['iguais_diff'] ?? 0),
        'faltando_na_scp [extra no filemaker]' => (int)($summaryRaw['faltando_na_scp [extra no filemaker]'] ?? 0),
        'extra_na_scp [faltando no filemaker]' => (int)($summaryRaw['extra_na_scp [faltando no filemaker]'] ?? 0),
    ];
    $details = is_array($decoded['details'] ?? null) ? $decoded['details'] : [];

    $matchedWithDiff = is_array($details['matched_with_differences'] ?? null)
        ? $details['matched_with_differences']
        : (is_array($decoded['matched_with_differences'] ?? null)
            ? $decoded['matched_with_differences']
            : (is_array($decoded['iguais_diff'] ?? null) ? $decoded['iguais_diff'] : []));

    // Normaliza nomenclatura antiga input/output para filemaker/scp.
    foreach ($matchedWithDiff as $idx => $row) {
        if (!is_array($row)) {
            continue;
        }

        if (!isset($row['filemaker']) && isset($row['input']) && is_array($row['input'])) {
            $matchedWithDiff[$idx]['filemaker'] = $row['input'];
        }
        if (!isset($row['scp']) && isset($row['output']) && is_array($row['output'])) {
            $matchedWithDiff[$idx]['scp'] = $row['output'];
        }

        $fm = is_array($matchedWithDiff[$idx]['filemaker'] ?? null) ? $matchedWithDiff[$idx]['filemaker'] : [];
        $sp = is_array($matchedWithDiff[$idx]['scp'] ?? null) ? $matchedWithDiff[$idx]['scp'] : [];

        $fmTransacao = '';
        foreach ([
            'transacao',
            'Transacao',
            'TRANSACAO',
            'Transação',
            'TRANSAÇÃO',
            '_kp_TransacaoID',
            '_KP_TRANSACAOID',
            'kp_TransacaoID',
            'SCPS_TRANSACOES_REP::_kp_TransacaoID',
        ] as $transKey) {
            $candidate = trim((string)($fm[$transKey] ?? ''));
            if ($candidate !== '') {
                $fmTransacao = $candidate;
                break;
            }
        }
        if ($fmTransacao === '' && isset($fm['raw']) && is_array($fm['raw'])) {
            foreach ([
                'transacao',
                'Transacao',
                'TRANSACAO',
                'Transação',
                'TRANSAÇÃO',
                '_kp_TransacaoID',
                '_KP_TRANSACAOID',
                'kp_TransacaoID',
                'SCPS_TRANSACOES_REP::_kp_TransacaoID',
            ] as $transKey) {
                $candidate = trim((string)($fm['raw'][$transKey] ?? ''));
                if ($candidate !== '') {
                    $fmTransacao = $candidate;
                    break;
                }
            }
        }

        // Saida minima: apenas data e valor por lado, priorizando campos normalizados.
        $matchedWithDiff[$idx]['filemaker'] = [
            'data' => (string)($fm['data_normalizada'] ?? $fm['data'] ?? ''),
            'valor' => (string)($fm['valor_normalizado'] ?? $fm['valor'] ?? ''),
            'descricao' => (string)($fm['descricao'] ?? ''),
            'transacao' => $fmTransacao,
        ];
        $matchedWithDiff[$idx]['scp'] = [
            'data' => (string)($sp['data_normalizada'] ?? $sp['data'] ?? ''),
            'valor' => (string)($sp['valor_normalizado'] ?? $sp['valor'] ?? ''),
            'descricao' => (string)($sp['descricao'] ?? ''),
        ];

        unset($matchedWithDiff[$idx]['input'], $matchedWithDiff[$idx]['output']);
    }

    $missingInScp = is_array($details['faltando_na_scp [extra no filemaker]'] ?? null)
        ? $details['faltando_na_scp [extra no filemaker]']
        : (is_array($decoded['faltando_na_scp [extra no filemaker]'] ?? null)
            ? $decoded['faltando_na_scp [extra no filemaker]']
            : []);

    $extraInScp = is_array($details['extra_na_scp [faltando no filemaker]'] ?? null)
        ? $details['extra_na_scp [faltando no filemaker]']
        : (is_array($decoded['extra_na_scp [faltando no filemaker]'] ?? null)
            ? $decoded['extra_na_scp [faltando no filemaker]']
            : []);

    return [
        'status' => (string)($decoded['status'] ?? 'ok'),
        'message' => 'Relatorio de diferencas gerado com sucesso.',
        'route' => $routeKey,
        'filemaker_file' => $decoded['filemaker_file'] ?? $decoded['input_file'] ?? null,
        'scp_file' => $decoded['scp_file'] ?? $decoded['output_file'] ?? $outputFile,
        'summary' => $summary,
        'details' => [
            'matched_with_differences' => $matchedWithDiff,
            'faltando_na_scp [extra no filemaker]' => $missingInScp,
            'extra_na_scp [faltando no filemaker]' => $extraInScp,
        ],
    ];
}

function scp_build_filemaker_payload(array $route, array $files, array $workerResult, array $transformResult, array $differenceReport): array
{
    if (isset($differenceReport['details']) && is_array($differenceReport['details'])) {
        unset($differenceReport['details']['missing_in_scp_by_date'], $differenceReport['details']['missing_in_output_by_date']);
    }
    unset(
        $differenceReport['missing_in_scp_by_date'],
        $differenceReport['missing_in_output_by_date'],
        $differenceReport['matched_with_differences'],
        $differenceReport['faltando_na_scp [extra no filemaker]'],
        $differenceReport['extra_na_scp [faltando no filemaker]']
    );

    $inputOriginal = [];
    foreach ($files as $file) {
        $name = trim((string)($file['name'] ?? ''));
        if ($name !== '') {
            $inputOriginal[] = $name;
        }
    }

    $outputFile = (string)($transformResult['output_file'] ?? '');
    $csvOutput = (string)($transformResult['csv_output'] ?? '');
    $outputFileUrl = $outputFile !== '' ? scp_build_output_public_url($outputFile) : null;
    $csvOutputUrl = $csvOutput !== '' ? scp_build_output_public_url($csvOutput) : null;
    $transformForJson = $transformResult;
    unset($transformForJson['xml_content'], $transformForJson['status'], $transformForJson['message'], $transformForJson['output_file'], $transformForJson['csv_output']);

    $uploadForJson = $workerResult;
    unset($uploadForJson['status'], $uploadForJson['message'], $uploadForJson['route'], $uploadForJson['csv_file']);

    return [
        'route' => (string)($route['key'] ?? ''),
        'empresa_id' => (string)($transformResult['empresa_id'] ?? scp_resolve_empresa_id($_GET + $_POST)),
        'input_file' => $inputOriginal,
        'output_file' => $transformResult['output_file'] ?? null,
        'output_file_url' => $outputFileUrl,
        'csv_output_url' => $csvOutputUrl,
        // Campos de apoio para depuracao/consumo legado.
        'status' => (string)($transformResult['status'] ?? ($workerResult['status'] ?? 'error')),
        'message' => (string)($transformResult['message'] ?? ($workerResult['message'] ?? '')),
        'json_content' => [
            'upload' => $uploadForJson,
            'transform' => $transformForJson,
        ],
        'response_snapshot' => null,
        'response_json_url' => null,
        'xml_content' => $transformResult['xml_content'] ?? null,
        'difference_report' => $differenceReport,
    ];
}

function scp_format_valor_br(string $value): string
{
    $value = trim($value);
    if ($value === '') {
        return '';
    }

    $negative = false;
    if (str_starts_with($value, '-')) {
        $negative = true;
        $value = substr($value, 1);
    }

    $normalized = preg_replace('/[^0-9,\.]/', '', $value) ?? '';
    if ($normalized === '') {
        return ($negative ? '-' : '') . trim($value);
    }

    $lastComma = strrpos($normalized, ',');
    $lastDot = strrpos($normalized, '.');
    $decimalPos = false;
    if ($lastComma !== false && $lastDot !== false) {
        $decimalPos = max($lastComma, $lastDot);
    } elseif ($lastComma !== false) {
        $decimalPos = $lastComma;
    } elseif ($lastDot !== false) {
        $decimalPos = $lastDot;
    }

    $intPart = $normalized;
    $decPart = '';
    if ($decimalPos !== false) {
        $intPart = substr($normalized, 0, $decimalPos);
        $decPart = substr($normalized, $decimalPos + 1);
    }

    $intDigits = preg_replace('/\D/', '', $intPart) ?? '';
    $decDigits = preg_replace('/\D/', '', $decPart) ?? '';

    if ($intDigits === '' && $decDigits === '') {
        return ($negative ? '-' : '') . trim($value);
    }

    if ($intDigits === '') {
        $intDigits = '0';
    }

    $formattedInt = preg_replace('/\B(?=(\d{3})+(?!\d))/', '.', $intDigits) ?? $intDigits;

    if ($decDigits === '') {
        $formatted = $formattedInt;
    } else {
        $decDigits = substr(str_pad($decDigits, 2, '0'), 0, 2);
        $formatted = $formattedInt . ',' . $decDigits;
    }

    return ($negative ? '-' : '') . $formatted;
}

function scp_format_diff_row_valor(array $row): array
{
    if (isset($row['valor'])) {
        $row['valor'] = scp_format_valor_br((string)$row['valor']);
    }

    if (isset($row['filemaker']) && is_array($row['filemaker']) && isset($row['filemaker']['valor'])) {
        $row['filemaker']['valor'] = scp_format_valor_br((string)$row['filemaker']['valor']);
    }

    if (isset($row['scp']) && is_array($row['scp']) && isset($row['scp']['valor'])) {
        $row['scp']['valor'] = scp_format_valor_br((string)$row['scp']['valor']);
    }

    return $row;
}

function scp_format_date_br_presentation(string $value): string
{
    $value = trim($value);
    if ($value === '') {
        return '';
    }

    if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $m) === 1) {
        return sprintf('%02d/%02d/%02d', (int)$m[3], (int)$m[2], (int)$m[1] % 100);
    }

    if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $value, $m) === 1) {
        return sprintf('%02d/%02d/%02d', (int)$m[1], (int)$m[2], (int)$m[3] % 100);
    }

    return $value;
}

function scp_format_diff_row_presentation(array $row): array
{
    $row = scp_format_diff_row_valor($row);

    if (isset($row['data'])) {
        $row['data'] = scp_format_date_br_presentation((string)$row['data']);
    }

    if (isset($row['filemaker']) && is_array($row['filemaker']) && isset($row['filemaker']['data'])) {
        $row['filemaker']['data'] = scp_format_date_br_presentation((string)$row['filemaker']['data']);
    }

    if (isset($row['scp']) && is_array($row['scp']) && isset($row['scp']['data'])) {
        $row['scp']['data'] = scp_format_date_br_presentation((string)$row['scp']['data']);
    }

    return $row;
}

function scp_extract_transacao_id_from_row(array $row): string
{
    foreach ([
        '_kp_TransacaoID',
        'SCPS_TRANSACOES_REP::_kp_TransacaoID',
        '_kf_transacaoID',
        '_kf_TransacaoID',
        'kf_TransacaoID',
        'transacao',
        'Transacao',
    ] as $key) {
        if (!array_key_exists($key, $row)) {
            continue;
        }
        $value = trim((string)$row[$key]);
        if ($value !== '') {
            return $value;
        }
    }

    return '';
}

function scp_sanitize_difference_report_payload(array $payload): array
{
    if (!isset($payload['difference_report']) || !is_array($payload['difference_report'])) {
        return $payload;
    }

    $report = $payload['difference_report'];
    $details = isset($report['details']) && is_array($report['details']) ? $report['details'] : [];

    $matchedWithDiff = is_array($details['matched_with_differences'] ?? null) ? $details['matched_with_differences'] : [];
    $missingInScp = is_array($details['faltando_na_scp [extra no filemaker]'] ?? null)
        ? $details['faltando_na_scp [extra no filemaker]']
        : [];
    $extraInScp = is_array($details['extra_na_scp [faltando no filemaker]'] ?? null)
        ? $details['extra_na_scp [faltando no filemaker]']
        : [];

    $embeddedDeleteIds = [];
    $filteredMissingInScp = [];
    foreach ($missingInScp as $row) {
        if (is_array($row) && array_key_exists('_lista a deletar no filemaker', $row)) {
            $rawIds = $row['_lista a deletar no filemaker'];
            if (is_array($rawIds)) {
                foreach ($rawIds as $id) {
                    $idStr = trim((string)$id);
                    if ($idStr !== '') {
                        $embeddedDeleteIds[] = $idStr;
                    }
                }
            }
            continue;
        }
        $filteredMissingInScp[] = $row;
    }
    $missingInScp = $filteredMissingInScp;

    foreach ($matchedWithDiff as $idx => $row) {
        if (is_array($row)) {
            $matchedWithDiff[$idx] = scp_format_diff_row_presentation($row);
        }
    }
    foreach ($missingInScp as $idx => $row) {
        if (is_array($row)) {
            $missingInScp[$idx] = scp_format_diff_row_presentation($row);
        }
    }
    foreach ($extraInScp as $idx => $row) {
        if (is_array($row)) {
            $extraInScp[$idx] = scp_format_diff_row_presentation($row);
        }
    }

    if (count($matchedWithDiff) === 0 && $missingInScp !== [] && $extraInScp !== []) {
        $normalizeNf = static function (string $nf): string {
            $nf = trim($nf);
            if ($nf === '') {
                return '';
            }
            if (preg_match_all('/\d+/', $nf, $all) === 1 && !empty($all[0])) {
                $token = (string)$all[0][count($all[0]) - 1];
                $trimmed = ltrim($token, '0');
                return $trimmed !== '' ? $trimmed : '0';
            }
            return preg_replace('/[^A-Z0-9]/', '', strtoupper($nf)) ?? '';
        };

        $parseAmount = static function (string $value): ?float {
            $value = trim($value);
            if ($value === '') {
                return null;
            }
            $negative = false;
            if (str_starts_with($value, '-')) {
                $negative = true;
                $value = substr($value, 1);
            }
            $normalized = preg_replace('/[^0-9,\.]/', '', $value) ?? '';
            if ($normalized === '') {
                return null;
            }
            $lastComma = strrpos($normalized, ',');
            $lastDot = strrpos($normalized, '.');
            $decimalPos = false;
            if ($lastComma !== false && $lastDot !== false) {
                $decimalPos = max($lastComma, $lastDot);
            } elseif ($lastComma !== false) {
                $decimalPos = $lastComma;
            } elseif ($lastDot !== false) {
                $decimalPos = $lastDot;
            }
            $intPart = $normalized;
            $decPart = '';
            if ($decimalPos !== false) {
                $intPart = substr($normalized, 0, $decimalPos);
                $decPart = substr($normalized, $decimalPos + 1);
            }
            $intDigits = preg_replace('/\D/', '', $intPart) ?? '';
            $decDigits = preg_replace('/\D/', '', $decPart) ?? '';
            if ($intDigits === '' && $decDigits === '') {
                return null;
            }
            if ($intDigits === '') {
                $intDigits = '0';
            }
            $numeric = (float)($intDigits . ($decDigits !== '' ? '.' . $decDigits : ''));
            return $negative ? -$numeric : $numeric;
        };

        $parseDateTs = static function (string $value): ?int {
            $value = trim($value);
            if ($value === '') {
                return null;
            }
            if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/', $value, $m) === 1) {
                $d = (int)$m[1];
                $mo = (int)$m[2];
                $y = (int)$m[3];
                if ($y < 100) {
                    $y += ($y >= 70 ? 1900 : 2000);
                }
                if (checkdate($mo, $d, $y)) {
                    $ts = strtotime(sprintf('%04d-%02d-%02d 00:00:00', $y, $mo, $d));
                    return $ts === false ? null : (int)$ts;
                }
            }
            if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value, $m) === 1) {
                $y = (int)$m[1];
                $mo = (int)$m[2];
                $d = (int)$m[3];
                if (checkdate($mo, $d, $y)) {
                    $ts = strtotime(sprintf('%04d-%02d-%02d 00:00:00', $y, $mo, $d));
                    return $ts === false ? null : (int)$ts;
                }
            }
            $ts = strtotime($value);
            return $ts === false ? null : (int)$ts;
        };

        $pairScore = static function (array $fm, array $sp) use ($parseAmount, $parseDateTs): float {
            $score = 0.0;
            $fDate = $parseDateTs((string)($fm['data'] ?? ''));
            $sDate = $parseDateTs((string)($sp['data'] ?? ''));
            if ($fDate !== null && $sDate !== null) {
                $score += (float)((int)floor(abs($fDate - $sDate) / 86400));
            }
            $fAmount = $parseAmount((string)($fm['valor'] ?? ''));
            $sAmount = $parseAmount((string)($sp['valor'] ?? ''));
            if ($fAmount !== null && $sAmount !== null) {
                $absDiff = abs($fAmount - $sAmount);
                $base = max(abs($fAmount), abs($sAmount), 1.0);
                $relDiff = $absDiff / $base;
                $score += $absDiff / 100.0;
                $score += $relDiff * 50.0;
            }
            return $score;
        };

        $isCompatible = static function (array $fm, array $sp) use ($parseAmount, $parseDateTs): bool {
            $fAmount = $parseAmount((string)($fm['valor'] ?? ''));
            $sAmount = $parseAmount((string)($sp['valor'] ?? ''));
            if ($fAmount === null || $sAmount === null) {
                return true;
            }
            $absDiff = abs($fAmount - $sAmount);
            $base = max(abs($fAmount), abs($sAmount), 1.0);
            $relDiff = $absDiff / $base;
            $fDate = $parseDateTs((string)($fm['data'] ?? ''));
            $sDate = $parseDateTs((string)($sp['data'] ?? ''));
            $dateGap = null;
            if ($fDate !== null && $sDate !== null) {
                $dateGap = (int)floor(abs($fDate - $sDate) / 86400);
            }

            if ($dateGap !== null && $dateGap >= 25 && $relDiff <= 0.03) {
                return false;
            }
            if ($dateGap !== null && $dateGap >= 25 && $absDiff >= 4000.0 && $relDiff >= 0.55) {
                return false;
            }
            return true;
        };

        $missingByNf = [];
        foreach ($missingInScp as $idx => $row) {
            if (!is_array($row)) {
                continue;
            }
            $nf = $normalizeNf((string)($row['nf'] ?? ''));
            if ($nf === '') {
                continue;
            }
            $missingByNf[$nf][] = $idx;
        }

        $extraByNf = [];
        foreach ($extraInScp as $idx => $row) {
            if (!is_array($row)) {
                continue;
            }
            $nf = $normalizeNf((string)($row['nf'] ?? ''));
            if ($nf === '') {
                continue;
            }
            $extraByNf[$nf][] = $idx;
        }

        $removeMissing = [];
        $removeExtra = [];
        foreach ($missingByNf as $nf => $mIndexes) {
            $eIndexes = $extraByNf[$nf] ?? [];
            if ($eIndexes === []) {
                continue;
            }

            foreach ($mIndexes as $mIdx) {
                if (!isset($missingInScp[$mIdx]) || !is_array($missingInScp[$mIdx])) {
                    continue;
                }
                $fmRow = $missingInScp[$mIdx];

                $bestExtraIdx = null;
                $bestScore = INF;
                foreach ($eIndexes as $eIdx) {
                    if (isset($removeExtra[$eIdx])) {
                        continue;
                    }
                    if (!isset($extraInScp[$eIdx]) || !is_array($extraInScp[$eIdx])) {
                        continue;
                    }
                    $spRow = $extraInScp[$eIdx];
                    if (!$isCompatible($fmRow, $spRow)) {
                        continue;
                    }
                    $score = $pairScore($fmRow, $spRow);
                    if ($score < $bestScore) {
                        $bestScore = $score;
                        $bestExtraIdx = $eIdx;
                    }
                }

                if ($bestExtraIdx === null) {
                    continue;
                }

                $removeMissing[$mIdx] = true;
                $removeExtra[$bestExtraIdx] = true;

                $fmMatched = $missingInScp[$mIdx];
                $spMatched = $extraInScp[$bestExtraIdx];
                $nfRaw = (string)($fmMatched['nf'] ?? $spMatched['nf'] ?? '');

                $fmData = (string)($fmMatched['data'] ?? '');
                $spData = (string)($spMatched['data'] ?? '');
                $fmValor = (string)($fmMatched['valor'] ?? '');
                $spValor = (string)($spMatched['valor'] ?? '');

                $diff = [];
                if ($fmData !== $spData) {
                    $diff[] = 'data';
                }
                if ($fmValor !== $spValor) {
                    $diff[] = 'valor';
                }

                $matchedWithDiff[] = [
                    'nf' => is_numeric($nfRaw) ? (int)$nfRaw : $nfRaw,
                    'filemaker' => [
                        'data' => $fmData,
                        'valor' => $fmValor,
                        'descricao' => (string)($fmMatched['descricao'] ?? ''),
                        'transacao' => (string)($fmMatched['transacao'] ?? ''),
                    ],
                    'scp' => [
                        'data' => $spData,
                        'valor' => $spValor,
                        'descricao' => (string)($spMatched['descricao'] ?? ''),
                    ],
                    'diff' => $diff,
                ];
            }
        }

        if ($removeMissing !== []) {
            foreach (array_keys($removeMissing) as $idx) {
                unset($missingInScp[$idx]);
            }
            $missingInScp = array_values($missingInScp);
        }
        if ($removeExtra !== []) {
            foreach (array_keys($removeExtra) as $idx) {
                unset($extraInScp[$idx]);
            }
            $extraInScp = array_values($extraInScp);
        }
    }

    $transacaoIdList = [];
    foreach ($missingInScp as $idx => $row) {
        if (!is_array($row)) {
            continue;
        }
        $transacaoId = scp_extract_transacao_id_from_row($row);
        if ($transacaoId !== '') {
            $missingInScp[$idx]['_kp_TransacaoID'] = $transacaoId;
            $transacaoIdList[] = $transacaoId;
        }
    }
    if ($transacaoIdList !== []) {
        $transacaoIdList = array_values(array_unique($transacaoIdList));
    }

    if ($transacaoIdList === [] && is_array($details['lista a deletar no filemaker'] ?? null)) {
        foreach ($details['lista a deletar no filemaker'] as $id) {
            $idStr = trim((string)$id);
            if ($idStr !== '') {
                $transacaoIdList[] = $idStr;
            }
        }
        if ($transacaoIdList !== []) {
            $transacaoIdList = array_values(array_unique($transacaoIdList));
        }
    }

    if ($transacaoIdList === [] && is_array($details['lista__kf_transacaoID'] ?? null)) {
        foreach ($details['lista__kf_transacaoID'] as $id) {
            $idStr = trim((string)$id);
            if ($idStr !== '') {
                $transacaoIdList[] = $idStr;
            }
        }
        if ($transacaoIdList !== []) {
            $transacaoIdList = array_values(array_unique($transacaoIdList));
        }
    }

    if ($transacaoIdList === [] && $embeddedDeleteIds !== []) {
        $transacaoIdList = array_values(array_unique($embeddedDeleteIds));
    }

    $cleanDetails = [
        'matched_with_differences' => $matchedWithDiff,
        'faltando_na_scp [extra no filemaker]' => $missingInScp,
        'lista a deletar no filemaker' => $transacaoIdList,
        'extra_na_scp [faltando no filemaker]' => $extraInScp,
    ];

    $summary = is_array($report['summary'] ?? null) ? $report['summary'] : null;
    if (is_array($summary)) {
        $matchedEqual = (int)($summary['matched_equal'] ?? 0);
        $matchedWithDiffCount = count($matchedWithDiff);
        $missingCount = count($missingInScp);
        $extraCount = count($extraInScp);

        unset($summary['missing_in_scp'], $summary['extra_in_scp [missing in filemaker]']);
        $summary['total_filemaker'] = $matchedEqual + $matchedWithDiffCount + $missingCount;
        $summary['total_scp'] = $matchedEqual + $matchedWithDiffCount + $extraCount;
        $summary['matched_with_differences'] = $matchedWithDiffCount;
        $summary['faltando_na_scp [extra no filemaker]'] = $missingCount;
        $summary['extra_na_scp [faltando no filemaker]'] = $extraCount;
        $summary['_matcher_runtime_marker'] = 'local-wrapper-2026-10-21-02';
    }

    $payload['difference_report'] = [
        'status' => (string)($report['status'] ?? ''),
        'message' => (string)($report['message'] ?? ''),
        'route' => (string)($report['route'] ?? ''),
        'filemaker_file' => $report['filemaker_file'] ?? null,
        'scp_file' => $report['scp_file'] ?? null,
        'summary' => $summary,
        'details' => $cleanDetails,
    ];

    $payload = scp_force_transacao_list_in_final_payload($payload);

    return $payload;
}

function scp_force_transacao_list_in_final_payload(array $payload): array
{
    if (!isset($payload['difference_report']) || !is_array($payload['difference_report'])) {
        return $payload;
    }

    if (!isset($payload['difference_report']['details']) || !is_array($payload['difference_report']['details'])) {
        return $payload;
    }

    $rows = $payload['difference_report']['details']['faltando_na_scp [extra no filemaker]'] ?? [];
    if (!is_array($rows)) {
        $rows = [];
    }

    $filteredRows = [];
    foreach ($rows as $row) {
        if (is_array($row) && array_key_exists('_lista a deletar no filemaker', $row)) {
            continue;
        }
        $filteredRows[] = $row;
    }
    $rows = $filteredRows;
    $payload['difference_report']['details']['faltando_na_scp [extra no filemaker]'] = $rows;

    $ids = [];
    foreach ($rows as $idx => $row) {
        if (!is_array($row)) {
            continue;
        }
        $id = scp_extract_transacao_id_from_row($row);
        if ($id === '') {
            continue;
        }
        $payload['difference_report']['details']['faltando_na_scp [extra no filemaker]'][$idx]['_kp_TransacaoID'] = $id;
        $ids[] = $id;
    }

    if ($ids !== []) {
        $ids = array_values(array_unique($ids));
    }

    $payload['difference_report']['details']['lista a deletar no filemaker'] = $ids;

    return $payload;
}

function scp_build_xml_content_atualizar_from_payload(array $payload): ?string
{
    $matchedRows = $payload['difference_report']['details']['matched_with_differences']
        ?? $payload['difference_report']['matched_with_differences']
        ?? $payload['matched_with_differences']
        ?? [];
    if (!is_array($matchedRows)) {
        $matchedRows = [];
    }

    $extractMetadataFromFmp = static function (?string $xmlContent): array {
        if (!is_string($xmlContent) || trim($xmlContent) === '') {
            return [];
        }

        libxml_use_internal_errors(true);
        $xml = @simplexml_load_string($xmlContent);
        if (!$xml instanceof SimpleXMLElement) {
            return [];
        }

        $fieldNodes = $xml->xpath('/*[local-name()="FMPXMLRESULT"]/*[local-name()="METADATA"]/*[local-name()="FIELD"]');
        if (!is_array($fieldNodes) || $fieldNodes === []) {
            return [];
        }

        $fields = [];
        foreach ($fieldNodes as $fieldNode) {
            $attrs = $fieldNode->attributes();
            $name = trim((string)($attrs['NAME'] ?? ''));
            if ($name === '') {
                continue;
            }
            $fields[] = [
                'NAME' => $name,
                'TYPE' => trim((string)($attrs['TYPE'] ?? 'TEXT')),
                'EMPTYOK' => trim((string)($attrs['EMPTYOK'] ?? 'YES')),
                'MAXREPEAT' => trim((string)($attrs['MAXREPEAT'] ?? '1')),
            ];
        }

        return $fields;
    };

    $extractMetadataFromXslt = static function (): array {
        $base = scp_root_dir();
        $candidates = [
            $base . '/scripts/xslts/transformTitulo[todos].xslt',
            $base . '/scripts/xslts/transform_csv.xslt',
            $base . '/scripts/xslts/transformTitulos[Bulk][todos].xslt',
        ];

        foreach ($candidates as $xsltPath) {
            if (!is_file($xsltPath) || !is_readable($xsltPath)) {
                continue;
            }

            libxml_use_internal_errors(true);
            $xslt = @simplexml_load_file($xsltPath);
            if (!$xslt instanceof SimpleXMLElement) {
                continue;
            }

            $fieldNodes = $xslt->xpath('//*[local-name()="METADATA"]/*[local-name()="FIELD"]');
            if (!is_array($fieldNodes) || $fieldNodes === []) {
                continue;
            }

            $fields = [];
            foreach ($fieldNodes as $fieldNode) {
                $attrs = $fieldNode->attributes();
                $name = trim((string)($attrs['NAME'] ?? ''));
                if ($name === '') {
                    continue;
                }
                $fields[] = [
                    'NAME' => $name,
                    'TYPE' => trim((string)($attrs['TYPE'] ?? 'TEXT')),
                    'EMPTYOK' => trim((string)($attrs['EMPTYOK'] ?? 'YES')),
                    'MAXREPEAT' => trim((string)($attrs['MAXREPEAT'] ?? '1')),
                ];
            }

            if ($fields !== []) {
                return $fields;
            }
        }

        return [];
    };

    $resolveFieldValue = static function (string $fieldName, array $row): string {
        $scp = is_array($row['scp'] ?? null) ? $row['scp'] : [];
        $nf = (string)($row['nf'] ?? '');

        $normalized = strtolower(unaccent($fieldName));
        $normalized = preg_replace('/[^a-z0-9_]/', '', $normalized) ?? '';

        if ($normalized === 'data') {
            return (string)($scp['data'] ?? '');
        }
        if ($normalized === 'valor' || $normalized === 'valormoeda') {
            return (string)($scp['valor'] ?? '');
        }
        if ($normalized === 'descricao') {
            return (string)($scp['descricao'] ?? '');
        }
        if ($normalized === 'nf') {
            return $nf;
        }

        // Requirement: xml_content_atualizar must use SCP values only.
        // Unknown/non-SCP fields are left empty to preserve imported layout.
        return '';
    };

    // Use the same metadata model from the executa_conta XSLT contract.
    $metadataFields = $extractMetadataFromXslt();
    if ($metadataFields === []) {
        $metadataFields = [
            ['NAME' => 'Data', 'TYPE' => 'DATE', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Valor', 'TYPE' => 'NUMBER', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Valor_Moeda', 'TYPE' => 'NUMBER', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => '_kf_FornecedorID', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Descricao', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => '_kf_CCustoID', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => '_kf_ContaID', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Titulo', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Codigo', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Contrato', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Pedido', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Categoria', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Observacoes', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'NF', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Contabil', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Periodo', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Duplicata', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Status', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Tipo', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => '_kf_EmpresaID', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'account_create', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'account_mod', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'timestamp_create', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'timestamp_mod', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
            ['NAME' => 'Scan', 'TYPE' => 'TEXT', 'EMPTYOK' => 'YES', 'MAXREPEAT' => '1'],
        ];
    }

    $doc = new DOMDocument('1.0', 'UTF-8');
    $doc->formatOutput = true;

    $fmp = $doc->createElementNS('http://www.filemaker.com/fmpxmlresult', 'FMPXMLRESULT');
    if (!$fmp) {
        return null;
    }
    $doc->appendChild($fmp);

    $errorCode = $doc->createElement('ERRORCODE', '0');
    $fmp->appendChild($errorCode);

    $product = $doc->createElement('PRODUCT');
    $product->setAttribute('BUILD', date('m-d-Y'));
    $product->setAttribute('NAME', 'FileMaker');
    $product->setAttribute('VERSION', 'Pro 21.1.1');
    $fmp->appendChild($product);

    $database = $doc->createElement('DATABASE');
    $database->setAttribute('DATEFORMAT', 'D/m/yyyy');
    $database->setAttribute('LAYOUT', '');
    $database->setAttribute('NAME', 'xml_content_atualizar');
    $database->setAttribute('RECORDS', '');
    $database->setAttribute('TIMEFORMAT', 'k:mm:ss ');
    $fmp->appendChild($database);

    $metadata = $doc->createElement('METADATA');
    $fmp->appendChild($metadata);

    foreach ($metadataFields as $fieldDef) {
        $field = $doc->createElement('FIELD');
        $field->setAttribute('EMPTYOK', (string)($fieldDef['EMPTYOK'] ?? 'YES'));
        $field->setAttribute('MAXREPEAT', (string)($fieldDef['MAXREPEAT'] ?? '1'));
        $field->setAttribute('NAME', (string)($fieldDef['NAME'] ?? ''));
        $field->setAttribute('TYPE', (string)($fieldDef['TYPE'] ?? 'TEXT'));
        $metadata->appendChild($field);
    }

    $resultset = $doc->createElement('RESULTSET');
    $resultset->setAttribute('FOUND', (string)count($matchedRows));
    $fmp->appendChild($resultset);

    foreach ($matchedRows as $row) {
        if (!is_array($row)) {
            continue;
        }

        $xmlRow = $doc->createElement('ROW');
        $resultset->appendChild($xmlRow);

        foreach ($metadataFields as $fieldDef) {
            $fieldName = (string)($fieldDef['NAME'] ?? '');
            $value = $resolveFieldValue($fieldName, $row);
            $col = $doc->createElement('COL');
            $dataNode = $doc->createElement('DATA');
            $dataNode->appendChild($doc->createTextNode($value));
            $col->appendChild($dataNode);
            $xmlRow->appendChild($col);
        }
    }

    return $doc->saveXML() ?: null;
}


function scp_write_xml_content_atualizar_file(string $routeKey, ?string $xmlContent): array
{
    $xmlContent = is_string($xmlContent) ? trim($xmlContent) : '';
    if ($xmlContent === '') {
        return [
            'ok' => false,
            'error' => 'empty_xml_content_atualizar',
        ];
    }

    $routeStem = scp_route_file_stem($routeKey);
    if ($routeStem === '') {
        $routeStem = preg_replace('/[^a-z0-9_\-]+/i', '_', strtolower($routeKey)) ?: 'route';
    }

    $outputDir = rtrim(scp_root_dir(), '/\\') . '/output';
    if (!is_dir($outputDir)) {
        @mkdir($outputDir, 0775, true);
    }

    if (!is_dir($outputDir) || !is_writable($outputDir)) {
        return [
            'ok' => false,
            'error' => 'output_dir_not_writable',
            'dir' => $outputDir,
        ];
    }

    $stamp = date('Ymd_His');
    $latestPath = $outputDir . '/xml_content_atualizar_' . $routeStem . '_latest.xml';
    $historyPath = $outputDir . '/xml_content_atualizar_' . $routeStem . '_' . $stamp . '.xml';

    $latestBytes = @file_put_contents($latestPath, $xmlContent);
    $historyBytes = @file_put_contents($historyPath, $xmlContent);

    if ($latestBytes === false || $historyBytes === false) {
        return [
            'ok' => false,
            'error' => 'write_failed',
            'latest_path' => $latestPath,
            'history_path' => $historyPath,
        ];
    }

    @chmod($latestPath, 0664);
    @chmod($historyPath, 0664);

    return [
        'ok' => true,
        'latest_path' => $latestPath,
        'history_path' => $historyPath,
        'latest_url' => scp_build_output_public_url($latestPath),
        'history_url' => scp_build_output_public_url($historyPath),
        'bytes' => (int)$latestBytes,
    ];
}


function scp_build_output_public_url(string $outputFile): ?string
{
    $outputFile = trim($outputFile);
    if ($outputFile === '') {
        return null;
    }

    $baseName = basename($outputFile);
    if ($baseName === '' || $baseName === '.' || $baseName === '..') {
        return null;
    }

    $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    $host = (string)($_SERVER['HTTP_HOST'] ?? 'localhost');

    return $scheme . '://' . $host . '/output/' . rawurlencode($baseName);
}

function scp_write_response_snapshot(string $routeKey, array $payload): array
{
    $payload = scp_sanitize_difference_report_payload($payload);

    $routeStem = scp_route_file_stem($routeKey);
    $outputDir = rtrim(scp_root_dir(), '/\\') . '/output';

    if (!is_dir($outputDir)) {
        @mkdir($outputDir, 0775, true);
    }

    if (!is_dir($outputDir) || !is_writable($outputDir)) {
        return [
            'ok' => false,
            'error' => 'output_dir_not_writable',
            'dir' => $outputDir,
        ];
    }

    $stamp = date('Ymd_His');
    $latestPath = $outputDir . '/executa_scp_' . $routeStem . '_latest.json';
    $historyPath = $outputDir . '/executa_scp_' . $routeStem . '_' . $stamp . '.json';
    $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE);

    if (!is_string($json) || $json === '') {
        return [
            'ok' => false,
            'error' => 'json_encode_failed',
            'dir' => $outputDir,
        ];
    }

    $latestOk = @file_put_contents($latestPath, $json) !== false;
    $historyOk = @file_put_contents($historyPath, $json) !== false;

    return [
        'ok' => ($latestOk || $historyOk),
        'dir' => $outputDir,
        'latest_file' => $latestOk ? $latestPath : null,
        'history_file' => $historyOk ? $historyPath : null,
        'latest_url' => $latestOk ? scp_build_output_public_url($latestPath) : null,
        'history_url' => $historyOk ? scp_build_output_public_url($historyPath) : null,
    ];
}

function scp_write_response_snapshot_to_uploads(string $routeKey, array $payload): array
{
    $payload = scp_sanitize_difference_report_payload($payload);

    $routeStem = scp_route_file_stem($routeKey);
    $uploadsDir = rtrim(scp_root_dir(), '/\\') . '/uploads';

    if (!is_dir($uploadsDir)) {
        @mkdir($uploadsDir, 0775, true);
    }

    if (!is_dir($uploadsDir) || !is_writable($uploadsDir)) {
        return [
            'ok' => false,
            'error' => 'uploads_dir_not_writable',
            'dir' => $uploadsDir,
        ];
    }

    $stamp = date('Ymd_His');
    $latestPath = $uploadsDir . '/executa_scp_' . $routeStem . '_latest.json';
    $historyPath = $uploadsDir . '/executa_scp_' . $routeStem . '_' . $stamp . '.json';
    $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE);

    if (!is_string($json) || $json === '') {
        return [
            'ok' => false,
            'error' => 'json_encode_failed',
            'dir' => $uploadsDir,
        ];
    }

    $latestOk = @file_put_contents($latestPath, $json) !== false;
    $historyOk = @file_put_contents($historyPath, $json) !== false;

    $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    $host = (string)($_SERVER['HTTP_HOST'] ?? 'localhost');

    return [
        'ok' => ($latestOk || $historyOk),
        'dir' => $uploadsDir,
        'latest_file' => $latestOk ? $latestPath : null,
        'history_file' => $historyOk ? $historyPath : null,
        'latest_url' => $latestOk ? ($scheme . '://' . $host . '/uploads/' . rawurlencode(basename($latestPath))) : null,
        'history_url' => $historyOk ? ($scheme . '://' . $host . '/uploads/' . rawurlencode(basename($historyPath))) : null,
    ];
}

function scp_render_form(array $routes, ?array $lastResult = null, ?string $errorMessage = null, ?string $selectedRouteKey = null): void
{
    global $scpDebugEnabled;

    $resolvedRouteKey = strtolower(trim((string)$selectedRouteKey));
    if ($resolvedRouteKey === '' || !isset($routes[$resolvedRouteKey])) {
        $resolvedRouteKey = 'pilao';
    }

    header('Content-Type: text/html; charset=utf-8');
    echo '<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">';
    echo '<title>Executa SCP - Upload PDF para Droplet</title>';
    echo '<style>';
    echo 'body{font-family:Arial,sans-serif;background:#f6f7f9;margin:0;padding:16px;color:#111827}';
    echo '.box{max-width:760px;margin:0 auto;background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:14px}';
    echo 'h1{margin:0 0 8px;font-size:20px}p{margin:0 0 12px;font-size:13px;color:#4b5563;line-height:1.4}';
    echo 'label{display:block;font-size:13px;font-weight:700;margin:10px 0 6px}';
    echo 'select{width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px;background:#fff}';
    echo '.row{display:flex;gap:8px;align-items:stretch;margin-top:10px}';
    echo '.btn{background:#0b5fff;border:none;color:#fff;padding:9px 12px;border-radius:8px;cursor:pointer;font-size:13px}';
    echo '.btn.pick{flex:1 1 0}.btn.send{background:#111827;width:46px;padding:8px 0;font-weight:700}.btn.stop{background:#b91c1c}';
    echo '.hint{margin-top:8px;font-size:12px;color:#6b7280}.error{margin-top:10px;background:#fef2f2;color:#991b1b;border:1px solid #fecaca;border-radius:8px;padding:8px;font-size:12px}';
    echo '.ok{margin-top:10px;background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;border-radius:8px;padding:8px;font-size:12px}';
    echo '.result{margin-top:10px;border:1px solid #e5e7eb;border-radius:8px;padding:8px;background:#fafafa}';
    echo '.result-table-scroll{margin-top:8px;max-height:46vh;overflow-y:auto;overflow-x:auto;-webkit-overflow-scrolling:touch;border:1px solid #e5e7eb;border-radius:8px;background:#fff}';
    echo '.result table{width:100%;border-collapse:collapse;font-size:12px} .result th,.result td{border-bottom:1px solid #e5e7eb;padding:6px;text-align:left;vertical-align:top}';
    echo '.mono{font-family:Menlo,Consolas,monospace;word-break:break-all}';
    echo '</style></head><body><div class="box">';
    echo '<h1>Executa SCP</h1>';
    echo '<form id="scpForm" method="post" enctype="multipart/form-data">';
    echo '<input type="hidden" name="route" value="' . htmlspecialchars($resolvedRouteKey, ENT_QUOTES, 'UTF-8') . '">';
    echo '<input id="picker" name="pdffiles[]" type="file" accept="application/pdf" multiple style="display:none">';
    echo '<div class="row">';
    echo '<button id="btnPick" class="btn pick" type="button" onclick="pickFiles()">Escolher PDFs</button>';
    echo '<button id="btnStop" class="btn stop" type="button" onclick="stopCurrentRoute()">Parar processamento</button>';
    echo '<button id="btnSend" class="btn send" type="submit" aria-label="Enviar" title="Enviar">&#10003;</button>';
    echo '</div>';
    echo '<div class="hint" id="status">Nenhum arquivo selecionado.</div>';
    echo '<div class="hint" id="runtimeStatus">Status: verificando...</div>';
    if ($scpDebugEnabled) {
        echo '<div class="hint" style="color:#b45309">Debug ativo (debug=1).</div>';
    }
    echo '</form>';

    if ($errorMessage !== null && $errorMessage !== '') {
        echo '<div class="error">' . htmlspecialchars($errorMessage, ENT_QUOTES, 'UTF-8') . '</div>';
    }

    if (is_array($lastResult)) {
        $summary = (array)($lastResult['summary'] ?? []);
        $total = (int)($summary['total'] ?? 0);
        $ok = (int)($summary['success'] ?? 0);
        $err = (int)($summary['error'] ?? 0);
        echo '<div class="ok">Resumo: total=' . $total . ' | sucesso=' . $ok . ' | erro=' . $err . '</div>';

        $routeName = htmlspecialchars((string)($lastResult['route']['name'] ?? ''), ENT_QUOTES, 'UTF-8');
        $routeUrl = htmlspecialchars((string)($lastResult['route']['upload_url'] ?? ''), ENT_QUOTES, 'UTF-8');
        echo '<div class="result"><strong>Rota:</strong> ' . $routeName . '<br><span class="mono">' . $routeUrl . '</span>';

        $transform = (array)($lastResult['transform'] ?? []);
        if ($transform !== []) {
            $transformStatus = htmlspecialchars((string)($transform['status'] ?? '-'), ENT_QUOTES, 'UTF-8');
            $transformMessage = htmlspecialchars((string)($transform['message'] ?? ''), ENT_QUOTES, 'UTF-8');
            $csvInput = (string)($transform['csv_input'] ?? '');
            $xmlOutput = (string)($transform['output_file'] ?? '');
            $childOutput = trim((string)($transform['child_output'] ?? ''));
            $xmlExists = ($xmlOutput !== '' && is_file($xmlOutput));
            $xmlPublicUrl = ($xmlOutput !== '' && $xmlExists) ? scp_build_output_public_url($xmlOutput) : null;

            echo '<div style="margin-top:8px;padding:8px;border:1px solid #e5e7eb;border-radius:8px;background:#fff">';
            echo '<strong>Transformacao:</strong> ' . $transformStatus . '<br>';
            if ($transformMessage !== '') {
                echo 'Mensagem: ' . $transformMessage . '<br>';
            }
            if ($csvInput !== '') {
                echo '<span class="mono">CSV: ' . htmlspecialchars($csvInput, ENT_QUOTES, 'UTF-8') . '</span><br>';
            }
            if ($xmlOutput !== '') {
                echo '<span class="mono">XML: ' . htmlspecialchars($xmlOutput, ENT_QUOTES, 'UTF-8') . '</span>';
                if (is_string($xmlPublicUrl) && $xmlPublicUrl !== '') {
                    $xmlPublicUrlEsc = htmlspecialchars($xmlPublicUrl, ENT_QUOTES, 'UTF-8');
                    echo '<br><a href="' . $xmlPublicUrlEsc . '" target="_blank" rel="noopener">Abrir XML gerado</a>';
                } elseif (!$xmlExists) {
                    echo '<br><span style="color:#991b1b">XML ainda nao foi gerado.</span>';
                }
                echo '<br>';
            }
            if ($childOutput !== '' && ($transform['status'] ?? '') !== 'success') {
                $childShort = mb_substr($childOutput, 0, 1500);
                echo '<details style="margin-top:6px"><summary>Detalhes da transformacao</summary><pre class="mono" style="white-space:pre-wrap">' . htmlspecialchars($childShort, ENT_QUOTES, 'UTF-8') . '</pre></details>';
            }
            echo '</div>';
        }

        $responseSnapshot = (array)($lastResult['response_snapshot'] ?? []);
        if ($responseSnapshot !== []) {
            $snapOk = !empty($responseSnapshot['ok']);
            $latestFile = (string)($responseSnapshot['latest_file'] ?? '');
            $historyFile = (string)($responseSnapshot['history_file'] ?? '');
            $latestUrl = (string)($responseSnapshot['latest_url'] ?? '');
            $historyUrl = (string)($responseSnapshot['history_url'] ?? '');

            echo '<div style="margin-top:8px;padding:8px;border:1px solid #e5e7eb;border-radius:8px;background:#fff">';
            echo '<strong>JSON de resposta:</strong> ' . ($snapOk ? 'gravado' : 'falha ao gravar') . '<br>';
            if ($latestFile !== '') {
                echo '<span class="mono">Latest: ' . htmlspecialchars($latestFile, ENT_QUOTES, 'UTF-8') . '</span>';
                if ($latestUrl !== '') {
                    $latestUrlEsc = htmlspecialchars($latestUrl, ENT_QUOTES, 'UTF-8');
                    echo ' - <a href="' . $latestUrlEsc . '" target="_blank" rel="noopener">abrir</a>';
                }
                echo '<br>';
            }
            if ($historyFile !== '') {
                echo '<span class="mono">Historico: ' . htmlspecialchars($historyFile, ENT_QUOTES, 'UTF-8') . '</span>';
                if ($historyUrl !== '') {
                    $historyUrlEsc = htmlspecialchars($historyUrl, ENT_QUOTES, 'UTF-8');
                    echo ' - <a href="' . $historyUrlEsc . '" target="_blank" rel="noopener">abrir</a>';
                }
                echo '<br>';
            }
            echo '</div>';
        }

        echo '<div class="result-table-scroll">';
        echo '<table><thead><tr><th>Arquivo</th><th>Status</th><th>URL</th><th>Mensagem</th></tr></thead><tbody>';
        foreach ((array)($lastResult['results'] ?? []) as $item) {
            $original = htmlspecialchars((string)($item['original_name'] ?? '-'), ENT_QUOTES, 'UTF-8');
            $status = htmlspecialchars((string)($item['status'] ?? '-'), ENT_QUOTES, 'UTF-8');
            $url = (string)($item['uploaded_file_url'] ?? '');
            $urlEsc = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
            $rawMessage = (string)($item['message'] ?? '');
            $responseJson = (array)($item['response_json'] ?? []);
            $responseError = trim((string)($responseJson['error'] ?? ''));
            $httpCode = (int)($item['http_code'] ?? 0);
            if ($responseError !== '') {
                $rawMessage .= ' (error=' . $responseError . ')';
            }
            if ($httpCode > 0) {
                $rawMessage .= ' [http=' . $httpCode . ']';
            }
            $msg = htmlspecialchars($rawMessage, ENT_QUOTES, 'UTF-8');

            echo '<tr>';
            echo '<td>' . $original . '</td>';
            echo '<td>' . $status . '</td>';
            echo '<td class="mono">' . ($urlEsc !== '' ? '<a href="' . $urlEsc . '" target="_blank" rel="noopener">' . $urlEsc . '</a>' : '-') . '</td>';
            echo '<td>' . ($msg !== '' ? $msg : '-') . '</td>';
            echo '</tr>';
        }
        echo '</tbody></table></div></div>';
    }

    echo '</div>';
    echo '<script>';
    echo 'const fixedRoute=' . json_encode($resolvedRouteKey, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ';';
    echo 'const form=document.getElementById("scpForm");const picker=document.getElementById("picker");const status=document.getElementById("status");';
    echo 'const runtimeStatus=document.getElementById("runtimeStatus");';
    echo 'const btnPick=document.getElementById("btnPick");const btnSend=document.getElementById("btnSend");const btnStop=document.getElementById("btnStop");';
    echo 'function currentRoute(){return fixedRoute||"pilao";}';
    echo 'async function refreshRuntimeStatus(){try{const u=new URL(window.location.href);u.searchParams.set("action","status");u.searchParams.set("route",currentRoute());u.searchParams.set("format","json");const r=await fetch(u.toString(),{headers:{"Accept":"application/json"}});const d=await r.json();if(d&&d.running){runtimeStatus.textContent=`Status: Em Processamento (PID ${d.pid})`;btnStop.disabled=false;btnStop.style.opacity="1";}else{runtimeStatus.textContent="Status: parado";btnStop.disabled=true;btnStop.style.opacity="0.6";}}catch(e){runtimeStatus.textContent="Status: nao foi possivel consultar";}}';
    echo 'async function stopCurrentRoute(){if(!confirm("Deseja encerrar a execucao em andamento desta rota?")){return;}btnStop.disabled=true;btnStop.style.opacity="0.6";const body=new URLSearchParams();body.set("action","stop");body.set("route",currentRoute());body.set("format","json");try{const r=await fetch(window.location.href,{method:"POST",headers:{"Accept":"application/json","Content-Type":"application/x-www-form-urlencoded;charset=UTF-8"},body:body.toString()});const d=await r.json();if(d&&d.message){runtimeStatus.textContent=`Status: ${d.message}`;}else{runtimeStatus.textContent="Status: comando enviado";}}catch(e){runtimeStatus.textContent="Status: falha ao enviar comando de parada";}setTimeout(refreshRuntimeStatus,500);}';
    echo 'picker.addEventListener("change",()=>{const n=picker.files?picker.files.length:0;status.textContent=n>0?`${n} arquivo(s) selecionado(s).`:"Nenhum arquivo selecionado.";});';
    echo 'function pickFiles(){picker.removeAttribute("webkitdirectory");picker.removeAttribute("directory");picker.click();}';
    echo 'form.addEventListener("submit",()=>{status.textContent="Enviando arquivos para o droplet...";runtimeStatus.textContent="Status: Em Processamento";btnPick.disabled=true;btnSend.disabled=true;btnStop.disabled=true;btnPick.style.opacity="0.7";btnSend.style.opacity="0.7";btnStop.style.opacity="0.6";});';
    echo 'refreshRuntimeStatus();setInterval(refreshRuntimeStatus,5000);';
    echo '</script></body></html>';
    exit(0);
}

$routes = scp_routes();
$requestPayload = scp_get_request_payload();
$scpDebugEnabled = scp_collect_debug_flag($requestPayload);
$scpDebugLog = getenv('DEBUG_LOG') ?: sys_get_temp_dir() . '/executa_scp_debug.log';
$scpDebugTrace = [
    'enabled' => $scpDebugEnabled,
    'started_at' => date('c'),
    'request_method' => (string)($_SERVER['REQUEST_METHOD'] ?? 'GET'),
    'script' => (string)($_SERVER['SCRIPT_NAME'] ?? ''),
    'route_input' => (string)($requestPayload['route'] ?? ''),
    'steps' => [],
];
scp_debug_add('Request recebido', [
    'query_keys' => array_keys((array)$_GET),
    'post_keys' => array_keys((array)$_POST),
    'payload_keys' => array_keys($requestPayload),
]);

$action = strtolower(trim((string)($_GET['action'] ?? $_POST['action'] ?? '')));
$routeArgument = trim((string)($requestPayload['route'] ?? $_GET['route'] ?? $_POST['route'] ?? ''));
$routeForAction = scp_normalize_route_key($routeArgument !== '' ? $routeArgument : 'pilao');
if ($routeArgument !== '' && !isset($routes[$routeForAction])) {
    scp_json_exit([
        'status' => 'error',
        'message' => 'Rota invalida informada.',
        'route' => $routeForAction,
        'allowed_routes' => array_keys($routes),
    ], 400);
}
scp_debug_add('Action resolvida', ['action' => $action, 'route' => $routeForAction]);

if ($action === 'status') {
    scp_debug_add('Consultando status de rota', ['route' => $routeForAction]);
    scp_json_exit(scp_status_payload($routeForAction), 200);
}

if ($action === 'stop') {
    scp_debug_add('Solicitado stop de rota', ['route' => $routeForAction]);
    $stopPayload = scp_stop_route($routeForAction);
    $http = (($stopPayload['status'] ?? 'error') === 'error') ? 500 : 200;
    scp_json_exit($stopPayload, $http);
}

if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
    scp_debug_add('Renderizando formulario (GET)', ['route_selected' => $routeForAction]);
    scp_render_form($routes, null, null, $routeForAction);
}

$selectedRouteKey = scp_normalize_route_key((string)($requestPayload['route'] ?? $_POST['route'] ?? $_GET['route'] ?? 'pilao'));
if ($selectedRouteKey !== '' && !isset($routes[$selectedRouteKey])) {
    scp_debug_add('Rota POST invalida', ['route' => $selectedRouteKey]);
    $msg = 'Rota invalida selecionada.';
    if (scp_is_json_request()) {
        scp_json_exit([
            'route' => $selectedRouteKey,
            'input_file' => [],
            'output_file' => [],
            'xml_content' => null,
            'child_output' => $msg,
            'status' => 'error',
            'message' => $msg,
        ], 400);
    }
    scp_render_form($routes, null, $msg, $selectedRouteKey);
}

$files = scp_collect_uploaded_pdfs();
scp_debug_add('Arquivos recebidos', ['count' => count($files)]);
if (empty($files)) {
    $msg = 'Nenhum PDF enviado nesta chamada; aguardando selecao via interface.';
    if (scp_is_json_request()) {
        scp_json_exit([
            'route' => (string)($routes[$selectedRouteKey]['key'] ?? $selectedRouteKey),
            'input_file' => [],
            'output_file' => [],
            'xml_content' => null,
            'child_output' => $msg,
            'status' => 'idle',
            'message' => $msg,
        ], 200);
    }
    scp_render_form($routes, null, null, $selectedRouteKey);
}

$maxFiles = scp_max_files();
scp_debug_add('Limite de arquivos aplicado', ['maxFiles' => $maxFiles]);
if (count($files) > $maxFiles) {
    $msg = 'Quantidade de arquivos excede o limite de seguranca (' . $maxFiles . ').';
    if (scp_is_json_request()) {
        scp_json_exit([
            'route' => (string)($routes[$selectedRouteKey]['key'] ?? $selectedRouteKey),
            'input_file' => [],
            'output_file' => [],
            'xml_content' => null,
            'child_output' => $msg,
            'status' => 'error',
            'message' => $msg,
        ], 400);
    }
    scp_render_form($routes, null, $msg, $selectedRouteKey);
}

$routeValidation = scp_validate_pdf_routes($files, $selectedRouteKey);
if (!(bool)($routeValidation['ok'] ?? false)) {
    $msg = (string)($routeValidation['message'] ?? 'Origem do PDF nao corresponde a rota informada.');
    scp_log('PDF rejeitado por rota divergente. rota=' . $selectedRouteKey . ' detalhe=' . json_encode($routeValidation, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    if (scp_is_json_request()) {
        scp_json_exit([
            'route' => (string)($routes[$selectedRouteKey]['key'] ?? $selectedRouteKey),
            'input_file' => [],
            'output_file' => [],
            'xml_content' => null,
            'child_output' => $msg,
            'status' => 'error',
            'message' => $msg,
            'route_validation' => $routeValidation,
        ], 400);
    }
    scp_render_form($routes, null, $msg, $selectedRouteKey);
}

$lock = scp_acquire_lock($selectedRouteKey);
if ($lock === null) {
    scp_debug_add('Execucao bloqueada por lock', ['route' => $selectedRouteKey]);
    $msg = 'Ja existe uma execucao em andamento para esta rota. Aguarde finalizar antes de iniciar outra.';
    scp_log('Execucao bloqueada por concorrencia. rota=' . $selectedRouteKey);
    if (scp_is_json_request()) {
        scp_json_exit([
            'route' => (string)($routes[$selectedRouteKey]['key'] ?? $selectedRouteKey),
            'input_file' => [],
            'output_file' => [],
            'xml_content' => null,
            'child_output' => $msg,
            'status' => 'error',
            'message' => $msg,
        ], 409);
    }
    scp_render_form($routes, null, $msg, $selectedRouteKey);
}

register_shutdown_function(static function () use (&$lock): void {
    scp_release_lock($lock);
});

$timeoutSeconds = scp_timeout_seconds();
@set_time_limit($timeoutSeconds + 30);
scp_debug_add('Execucao iniciada', ['route' => $selectedRouteKey, 'timeoutSeconds' => $timeoutSeconds]);
scp_log('Iniciando upload. rota=' . $selectedRouteKey . ' arquivos=' . count($files) . ' timeout=' . $timeoutSeconds);

$routeStem = scp_route_file_stem($selectedRouteKey);
$routeCsvOutput = rtrim(scp_root_dir(), '/\\') . '/output/' . $routeStem . '.csv';
$routeCcustoDefaults = [
    'saned' => 'SAN',
    'pilao' => 'PILAO',
    'construdaher' => 'CDH',
    'progredior' => 'PRG',
    'consitec' => 'THE',
];
$routeCcustoId = (string)($requestPayload['ccustoID'] ?? ($routeCcustoDefaults[$selectedRouteKey] ?? 'SAN'));

$result = scp_worker_execute_route($files, $routes[$selectedRouteKey], $timeoutSeconds, $routeCsvOutput, $routeCcustoId);
scp_debug_add('Worker finalizado', [
    'status' => (string)($result['status'] ?? 'unknown'),
    'summary' => (array)($result['summary'] ?? []),
]);
$transformResult = scp_transform_csv_to_filemaker($requestPayload, $routes[$selectedRouteKey], $result, $files);
scp_debug_add('Transform finalizada', [
    'status' => (string)($transformResult['status'] ?? 'unknown'),
    'output_file' => (string)($transformResult['output_file'] ?? ''),
    'csv_input' => (string)($transformResult['csv_input'] ?? ''),
]);
$differenceReport = scp_build_difference_report($routes[$selectedRouteKey], $transformResult);
if (isset($differenceReport['details']) && is_array($differenceReport['details'])) {
    unset($differenceReport['details']['missing_in_scp_by_date'], $differenceReport['details']['missing_in_output_by_date']);
}
unset($differenceReport['missing_in_scp_by_date'], $differenceReport['missing_in_output_by_date']);
scp_debug_add('Comparacao input/output finalizada', [
    'status' => (string)($differenceReport['status'] ?? 'unknown'),
    'route' => (string)($differenceReport['route'] ?? ''),
]);
$filemakerPayload = scp_build_filemaker_payload($routes[$selectedRouteKey], $files, $result, $transformResult, $differenceReport);
$filemakerPayload = scp_sanitize_difference_report_payload($filemakerPayload);
$filemakerPayload['xml_content_atualizar'] = scp_build_xml_content_atualizar_from_payload($filemakerPayload);
$responseSnapshot = scp_write_response_snapshot((string)($routes[$selectedRouteKey]['key'] ?? $selectedRouteKey), $filemakerPayload);
$filemakerPayload['response_snapshot'] = $responseSnapshot;
$filemakerPayload['response_json_url'] = (string)($responseSnapshot['latest_url'] ?? $responseSnapshot['history_url'] ?? '');
// Re-write snapshot so persisted JSON also includes response_snapshot and response_json_url.
$filemakerPayload = scp_sanitize_difference_report_payload($filemakerPayload);
$filemakerPayload['xml_content_atualizar'] = scp_build_xml_content_atualizar_from_payload($filemakerPayload);
$xmlAtualizarFile = scp_write_xml_content_atualizar_file((string)($routes[$selectedRouteKey]['key'] ?? $selectedRouteKey), $filemakerPayload['xml_content_atualizar'] ?? null);
$filemakerPayload['xml_content_atualizar_file'] = (string)($xmlAtualizarFile['latest_path'] ?? '');
$filemakerPayload['xml_content_atualizar_url'] = (string)($xmlAtualizarFile['latest_url'] ?? '');
$filemakerPayload['xml_content_atualizar_snapshot'] = $xmlAtualizarFile;
$responseSnapshot = scp_write_response_snapshot((string)($routes[$selectedRouteKey]['key'] ?? $selectedRouteKey), $filemakerPayload);
$filemakerPayload['response_snapshot'] = $responseSnapshot;
$filemakerPayload['response_json_url'] = (string)($responseSnapshot['latest_url'] ?? $responseSnapshot['history_url'] ?? '');
scp_log('Fim upload. rota=' . $selectedRouteKey . ' status=' . (string)($result['status'] ?? 'unknown'));
scp_log('Fim transformacao. rota=' . $selectedRouteKey . ' status=' . (string)($transformResult['status'] ?? 'unknown') . ' csv=' . (string)($transformResult['csv_input'] ?? ''));
scp_release_lock($lock);
scp_debug_add('Resposta preparada', ['status' => (string)($filemakerPayload['status'] ?? 'unknown')]);

// Enriquecimento para renderizacao HTML: mostra artefatos de transformacao.
$renderResult = $result;
$renderResult['transform'] = $transformResult;
$renderResult['response_snapshot'] = $responseSnapshot;

if (scp_is_json_request()) {
    $filemakerPayload = scp_sanitize_difference_report_payload($filemakerPayload);
    $filemakerPayload['xml_content_atualizar'] = scp_build_xml_content_atualizar_from_payload($filemakerPayload);
    $xmlAtualizarFile = scp_write_xml_content_atualizar_file((string)($routes[$selectedRouteKey]['key'] ?? $selectedRouteKey), $filemakerPayload['xml_content_atualizar'] ?? null);
    $filemakerPayload['xml_content_atualizar_file'] = (string)($xmlAtualizarFile['latest_path'] ?? '');
    $filemakerPayload['xml_content_atualizar_url'] = (string)($xmlAtualizarFile['latest_url'] ?? '');
    $filemakerPayload['xml_content_atualizar_snapshot'] = $xmlAtualizarFile;
    $http = (($filemakerPayload['status'] ?? 'error') === 'error') ? 500 : 200;
    scp_json_exit($filemakerPayload, $http);
}

scp_render_form($routes, $renderResult, null, $selectedRouteKey);
