<?php

/**
 * LogManager - Dynamic logging for CodePress CMS
 *
 * Supports three drivers:
 * - syslog:  Send log entries to a remote syslog server (UDP) or local syslog.
 * - sqlite:  Store log entries in a SQLite database (default when no syslog
 *            server is configured and SQLite is available).
 * - file:    Fallback to plain text files when SQLite is unavailable.
 *
 * Which event types are recorded is controlled dynamically via the
 * "logging.events" config section (admin, requests, errors, security,
 * content, system).
 */
class LogManager {
    const EVENT_ADMIN = 'admin';
    const EVENT_REQUESTS = 'requests';
    const EVENT_ERRORS = 'errors';
    const EVENT_SECURITY = 'security';
    const EVENT_CONTENT = 'content';
    const EVENT_SYSTEM = 'system';

    private static $config = null;
    private static $pdo = null;
    private static $dbPath = null;

    /**
     * Initialize the log manager with the logging config section.
     *
     * @param array $loggingConfig The "logging" section from config.json
     */
    public static function init(array $loggingConfig): void
    {
        self::$config = $loggingConfig;
        self::$dbPath = dirname(__DIR__, 3) . '/admin/storage/logs/codepress.sqlite';
    }

    /**
     * Whether logging is enabled at all.
     */
    public static function isEnabled(): bool
    {
        return !empty(self::$config['enabled']);
    }

    /**
     * Whether a given event type should be recorded.
     *
     * @param string $event One of the EVENT_* constants
     */
    public static function isEventEnabled(string $event): bool
    {
        if (!self::isEnabled()) {
            return false;
        }
        $events = self::$config['events'] ?? [];
        return !empty($events[$event]);
    }

    /**
     * Get the local storage driver: 'sqlite' (falling back to 'file' if
     * SQLite is unavailable). Syslog is an additional output, not a storage
     * driver, so it never replaces local storage.
     */
    public static function getDriver(): string
    {
        $driver = self::$config['driver'] ?? 'sqlite';

        if ($driver === 'sqlite') {
            return self::sqliteAvailable() ? 'sqlite' : 'file';
        }

        return 'file';
    }

    /**
     * Record a log entry if the event type is enabled.
     *
     * @param string $event Event type (EVENT_* constant)
     * @param string $level Log level (info, warning, error, debug)
     * @param string $message Log message
     * @param array $context Additional structured context
     */
    public static function log(string $event, string $level, string $message, array $context = []): void
    {
        if (!self::isEventEnabled($event)) {
            return;
        }

        $entry = [
            'time' => date('Y-m-d H:i:s'),
            'event' => $event,
            'level' => $level,
            'message' => $message,
            'ip' => $context['ip'] ?? (class_exists('RequestLogger') ? RequestLogger::getClientIp() : ''),
            'context' => $context,
        ];

        $driver = self::getDriver();

        // Always store locally (sqlite or file fallback) so the dynamic log
        // in the admin always has entries.
        if ($driver === 'sqlite') {
            self::writeSqlite($entry);
        } else {
            self::writeFile($entry);
        }

        // Additionally forward to a remote syslog server if one is configured.
        $syslogHost = trim(self::$config['syslog_host'] ?? '');
        if ($syslogHost !== '') {
            self::writeSyslog($entry);
        }
    }

    /**
     * Send a log entry to a remote syslog server over UDP.
     */
    private static function writeSyslog(array $entry): void
    {
        $host = trim(self::$config['syslog_host'] ?? '');
        $port = (int)(self::$config['syslog_port'] ?? 514);
        $facility = self::syslogFacility(self::$config['syslog_facility'] ?? 'local0');
        $ident = self::$config['syslog_ident'] ?? 'codepress';

        $severity = self::syslogSeverity($entry['level']);
        $pri = ($facility * 8) + $severity;

        $msg = '<' . $pri . '>' . date('M d H:i:s') . ' ' . $ident . '[' . getmypid() . ']: '
            . '[' . $entry['event'] . '] [' . $entry['level'] . '] ' . $entry['message'];

        $sock = @fsockopen('udp://' . $host, $port, $errno, $errstr, 2);
        if ($sock) {
            @fwrite($sock, $msg . "\n");
            @fclose($sock);
        }
    }

    /**
     * Store a log entry in the SQLite database.
     */
    private static function writeSqlite(array $entry): void
    {
        $pdo = self::getPdo();
        if ($pdo === null) {
            // SQLite failed -> fall back to file
            self::writeFile($entry);
            return;
        }

        try {
            $stmt = $pdo->prepare(
                'INSERT INTO logs (time, event, level, message, ip, context) VALUES (:time, :event, :level, :message, :ip, :context)'
            );
            $stmt->execute([
                ':time' => $entry['time'],
                ':event' => $entry['event'],
                ':level' => $entry['level'],
                ':message' => $entry['message'],
                ':ip' => $entry['ip'],
                ':context' => json_encode($entry['context']),
            ]);
        } catch (\Throwable $e) {
            self::writeFile($entry);
        }
    }

    /**
     * Append a log entry to a plain text file (fallback driver).
     */
    private static function writeFile(array $entry): void
    {
        $dir = dirname(self::$dbPath);
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        $file = $dir . '/codepress.log';
        $line = '[' . $entry['time'] . '] [' . $entry['event'] . '] [' . $entry['level'] . '] ['
            . $entry['ip'] . '] ' . $entry['message'] . "\n";
        @file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
    }

