<?php
/**
 * 🛠️ THE TRIANGLE SYNC BOT (V17.0 - EDITOR EDITION)
 * --------------------------------------------------
 * Platforms: Telegram | Bale | Eitaa (Eitaayar)
 * New Feature: LIVE EDIT (Add/Update channels in existing connections)
 * * HOW TO USE EDIT:
 * 1. Click '📋 List' to see Group IDs.
 * 2. Click '✏️ Edit'.
 * 3. Enter Group ID.
 * 4. Choose Platform (Eitaa, Telegram, Bale).
 * 5. Send new Channel ID.
 */

// Load all runtime configuration from config.php
$config = require __DIR__ . '/config.php';

ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', $config['paths']['error_log']);
ini_set('max_execution_time', 300);
ini_set('memory_limit', '512M');

// ====================================================
// 1. CONFIGURATION
// ====================================================

define('TG_TOKEN', $config['tokens']['telegram']);
define('BALE_TOKEN', $config['tokens']['bale']);
define('EITAA_TOKEN', $config['tokens']['eitaa']);
define('RUBIKA_TOKEN', $config['tokens']['rubika']);
define('ADMIN_IDS', $config['admins']);

define('TG_PROXY', $config['network']['telegram_proxy']);
define('DB_FILE', $config['paths']['db']);
define('DEBUG_LOG_FILE', $config['paths']['debug_log']);
define('ERROR_LOG_FILE', $config['paths']['error_log']);
define('DELETE_TRIGGER', $config['bot']['delete_trigger']);
define('SUPPORTED_SOURCES', $config['bot']['supported_sources']);
define('NET_CONNECT_TIMEOUT', (int)($config['network']['connect_timeout'] ?? 10));
define('NET_TIMEOUT', (int)($config['network']['timeout'] ?? 60));
define('NET_RETRY_COUNT', (int)($config['network']['retry_count'] ?? 2));
define('NET_RETRY_DELAY_MS', (int)($config['network']['retry_delay_ms'] ?? 600));

// PHP 7 compatibility shims
if(!function_exists('str_starts_with')) {
    function str_starts_with($haystack, $needle) {
        $h = (string)$haystack;
        $n = (string)$needle;
        if($n === '') return true;
        return substr($h, 0, strlen($n)) === $n;
    }
}
if(!function_exists('str_contains')) {
    function str_contains($haystack, $needle) {
        $h = (string)$haystack;
        $n = (string)$needle;
        if($n === '') return true;
        return strpos($h, $n) !== false;
    }
}

// ====================================================
// 2. DATABASE LAYER
// ====================================================

class DB {
    private $pdo;
    public function __construct() {
        if (!file_exists(DB_FILE)) { touch(DB_FILE); chmod(DB_FILE, 0777); }
        $this->pdo = new PDO("sqlite:" . DB_FILE);
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->init();
    }

    private function init() {
        $this->pdo->exec("CREATE TABLE IF NOT EXISTS channels (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            tg_id TEXT,
            bale_id TEXT,
            eitaa_id TEXT,
            rubika_id TEXT,
            route_tg INTEGER DEFAULT 1,
            route_bale INTEGER DEFAULT 1,
            route_eitaa INTEGER DEFAULT 1,
            route_rubika INTEGER DEFAULT 1,
            sync_text INTEGER DEFAULT 1,
            sync_media INTEGER DEFAULT 1,
            sync_edit INTEGER DEFAULT 1,
            sync_delete INTEGER DEFAULT 1,
            strip_hashtags INTEGER DEFAULT 1,
            text_prefix TEXT,
            text_suffix TEXT
        )");
        $this->pdo->exec("CREATE TABLE IF NOT EXISTS message_map (
            id INTEGER PRIMARY KEY AUTOINCREMENT, origin_key TEXT, 
            tg_msg_id TEXT, bale_msg_id TEXT, eitaa_msg_id TEXT, rubika_msg_id TEXT, timestamp INTEGER
        )");
        $this->pdo->exec("CREATE TABLE IF NOT EXISTS user_states (user_id TEXT PRIMARY KEY, step TEXT, data TEXT)");
        $this->pdo->exec("CREATE TABLE IF NOT EXISTS admin_audit (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            platform TEXT,
            admin_id TEXT,
            action TEXT,
            payload TEXT,
            created_at INTEGER
        )");
        $this->pdo->exec("CREATE TABLE IF NOT EXISTS runtime_flags (
            k TEXT PRIMARY KEY,
            v TEXT,
            ts INTEGER
        )");
        $this->migrateChannels();
        $this->migrateMessageMap();

        // Ensure fast/consistent lookup and idempotency enforcement for origin_key.
        $this->pdo->exec("CREATE INDEX IF NOT EXISTS idx_message_map_origin_key ON message_map(origin_key)");
        try {
            $this->pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_message_map_origin_unique ON message_map(origin_key)");
        } catch (Exception $e) {
            // Legacy DB can contain duplicates; keep latest and then enforce uniqueness.
            $this->pdo->exec("DELETE FROM message_map WHERE id NOT IN (SELECT MAX(id) FROM message_map GROUP BY origin_key)");
            $this->pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_message_map_origin_unique ON message_map(origin_key)");
        }
    }

    private function migrateChannels() {
        $existing = [];
        foreach ($this->pdo->query("PRAGMA table_info(channels)") as $col) $existing[$col['name']] = true;
        $required = [
            'rubika_id' => "ALTER TABLE channels ADD COLUMN rubika_id TEXT",
            'route_tg' => "ALTER TABLE channels ADD COLUMN route_tg INTEGER DEFAULT 1",
            'route_bale' => "ALTER TABLE channels ADD COLUMN route_bale INTEGER DEFAULT 1",
            'route_eitaa' => "ALTER TABLE channels ADD COLUMN route_eitaa INTEGER DEFAULT 1",
            'route_rubika' => "ALTER TABLE channels ADD COLUMN route_rubika INTEGER DEFAULT 1",
            'sync_text' => "ALTER TABLE channels ADD COLUMN sync_text INTEGER DEFAULT 1",
            'sync_media' => "ALTER TABLE channels ADD COLUMN sync_media INTEGER DEFAULT 1",
            'sync_edit' => "ALTER TABLE channels ADD COLUMN sync_edit INTEGER DEFAULT 1",
            'sync_delete' => "ALTER TABLE channels ADD COLUMN sync_delete INTEGER DEFAULT 1",
            'strip_hashtags' => "ALTER TABLE channels ADD COLUMN strip_hashtags INTEGER DEFAULT 1",
            'text_prefix' => "ALTER TABLE channels ADD COLUMN text_prefix TEXT",
            'text_suffix' => "ALTER TABLE channels ADD COLUMN text_suffix TEXT"
        ];
        foreach ($required as $name => $sql) if (!isset($existing[$name])) $this->pdo->exec($sql);
    }

    private function migrateMessageMap() {
        $existing = [];
        foreach ($this->pdo->query("PRAGMA table_info(message_map)") as $col) $existing[$col['name']] = true;
        if (!isset($existing['rubika_msg_id'])) {
            $this->pdo->exec("ALTER TABLE message_map ADD COLUMN rubika_msg_id TEXT");
        }
    }

    // CRUD Operations
    public function addGroup($d) { 
        $this->pdo->prepare("INSERT INTO channels (
            tg_id, bale_id, eitaa_id, rubika_id, route_tg, route_bale, route_eitaa, route_rubika, sync_text, sync_media, sync_edit, sync_delete, strip_hashtags, text_prefix, text_suffix
        ) VALUES (?,?,?,?,1,1,1,1,1,1,1,1,1,NULL,NULL)")
             ->execute([$d['tg'], $d['bale'], $d['eitaa'], $d['rubika'] ?? null]); 
    }
    
