<?php
declare(strict_types=1);

require_once __DIR__ . '/config.php';

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');

function respond(array $payload, int $status = 200): void
{
    http_response_code($status);
    echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function now_iso(): string
{
    return (new DateTimeImmutable('now'))->format('Y-m-d\TH:i:s.v');
}

function read_json(): array
{
    $raw = file_get_contents('php://input') ?: '';
    if ($raw === '') {
        return [];
    }

    $data = json_decode($raw, true);
    if (!is_array($data)) {
        respond(['ok' => false, 'error' => 'Invalid JSON body.'], 400);
    }

    return $data;
}

function request_token(array $body): string
{
    $headers = function_exists('getallheaders') ? getallheaders() : [];
    foreach ($headers as $key => $value) {
        if (strtolower((string)$key) === 'x-api-token') {
            return (string)$value;
        }
    }

    return (string)($body['token'] ?? $_GET['token'] ?? $_POST['token'] ?? '');
}

function require_api_token(array $body): void
{
    if (!hash_equals(API_TOKEN, request_token($body))) {
        respond(['ok' => false, 'error' => 'Invalid API token.'], 401);
    }
}

function require_admin_token(array $body): void
{
    if (!hash_equals(ADMIN_TOKEN, request_token($body))) {
        respond(['ok' => false, 'error' => 'Invalid admin token.'], 401);
    }
}

function is_default_admin_token(): bool
{
    return ADMIN_TOKEN === '' || strpos(ADMIN_TOKEN, 'CHANGE_ME') === 0;
}

function php_single_quote(string $value): string
{
    return str_replace(["\\", "'"], ["\\\\", "\\'"], $value);
}

function update_config_constant(string $content, string $name, string $value): string
{
    $pattern = "/const\s+" . preg_quote($name, '/') . "\s*=\s*'[^']*';/";
    $replacement = "const " . $name . " = '" . php_single_quote($value) . "';";
    $updated = preg_replace($pattern, $replacement, $content, 1, $count);

    if ($updated === null || $count === 0) {
        throw new RuntimeException("Constant " . $name . " was not found in config.php.");
    }

    return $updated;
}

function update_config_int_constant(string $content, string $name, int $value): string
{
    $pattern = "/const\s+" . preg_quote($name, '/') . "\s*=\s*[0-9]+;/";
    $replacement = "const " . $name . " = " . $value . ";";
    $updated = preg_replace($pattern, $replacement, $content, 1, $count);

    if ($updated === null) {
        throw new RuntimeException("Constant " . $name . " was not found in config.php.");
    }

    if ($count === 0) {
        return rtrim($content) . "\n" . $replacement . "\n";
    }

    return $updated;
}

function config_int(string $name, int $default): int
{
    return defined($name) ? (int)constant($name) : $default;
}

function db(): PDO
{
    static $db = null;
    if ($db instanceof PDO) {
        return $db;
    }

    $dir = dirname(DATABASE_PATH);
    if (!is_dir($dir)) {
        mkdir($dir, 0775, true);
    }

    $db = new PDO('sqlite:' . DATABASE_PATH);
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $db->exec('PRAGMA journal_mode = WAL;');
    ensure_schema($db);
    return $db;
}

function ensure_schema(PDO $db): void
{
    $db->exec("
        CREATE TABLE IF NOT EXISTS devices (
            id INTEGER PRIMARY KEY,
            device_id TEXT UNIQUE,
            device_name TEXT,
            app_type TEXT,
            app_version TEXT,
            hostname TEXT,
            connection_type TEXT,
            port TEXT,
            available_ports TEXT NULL,
            available_baudrates TEXT NULL,
            baudrate INTEGER,
            last_seen TEXT,
            last_ip TEXT,
            enabled INTEGER DEFAULT 1,
            created_at TEXT,
            updated_at TEXT
        );
        CREATE TABLE IF NOT EXISTS logs (
            id INTEGER PRIMARY KEY,
            device_id TEXT,
            created_at TEXT,
            direction TEXT,
            format TEXT,
            data TEXT,
            level TEXT DEFAULT 'info'
        );
        CREATE TABLE IF NOT EXISTS rules (
            id INTEGER PRIMARY KEY,
            device_id TEXT NULL,
            name TEXT,
            enabled INTEGER DEFAULT 1,
            match_type TEXT,
            pattern TEXT,
            action_email INTEGER DEFAULT 0,
            action_alarm INTEGER DEFAULT 0,
            action_highlight INTEGER DEFAULT 0,
            highlight_level TEXT,
            cooldown_seconds INTEGER DEFAULT 1800,
            last_triggered_at TEXT NULL
        );
        CREATE TABLE IF NOT EXISTS alarms (
            id INTEGER PRIMARY KEY,
            device_id TEXT,
            rule_id INTEGER,
            created_at TEXT,
            message TEXT,
            status TEXT DEFAULT 'active',
            acknowledged_at TEXT NULL,
            acknowledged_by TEXT NULL
        );
        CREATE TABLE IF NOT EXISTS commands (
            id INTEGER PRIMARY KEY,
            device_id TEXT,
            created_at TEXT,
            command TEXT,
            command_type TEXT DEFAULT 'send',
            payload TEXT NULL,
            status TEXT DEFAULT 'pending',
            response TEXT NULL,
            sent_at TEXT NULL,
            done_at TEXT NULL
        );
        CREATE INDEX IF NOT EXISTS idx_logs_device_created ON logs(device_id, created_at);
        CREATE INDEX IF NOT EXISTS idx_alarms_status_created ON alarms(status, created_at);
        CREATE INDEX IF NOT EXISTS idx_rules_enabled ON rules(enabled);
        CREATE INDEX IF NOT EXISTS idx_commands_device_status ON commands(device_id, status);
    ");

    ensure_command_columns($db);
    ensure_device_columns($db);
}

function ensure_command_columns(PDO $db): void
{
    $columns = [];
    foreach ($db->query("PRAGMA table_info(commands)")->fetchAll(PDO::FETCH_ASSOC) as $column) {
        $columns[(string)$column['name']] = true;
    }

    $definitions = [
        'format' => "ALTER TABLE commands ADD COLUMN format TEXT DEFAULT 'text'",
        'add_asterisk' => "ALTER TABLE commands ADD COLUMN add_asterisk INTEGER DEFAULT 0",
        'add_cr' => "ALTER TABLE commands ADD COLUMN add_cr INTEGER DEFAULT 0",
        'add_lf' => "ALTER TABLE commands ADD COLUMN add_lf INTEGER DEFAULT 0",
        'command_type' => "ALTER TABLE commands ADD COLUMN command_type TEXT DEFAULT 'send'",
        'payload' => "ALTER TABLE commands ADD COLUMN payload TEXT NULL",
    ];

    foreach ($definitions as $name => $sql) {
        if (empty($columns[$name])) {
            $db->exec($sql);
        }
    }
}

function ensure_device_columns(PDO $db): void
{
    $columns = [];
    foreach ($db->query("PRAGMA table_info(devices)")->fetchAll(PDO::FETCH_ASSOC) as $column) {
        $columns[(string)$column['name']] = true;
    }

    if (empty($columns['available_ports'])) {
        $db->exec("ALTER TABLE devices ADD COLUMN available_ports TEXT NULL");
    }

    if (empty($columns['available_baudrates'])) {
        $db->exec("ALTER TABLE devices ADD COLUMN available_baudrates TEXT NULL");
    }
}

function clean_string($value, int $max = 255): string
{
    $value = trim((string)$value);
    if (function_exists('mb_substr')) {
        return mb_substr($value, 0, $max, 'UTF-8');
    }
    return substr($value, 0, $max);
}

function limit_text($value, int $max): string
{
    $value = (string)$value;
    if (function_exists('mb_substr')) {
        return mb_substr($value, 0, $max, 'UTF-8');
    }
    return substr($value, 0, $max);
}

function push_log(array $body): void
{
    require_api_token($body);

    $deviceId = clean_string($body['device_id'] ?? '', 120);
    if ($deviceId === '') {
        respond(['ok' => false, 'error' => 'device_id is required.'], 400);
    }

    $items = $body['items'] ?? [];
    if (!is_array($items)) {
        respond(['ok' => false, 'error' => 'items must be an array.'], 400);
    }

    if (count($items) > MAX_LOG_ITEMS_PER_REQUEST) {
        respond(['ok' => false, 'error' => 'Too many log items.'], 413);
    }

    $db = db();
    $now = now_iso();
    $ip = $_SERVER['REMOTE_ADDR'] ?? '';

    $db->beginTransaction();
    try {
        upsert_device($db, $body, $deviceId, $now, $ip);

        $insert = $db->prepare('INSERT INTO logs (device_id, created_at, direction, format, data, level) VALUES (?, ?, ?, ?, ?, ?)');
        $saved = 0;
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }

            $direction = strtoupper(clean_string($item['direction'] ?? 'RX', 8));
            if (!in_array($direction, ['RX', 'TX', 'SYS'], true)) {
                $direction = 'RX';
            }

            $format = strtolower(clean_string($item['format'] ?? 'text', 16));
            if (!in_array($format, ['text', 'hex'], true)) {
                $format = 'text';
            }

            $data = limit_text($item['data'] ?? '', MAX_LOG_LENGTH);
            if ($data === '') {
                continue;
            }

            $createdAt = clean_string($item['timestamp'] ?? $now, 40);
            $level = evaluate_rules($db, $deviceId, $data, $createdAt);
            $insert->execute([$deviceId, $createdAt, $direction, $format, $data, $level]);
            $saved++;
        }

        $db->commit();
        purge_old_logs($db);
        respond(['ok' => true, 'saved' => $saved]);
    } catch (Throwable $e) {
        $db->rollBack();
        respond(['ok' => false, 'error' => $e->getMessage()], 500);
    }
}