    /**
     * Get (and lazily create) the PDO connection to the SQLite database.
     */
    private static function getPdo(): ?\PDO
    {
        if (self::$pdo !== null) {
            return self::$pdo;
        }
        if (!self::sqliteAvailable()) {
            return null;
        }

        $dir = dirname(self::$dbPath);
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }

        try {
            self::$pdo = new \PDO('sqlite:' . self::$dbPath);
            self::$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
            self::$pdo->exec(
                'CREATE TABLE IF NOT EXISTS logs (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    time TEXT NOT NULL,
                    event TEXT NOT NULL,
                    level TEXT NOT NULL,
                    message TEXT NOT NULL,
                    ip TEXT,
                    context TEXT
                )'
            );
            self::$pdo->exec('CREATE INDEX IF NOT EXISTS idx_logs_event ON logs (event)');
            self::$pdo->exec('CREATE INDEX IF NOT EXISTS idx_logs_time ON logs (time)');
            return self::$pdo;
        } catch (\Throwable $e) {
            self::$pdo = null;
            return null;
        }
    }

    /**
     * Whether the SQLite PDO driver is available.
     */
    private static function sqliteAvailable(): bool
    {
        return class_exists('PDO') && in_array('sqlite', \PDO::getAvailableDrivers(), true);
    }

    /**
     * Map a facility name to its syslog numeric value.
     */
    private static function syslogFacility(string $facility): int
    {
        $map = [
            'kern' => 0, 'user' => 1, 'mail' => 2, 'daemon' => 3,
            'auth' => 4, 'syslog' => 5, 'lpr' => 6, 'news' => 7,
            'uucp' => 8, 'cron' => 9, 'authpriv' => 10, 'ftp' => 11,
            'local0' => 16, 'local1' => 17, 'local2' => 18, 'local3' => 19,
            'local4' => 20, 'local5' => 21, 'local6' => 22, 'local7' => 23,
        ];
        return $map[$facility] ?? 16;
    }

    /**
     * Map a log level to its syslog severity value.
     */
    private static function syslogSeverity(string $level): int
    {
        $map = [
            'debug' => 7,
            'info' => 6,
            'notice' => 5,
            'warning' => 4,
            'error' => 3,
            'critical' => 2,
            'alert' => 1,
            'emergency' => 0,
        ];
        return $map[strtolower($level)] ?? 6;
    }

    /**
     * Query recent log entries from the active store.
     *
     * @param int $limit Number of entries to return
     * @param string|null $event Optional event filter
     * @param string|null $level Optional level filter (info, warning, error, ...)
     * @param string|null $search Optional text search on the message
     * @return array List of log entries (newest first)
     */
    public static function getLogs(int $limit = 200, ?string $event = null, ?string $level = null, ?string $search = null): array
    {
        $driver = self::getDriver();

        if ($driver === 'sqlite') {
            $pdo = self::getPdo();
            if ($pdo !== null) {
                try {
                    $sql = 'SELECT time, event, level, message, ip FROM logs';
                    $conds = [];
                    $params = [];
                    if ($event !== null && $event !== '') {
                        $conds[] = 'event = :event';
                        $params[':event'] = $event;
                    }
                    if ($level !== null && $level !== '') {
                        $conds[] = 'level = :level';
                        $params[':level'] = $level;
                    }
                    if ($search !== null && $search !== '') {
                        $conds[] = 'message LIKE :search';
                        $params[':search'] = '%' . $search . '%';
                    }
                    if (!empty($conds)) {
                        $sql .= ' WHERE ' . implode(' AND ', $conds);
                    }
                    $sql .= ' ORDER BY id DESC LIMIT ' . (int)$limit;
                    $stmt = $pdo->prepare($sql);
                    $stmt->execute($params);
                    return $stmt->fetchAll(\PDO::FETCH_ASSOC);
                } catch (\Throwable $e) {
                    return [];
                }
            }
        }

        // File fallback
        $dir = dirname(self::$dbPath);
        $file = $dir . '/codepress.log';
        if (!file_exists($file)) {
            return [];
        }
        $lines = file($file);
        $lines = array_slice($lines, -$limit);
        $logs = [];
        foreach ($lines as $line) {
            if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] (.+)$/', trim($line), $m)) {
                if ($event !== null && $event !== '' && $m[2] !== $event) {
                    continue;
                }
                if ($level !== null && $level !== '' && $m[3] !== $level) {
                    continue;
                }
                if ($search !== null && $search !== '' && stripos($m[5], $search) === false) {
                    continue;
                }
                $logs[] = [
                    'time' => $m[1],
                    'event' => $m[2],
                    'level' => $m[3],
                    'ip' => $m[4],
                    'message' => $m[5],
                ];
            }
        }
        return array_reverse($logs);
    }

    /**
     * Clear all stored log entries.
     */
    public static function clear(): void
    {
        $driver = self::getDriver();
        if ($driver === 'sqlite') {
            $pdo = self::getPdo();
            if ($pdo !== null) {
                try {
                    $pdo->exec('DELETE FROM logs');
                    return;
                } catch (\Throwable $e) {
                    // fall through to file
                }
            }
        }
        $dir = dirname(self::$dbPath);
        $file = $dir . '/codepress.log';
        if (file_exists($file)) {
            @file_put_contents($file, '');
        }
    }
}
