<?php

declare(strict_types=1);

namespace PpPhp\Modules\Misc\Controllers;

use PpPhp\Core\Config;
use PpPhp\Core\ModuleManager;
use PpPhp\Core\DB;
use PpPhp\Core\Security;
use PpPhp\Core\ProjectNav;
use PpPhp\Core\ModuleState;

class MiscController
{
    private array $project = [];
    private string $slug = '';
    /** True when the open project is standalone (no database). */
    private bool $standalone = false;

    public function __construct(
        private Config $config,
        private ModuleManager $moduleManager
    ) {}

    public function index(string $slug): string
    {
        $this->initProject($slug);
        $project = $this->project;

        // Load misc state from its own file
        $miscState = ModuleState::load($slug, 'misc');

        // Load Tables module state to get selected tables
        $tablesState = ModuleState::load($slug, 'tables');
        $selectedTableNames = $tablesState['selectedTables'] ?? [];

        // Build table list for left panel (using selected tables or all if none)
        $tables = [];
        $standalone = $this->standalone;   // exposed to the view
        if ($standalone) {
            $selectedTableNames = [];      // no database → no tables/table settings
        } elseif (empty($selectedTableNames)) {
            $rows = DB::Query('SHOW FULL TABLES')->fetchAll(\PDO::FETCH_NUM);
            foreach ($rows as $row) {
                $tables[] = ['name' => $row[0], 'type' => ($row[1] === 'VIEW') ? 'VIEW' : 'TABLE'];
            }
            usort($tables, fn($a, $b) => $a['name'] <=> $b['name']);
            // Also use all tables for landing page options if none selected
            $selectedTableNames = array_column($tables, 'name');
        } else {
            foreach ($selectedTableNames as $n) {
                $tables[] = ['name' => $n, 'type' => 'TABLE'];
            }
        }

        // Build SMS driver list for the frontend
        $smsDrivers = \PpPhp\Core\SMS::GetDrivers();
        $smsDriverList = [];
        foreach ($smsDrivers as $key => $driver) {
            $smsDriverList[$key] = [
                'label'  => $driver->label(),
                'help'   => $driver->helpText(),
                'fields' => $driver->requiredFields(),
            ];
        }

        // Merge custom providers (now read from ModuleState)
        $customProviders = $miscState['sms']['customProviders'] ?? [];
        foreach ($customProviders as $key => $cp) {
            $smsDriverList[$key] = [
                'label'    => $cp['label'] ?? $key,
                'help'     => 'Custom HTTP provider.',
                'fields'   => ['url' => 'API URL', 'method' => 'Method (POST/GET)', 'toParam' => '"to" param name', 'msgParam' => '"message" param name', 'fromParam' => '"from" param name', 'extraParams' => 'Extra params (key=value, comma sep)'],
                'custom'   => true,
            ];
        }

        $navItems = ProjectNav::items('/misc');
        $baseUrl   = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');
        $user      = $_SESSION['user'];
        $pageTitle = 'Misc';

        // Load Pages configuration to know which page types are enabled per table
        $pagesState = ModuleState::load($slug, 'pages');
        $pagesByTable = $pagesState['byTable'] ?? [];

        // Build table list and their available page types for landing page selector
        $tablesWithPageTypes = [];
        foreach ($selectedTableNames as $tableName) {
            $tableConfig = $pagesByTable[$tableName] ?? [];
            $pageTypes = $tableConfig['pageTypes'] ?? ['list']; // fallback
            $tablesWithPageTypes[] = [
                'table' => $tableName,
                'pageTypes' => $pageTypes
            ];
        }

        $miscConfig = $miscState;
        // Never send the AI API key to the browser — expose only whether one is set,
        // so the AI tab can show "key configured" without echoing the secret.
        if (isset($miscConfig['ai']) && is_array($miscConfig['ai'])) {
            $miscConfig['ai']['has_key'] = trim((string) ($miscConfig['ai']['api_key'] ?? '')) !== '';
            unset($miscConfig['ai']['api_key']);
        }
        $smsDriverList = $smsDriverList ?? [];
        $tablesWithPageTypesJson = json_encode($tablesWithPageTypes);

        $pagesConfig = $pagesState['byTable'] ?? [];

        // App Strings catalogue (UI chrome) for the Label Editor → App Strings tab.
        $uiStringGroups = \PpPhp\Core\I18n\UiStrings::groups();
        // Menu labels (Style → Menu) for the Label Editor → Menu tab, flattened to
        // key => default label so each nav item can be localised (menu.<key>).
        $menuItems = [];
        try {
            $menuTree = \PpPhp\Core\Theme\ThemeRegistry::menu($slug);
            $flatten = function ($items) use (&$flatten, &$menuItems): void {
                foreach ((array) $items as $it) {
                    if (($it['type'] ?? '') === 'divider' || !empty($it['divider'])) continue;
                    // Key mirrors the build's menu tkey: the raw item id (custom menus).
                    $key = (string) ($it['id'] ?? $it['key'] ?? '');
                    $label = trim((string) ($it['label'] ?? ''));
                    if ($key !== '' && $label !== '') $menuItems[$key] = $label;
                    if (!empty($it['children'])) $flatten($it['children']);
                }
            };
            $flatten($menuTree['items'] ?? []);
            // The standalone Calendar app is auto-added to the generated nav (tkey
            // 'calendar') — mirror it here so the Menu tab can localise it too.
            $cal = ModuleState::load($slug, 'calendar') ?: [];
            if (!empty($cal['enabled'])) {
                $menuItems['calendar'] = trim((string) ($cal['menuLabel'] ?? '')) ?: 'Calendar';
            }
        } catch (\Throwable $e) {
            $menuItems = [];
        }

        ob_start();
        include __DIR__ . '/../Views/index.php';
        return ob_get_clean();
    }