function purge_old_logs(PDO $db): int
{
    $days = config_int('LOG_RETENTION_DAYS', 30);
    if ($days <= 0) {
        return 0;
    }

    $cutoff = (new DateTimeImmutable('now'))
        ->modify('-' . $days . ' days')
        ->format('Y-m-d\TH:i:s.v');

    $stmt = $db->prepare('DELETE FROM logs WHERE created_at < ?');
    $stmt->execute([$cutoff]);
    return $stmt->rowCount();
}

function upsert_device(PDO $db, array $body, string $deviceId, string $now, string $ip): void
{
    $availablePorts = [];
    if (isset($body['available_ports']) && is_array($body['available_ports'])) {
        foreach ($body['available_ports'] as $port) {
            $port = clean_string((string)$port, 80);
            if ($port !== '') {
                $availablePorts[] = $port;
            }
        }
    }
    $availablePorts = array_values(array_unique($availablePorts));
    $availableBaudrates = [];
    if (isset($body['available_baudrates']) && is_array($body['available_baudrates'])) {
        foreach ($body['available_baudrates'] as $baudrate) {
            $baudrate = (int)$baudrate;
            if ($baudrate > 0) {
                $availableBaudrates[] = $baudrate;
            }
        }
    }
    $availableBaudrates = array_values(array_unique($availableBaudrates));
    sort($availableBaudrates);

    $stmt = $db->prepare('
        INSERT INTO devices (device_id, device_name, app_type, app_version, hostname, connection_type, port, available_ports, available_baudrates, baudrate, last_seen, last_ip, created_at, updated_at)
        VALUES (:device_id, :device_name, :app_type, :app_version, :hostname, :connection_type, :port, :available_ports, :available_baudrates, :baudrate, :last_seen, :last_ip, :created_at, :updated_at)
        ON CONFLICT(device_id) DO UPDATE SET
            device_name = excluded.device_name,
            app_type = excluded.app_type,
            app_version = excluded.app_version,
            hostname = excluded.hostname,
            connection_type = excluded.connection_type,
            port = excluded.port,
            available_ports = excluded.available_ports,
            available_baudrates = excluded.available_baudrates,
            baudrate = excluded.baudrate,
            last_seen = excluded.last_seen,
            last_ip = excluded.last_ip,
            updated_at = excluded.updated_at
    ');

    $stmt->execute([
        ':device_id' => $deviceId,
        ':device_name' => clean_string($body['device_name'] ?? $deviceId, 200),
        ':app_type' => clean_string($body['app_type'] ?? 'SST', 40),
        ':app_version' => clean_string($body['app_version'] ?? '', 40),
        ':hostname' => clean_string($body['hostname'] ?? '', 200),
        ':connection_type' => clean_string($body['connection_type'] ?? '', 40),
        ':port' => clean_string($body['port'] ?? '', 80),
        ':available_ports' => json_encode($availablePorts, JSON_UNESCAPED_UNICODE),
        ':available_baudrates' => json_encode($availableBaudrates, JSON_UNESCAPED_UNICODE),
        ':baudrate' => isset($body['baudrate']) ? (int)$body['baudrate'] : null,
        ':last_seen' => $now,
        ':last_ip' => $ip,
        ':created_at' => $now,
        ':updated_at' => $now,
    ]);
}

function evaluate_rules(PDO $db, string $deviceId, string $data, string $createdAt): string
{
    $rules = $db->query("SELECT * FROM rules WHERE enabled = 1 AND (device_id IS NULL OR device_id = '' OR device_id = " . $db->quote($deviceId) . ")")->fetchAll(PDO::FETCH_ASSOC);
    $level = 'info';

    foreach ($rules as $rule) {
        if (!rule_matches((string)$rule['match_type'], (string)$rule['pattern'], $data)) {
            continue;
        }

        $ruleLevel = clean_string($rule['highlight_level'] ?? 'warning', 40);
        if ((int)$rule['action_highlight'] === 1 && severity_rank($ruleLevel) > severity_rank($level)) {
            $level = $ruleLevel;
        }

        $cooldown = max(0, (int)$rule['cooldown_seconds']);
        $lastTriggered = $rule['last_triggered_at'] ? strtotime((string)$rule['last_triggered_at']) : 0;
        $cooldownExpired = $lastTriggered === 0 || (time() - $lastTriggered) >= $cooldown;

        if (!$cooldownExpired) {
            continue;
        }

        $message = '[' . $deviceId . '] ' . (string)$rule['name'] . ': ' . $data;
        if ((int)$rule['action_alarm'] === 1) {
            $alarm = $db->prepare('INSERT INTO alarms (device_id, rule_id, created_at, message, status) VALUES (?, ?, ?, ?, ?)');
            $alarm->execute([$deviceId, (int)$rule['id'], $createdAt, $message, 'active']);
        }

        if ((int)$rule['action_email'] === 1) {
            send_alarm_email((string)$rule['name'], $message);
        }

        $update = $db->prepare('UPDATE rules SET last_triggered_at = ? WHERE id = ?');
        $update->execute([$createdAt, (int)$rule['id']]);
    }

    return $level;
}

function severity_rank(string $level): int
{
    $level = strtolower($level);
    return ['info' => 0, 'notice' => 1, 'warning' => 2, 'error' => 3, 'alarm' => 4][$level] ?? 1;
}

function rule_matches(string $type, string $pattern, string $data): bool
{
    if ($pattern === '') {
        return false;
    }

    $type = strtolower($type);
    if ($type === 'contains') {
        return stripos($data, $pattern) !== false;
    }

    if ($type === 'wildcard') {
        $regex = '/^' . str_replace('\*', '.*', preg_quote($pattern, '/')) . '$/iu';
        return preg_match($regex, $data) === 1;
    }

    if ($type === 'regex') {
        set_error_handler(static function (): bool { return true; });
        $result = preg_match('/' . str_replace('/', '\/', $pattern) . '/u', $data) === 1;
        restore_error_handler();
        return $result;
    }

    return false;
}

function send_alarm_email(string $subject, string $message): void
{
    if (ADMIN_EMAIL === '' || ADMIN_EMAIL === 'admin@example.com') {
        return;
    }

    $headers = 'From: ' . EMAIL_FROM . "\r\n" .
        'Content-Type: text/plain; charset=UTF-8';
    @mail(ADMIN_EMAIL, 'SST Web Monitor: ' . $subject, $message, $headers);
}

function devices(): void
{
    $rows = db()->query('SELECT * FROM devices ORDER BY last_seen DESC')->fetchAll(PDO::FETCH_ASSOC);
    $now = time();
    foreach ($rows as &$row) {
        $lastSeen = $row['last_seen'] ? strtotime((string)$row['last_seen']) : 0;
        $row['online'] = $lastSeen > 0 && ($now - $lastSeen) <= DEVICE_OFFLINE_TIMEOUT;
        if (!$row['online']) {
            $row['runtime_status'] = 'offline';
        } else {
            $connectionType = strtoupper((string)($row['connection_type'] ?? ''));
            $row['runtime_status'] = ($connectionType === 'COM' || $connectionType === 'TCP') ? 'online' : 'ready';
        }
    }
    respond(['ok' => true, 'devices' => $rows, 'offline_timeout' => DEVICE_OFFLINE_TIMEOUT]);
}

function delete_device(array $body): void
{
    require_admin_token($body);

    $deviceId = clean_string($body['device_id'] ?? '', 120);
    if ($deviceId === '') {
        respond(['ok' => false, 'error' => 'device_id is required.'], 400);
    }

    $db = db();
    $db->beginTransaction();
    try {
        $deleted = 0;
        foreach (['logs', 'alarms', 'commands', 'rules', 'devices'] as $table) {
            $stmt = $db->prepare("DELETE FROM {$table} WHERE device_id = ?");
            $stmt->execute([$deviceId]);
            $deleted += $stmt->rowCount();
        }

        $db->commit();
        respond(['ok' => true, 'deleted' => $deleted]);
    } catch (Throwable $e) {
        $db->rollBack();
        respond(['ok' => false, 'error' => $e->getMessage()], 500);
    }
}

function delete_logs(array $body): void
{
    require_admin_token($body);

    $deviceId = clean_string($body['device_id'] ?? '', 120);
    if ($deviceId === '') {
        respond(['ok' => false, 'error' => 'device_id is required.'], 400);
    }

    $stmt = db()->prepare('DELETE FROM logs WHERE device_id = ?');
    $stmt->execute([$deviceId]);

    respond(['ok' => true, 'deleted' => $stmt->rowCount()]);
}

function settings(array $body): void
{
    if (!is_default_admin_token()) {
        require_admin_token($body);
    }

    respond([
        'ok' => true,
        'settings' => [
            'api_token_configured' => API_TOKEN !== '' && strpos(API_TOKEN, 'CHANGE_ME') !== 0,
            'admin_token_configured' => !is_default_admin_token(),
            'admin_email' => ADMIN_EMAIL,
            'email_from' => EMAIL_FROM,
            'device_offline_timeout' => DEVICE_OFFLINE_TIMEOUT,
            'log_retention_days' => config_int('LOG_RETENTION_DAYS', 30),
        ],
    ]);
}

function save_settings(array $body): void
{
    if (!is_default_admin_token()) {
        require_admin_token($body);
    }

    $apiToken = clean_string($body['api_token'] ?? '', 200);
    $adminToken = clean_string($body['admin_token'] ?? '', 200);
    $adminEmail = clean_string($body['admin_email'] ?? '', 200);
    $emailFrom = clean_string($body['email_from'] ?? '', 200);
    $offlineTimeout = isset($body['device_offline_timeout']) ? (int)$body['device_offline_timeout'] : DEVICE_OFFLINE_TIMEOUT;
    $logRetentionDays = isset($body['log_retention_days']) ? (int)$body['log_retention_days'] : config_int('LOG_RETENTION_DAYS', 30);

    if ($apiToken !== '' && strlen($apiToken) < 16) {
        respond(['ok' => false, 'error' => 'API token is too short.'], 400);
    }

    if ($adminToken !== '' && strlen($adminToken) < 16) {
        respond(['ok' => false, 'error' => 'Admin token is too short.'], 400);
    }

    if ($adminEmail !== '' && !filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
        respond(['ok' => false, 'error' => 'Admin e-mail is invalid.'], 400);
    }

    if ($emailFrom !== '' && !filter_var($emailFrom, FILTER_VALIDATE_EMAIL)) {
        respond(['ok' => false, 'error' => 'Sender e-mail is invalid.'], 400);
    }

    $configPath = __DIR__ . '/config.php';
    if (!is_writable($configPath)) {
        respond(['ok' => false, 'error' => 'config.php is not writable.'], 500);
    }

    $content = file_get_contents($configPath);
    if ($content === false) {
        respond(['ok' => false, 'error' => 'Unable to read config.php.'], 500);
    }

    try {
        if ($apiToken !== '') {
            $content = update_config_constant($content, 'API_TOKEN', $apiToken);
        }

        if ($adminToken !== '') {
            $content = update_config_constant($content, 'ADMIN_TOKEN', $adminToken);
        }

        $content = update_config_constant($content, 'ADMIN_EMAIL', $adminEmail);
        $content = update_config_constant($content, 'EMAIL_FROM', $emailFrom);
        $content = update_config_int_constant($content, 'DEVICE_OFFLINE_TIMEOUT', max(10, min(3600, $offlineTimeout)));
        $content = update_config_int_constant($content, 'LOG_RETENTION_DAYS', max(0, min(3650, $logRetentionDays)));

        if (file_put_contents($configPath, $content, LOCK_EX) === false) {
            respond(['ok' => false, 'error' => 'Unable to write config.php.'], 500);
        }

        purge_old_logs(db());
        respond(['ok' => true]);
    } catch (Throwable $e) {
        respond(['ok' => false, 'error' => $e->getMessage()], 500);
    }
}

function logs(): void
{
    $deviceId = clean_string($_GET['device_id'] ?? '', 120);
    $limit = min(1000, max(1, (int)($_GET['limit'] ?? 250)));
    if ($deviceId === '') {
        respond(['ok' => false, 'error' => 'device_id is required.'], 400);
    }

    $stmt = db()->prepare('SELECT * FROM logs WHERE device_id = ? ORDER BY id DESC LIMIT ?');
    $stmt->bindValue(1, $deviceId, PDO::PARAM_STR);
    $stmt->bindValue(2, $limit, PDO::PARAM_INT);
    $stmt->execute();
    $rows = array_reverse($stmt->fetchAll(PDO::FETCH_ASSOC));
    respond(['ok' => true, 'logs' => $rows]);
}

function export_logs(): void
{
    $deviceId = clean_string($_GET['device_id'] ?? '', 120);
    $format = strtolower(clean_string($_GET['format'] ?? 'txt', 12));
    $limit = min(20000, max(1, (int)($_GET['limit'] ?? 5000)));

    if ($deviceId === '') {
        respond(['ok' => false, 'error' => 'device_id is required.'], 400);
    }

    if (!in_array($format, ['txt', 'csv'], true)) {
        respond(['ok' => false, 'error' => 'format must be txt or csv.'], 400);
    }

    $stmt = db()->prepare('SELECT * FROM logs WHERE device_id = ? ORDER BY id DESC LIMIT ?');
    $stmt->bindValue(1, $deviceId, PDO::PARAM_STR);
    $stmt->bindValue(2, $limit, PDO::PARAM_INT);
    $stmt->execute();
    $rows = array_reverse($stmt->fetchAll(PDO::FETCH_ASSOC));

    $safeName = preg_replace('/[^a-z0-9_-]+/i', '_', $deviceId) ?: 'sst';
    $date = (new DateTimeImmutable('now'))->format('Ymd_His');

    if ($format === 'csv') {
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $safeName . '_' . $date . '.csv"');
        $out = fopen('php://output', 'wb');
        fputcsv($out, ['id', 'created_at', 'direction', 'level', 'format', 'data'], ';');
        foreach ($rows as $row) {
            fputcsv($out, [
                $row['id'] ?? '',
                $row['created_at'] ?? '',
                $row['direction'] ?? '',
                $row['level'] ?? '',
                $row['format'] ?? '',
                $row['data'] ?? '',
            ], ';');
        }
        fclose($out);
        exit;
    }

    header('Content-Type: text/plain; charset=utf-8');
    header('Content-Disposition: attachment; filename="' . $safeName . '_' . $date . '.txt"');
    foreach ($rows as $row) {
        echo '[' . format_export_time((string)($row['created_at'] ?? '')) . '] '
            . str_pad((string)($row['direction'] ?? ''), 3) . ' '
            . str_pad(strtoupper((string)($row['level'] ?? '')), 7) . ' '
            . rtrim((string)($row['data'] ?? ''), "\r\n")
            . "\r\n";
    }
    exit;
}