    public function getGroups() { return $this->pdo->query("SELECT * FROM channels")->fetchAll(PDO::FETCH_ASSOC); }
    public function getGroup($id) { // دریافت یک گروه خاص برای ویرایش
        $stmt = $this->pdo->prepare("SELECT * FROM channels WHERE id = ?");
        $stmt->execute([$id]); return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    public function updateGroup($id, $col, $val) { // متد جدید آپدیت
        // جلوگیری از تزریق SQL در نام ستون
        $allowed = ['tg_id', 'bale_id', 'eitaa_id', 'rubika_id', 'route_tg', 'route_bale', 'route_eitaa', 'route_rubika', 'sync_text', 'sync_media', 'sync_edit', 'sync_delete', 'strip_hashtags', 'text_prefix', 'text_suffix'];
        if(!in_array($col, $allowed)) return false;
        
        $stmt = $this->pdo->prepare("UPDATE channels SET $col = ? WHERE id = ?");
        return $stmt->execute([$val, $id]);
    }

    public function deleteGroup($id) { $this->pdo->prepare("DELETE FROM channels WHERE id = ?")->execute([$id]); }
    
    public function findGroup($chatId, $platform) {
        $col = ['telegram'=>'tg_id', 'bale'=>'bale_id', 'rubika'=>'rubika_id'][$platform] ?? null;
        if (!$col) return null;
        $stmt = $this->pdo->prepare("SELECT * FROM channels WHERE $col = ?");
        $stmt->execute([$chatId]); return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    public function saveMap($key, $ids) {
        $this->pdo->prepare("INSERT OR IGNORE INTO message_map (origin_key, tg_msg_id, bale_msg_id, eitaa_msg_id, rubika_msg_id, timestamp) VALUES (?,?,?,?,?,?)")
             ->execute([$key, $ids['telegram'], $ids['bale'], $ids['eitaa'], $ids['rubika'], time()]);
    }

    public function getMap($plat, $chatId, $msgId) {
        $key = "{$plat}_{$chatId}_{$msgId}";
        $stmt = $this->pdo->prepare("SELECT * FROM message_map WHERE origin_key = ?");
        $stmt->execute([$key]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if($row) return $row;

        $col = [
            'telegram' => 'tg_msg_id',
            'bale' => 'bale_msg_id',
            'eitaa' => 'eitaa_msg_id',
            'rubika' => 'rubika_msg_id'
        ][$plat] ?? null;
        if(!$col) return null;

        $stmt = $this->pdo->prepare("SELECT * FROM message_map WHERE {$col} = ? ORDER BY id DESC LIMIT 1");
        $stmt->execute([$msgId]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    public function getMapByOrigin($originKey) {
        $stmt = $this->pdo->prepare("SELECT * FROM message_map WHERE origin_key = ? ORDER BY id DESC LIMIT 1");
        $stmt->execute([$originKey]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    public function getMapByDest($platform, $msgId) {
        $col = ['telegram'=>'tg_msg_id', 'bale'=>'bale_msg_id', 'eitaa'=>'eitaa_msg_id', 'rubika'=>'rubika_msg_id'][$platform] ?? null;
        if(!$col) return null;
        $stmt = $this->pdo->prepare("SELECT * FROM message_map WHERE {$col} = ? ORDER BY id DESC LIMIT 1");
        $stmt->execute([$msgId]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    // State Manager
    public function setState($u, $s, $d=[]) { $this->pdo->prepare("INSERT OR REPLACE INTO user_states (user_id, step, data) VALUES (?, ?, ?)")->execute([$u, $s, json_encode($d)]); }
    public function getState($u) { $stmt = $this->pdo->prepare("SELECT * FROM user_states WHERE user_id = ?"); $stmt->execute([$u]); $r=$stmt->fetch(PDO::FETCH_ASSOC); return $r ? ['step'=>$r['step'], 'data'=>json_decode($r['data'],true)] : null; }
    public function clearState($u) { $this->pdo->prepare("DELETE FROM user_states WHERE user_id = ?")->execute([$u]); }

    public function audit($platform, $adminId, $action, $payload=[]) {
        $this->pdo->prepare("INSERT INTO admin_audit (platform, admin_id, action, payload, created_at) VALUES (?, ?, ?, ?, ?)")
            ->execute([$platform, (string)$adminId, $action, json_encode($payload, JSON_UNESCAPED_UNICODE), time()]);
    }

    public function getRecentAudit($limit=10) {
        $st = $this->pdo->prepare("SELECT * FROM admin_audit ORDER BY id DESC LIMIT ?");
        $st->bindValue(1, (int)$limit, PDO::PARAM_INT);
        $st->execute();
        return $st->fetchAll(PDO::FETCH_ASSOC);
    }

    public function countGroups() { return (int)$this->pdo->query("SELECT COUNT(*) FROM channels")->fetchColumn(); }
    public function countMap24h() { 
        $st = $this->pdo->prepare("SELECT COUNT(*) FROM message_map WHERE timestamp >= ?");
        $st->execute([time()-86400]);
        return (int)$st->fetchColumn();
    }

    public function getFlag($k) {
        $st = $this->pdo->prepare("SELECT v, ts FROM runtime_flags WHERE k = ? LIMIT 1");
        $st->execute([$k]);
        return $st->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    public function setFlag($k, $v, $ts=null) {
        $t = $ts ?? time();
        $st = $this->pdo->prepare("INSERT OR REPLACE INTO runtime_flags (k, v, ts) VALUES (?, ?, ?)");
        return $st->execute([$k, (string)$v, (int)$t]);
    }

    public function updateMapMsgId($mapId, $platform, $msgId) {
        $col = ['telegram'=>'tg_msg_id','bale'=>'bale_msg_id','eitaa'=>'eitaa_msg_id','rubika'=>'rubika_msg_id'][$platform] ?? null;
        if(!$col) return false;
        $st = $this->pdo->prepare("UPDATE message_map SET {$col} = ? WHERE id = ?");
        return $st->execute([$msgId, $mapId]);
    }
}

// ====================================================
// 3. NETWORK DRIVER
// ====================================================

class Network {
    private function log($text) {
        file_put_contents(DEBUG_LOG_FILE, "[".date("Y-m-d H:i:s")."] ".$text."\n", FILE_APPEND);
    }

    private function jlog($event, $ctx=[]) {
        $line = [
            'event' => $event,
            'ts' => date('c'),
            'ctx' => $ctx
        ];
        $this->log(json_encode($line, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    private function maskData($data) {
        if(!is_array($data)) return $data;
        $out = [];
        foreach($data as $k => $v) {
            if($v instanceof CURLFile) {
                $out[$k] = [
                    'type' => 'CURLFile',
                    'name' => $v->getPostFilename(),
                    'mime' => $v->getMimeType()
                ];
                continue;
            }
            if(is_array($v)) {
                $out[$k] = $this->maskData($v);
                continue;
            }
            if(is_string($v) && strlen($v) > 180) {
                $out[$k] = substr($v, 0, 180).'...';
                continue;
            }
            $out[$k] = $v;
        }
        return $out;
    }

    private function getBase($p) {
        switch($p) {
            case 'telegram': return "https://api.telegram.org/bot".TG_TOKEN;
            case 'bale':     return "https://tapi.bale.ai/bot".BALE_TOKEN;
            case 'eitaa':    return "https://eitaayar.ir/api/".EITAA_TOKEN;
            case 'rubika':   return "https://botapi.rubika.ir/v3/".RUBIKA_TOKEN;
        }
        return "";
    }

    public function req($method, $data, $plat, $isFile=false, $retry=null) {
        $url = $this->getBase($plat) . "/$method";
        $reqId = uniqid($plat.'_'.$method.'_', true);
        if($retry === null) $retry = NET_RETRY_COUNT;
        $connectTimeout = NET_CONNECT_TIMEOUT;
        $timeout = NET_TIMEOUT;
        // Do not let a slow Eitaa endpoint block the full sync worker.
        if($plat === 'eitaa') {
            $timeout = min($timeout, 15);
            $retry = min((int)$retry, 0);
        }
        for($i=0; $i<=$retry; $i++) {
            $this->jlog('api_call', [
                'id' => $reqId,
                'platform' => $plat,
                'method' => $method,
                'try' => $i+1,
                'is_file' => $isFile,
                'payload' => $this->maskData($data)
            ]);
            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connectTimeout);
            curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
            
            if($isFile) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
            } else {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
                curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
            }
            
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            if($plat=='telegram' && TG_PROXY) curl_setopt($ch, CURLOPT_PROXY, TG_PROXY);
            
            $res = curl_exec($ch);
            $err = curl_error($ch);
            curl_close($ch);

            if($res !== false) {
                $json = json_decode($res, true);
                if(is_array($json)) {
                    if(($json['ok'] ?? false) || (($json['status'] ?? '') === 'OK')) {
                        $this->jlog('api_ok', [
                            'id' => $reqId,
                            'platform' => $plat,
                            'method' => $method,
                            'try' => $i+1,
                            'response' => $this->maskData($json)
                        ]);
                        return $json;
                    }
                    $this->jlog('api_fail', [
                        'id' => $reqId,
                        'platform' => $plat,
                        'method' => $method,
                        'try' => $i+1,
                        'response_raw' => $res,
                        'response' => $this->maskData($json),
                        'payload' => $this->maskData($data)
                    ]);
                } else {
                    $this->jlog('api_bad_json', [
                        'id' => $reqId,
                        'platform' => $plat,
                        'method' => $method,
                        'try' => $i+1,
                        'response_raw' => $res
                    ]);
                }
            } else {
                $this->jlog('api_curl_fail', [
                    'id' => $reqId,
                    'platform' => $plat,
                    'method' => $method,
                    'try' => $i+1,
                    'error' => $err
                ]);
            }

            if($i < $retry) usleep(NET_RETRY_DELAY_MS * 1000);
        }
        return ['ok'=>false, 'description'=>'request_failed'];
    }

    public function download($fileId, $plat, &$meta=null) {
        $res = $this->req('getFile', ['file_id'=>$fileId], $plat);
        $url = "";
        $path = "";
        if ($plat == 'rubika') {
            $url = $res['data']['download_url'] ?? '';
        } else {
            if(!isset($res['result']['file_path'])) return null;
            $path = $res['result']['file_path'];
            if ($plat == 'telegram') $url = "https://api.telegram.org/file/bot".TG_TOKEN."/$path";
            elseif ($plat == 'bale') $url = "https://tapi.bale.ai/file/bot".BALE_TOKEN."/$path";
        }
        
        if(!$url) {
            $this->jlog('file_download_prepare_fail', ['platform'=>$plat, 'file_id'=>$fileId, 'meta'=>$this->maskData($res)]);
            return null;
        }
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        if($plat=='telegram' && TG_PROXY) curl_setopt($ch, CURLOPT_PROXY, TG_PROXY);
        $bin = curl_exec($ch);
        $err = curl_error($ch);
        curl_close($ch);
        if($bin === false) {
            $this->jlog('file_download_fail', ['platform'=>$plat, 'file_id'=>$fileId, 'url'=>$url, 'error'=>$err]);
            return null;
        }
        if(is_array($meta)) {
            $meta['file_path'] = $path;
            $meta['download_url'] = $url;
            $meta['bytes'] = strlen((string)$bin);
        }
        $this->jlog('file_download_ok', ['platform'=>$plat, 'file_id'=>$fileId, 'bytes'=>strlen((string)$bin), 'file_path'=>$path]);
        return $bin;
    }

    public function uploadRubika($filePath, $type, &$info=null) {
        $ext = strtolower((string)pathinfo($filePath, PATHINFO_EXTENSION));
        $mime = (string)(mime_content_type($filePath) ?: 'application/octet-stream');
        $candidates = ['File'];
        if($type=='photo') $candidates = ['Image','File'];
        elseif($type=='video' || $type=='animation') $candidates = ['Video','File'];
        elseif($type=='voice' || $type=='audio') {
            // Rubika Voice expects mp3; Telegram voice is usually ogg/opus, so keep File fallback.
            $candidates = in_array($ext, ['mp3'], true) ? ['Voice','File'] : ['File','Voice'];
        }

        foreach($candidates as $rType) {
            $this->jlog('rubika_upload_try', ['type'=>$type, 'rubika_type'=>$rType, 'ext'=>$ext, 'mime'=>$mime]);
            $req = $this->req('requestSendFile', ['type'=>$rType], 'rubika');
            $uploadUrl = $req['data']['upload_url'] ?? null;
            if(!$uploadUrl) {
                $this->jlog('rubika_upload_skip_no_url', ['rubika_type'=>$rType, 'req'=>$this->maskData($req)]);
                continue;
            }

            $cfile = new CURLFile($filePath, $mime, basename($filePath));
            $ch = curl_init($uploadUrl);
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, ['file' => $cfile]);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
            curl_setopt($ch, CURLOPT_TIMEOUT, 120);
            $res = curl_exec($ch);
            $err = curl_error($ch);
            curl_close($ch);
            if($res === false) {
                $this->jlog('rubika_upload_http_fail', ['rubika_type'=>$rType, 'error'=>$err]);
                continue;
            }
            $up = json_decode($res, true);
            $fid = $up['data']['file_id'] ?? null;
            if($fid) {
                if(is_array($info)) {
                    $info['rubika_type'] = $rType;
                    $info['ext'] = $ext;
                    $info['mime'] = $mime;
                }
                $this->jlog('rubika_upload_ok', ['rubika_type'=>$rType, 'file_id'=>$fid]);
                return $fid;
            }
            $this->jlog('rubika_upload_fail', ['rubika_type'=>$rType, 'response_raw'=>$res, 'response'=>$this->maskData($up)]);
        }
        return null;
    }
}

// ====================================================
// 4. LOGIC CORE (Brain)
// ====================================================

class Robot {
    private $db, $net;
    public function __construct() { $this->db = new DB(); $this->net = new Network(); }

    private function logx($event, $ctx=[]) {
        $line = [
            'event' => $event,
            'ts' => date('c'),
            'ctx' => $ctx
        ];
        file_put_contents(DEBUG_LOG_FILE, "[".date("Y-m-d H:i:s")."] ".json_encode($line, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)."\n", FILE_APPEND);
    }

    private function shortRes($res) {
        if(!is_array($res)) return $res;
        return [
            'ok' => $res['ok'] ?? null,
            'status' => $res['status'] ?? null,
            'error_code' => $res['error_code'] ?? null,
            'description' => $res['description'] ?? null,
            'message_id' => $res['result']['message_id'] ?? $res['message_id'] ?? $res['data']['message_id'] ?? null
        ];
    }

    private function sendAdminAlert($text, $key='generic', $minIntervalSec=120) {
        $now = time();
        $flagKey = "alert:$key";
        $last = $this->db->getFlag($flagKey);
        if($last && ($now - (int)$last['ts']) < $minIntervalSec) return;
        $this->db->setFlag($flagKey, '1', $now);

        $msg = "⚠️ هشدار همگام‌سازی\n".$text."\n".date('Y-m-d H:i:s');
        foreach((ADMIN_IDS['telegram'] ?? []) as $id) {
            $this->net->req('sendMessage', ['chat_id'=>$id, 'text'=>$msg], 'telegram');
        }
        foreach((ADMIN_IDS['bale'] ?? []) as $id) {
            $this->net->req('sendMessage', ['chat_id'=>$id, 'text'=>$msg], 'bale');
        }
        foreach((ADMIN_IDS['rubika'] ?? []) as $id) {
            $this->net->req('sendMessage', ['chat_id'=>$id, 'text'=>$msg], 'rubika');
        }
    }

    private function isAdmin($uid, $plat) {
        $admins = ADMIN_IDS[$plat] ?? [];
        if($plat === 'rubika') {
            return in_array((string)$uid, array_map('strval', $admins), true);
        }
        return in_array((int)$uid, array_map('intval', $admins), true);
    }

    public function start() {
        $input = file_get_contents("php://input");
        $update = json_decode($input, true);
        $src = $_GET['source'] ?? ''; 

        if(!$update) { echo "Triangle Sync Bot V18.0 (Phase 1+2) Online."; return; }
        if($src === 'rubika') $update = $this->normalizeRubikaUpdate($update);
        if(in_array($src, SUPPORTED_SOURCES, true)) $this->handle($update, $src);
    }

    private function normalizeRubikaUpdate($up) {
        $u = $up['update'] ?? null;
        if(!$u || !isset($u['type'])) return $up;
        $type = $u['type'];
        $chatId = $u['chat_id'] ?? null;
        if(!$chatId) return $up;
        $chatType = $this->guessRubikaChatType($chatId);

        if($type === 'NewMessage' && isset($u['new_message'])) {
            $m = $u['new_message'];
            $msg = [
                'message_id' => $m['message_id'],
                'chat' => ['id' => $chatId, 'type' => $chatType],
                'from' => ['id' => $m['sender_id'] ?? $chatId],
                'text' => $m['text'] ?? null,
                'date' => $m['time'] ?? time()
            ];
            if(isset($m['reply_to_message_id'])) $msg['reply_to_message'] = ['message_id' => $m['reply_to_message_id']];
            if(isset($m['file'])) $msg['document'] = ['file_id' => $m['file']['file_id']];
            if(isset($m['forwarded_from'])) {
                $f = $m['forwarded_from'];
                $msg['forward_from_chat'] = [
                    'id' => $f['from_chat_id'] ?? $f['object_guid'] ?? $f['chat_id'] ?? null,
                    'title' => $f['title'] ?? 'forwarded'
                ];
                // keep original payload too; some accounts use different keys
                $msg['forwarded_from'] = $f;
            }
            return ['channel_post' => $msg];
        }

        if($type === 'UpdatedMessage' && isset($u['updated_message'])) {
            $m = $u['updated_message'];
            $msg = [
                'message_id' => $m['message_id'],
                'chat' => ['id' => $chatId, 'type' => $chatType],
                'from' => ['id' => $m['sender_id'] ?? $chatId],
                'text' => $m['text'] ?? null,
                'date' => $m['time'] ?? time()
            ];
            if(isset($m['reply_to_message_id'])) $msg['reply_to_message'] = ['message_id' => $m['reply_to_message_id']];
            return ['edited_channel_post' => $msg];
        }

        if($type === 'RemovedMessage' && isset($u['removed_message_id'])) {
            return [
                'removed_channel_post' => [
                    'message_id' => $u['removed_message_id'],
                    'chat' => ['id' => $chatId, 'type' => $chatType],
                ]
            ];
        }

        if(($type === 'StartedBot' || $type === 'StoppedBot') && $chatType === 'private') {
            return ['message' => [
                'message_id' => time(),
                'chat' => ['id' => $chatId, 'type' => 'private'],
                'from' => ['id' => $u['sender_id'] ?? $chatId],
                'text' => '/start',
                'date' => time()
            ]];
        }

        if(isset($up['inline_message'])) {
            $im = $up['inline_message'];
            return ['message' => [
                'message_id' => $im['message_id'] ?? time(),
                'chat' => ['id' => $im['chat_id'] ?? $chatId, 'type' => 'private'],
                'from' => ['id' => $im['sender_id'] ?? ($im['chat_id'] ?? $chatId)],
                'text' => $im['text'] ?? '',
                'date' => time()
            ]];
        }

        return $up;
    }

    private function guessRubikaChatType($chatId) {
        $id = (string)$chatId;
        if(str_starts_with($id, 'b') || str_starts_with($id, 'u')) return 'private';
        return 'channel';
    }

    private function handle($up, $plat) {
        $msg = $up['message'] ?? $up['channel_post'] ?? $up['edited_channel_post'] ?? null;
        $removed = $up['removed_channel_post'] ?? null;
        if(!$msg && !$removed) return;

        $cid = $msg['chat']['id'] ?? $removed['chat']['id'];
        $uid = $msg['from']['id'] ?? $cid;
        $txt = $msg['text'] ?? $msg['caption'] ?? '';
        $isEdit = isset($up['edited_channel_post']);
        $chatType = $msg['chat']['type'] ?? $removed['chat']['type'] ?? '';

        if($chatType == 'private') {
            if($this->isAdmin($uid, $plat)) {
                $this->wizard($uid, $cid, $txt, $plat, $msg);
                return;
            }
            if($plat === 'rubika') {
                $this->sendTxt($cid, "ربات فعال است.\nآیدی شما: {$uid}\nبرای دسترسی مدیریت، این آیدی را در config.php -> admins -> rubika قرار دهید.", 'rubika');
                return;
            }
        }

        if($chatType == 'channel') {
            // One-way mode: only Telegram channel events are allowed to sync outward.
            if($plat !== 'telegram') {
                $this->logx('skip_non_telegram_source', [
                    'platform' => $plat,
                    'chat_id' => $cid,
                    'message_id' => $msg['message_id'] ?? ($removed['message_id'] ?? null),
                    'is_edit' => $isEdit,
                    'is_delete' => (bool)$removed
                ]);
                return;
            }

            // Ignore reflected posts generated by the sync itself (recent window).
            if(!$removed && !$isEdit && isset($msg['message_id'])) {
                $mirrorMap = $this->db->getMapByDest($plat, $msg['message_id']);
                if($mirrorMap && ((int)($mirrorMap['timestamp'] ?? 0) > (time() - 3600))) {
                    $originKey = "{$plat}_{$cid}_{$msg['message_id']}";
                    if(($mirrorMap['origin_key'] ?? '') !== $originKey) {
                        $this->logx('skip_mirror_message', ['platform'=>$plat, 'chat_id'=>$cid, 'message_id'=>$msg['message_id'], 'map_origin'=>$mirrorMap['origin_key'] ?? null]);
                        return;
                    }
                }
            }

            $grp = $this->db->findGroup($cid, $plat);
            if(!$grp) return;

            if($removed) {
                if((int)$grp['sync_delete'] === 1) $this->syncDelete($removed['chat']['id'], $removed['message_id'], $plat);
                return;
            }

            if($isEdit) {
                if((int)$grp['sync_edit'] === 1) $this->syncEdit($cid, $msg['message_id'], $msg, $plat);
                return;
            }

            if(isset($msg['text']) && (int)$grp['sync_text'] !== 1) return;
            if(!isset($msg['text']) && (int)$grp['sync_media'] !== 1) return;
            $this->distribute($grp, $msg, $plat);
        }
    }

    private function cleanForSync($text, $grp) {
        $out = str_ireplace('#nosync', '', (string)$text);
        if((int)$grp['strip_hashtags'] === 1) {
            $out = str_ireplace(['#tgonly','#baleonly','#eitaaonly','#rubikaonly','#tgon','#baleon','#eitaaon','#rubikaon'], '', $out);
        }
        $prefix = trim((string)($grp['text_prefix'] ?? ''));
        $suffix = trim((string)($grp['text_suffix'] ?? ''));
        if($prefix !== '') $out = $prefix . "\n" . ltrim($out);
        if($suffix !== '') $out = rtrim($out) . "\n" . $suffix;
        return trim($out);
    }

    private function isUsableTargetId($id) {
        if($id === null) return false;
        $v = trim((string)$id);
        if($v === '') return false;
        if(strtolower($v) === 'skip') return false;
        return true;
    }

    private function routeTagAllow($dest, $text) {
        $t = mb_strtolower((string)$text, 'UTF-8');
        $tg = (strpos($t, '#tgonly') !== false || strpos($t, '#tgon') !== false);
        $bale = (strpos($t, '#baleonly') !== false || strpos($t, '#baleon') !== false);
        $eitaa = (strpos($t, '#eitaaonly') !== false || strpos($t, '#eitaaon') !== false);
        $rubika = (strpos($t, '#rubikaonly') !== false || strpos($t, '#rubikaon') !== false);
        if(!($tg || $bale || $eitaa || $rubika)) return true;
        if($dest === 'telegram') return $tg;
        if($dest === 'bale') return $bale;
        if($dest === 'eitaa') return $eitaa;
        if($dest === 'rubika') return $rubika;
        return true;
    }

    private function isRouteEnabled($grp, $dest) {
        $col = ['telegram'=>'route_tg', 'bale'=>'route_bale', 'eitaa'=>'route_eitaa', 'rubika'=>'route_rubika'][$dest] ?? null;
        if(!$col) return true;
        // For old rows where column may be null, keep backward-compatible default = enabled.
        if(!array_key_exists($col, $grp) || $grp[$col] === null || $grp[$col] === '') return true;
        return ((int)$grp[$col] === 1);
    }

    private function distribute($grp, $msg, $src) {
        $originKey = "{$src}_{$msg['chat']['id']}_{$msg['message_id']}";
        
        // 1. Check or Create Map (Locking mechanism to prevent duplicates on retry)
        $map = $this->db->getMapByOrigin($originKey);
        if (!$map) {
            $this->db->saveMap($originKey, ['telegram'=>null, 'bale'=>null, 'eitaa'=>null, 'rubika'=>null]);
            $map = $this->db->getMapByOrigin($originKey);
        }
        // Ensure source message ID is recorded
        $srcCol = ['telegram'=>'tg_msg_id', 'bale'=>'bale_msg_id', 'eitaa'=>'eitaa_msg_id', 'rubika'=>'rubika_msg_id'][$src] ?? null;
        if ($srcCol && empty($map[$srcCol])) {
            $this->db->updateMapMsgId($map['id'], $src, $msg['message_id']);
            $map[$srcCol] = $msg['message_id'];
        }

        $this->logx('distribute_start', [
            'origin_key' => $originKey,
            'src' => $src,
            'chat_id' => $msg['chat']['id'] ?? null,
            'message_id' => $msg['message_id'] ?? null,
            'has_text' => isset($msg['text']),
            'has_caption' => isset($msg['caption'])
        ]);
        $hitKey = "hit:$originKey";
        $hit = $this->db->getFlag($hitKey);
        $hitCount = 1;
        if($hit && (time() - (int)$hit['ts']) <= 600) {
            $hitCount = ((int)$hit['v']) + 1;
        }
        $this->db->setFlag($hitKey, (string)$hitCount, time());
        if($hitCount >= 4) {
            $this->sendAdminAlert("پردازش تکراری origin_key=$originKey (count=$hitCount)", "dup:$originKey", 300);
        }

        $targets = [];
        if($src != 'telegram' && $this->isUsableTargetId($grp['tg_id'] ?? null)) $targets['telegram'] = $grp['tg_id'];
        if($src != 'bale' && $this->isUsableTargetId($grp['bale_id'] ?? null))   $targets['bale'] = $grp['bale_id'];
        if($this->isUsableTargetId($grp['eitaa_id'] ?? null)) $targets['eitaa'] = $grp['eitaa_id'];
        if($src != 'rubika' && $this->isUsableTargetId($grp['rubika_id'] ?? null)) $targets['rubika'] = $grp['rubika_id'];

        $rawText = $msg['text']??$msg['caption']??'';
        if(stripos($msg['text']??'', '#nosync') !== false) return;
        $cleanText = $this->cleanForSync($rawText, $grp);
        $replySrcMsgId = $msg['reply_to_message']['message_id']
            ?? $msg['reply_to_message_id']
            ?? ($msg['reply_to']['message_id'] ?? null);

        foreach($targets as $dest => $destId) {
            if(!$this->routeTagAllow($dest, $rawText)) continue;
            if(!$this->isRouteEnabled($grp, $dest)) {
                $this->logx('distribute_skip_route_disabled', ['dest'=>$dest, 'origin_key'=>$originKey]);
                continue;
            }
            
            // Check if already sent (Idempotency)
            $destCol = ['telegram'=>'tg_msg_id', 'bale'=>'bale_msg_id', 'eitaa'=>'eitaa_msg_id', 'rubika'=>'rubika_msg_id'][$dest] ?? null;
            if ($destCol && !empty($map[$destCol])) {
                $this->logx('distribute_skip_duplicate', ['dest'=>$dest, 'origin_key'=>$originKey]);
                continue;
            }

            $replyDestMsgId = $this->resolveReplyTarget($src, $msg['chat']['id'], $replySrcMsgId, $dest);

            $res = null;
            $sentId = null;
            if(isset($msg['text'])) {
                if ($dest == 'eitaa') {
                    $payload = ['chat_id'=>$destId, 'text'=>$cleanText];
                    if($replyDestMsgId) $payload['reply_to_message_id'] = $replyDestMsgId;
                    $res = $this->net->req('sendMessage', $payload, 'eitaa');
                } elseif ($dest == 'rubika') {
                    $payload = ['chat_id'=>$destId, 'text'=>$cleanText];
                    if($replyDestMsgId) $payload['reply_to_message_id'] = $replyDestMsgId;
                    $res = $this->net->req('sendMessage', $payload, 'rubika');
                } else $res = $this->sendTxt($destId, $cleanText, $dest, $replyDestMsgId);
            } else {
                $res = $this->sendMedia($destId, $msg, $src, $dest, $cleanText, $replyDestMsgId);
            }

            if($res && (isset($res['result']['message_id']) || isset($res['message_id']) || isset($res['data']['message_id']))) {
                $sentId = $res['result']['message_id'] ?? $res['message_id'] ?? $res['data']['message_id'];
                $this->db->updateMapMsgId($map['id'], $dest, $sentId);
            }
            $this->logx('distribute_send_result', [
                'origin_key' => $originKey,
                'src' => $src,
                'dest' => $dest,
                'dest_chat_id' => $destId,
                'reply_to' => $replyDestMsgId,
                'result' => $this->shortRes($res),
                'dest_msg_id' => $sentId ?? null
            ]);
            $ok = ($res['ok'] ?? false) || (($res['status'] ?? '') === 'OK');
            if(!$ok) {
                $desc = $res['description'] ?? ($res['status'] ?? 'request_failed');
                $this->sendAdminAlert("ارسال ناموفق به {$dest}\norigin_key={$originKey}\nchat_id={$destId}\nreason={$desc}", "sendfail:$dest:$originKey", 180);
            }
        }
    }

    private function resolveReplyTarget($src, $chatId, $replyMsgId, $dest) {
        if(!$replyMsgId) return null;
        $map = $this->db->getMap($src, $chatId, $replyMsgId);
        if(!$map) return null;
        $col = ['telegram'=>'tg_msg_id', 'bale'=>'bale_msg_id', 'eitaa'=>'eitaa_msg_id', 'rubika'=>'rubika_msg_id'][$dest] ?? null;
        if(!$col) return null;
        return $map[$col] ?? null;
    }

    private function sendMedia($tid, $msg, $src, $dest, $cap, $replyTo=null) {
        [$type, $ext, $field] = $this->detectMediaTypeAndExt($msg);
        if(!$type) return null;

        $srcField = $field ?: $type;
        $fObj = ($srcField=='photo') ? end($msg[$srcField]) : $msg[$srcField];
        $this->logx('media_detected', [
            'src'=>$src,
            'dest'=>$dest,
            'type'=>$type,
            'ext'=>$ext,
            'source_field'=>$srcField,
            'file_id'=>$fObj['file_id'] ?? null
        ]);
        $meta = [];
        $bin = $this->net->download($fObj['file_id'], $src, $meta);
        if(!$bin) {
            $this->logx('media_download_fail', ['src'=>$src, 'dest'=>$dest, 'file_id'=>$fObj['file_id'] ?? null]);
            return null;
        }

        if(!$ext) $ext = 'bin';
        if($ext === 'bin' && !empty($meta['file_path'])) {
            $pExt = strtolower((string)pathinfo((string)$meta['file_path'], PATHINFO_EXTENSION));
            if($pExt !== '') $ext = $pExt;
        }
        
        $tmp = sys_get_temp_dir() . "/s_".time().rand().".$ext";
        file_put_contents($tmp, $bin);
        $cfile = new CURLFile($tmp, mime_content_type($tmp), basename($tmp));

        $res = null;
        if ($dest == 'rubika') {
            $upInfo = [];
            $fid = $this->net->uploadRubika($tmp, $type, $upInfo);
            if($fid) {
                $payload = ['chat_id'=>$tid, 'file_id'=>$fid];
                if(trim((string)$cap) !== '') $payload['text'] = $cap;
                if($replyTo) $payload['reply_to_message_id'] = $replyTo;
                $res = $this->net->req('sendFile', $payload, 'rubika');
                $this->logx('rubika_send_file', [
                    'chat_id'=>$tid,
                    'type'=>$type,
                    'upload'=>$upInfo,
                    'payload'=>$payload,
                    'result'=>$this->shortRes($res)
                ]);
            } else {
                $this->logx('rubika_upload_failed_all', ['chat_id'=>$tid, 'type'=>$type, 'tmp'=>$tmp, 'meta'=>$meta]);
            }
        } elseif ($dest == 'eitaa') { // Eitaayar
            $data = ['chat_id' => $tid, 'caption' => $cap, 'file' => $cfile];
            if ($type == 'sticker') $data['title'] = 'webp';
            if ($type == 'animation') $data['title'] = 'gif';
            if($replyTo) $data['reply_to_message_id'] = $replyTo;
            $res = $this->net->req('sendFile', $data, 'eitaa', true);
        } else {
            $method = ['photo'=>'sendPhoto','video'=>'sendVideo','voice'=>'sendVoice','audio'=>'sendAudio','document'=>'sendDocument','sticker'=>'sendSticker','animation'=>'sendAnimation'][$type] ?? 'sendDocument';
            $payload = ['chat_id'=>$tid, 'caption'=>$cap, $type=>$cfile];
            if($replyTo) $payload['reply_to_message_id'] = $replyTo;
            $res = $this->net->req($method, $payload, $dest, true);
        }
        $this->logx('media_send_result', [
            'src'=>$src,
            'dest'=>$dest,
            'type'=>$type,
            'chat_id'=>$tid,
            'reply_to'=>$replyTo,
            'file_meta'=>$meta,
            'result'=>$this->shortRes($res)
        ]);
        @unlink($tmp);
        return $res;
    }

    private function syncEdit($cid, $mid, $msg, $src) {
        $txt = trim($msg['text'] ?? $msg['caption'] ?? '');
        $map = $this->db->getMap($src, $cid, $mid);
        if(!$map) return;
        $grp = $this->db->findGroup($cid, $src);
        if(!$grp) return;
        $this->logx('sync_edit_start', [
            'src'=>$src,
            'chat_id'=>$cid,
            'message_id'=>$mid,
            'text_len'=>mb_strlen($txt, 'UTF-8'),
            'is_text'=>isset($msg['text']) && !isset($msg['caption']),
            'has_caption'=>isset($msg['caption'])
        ]);

        if($txt === DELETE_TRIGGER) { // Ghost Delete
            if((int)$grp['sync_delete'] !== 1) return;
            if($grp['tg_id'] && $map['tg_msg_id']) $this->net->req('deleteMessage', ['chat_id'=>$grp['tg_id'], 'message_id'=>$map['tg_msg_id']], 'telegram');
            if($grp['bale_id'] && $map['bale_msg_id']) $this->net->req('deleteMessage', ['chat_id'=>$grp['bale_id'], 'message_id'=>$map['bale_msg_id']], 'bale');
            if($grp['eitaa_id'] && $map['eitaa_msg_id']) $this->net->req('deleteMessage', ['chat_id'=>$grp['eitaa_id'], 'message_id'=>$map['eitaa_msg_id']], 'eitaa');
            if($grp['rubika_id'] && $map['rubika_msg_id']) $this->net->req('deleteMessage', ['chat_id'=>$grp['rubika_id'], 'message_id'=>$map['rubika_msg_id']], 'rubika');
            if(isset($map['id'])) $this->db->deleteMap($map['id']);
            return;
        }

        $targets = ['telegram'=>'tg', 'bale'=>'bale', 'rubika'=>'rubika'];
        foreach($targets as $p => $pre) {
            if($p == $src || empty($map["{$pre}_msg_id"])) continue;
            if(empty($grp["{$pre}_id"])) continue;
            if($p === 'rubika') {
                $this->net->req('editMessageText', ['chat_id'=>$grp["{$pre}_id"], 'message_id'=>$map["{$pre}_msg_id"], 'text'=>$txt], 'rubika');
            } else {
                $isText = isset($msg['text']) && !isset($msg['caption']);
                $isBale = ($p === 'bale');
                if($isText) {
                    $this->net->req('editMessageText', ['chat_id'=>$grp["{$pre}_id"], 'message_id'=>$map["{$pre}_msg_id"], 'text'=>$txt], $p);
                } else {
                    if($isBale) {
                        [$mType] = $this->detectMediaTypeAndExt($msg);
                        $fallbackKey = "bale_fallback_once:".$map['id'].":".$map["{$pre}_msg_id"];
                        $baleCaptionEditable = in_array($mType, ['photo','video','animation'], true);
                        if(!$baleCaptionEditable) {
                            $this->logx('bale_edit_caption_skip_unsupported_type', [
                                'chat_id'=>$grp["{$pre}_id"],
                                'message_id'=>$map["{$pre}_msg_id"],
                                'media_type'=>$mType
                            ]);
                            if($this->db->getFlag($fallbackKey)) {
                                $this->logx('bale_edit_caption_fallback_once_skipped', ['chat_id'=>$grp["{$pre}_id"], 'message_id'=>$map["{$pre}_msg_id"]]);
                                continue;
                            }
                            $del = $this->net->req('deleteMessage', ['chat_id'=>$grp["{$pre}_id"], 'message_id'=>$map["{$pre}_msg_id"]], $p);
                            $this->logx('bale_edit_caption_fallback_delete', [
                                'chat_id'=>$grp["{$pre}_id"],
                                'message_id'=>$map["{$pre}_msg_id"],
                                'result'=>$this->shortRes($del)
                            ]);
                            if(($del['ok'] ?? false)) {
                                $new = $this->sendMedia($grp["{$pre}_id"], $msg, $src, $p, $txt, null);
                                $newMsgId = $new['result']['message_id'] ?? $new['message_id'] ?? $new['data']['message_id'] ?? null;
                                $this->logx('bale_edit_caption_fallback_resend', [
                                    'chat_id'=>$grp["{$pre}_id"],
                                    'old_message_id'=>$map["{$pre}_msg_id"],
                                    'new_message_id'=>$newMsgId,
                                    'result'=>$this->shortRes($new)
                                ]);
                                $this->db->setFlag($fallbackKey, '1', time());
                                $this->sendAdminAlert("Fallback ادیت بله اجرا شد (تنها یک‌بار)\nchat_id=".$grp["{$pre}_id"]."\nold=".$map["{$pre}_msg_id"]."\nnew=".$newMsgId, "bale_fb:".$map['id'], 600);
                                if($newMsgId && !empty($map['id'])) $this->db->updateMapMsgId($map['id'], $p, $newMsgId);
                            }
                            continue;
                        }
                        // Try real caption edit on Bale first, then replace as fallback.
                        $res = $this->net->req('editMessageCaption', ['chat_id'=>$grp["{$pre}_id"], 'message_id'=>$map["{$pre}_msg_id"], 'caption'=>$txt], $p);
                        $this->logx('bale_edit_caption_attempt', [
                            'chat_id'=>$grp["{$pre}_id"],
                            'message_id'=>$map["{$pre}_msg_id"],
                            'caption_len'=>mb_strlen($txt, 'UTF-8'),
                            'result'=>$this->shortRes($res)
                        ]);
                        if(!($res['ok']??false)) {
                            if($this->db->getFlag($fallbackKey)) {
                                $this->logx('bale_edit_caption_fallback_once_skipped', ['chat_id'=>$grp["{$pre}_id"], 'message_id'=>$map["{$pre}_msg_id"]]);
                                continue;
                            }
                            $del = $this->net->req('deleteMessage', ['chat_id'=>$grp["{$pre}_id"], 'message_id'=>$map["{$pre}_msg_id"]], $p);
                            $this->logx('bale_edit_caption_fallback_delete', [
                                'chat_id'=>$grp["{$pre}_id"],
                                'message_id'=>$map["{$pre}_msg_id"],
                                'result'=>$this->shortRes($del)
                            ]);
                            if(($del['ok'] ?? false)) {
                                $new = $this->sendMedia($grp["{$pre}_id"], $msg, $src, $p, $txt, null);
                                $newMsgId = $new['result']['message_id'] ?? $new['message_id'] ?? $new['data']['message_id'] ?? null;
                                $this->logx('bale_edit_caption_fallback_resend', [
                                    'chat_id'=>$grp["{$pre}_id"],
                                    'old_message_id'=>$map["{$pre}_msg_id"],
                                    'new_message_id'=>$newMsgId,
                                    'result'=>$this->shortRes($new)
                                ]);
                                $this->db->setFlag($fallbackKey, '1', time());
                                $this->sendAdminAlert("Fallback ادیت بله اجرا شد (تنها یک‌بار)\nchat_id=".$grp["{$pre}_id"]."\nold=".$map["{$pre}_msg_id"]."\nnew=".$newMsgId, "bale_fb:".$map['id'], 600);
                                if($newMsgId && !empty($map['id'])) $this->db->updateMapMsgId($map['id'], $p, $newMsgId);
                            }
                        }
                    } else {
                        $res = $this->net->req('editMessageCaption', ['chat_id'=>$grp["{$pre}_id"], 'message_id'=>$map["{$pre}_msg_id"], 'caption'=>$txt], $p);
                        if(!($res['ok']??false)) {
                            $del = $this->net->req('deleteMessage', ['chat_id'=>$grp["{$pre}_id"], 'message_id'=>$map["{$pre}_msg_id"]], $p);
                            if(($del['ok'] ?? false)) {
                                $new = $this->sendMedia($grp["{$pre}_id"], $msg, $src, $p, $txt, null);
                                $newMsgId = $new['result']['message_id'] ?? $new['message_id'] ?? $new['data']['message_id'] ?? null;
                                if($newMsgId && !empty($map['id'])) $this->db->updateMapMsgId($map['id'], $p, $newMsgId);
                            }
                        }
                    }
                }
            }
        }
    }

    private function syncDelete($cid, $mid, $src) {
        $map = $this->db->getMap($src, $cid, $mid);
        if(!$map) return;
        $grp = $this->db->findGroup($cid, $src);
        if(!$grp) return;

        if($grp['tg_id'] && !empty($map['tg_msg_id'])) $this->net->req('deleteMessage', ['chat_id'=>$grp['tg_id'], 'message_id'=>$map['tg_msg_id']], 'telegram');
        if($grp['bale_id'] && !empty($map['bale_msg_id'])) $this->net->req('deleteMessage', ['chat_id'=>$grp['bale_id'], 'message_id'=>$map['bale_msg_id']], 'bale');
        if($grp['eitaa_id'] && !empty($map['eitaa_msg_id'])) $this->net->req('deleteMessage', ['chat_id'=>$grp['eitaa_id'], 'message_id'=>$map['eitaa_msg_id']], 'eitaa');
        if($grp['rubika_id'] && !empty($map['rubika_msg_id'])) $this->net->req('deleteMessage', ['chat_id'=>$grp['rubika_id'], 'message_id'=>$map['rubika_msg_id']], 'rubika');
    }

    // --- ADMIN WIZARD ---
    private function wizard($uid, $cid, $txt, $plat, $msg=null) {
        $st = $this->db->getState($uid);
        $step = $st['step'] ?? 'MAIN';

        if($txt=='/start' || $txt=='بازگشت' || $txt=='Back') {
            $this->db->clearState($uid);
            $menuText = "منوی مدیریت:";
            if($plat === 'rubika') $menuText .= "\n".$this->menuTextRubika();
            $this->sendTxt($cid, $menuText, $plat, null, $this->mainKeyboard());
            return;
        }

        if($txt=='/id') {
            $this->sendTxt($cid, "Your ID: {$uid}", $plat);
            return;
        }

        if($txt=='/getid' || $txt=='🆔 دریافت آیدی کانال') {
            $this->db->setState($uid, 'WAIT_FORWARD_ID');
            $this->sendTxt($cid, "یک پیام از کانال/گروه موردنظر فوروارد کنید تا آیدی دقیق را بدهم.", $plat, null, [['بازگشت']]);
            return;
        }

        if($step == 'MAIN') {
            if($txt=='➕ اتصال جدید') { $this->db->setState($uid, 'GET_TG'); $this->sendTxt($cid, "1) آیدی کانال تلگرام (یا skip):", $plat); }
            elseif($txt=='📋 لیست') {
                $ls = $this->db->getGroups(); $out="لیست اتصالات:\n";
                foreach($ls as $g) {
                    $out.="🆔 {$g['id']}\nTG: {$g['tg_id']}\nBale: {$g['bale_id']}\nEitaa: {$g['eitaa_id']}\nRubika: {$g['rubika_id']}\n";
                    $out.="Rules: T".$this->bit($g['sync_text'])." M".$this->bit($g['sync_media'])." E".$this->bit($g['sync_edit'])." D".$this->bit($g['sync_delete'])."\n----------\n";
                }
                $this->sendTxt($cid, $out?:'لیست خالی است.', $plat);
            }
            elseif($txt=='✏️ ویرایش') { 
                $this->db->setState($uid, 'EDIT_GET_ID'); 
                $this->sendTxt($cid, "شناسه اتصال را بفرستید:", $plat, null, [['بازگشت']]); 
            }
            elseif($txt=='➖ حذف') { $this->db->setState($uid,'DEL'); $this->sendTxt($cid,"شناسه اتصال برای حذف:",$plat, null, [['بازگشت']]); }
            elseif($txt=='🧪 تست اتصال') { $this->db->setState($uid,'TEST_GET_ID'); $this->sendTxt($cid,"شناسه اتصال برای تست:",$plat, null, [['بازگشت']]); }
            elseif($txt=='❤️ سلامت') { $this->sendTxt($cid, $this->healthReport(), $plat); }
            elseif($txt=='⚙️ قوانین اتصال') { $this->db->setState($uid,'RULE_GET_ID'); $this->sendTxt($cid,"شناسه اتصال برای مدیریت قوانین:",$plat, null, [['بازگشت']]); }
            elseif($txt=='🗒 گزارشات') { $this->sendTxt($cid, $this->auditReport(), $plat); }
            elseif($plat==='rubika' && $txt=='منو') { $this->sendTxt($cid, $this->menuTextRubika(), $plat, null, $this->mainKeyboard()); }
        }
        elseif($step == 'WAIT_FORWARD_ID') {
            $fid = $this->extractForwardChatId($msg, $plat);
            if(!$fid) {
                $this->sendTxt($cid, "❌ آیدی پیدا نشد. لطفا حتما پیام را به صورت فوروارد از کانال/گروه بفرستید.", $plat, null, [['بازگشت']]);
                return;
            }
            $title = $msg['forward_from_chat']['title'] ?? ($msg['forwarded_from']['title'] ?? 'unknown');
            $this->db->clearState($uid);
            $this->sendTxt($cid, "✅ آیدی پیدا شد\nTitle: {$title}\nID: {$fid}", $plat, null, $this->mainKeyboard());
        }
        elseif($step == 'GET_TG') { 
            $this->db->setState($uid, 'GET_BALE', ['tg'=>$this->clean($txt)]); $this->sendTxt($cid, "2) آیدی بله (یا skip):", $plat); 
        }
        elseif($step == 'GET_BALE') { 
            $d=$st['data']; $d['bale']=$this->clean($txt); $this->db->setState($uid, 'GET_EITAA', $d); $this->sendTxt($cid, "3) آیدی ایتا (یا skip):", $plat); 
        }
        elseif($step == 'GET_EITAA') { 
            $val=$this->clean($txt); if($val && $val[0]=='@') $val=substr($val,1);
            $d=$st['data']; $d['eitaa']=$val;
            $this->db->setState($uid, 'GET_RUBIKA', $d);
            $this->sendTxt($cid, "4) آیدی روبیکا (GUID) را بفرستید یا یک پیام از کانال روبیکا فوروارد کنید. (یا skip)", $plat);
        }
        elseif($step == 'GET_RUBIKA') {
            $forwardId = $this->extractForwardChatId($msg, $plat);
            $manual = $this->clean($txt);

            if($forwardId) {
                $rubikaId = $forwardId;
            } else {
                if($manual && str_starts_with($manual, '@')) {
                    $this->sendTxt($cid, "❌ برای روبیکا از username نمی‌شود استفاده کرد.\nلطفا GUID/Chat ID بفرستید یا پیام کانال را فوروارد کنید.", $plat, null, [['بازگشت']]);
                    return;
                }
                if($manual && !$this->isValidRubikaChatId($manual)) {
                    $this->sendTxt($cid, "❌ مقدار ارسالی شبیه آیدی معتبر روبیکا نیست.\nلطفا پیامی از کانال روبیکا فوروارد کنید تا آیدی دقیق استخراج شود.", $plat, null, [['بازگشت']]);
                    return;
                }
                $rubikaId = $manual; // null when skip
            }

            $d=$st['data']; $d['rubika']=$rubikaId;
            $this->db->addGroup($d); $this->db->clearState($uid);
            $this->db->audit($plat, $uid, 'add_connection', $d);
            $this->sendTxt($cid, "✅ متصل شد.", $plat, null, [['➕ اتصال جدید','📋 لیست'],['بازگشت']]); 
        }
        elseif($step == 'EDIT_GET_ID') {
            $grp = $this->db->getGroup((int)$txt);
            if(!$grp) { $this->sendTxt($cid, "❌ شناسه یافت نشد. دوباره تلاش کنید:", $plat); return; }
            
            $info = "گروه پیدا شد:\nTG: {$grp['tg_id']}\nBale: {$grp['bale_id']}\nEitaa: {$grp['eitaa_id']}\nRubika: {$grp['rubika_id']}\n\nکدام را تغییر می‌دهید؟";
            $this->db->setState($uid, 'EDIT_SELECT_PLAT', ['id'=>(int)$txt]);
            $this->sendTxt($cid, $info, $plat, null, [['تلگرام','بله','ایتا'],['روبیکا','بازگشت']]);
        }
        elseif($step == 'EDIT_SELECT_PLAT') {
            $col = '';
            if($txt=='تلگرام') $col='tg_id';
            elseif($txt=='بله') $col='bale_id';
            elseif($txt=='ایتا') $col='eitaa_id';
            elseif($txt=='روبیکا') $col='rubika_id';
            else { $this->sendTxt($cid, "لطفا از دکمه‌ها انتخاب کنید.", $plat); return; }
            
            $d=$st['data']; $d['col']=$col;
            $this->db->setState($uid, 'EDIT_SET_VAL', $d);
            $this->sendTxt($cid, "آیدی جدید را وارد کنید (یا بنویسید skip برای خالی کردن):", $plat, null, [['بازگشت']]);
        }
        elseif($step == 'EDIT_SET_VAL') {
            $val = $this->clean($txt);
            if($st['data']['col']=='eitaa_id' && $val && $val[0]=='@') $val=substr($val,1);
            
            $this->db->updateGroup($st['data']['id'], $st['data']['col'], $val);
            $this->db->audit($plat, $uid, 'edit_connection', ['id'=>$st['data']['id'], 'col'=>$st['data']['col']]);
            $this->db->clearState($uid);
            $this->sendTxt($cid, "✅ ویرایش انجام شد.", $plat, null, [['✏️ ویرایش','📋 لیست'],['بازگشت']]);
        }
        elseif($step == 'DEL') { 
            $this->db->deleteGroup((int)$txt); 
            $this->db->audit($plat, $uid, 'delete_connection', ['id'=>(int)$txt]);
            $this->db->clearState($uid); 
            $this->sendTxt($cid, "حذف شد.", $plat, null, [['📋 لیست'],['بازگشت']]); 
        }
        elseif($step == 'TEST_GET_ID') {
            $grp = $this->db->getGroup((int)$txt);
            if(!$grp) { $this->sendTxt($cid, "❌ شناسه یافت نشد.", $plat); return; }
            $this->db->clearState($uid);
            $this->db->audit($plat, $uid, 'test_connection', ['id'=>(int)$txt]);
            $this->sendTxt($cid, $this->runConnectionTest($grp), $plat, null, [['بازگشت']]);
        }
        elseif($step == 'RULE_GET_ID') {
            $grp = $this->db->getGroup((int)$txt);
            if(!$grp) { $this->sendTxt($cid, "❌ شناسه یافت نشد.", $plat); return; }
            $this->db->setState($uid, 'RULE_SELECT', ['id'=>(int)$txt]);
            $this->sendTxt($cid, $this->formatRules($grp), $plat, null, [['متن','رسانه','ادیت'],['حذف همگام','پیشوند','پسوند'],['ارسال تلگرام','ارسال بله'],['ارسال ایتا','ارسال روبیکا'],['بازگشت']]);
        }
        elseif($step == 'RULE_SELECT') {
            $field = [
                'متن'=>'sync_text',
                'رسانه'=>'sync_media',
                'ادیت'=>'sync_edit',
                'حذف همگام'=>'sync_delete',
                'ارسال تلگرام'=>'route_tg',
                'ارسال بله'=>'route_bale',
                'ارسال ایتا'=>'route_eitaa',
                'ارسال روبیکا'=>'route_rubika'
            ][$txt] ?? null;
            if($field) {
                $grp = $this->db->getGroup((int)$st['data']['id']);
                $newVal = ((int)$grp[$field] === 1) ? 0 : 1;
                $this->db->updateGroup($grp['id'], $field, $newVal);
                $this->db->audit($plat, $uid, 'toggle_rule', ['id'=>$grp['id'],'field'=>$field,'value'=>$newVal]);
                $this->sendTxt($cid, "✅ قانون تغییر کرد.\n".$this->formatRules($this->db->getGroup($grp['id'])), $plat, null, [['متن','رسانه','ادیت'],['حذف همگام','پیشوند','پسوند'],['ارسال تلگرام','ارسال بله'],['ارسال ایتا','ارسال روبیکا'],['بازگشت']]);
                return;
            }
            if($txt=='پیشوند' || $txt=='پسوند') {
                $d = $st['data']; $d['field'] = ($txt=='پیشوند') ? 'text_prefix' : 'text_suffix';
                $this->db->setState($uid, 'RULE_SET_TEXT', $d);
                $this->sendTxt($cid, "متن را بفرستید (یا skip برای حذف):", $plat, null, [['بازگشت']]);
            }
        }
        elseif($step == 'RULE_SET_TEXT') {
            $val = $this->clean($txt);
            $this->db->updateGroup($st['data']['id'], $st['data']['field'], $val);
            $this->db->audit($plat, $uid, 'set_rule_text', ['id'=>$st['data']['id'],'field'=>$st['data']['field']]);
            $this->db->setState($uid, 'RULE_SELECT', ['id'=>$st['data']['id']]);
            $this->sendTxt($cid, "✅ ذخیره شد.\n".$this->formatRules($this->db->getGroup($st['data']['id'])), $plat, null, [['متن','رسانه','ادیت'],['حذف همگام','پیشوند','پسوند'],['ارسال تلگرام','ارسال بله'],['ارسال ایتا','ارسال روبیکا'],['بازگشت']]);
        }
    }

    private function mainKeyboard() {
        return [['➕ اتصال جدید','✏️ ویرایش'],['➖ حذف','📋 لیست'],['🧪 تست اتصال','❤️ سلامت'],['⚙️ قوانین اتصال','🗒 گزارشات'],['🆔 دریافت آیدی کانال']];
    }

    private function menuTextRubika() {
        return "دستورها:\n".
            "➕ اتصال جدید\n".
            "✏️ ویرایش\n".
            "➖ حذف\n".
            "📋 لیست\n".
            "🧪 تست اتصال\n".
            "❤️ سلامت\n".
            "⚙️ قوانین اتصال\n".
            "🗒 گزارشات\n".
            "🆔 دریافت آیدی کانال\n".
            "یا بنویس: منو";
    }

    private function formatRules($g) {
        return "قوانین اتصال #{$g['id']}:\n".
            "متن: ".(((int)$g['sync_text']===1)?'روشن':'خاموش')."\n".
            "رسانه: ".(((int)$g['sync_media']===1)?'روشن':'خاموش')."\n".
            "ادیت: ".(((int)$g['sync_edit']===1)?'روشن':'خاموش')."\n".
            "حذف همگام: ".(((int)$g['sync_delete']===1)?'روشن':'خاموش')."\n".
            "ارسال به تلگرام: ".(((int)($g['route_tg'] ?? 1)===1)?'روشن':'خاموش')."\n".
            "ارسال به بله: ".(((int)($g['route_bale'] ?? 1)===1)?'روشن':'خاموش')."\n".
            "ارسال به ایتا: ".(((int)($g['route_eitaa'] ?? 1)===1)?'روشن':'خاموش')."\n".
            "ارسال به روبیکا: ".(((int)($g['route_rubika'] ?? 1)===1)?'روشن':'خاموش')."\n".
            "پیشوند: ".(($g['text_prefix']!=='')?$g['text_prefix']:'ندارد')."\n".
            "پسوند: ".(($g['text_suffix']!=='')?$g['text_suffix']:'ندارد');
    }

    private function runConnectionTest($g) {
        $msg = "🧪 Test #{$g['id']} - ".date('Y-m-d H:i:s');
        $out = ["نتیجه تست اتصال #{$g['id']}:"];
        if($g['tg_id']) $out[] = "Telegram: ".((($this->sendTxt($g['tg_id'], $msg, 'telegram')['ok']??false)?'✅':'❌'));
        if($g['bale_id']) $out[] = "Bale: ".((($this->sendTxt($g['bale_id'], $msg, 'bale')['ok']??false)?'✅':'❌'));
        if($g['eitaa_id']) $out[] = "Eitaa: ".((($this->net->req('sendMessage',['chat_id'=>$g['eitaa_id'],'text'=>$msg],'eitaa')['ok']??false)?'✅':'❌'));
        if(!empty($g['rubika_id'])) {
            $r = $this->net->req('sendMessage',['chat_id'=>$g['rubika_id'],'text'=>$msg],'rubika');
            $out[] = "Rubika: ".(((($r['status']??'')==='OK') || ($r['ok']??false)) ? '✅':'❌');
        }
        return implode("\n", $out);
    }

    private function healthReport() {
        $tail = $this->tailFile(DEBUG_LOG_FILE, 6);
        return "❤️ گزارش سلامت\n".
            "اتصال‌ها: ".$this->db->countGroups()."\n".
            "مپ 24 ساعت اخیر: ".$this->db->countMap24h()."\n".
            "زمان: ".date('Y-m-d H:i:s')."\n\n".
            "آخرین لاگ‌ها:\n".($tail?:'خالی');
    }

    private function auditReport() {
        $rows = $this->db->getRecentAudit(10);
        if(!$rows) return 'گزارش ادمین خالی است.';
        $out = "🗒 آخرین عملیات:\n";
        foreach($rows as $r) $out .= "#{$r['id']} | ".date('m-d H:i', (int)$r['created_at'])." | {$r['platform']} | {$r['action']}\n";
        return trim($out);
    }

    private function tailFile($path, $lines=5) {
        if(!file_exists($path)) return '';
        $arr = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if(!$arr) return '';
        return implode("\n", array_slice($arr, -1*(int)$lines));
    }

    private function extractForwardChatId($msg, $plat) {
        if(!$msg) return null;
        if(isset($msg['forward_from_chat']['id']) && $msg['forward_from_chat']['id']) return (string)$msg['forward_from_chat']['id'];
        if($plat === 'rubika') {
            if(isset($msg['forwarded_from']['from_chat_id']) && $msg['forwarded_from']['from_chat_id']) return (string)$msg['forwarded_from']['from_chat_id'];
            if(isset($msg['forwarded_from']['object_guid']) && $msg['forwarded_from']['object_guid']) return (string)$msg['forwarded_from']['object_guid'];
            // fallback for alternate payload shapes
            if(isset($msg['forwarded_from']['chat_id']) && $msg['forwarded_from']['chat_id']) return (string)$msg['forwarded_from']['chat_id'];
        }
        return null;
    }

    private function detectMediaTypeAndExt($msg) {
        if(isset($msg['photo'])) return ['photo', 'jpg', 'photo'];
        if(isset($msg['video'])) return ['video', 'mp4', 'video'];
        if(isset($msg['voice'])) return ['voice', 'ogg', 'voice'];
        if(isset($msg['audio'])) return ['audio', 'mp3', 'audio'];
        if(isset($msg['sticker'])) return ['sticker', 'webp', 'sticker'];
        if(isset($msg['animation'])) return ['animation', 'gif', 'animation'];
        if(!isset($msg['document'])) return [null, null, null];

        $doc = $msg['document'];
        $name = strtolower((string)($doc['file_name'] ?? ''));
        $mime = strtolower((string)($doc['mime_type'] ?? ''));
        $ext = strtolower((string)pathinfo($name, PATHINFO_EXTENSION));

        $imageExt = ['jpg','jpeg','png','webp','bmp'];
        $videoExt = ['mp4','mkv','mov','webm','3gp'];
        $voiceExt = ['ogg','opus'];
        $audioExt = ['mp3','m4a','aac','wav','flac'];

        if(in_array($ext, $imageExt, true) || str_starts_with($mime, 'image/')) return ['photo', $ext ?: 'jpg', 'document'];
        if(in_array($ext, $videoExt, true) || str_starts_with($mime, 'video/')) return ['video', $ext ?: 'mp4', 'document'];
        if(in_array($ext, $voiceExt, true) || str_contains($mime, 'ogg') || str_contains($mime, 'opus')) return ['voice', $ext ?: 'ogg', 'document'];
        if(in_array($ext, $audioExt, true) || str_starts_with($mime, 'audio/')) return ['audio', $ext ?: 'mp3', 'document'];
        if($ext === 'webp') return ['sticker', 'webp', 'document'];
        return ['document', $ext ?: 'bin', 'document'];
    }

    private function isValidRubikaChatId($id) {
        $v = trim((string)$id);
        if($v === '') return false;
        if(str_contains($v, ' ')) return false;
        // rubika chat IDs are usually GUID-like alnum strings
        return (bool)preg_match('/^[A-Za-z0-9_-]{12,}$/', $v);
    }

    private function bit($v) { return ((int)$v===1)?'1':'0'; }
    private function clean($t) { return (strtolower($t)=='skip') ? null : trim($t); }
    private function sendTxt($cid, $t, $p, $r=null, $k=null) {
        $d = ['chat_id'=>$cid, 'text'=>$t];
        if($k && $p === 'rubika') {
            $rows = [];
            $btnId = 1000;
            foreach($k as $row) {
                $buttons = [];
                foreach($row as $label) {
                    $buttons[] = ['id'=>(string)$btnId++, 'type'=>'Simple', 'button_text'=>$label];
                }
                $rows[] = ['buttons'=>$buttons];
            }
            $d['chat_keypad_type'] = 'New';
            $d['chat_keypad'] = ['rows'=>$rows, 'resize_keyboard'=>true, 'one_time_keyboard'=>false];
        } elseif($k) {
            $d['reply_markup'] = json_encode(['keyboard'=>$k, 'resize_keyboard'=>true]);
        }
        if($r) $d['reply_to_message_id'] = $r;
        return $this->net->req('sendMessage', $d, $p);
    }
}

try { $bot = new Robot(); $bot->start(); } 
catch(Exception $e) { file_put_contents(ERROR_LOG_FILE, "[".date("Y-m-d H:i:s")."] ".$e->getMessage()."\n", FILE_APPEND); }
?>