    private function initProject(string $slug): void
    {
        Security::RequireAuth();
        $this->slug = $slug;
        DB::Init();
        $project = DB::Select('projects', ['slug' => $slug, 'created_by' => Security::CurrentUserData()['id']]);
        if (empty($project) && Security::IsAdmin()) $project = DB::Select('projects', ['slug' => $slug]);
        if (empty($project)) {
            http_response_code(404);
            echo 'Project not found.';
            exit;
        }
        $this->project = $project[0];
        // Standalone (database-free) projects: only the Project tab of Misc is
        // meaningful. Skip the project-DB connection; the view hides the rest.
        $this->standalone = \PpPhp\Core\ProjectDatabaseProfile::isStandaloneSlug($slug);
        if ($this->standalone) {
            return;
        }
        DB::Init($slug);
        DB::SetConnection($slug);
    }

    /**
     * Save Misc configuration (project, table, advanced, email, sms).
     */
    public function save(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);

        $input   = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $section = $input['section'] ?? 'project';
        $data    = $input['data']    ?? [];

        // Load current misc state – ensure it's an associative array
        $state = ModuleState::load($slug, 'misc');
        if (!is_array($state) || array_is_list($state)) {
            $state = []; // convert empty list or malformed to empty associative array
        }

        // Update only the relevant section inside misc
        if ($section === 'table') {
            $tableName = $input['tableName'] ?? '';
            if ($tableName === '') {
                http_response_code(400);
                echo json_encode(['success' => false, 'message' => 'Table name is required.']);
                exit;
            }
            $state['byTable'][$tableName] = $data;
        } elseif ($section === 'advanced') {
            // Merge, don't replace: the Advanced form doesn't carry every advanced
            // key (e.g. auditMode/lockMode are set from the Security module). A
            // wholesale assignment would silently drop those on the next autosave.
            $state['advanced'] = array_merge((array) ($state['advanced'] ?? []), $data);
        } elseif ($section === 'email') {
            $state['email'] = $data;
        } elseif ($section === 'sms') {
            $state['sms'] = array_merge($state['sms'] ?? [], $data);
        } elseif ($section === 'ai') {
            // AI provider config. The api_key is a secret — a blank key on save means
            // "leave the stored key untouched" (the UI never echoes the real key back),
            // and clear_key:true removes it outright.
            $prev = (array) ($state['ai'] ?? []);
            $clear = !empty($data['clear_key']);
            unset($data['clear_key']);
            if ($clear) {
                unset($prev['api_key'], $data['api_key']);
            } elseif (!isset($data['api_key']) || trim((string) $data['api_key']) === '') {
                unset($data['api_key']);
            }
            $state['ai'] = array_merge($prev, $data);
        } elseif ($section === 'project') {
            $state['project'] = $data;
        } elseif ($section === 'languages') {
            $state['languages'] = $data['languages'] ?? [];
        }

        // Debug: log what we are about to save
        error_log("Saving misc state for section {$section}: " . json_encode($state));
        error_log("ModuleState::save received data: " . json_encode($data));