function format_export_time(string $value): string
{
    return substr(str_replace('T', ' ', $value), 0, 23);
}

function rules(): void
{
    $rows = db()->query('SELECT * FROM rules ORDER BY id DESC')->fetchAll(PDO::FETCH_ASSOC);
    respond(['ok' => true, 'rules' => $rows]);
}

function save_rule(array $body): void
{
    require_admin_token($body);

    $db = db();
    $id = (int)($body['id'] ?? 0);
    $values = [
        'device_id' => clean_string($body['device_id'] ?? '', 120),
        'name' => clean_string($body['name'] ?? 'Rule', 200),
        'enabled' => !empty($body['enabled']) ? 1 : 0,
        'match_type' => clean_string($body['match_type'] ?? 'contains', 40),
        'pattern' => clean_string($body['pattern'] ?? '', 1000),
        'action_email' => !empty($body['action_email']) ? 1 : 0,
        'action_alarm' => !empty($body['action_alarm']) ? 1 : 0,
        'action_highlight' => !empty($body['action_highlight']) ? 1 : 0,
        'highlight_level' => clean_string($body['highlight_level'] ?? 'warning', 40),
        'cooldown_seconds' => max(0, (int)($body['cooldown_seconds'] ?? 1800)),
    ];

    if ($id > 0) {
        $stmt = $db->prepare('UPDATE rules SET device_id = :device_id, name = :name, enabled = :enabled, match_type = :match_type, pattern = :pattern, action_email = :action_email, action_alarm = :action_alarm, action_highlight = :action_highlight, highlight_level = :highlight_level, cooldown_seconds = :cooldown_seconds WHERE id = :id');
        $values['id'] = $id;
    } else {
        $stmt = $db->prepare('INSERT INTO rules (device_id, name, enabled, match_type, pattern, action_email, action_alarm, action_highlight, highlight_level, cooldown_seconds) VALUES (:device_id, :name, :enabled, :match_type, :pattern, :action_email, :action_alarm, :action_highlight, :highlight_level, :cooldown_seconds)');
    }

    $stmt->execute($values);
    respond(['ok' => true]);
}

function delete_rule(array $body): void
{
    require_admin_token($body);

    $id = (int)($body['id'] ?? 0);
    if ($id <= 0) {
        respond(['ok' => false, 'error' => 'id is required.'], 400);
    }

    $stmt = db()->prepare('DELETE FROM rules WHERE id = ?');
    $stmt->execute([$id]);
    respond(['ok' => true, 'deleted' => $stmt->rowCount()]);
}

function alarms(): void
{
    $rows = db()->query("SELECT * FROM alarms WHERE status = 'active' ORDER BY created_at DESC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);
    respond(['ok' => true, 'alarms' => $rows]);
}

function ack_alarm(array $body): void
{
    require_admin_token($body);
    $id = (int)($body['id'] ?? 0);
    if ($id <= 0) {
        respond(['ok' => false, 'error' => 'id is required.'], 400);
    }

    $stmt = db()->prepare("UPDATE alarms SET status = 'acknowledged', acknowledged_at = ?, acknowledged_by = ? WHERE id = ?");
    $stmt->execute([now_iso(), clean_string($body['acknowledged_by'] ?? 'web', 120), $id]);
    respond(['ok' => true]);
}

function send_command(array $body): void
{
    require_admin_token($body);

    $deviceId = clean_string($body['device_id'] ?? '', 120);
    $command = (string)($body['command'] ?? '');
    $format = clean_string($body['format'] ?? 'text', 20);

    if ($deviceId === '') {
        respond(['ok' => false, 'error' => 'device_id is required.'], 400);
    }

    if ($command === '') {
        respond(['ok' => false, 'error' => 'command is required.'], 400);
    }

    if ($format !== 'text' && $format !== 'hex') {
        respond(['ok' => false, 'error' => 'format must be text or hex.'], 400);
    }

    $stmt = db()->prepare('
        INSERT INTO commands (device_id, created_at, command, command_type, format, add_asterisk, add_cr, add_lf, status)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    ');
    $stmt->execute([
        $deviceId,
        now_iso(),
        substr($command, 0, MAX_LOG_LENGTH),
        'send',
        $format,
        !empty($body['add_asterisk']) ? 1 : 0,
        !empty($body['add_cr']) ? 1 : 0,
        !empty($body['add_lf']) ? 1 : 0,
        'pending',
    ]);

    respond(['ok' => true, 'id' => (int)db()->lastInsertId()]);
}

function send_connection_command(array $body): void
{
    require_admin_token($body);

    $deviceId = clean_string($body['device_id'] ?? '', 120);
    $action = clean_string($body['action'] ?? '', 40);

    if ($deviceId === '') {
        respond(['ok' => false, 'error' => 'device_id is required.'], 400);
    }

    if (!in_array($action, ['disconnect', 'connect_com', 'connect_tcp'], true)) {
        respond(['ok' => false, 'error' => 'invalid connection action.'], 400);
    }

    $payload = [
        'action' => $action,
        'connection_type' => clean_string($body['connection_type'] ?? '', 20),
        'port' => clean_string($body['port'] ?? '', 80),
        'baudrate' => isset($body['baudrate']) ? (int)$body['baudrate'] : null,
        'ip_address' => clean_string($body['ip_address'] ?? '', 80),
        'tcp_port' => isset($body['tcp_port']) ? (int)$body['tcp_port'] : null,
    ];

    $stmt = db()->prepare('
        INSERT INTO commands (device_id, created_at, command, command_type, payload, format, status)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    ');
    $stmt->execute([
        $deviceId,
        now_iso(),
        $action,
        'connection',
        json_encode($payload, JSON_UNESCAPED_UNICODE),
        'json',
        'pending',
    ]);

    respond(['ok' => true, 'id' => (int)db()->lastInsertId()]);
}

function get_commands(array $body): void
{
    require_api_token($body);

    $deviceId = clean_string($body['device_id'] ?? '', 120);
    if ($deviceId === '') {
        respond(['ok' => false, 'error' => 'device_id is required.'], 400);
    }

    $db = db();
    $db->beginTransaction();
    try {
        $stmt = $db->prepare("SELECT * FROM commands WHERE device_id = ? AND status = 'pending' ORDER BY id ASC LIMIT 5");
        $stmt->execute([$deviceId]);
        $commands = $stmt->fetchAll(PDO::FETCH_ASSOC);

        if ($commands) {
            $ids = [];
            foreach ($commands as $command) {
                $ids[] = (int)$command['id'];
            }

            $placeholders = implode(',', array_fill(0, count($ids), '?'));
            $update = $db->prepare("UPDATE commands SET status = 'sent', sent_at = ? WHERE id IN ($placeholders)");
            $update->execute(array_merge([now_iso()], $ids));
        }

        $db->commit();
        respond(['ok' => true, 'commands' => $commands, 'enabled' => true]);
    } catch (Throwable $e) {
        $db->rollBack();
        respond(['ok' => false, 'error' => $e->getMessage()], 500);
    }
}

function ack_command(array $body): void
{
    require_api_token($body);

    $id = (int)($body['id'] ?? 0);
    $status = clean_string($body['status'] ?? '', 20);
    $response = clean_string($body['response'] ?? '', 1000);

    if ($id <= 0) {
        respond(['ok' => false, 'error' => 'id is required.'], 400);
    }

    if ($status !== 'done' && $status !== 'failed') {
        respond(['ok' => false, 'error' => 'status must be done or failed.'], 400);
    }

    $stmt = db()->prepare('UPDATE commands SET status = ?, response = ?, done_at = ? WHERE id = ?');
    $stmt->execute([$status, $response, now_iso(), $id]);
    respond(['ok' => true]);
}

$action = (string)($_GET['action'] ?? '');
$body = $_SERVER['REQUEST_METHOD'] === 'POST' ? read_json() : [];

try {
    switch ($action) {
        case 'push-log':
            push_log($body);
            break;
        case 'devices':
            devices();
            break;
        case 'delete-device':
            delete_device($body);
            break;
        case 'delete-logs':
            delete_logs($body);
            break;
        case 'settings':
            settings($body);
            break;
        case 'save-settings':
            save_settings($body);
            break;
        case 'logs':
            logs();
            break;
        case 'export-logs':
            export_logs();
            break;
        case 'rules':
            rules();
            break;
        case 'save-rule':
            save_rule($body);
            break;
        case 'delete-rule':
            delete_rule($body);
            break;
        case 'alarms':
            alarms();
            break;
        case 'ack-alarm':
            ack_alarm($body);
            break;
        case 'send-command':
            send_command($body);
            break;
        case 'send-connection-command':
            send_connection_command($body);
            break;
        case 'get-commands':
            get_commands($body);
            break;
        case 'ack-command':
            ack_command($body);
            break;
        default:
            respond(['ok' => false, 'error' => 'Unknown action.'], 404);
    }
} catch (Throwable $e) {
    respond(['ok' => false, 'error' => $e->getMessage()], 500);
}