        // Save back to misc.json
        ModuleState::save($slug, 'misc', $state);

        echo json_encode(['success' => true, 'message' => 'Settings saved.']);
        exit;
    }

    /**
     * Load all Misc configuration.
     */
    public function load(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $config = ModuleState::load($slug, 'misc');
        // Never send the AI API key to the browser — expose only a has_key flag so
        // the AI tab knows a key is stored without the secret leaving the server.
        if (is_array($config) && isset($config['ai']) && is_array($config['ai'])) {
            $config['ai']['has_key'] = trim((string) ($config['ai']['api_key'] ?? '')) !== '';
            unset($config['ai']['api_key']);
        }
        // If config is empty (like []), convert to empty object
        if (empty($config)) {
            $config = (object) [];
        }
        echo json_encode(['success' => true, 'config' => $config]);
        exit;
    }



    /**
     * Load Misc configuration for a specific table.
     */
    public function loadTable(string $slug, string $table): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $state = ModuleState::load($slug, 'misc');
        $config = $state['byTable'][$table] ?? [
            'recordsPerPage' => 20,
            'recordsPerPageOptions' => '10,20,50,100',
            'rowHighlight' => true,
            'nextPrevButtons' => true,
            'warnLeave' => true,
            'loadingBox' => false,
            'hideUntilSearch' => false,
            'defaultSortField' => '',
            'defaultSortOrder' => 'ASC'
        ];
        echo json_encode(['success' => true, 'config' => $config]);
        exit;
    }

    public function testEmail(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $to = $input['to'] ?? '';
        $settings = $input['settings'] ?? [];

        if (empty($to)) {
            echo json_encode(['success' => false, 'message' => 'Recipient required.']);
            exit;
        }
        if (empty($settings['host'])) {
            echo json_encode(['success' => false, 'message' => 'SMTP host is not configured.']);
            exit;
        }

        \PpPhp\Core\Email::setSettings($settings);
        $result = \PpPhp\Core\Email::send([
            'to'      => $to,
            'subject' => 'Test from PPPHP',
            'body'    => 'This is a test email sent from PPPHP. Your email configuration is working!'
        ]);

        echo json_encode(['success' => $result['mailed'], 'message' => $result['message']]);
        exit;
    }

    /**
     * Validate the AI provider key with a tiny live request. Uses the key typed in
     * the form, or — if blank — the one already stored in misc.json (the console has
     * no config/ai.php, so we must supply the stored key explicitly).
     */
    public function testAi(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input    = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $key      = trim((string) ($input['api_key'] ?? ''));
        $model    = trim((string) ($input['model'] ?? ''));
        $provider = trim((string) ($input['provider'] ?? ''));
        $baseUrl  = trim((string) ($input['base_url'] ?? ''));
        // Fall back to the stored config for anything the form left blank (the console
        // has no config/ai.php, so the stored key/model must be supplied explicitly).
        $misc = ModuleState::load($slug, 'misc') ?: [];
        $ai   = (array) ($misc['ai'] ?? []);
        if ($key === '')      $key = trim((string) ($ai['api_key'] ?? ''));
        if ($model === '')    $model = trim((string) ($ai['model'] ?? ''));
        if ($provider === '') $provider = trim((string) ($ai['provider'] ?? 'anthropic'));
        if ($baseUrl === '')  $baseUrl = trim((string) ($ai['base_url'] ?? ''));
        if ($key === '') {
            echo json_encode(['success' => false, 'message' => 'No API key to test — enter one or save it first.']);
            exit;
        }
        $result = \PpPhp\Core\Ai\AiClient::ping($key, $model, $provider, $baseUrl);
        echo json_encode($result);
        exit;
    }

    public function testSms(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $to = $input['to'] ?? '';
        $settings = $input['settings'] ?? [];

        if (empty($to)) {
            echo json_encode(['success' => false, 'error' => 'Recipient required.']);
            exit;
        }

        \PpPhp\Core\SMS::SetConfig($settings);
        $result = \PpPhp\Core\SMS::Send($to, 'Test SMS from PPPHP. Your configuration is working!');
        echo json_encode($result);
        exit;
    }

    public function saveCustomProvider(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $key  = $input['key'] ?? '';
        $data = $input['data'] ?? [];

        if (empty($key)) {
            echo json_encode(['success' => false, 'message' => 'Provider key required.']);
            exit;
        }

        $state = ModuleState::load($slug, 'misc');
        if ($data === null) {
            unset($state['sms']['customProviders'][$key]);
        } else {
            $state['sms']['customProviders'][$key] = $data;
        }
        ModuleState::save($slug, 'misc', $state);

        echo json_encode(['success' => true]);
        exit;
    }

    public function initNotifTable(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $tableName = trim($input['tableName'] ?? 'notifications');
        $tableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName);

        try {
            \PpPhp\Core\Notification::Enable($tableName);
            \PpPhp\Core\Notification::SetTableName($tableName);

            $state = ModuleState::load($slug, 'misc');
            $state['advanced']['notifEnabled'] = true;
            $state['advanced']['notifTableName'] = $tableName;
            ModuleState::save($slug, 'misc', $state);

            echo json_encode(['success' => true, 'message' => "Table '{$tableName}' created successfully."]);
        } catch (\Exception $e) {
            http_response_code(500);
            echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
        }
        exit;
    }

    public function disableNotif(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $tableName = trim($input['tableName'] ?? 'notifications');
        $dropTable = (bool)($input['dropTable'] ?? false);
        $tableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName);

        try {
            if ($dropTable) {
                DB::Exec("DROP TABLE IF EXISTS `{$tableName}`");
            }

            $state = ModuleState::load($slug, 'misc');
            $state['advanced']['notifEnabled'] = false;
            $state['advanced']['notifTableName'] = $tableName;
            ModuleState::save($slug, 'misc', $state);

            echo json_encode(['success' => true, 'message' => 'Notifications disabled.']);
        } catch (\Exception $e) {
            http_response_code(500);
            echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
        }
        exit;
    }


    /**
     * Initialize audit log table.
     */
    public function initAuditTable(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $tableName = trim($input['tableName'] ?? 'audit_log');
        $tableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName);

        try {
            $sql = "CREATE TABLE IF NOT EXISTS `{$tableName}` (
            id INT AUTO_INCREMENT PRIMARY KEY,
            user_id INT NOT NULL,
            action VARCHAR(100) NOT NULL,
            table_name VARCHAR(100) NOT NULL,
            record_id VARCHAR(100) DEFAULT NULL,
            old_data JSON DEFAULT NULL,
            new_data JSON DEFAULT NULL,
            ip_address VARCHAR(45) DEFAULT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            INDEX idx_user (user_id),
            INDEX idx_table (table_name),
            INDEX idx_action (action)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
            DB::Exec($sql);

            // Update state
            $state = ModuleState::load($slug, 'misc');
            $state['advanced']['auditEnabled'] = true;
            $state['advanced']['auditTable'] = $tableName;
            ModuleState::save($slug, 'misc', $state);

            echo json_encode(['success' => true, 'message' => "Audit table '{$tableName}' created successfully."]);
        } catch (\Exception $e) {
            http_response_code(500);
            echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
        }
        exit;
    }

    /**
     * Disable and optionally drop audit log table.
     */
    public function disableAudit(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $tableName = trim($input['tableName'] ?? 'audit_log');
        $dropTable = (bool)($input['dropTable'] ?? false);
        $tableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName);

        try {
            if ($dropTable) {
                DB::Exec("DROP TABLE IF EXISTS `{$tableName}`");
            }
            $state = ModuleState::load($slug, 'misc');
            $state['advanced']['auditEnabled'] = false;
            $state['advanced']['auditTable'] = $tableName;
            ModuleState::save($slug, 'misc', $state);

            echo json_encode(['success' => true, 'message' => 'Audit trail disabled.' . ($dropTable ? ' Table dropped.' : '')]);
        } catch (\Exception $e) {
            http_response_code(500);
            echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
        }
        exit;
    }

    /**
     * Initialize record locking table.
     */
    public function initLockingTable(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $tableName = trim($input['tableName'] ?? 'record_locks');
        $tableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName);

        try {
            // Migration guard: earlier builds created a `record_locks` table with a
            // different, incompatible schema (a NOT-NULL `lock_token` column and no
            // UNIQUE(table_name, record_id) key). That breaks the built app's
            // INSERT … ON DUPLICATE KEY lock upsert. Locks are transient, so if a
            // pre-existing table is missing the unique key we can safely drop and
            // recreate it with the correct schema — no real data is lost.
            $exists = DB::FetchOne(
                "SELECT 1 AS ok FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ? LIMIT 1",
                [$tableName]
            );
            if ($exists) {
                $hasUnique = DB::FetchOne(
                    "SELECT 1 AS ok FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = ? AND index_name = 'unique_lock' LIMIT 1",
                    [$tableName]
                );
                if (!$hasUnique) {
                    DB::Exec("DROP TABLE `{$tableName}`");
                }
            }

            $sql = "CREATE TABLE IF NOT EXISTS `{$tableName}` (
            id INT AUTO_INCREMENT PRIMARY KEY,
            table_name VARCHAR(100) NOT NULL,
            record_id VARCHAR(100) NOT NULL,
            user_id INT NOT NULL,
            locked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            expires_at TIMESTAMP NULL DEFAULT NULL,
            UNIQUE KEY unique_lock (table_name, record_id),
            INDEX idx_user (user_id),
            INDEX idx_expires (expires_at)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
            DB::Exec($sql);

            $state = ModuleState::load($slug, 'misc');
            $state['advanced']['recordLocking'] = true;
            $state['advanced']['lockTable'] = $tableName;
            ModuleState::save($slug, 'misc', $state);

            echo json_encode(['success' => true, 'message' => "Locking table '{$tableName}' created successfully."]);
        } catch (\Exception $e) {
            http_response_code(500);
            echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
        }
        exit;
    }

    /**
     * Disable and optionally drop record locking table.
     */
    public function disableLocking(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $tableName = trim($input['tableName'] ?? 'record_locks');
        $dropTable = (bool)($input['dropTable'] ?? false);
        $tableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName);

        try {
            if ($dropTable) {
                DB::Exec("DROP TABLE IF EXISTS `{$tableName}`");
            }
            $state = ModuleState::load($slug, 'misc');
            $state['advanced']['recordLocking'] = false;
            $state['advanced']['lockTable'] = $tableName;
            ModuleState::save($slug, 'misc', $state);

            echo json_encode(['success' => true, 'message' => 'Record locking disabled.' . ($dropTable ? ' Table dropped.' : '')]);
        } catch (\Exception $e) {
            http_response_code(500);
            echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
        }
        exit;
    }
    

    public function initLabels(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);

        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $langCode = trim($input['langCode'] ?? '');

        if ($langCode === '') {
            echo json_encode(['success' => false, 'message' => 'Language code is required.']);
            exit;
        }

        $state = ModuleState::load($slug, 'misc');
        if (!isset($state['labels'][$langCode])) {
            $state['labels'][$langCode] = [
                'tables'       => [],
                'labels'       => [],   // per-field field labels (parallels placeholders/tooltips)
                'custom'       => [],
                'tooltips'     => [],
                'titles'       => [],
                'placeholders' => [],
            ];
            ModuleState::save($slug, 'misc', $state);
        }

        echo json_encode(['success' => true]);
        exit;
    }

    public function saveLabel(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);

        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $langCode = $input['langCode'] ?? '';
        $section  = $input['section']  ?? '';
        $key      = $input['key']      ?? '';
        $value    = $input['value']    ?? '';

        if ($langCode === '' || $section === '' || $key === '') {
            http_response_code(400);
            echo json_encode(['success' => false, 'message' => 'langCode, section, and key are required.']);
            exit;
        }

        $state = ModuleState::load($slug, 'misc');
        $state['labels'][$langCode][$section][$key] = $value;
        ModuleState::save($slug, 'misc', $state);

        echo json_encode(['success' => true]);
        exit;
    }

    public function loadLabels(string $slug, string $langCode): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        $state = ModuleState::load($slug, 'misc');
        $labels = $state['labels'][$langCode] ?? [];
        echo json_encode(['success' => true, 'labels' => $labels]);
        exit;
    }

    public function copyLabels(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);

        $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
        $fromLang = $input['fromLang'] ?? '';
        $toLang   = $input['toLang']   ?? '';

        if ($fromLang === '' || $toLang === '') {
            http_response_code(400);
            echo json_encode(['success' => false, 'message' => 'Source and target languages are required.']);
            exit;
        }

        $state = ModuleState::load($slug, 'misc');
        $sourceLabels = $state['labels'][$fromLang] ?? [];
        if (empty($sourceLabels)) {
            echo json_encode(['success' => false, 'message' => 'Source language has no labels to copy.']);
            exit;
        }

        $state['labels'][$toLang] = $sourceLabels;
        ModuleState::save($slug, 'misc', $state);

        echo json_encode(['success' => true, 'message' => 'Labels copied.']);
        exit;
    }
}
