<?php
/**
 * FLOWB0T NEXUS - Unified Logging System
 * Supports file logging and database logging
 *
 * @package Flowb0t\Core
 * @version 1.0.0
 */

namespace Flowb0t\Core;

class Logger {
    const DEBUG = 'DEBUG';
    const INFO = 'INFO';
    const WARNING = 'WARNING';
    const ERROR = 'ERROR';
    const CRITICAL = 'CRITICAL';

    private ?string $jobId;
    private string $logDir;
    private bool $logToFile = true;
    private bool $logToDb = true;
    private string $minLevel = self::DEBUG;
    private ?Database $db = null;

    private static array $levels = [
        'DEBUG' => 0,
        'INFO' => 1,
        'WARNING' => 2,
        'ERROR' => 3,
        'CRITICAL' => 4
    ];

    /**
     * Constructor
     *
     * @param string|null $jobId Optional job ID for context
     */
    public function __construct(?string $jobId = null) {
        $this->jobId = $jobId;
        $this->logDir = dirname(__DIR__) . '/logs';
        $this->ensureLogDirectory();
    }

    /**
     * Ensure log directory exists
     */
    private function ensureLogDirectory(): void {
        $dirs = [
            $this->logDir,
            $this->logDir . '/app',
            $this->logDir . '/crawl',
            $this->logDir . '/error',
            $this->logDir . '/access'
        ];

        foreach ($dirs as $dir) {
            if (!is_dir($dir)) {
                mkdir($dir, 0777, true);
            }
        }
    }

    /**
     * Set minimum log level
     */
    public function setMinLevel(string $level): self {
        $this->minLevel = $level;
        return $this;
    }

    /**
     * Enable/disable file logging
     */
    public function setFileLogging(bool $enabled): self {
        $this->logToFile = $enabled;
        return $this;
    }

    /**
     * Enable/disable database logging
     */
    public function setDbLogging(bool $enabled): self {
        $this->logToDb = $enabled;
        return $this;
    }

    /**
     * Set job ID for context
     */
    public function setJobId(string $jobId): self {
        $this->jobId = $jobId;
        return $this;
    }

    /**
     * Log a debug message
     */
    public function debug(string $category, string $message, array $context = []): void {
        $this->log(self::DEBUG, $category, $message, $context);
    }

    /**
     * Log an info message
     */
    public function info(string $category, string $message, array $context = []): void {
        $this->log(self::INFO, $category, $message, $context);
    }

    /**
     * Log a warning message
     */
    public function warning(string $category, string $message, array $context = []): void {
        $this->log(self::WARNING, $category, $message, $context);
    }

    /**
     * Log an error message
     */
    public function error(string $category, string $message, array $context = []): void {
        $this->log(self::ERROR, $category, $message, $context);
    }

    /**
     * Log a critical message
     */
    public function critical(string $category, string $message, array $context = []): void {
        $this->log(self::CRITICAL, $category, $message, $context);
    }

    /**
     * Main log method
     */
    public function log(string $level, string $category, string $message, array $context = []): void {
        // Check minimum level
        if (self::$levels[$level] < self::$levels[$this->minLevel]) {
            return;
        }

        $timestamp = date('Y-m-d H:i:s.') . substr(microtime(), 2, 3);

        // Add job ID to context if available
        if ($this->jobId) {
            $context['job_id'] = $this->jobId;
        }

        // Log to file
        if ($this->logToFile) {
            $this->writeToFile($level, $category, $message, $context, $timestamp);
        }

        // Log to database
        if ($this->logToDb) {
            $this->writeToDb($level, $category, $message, $context);
        }
    }

    /**
     * Write log to file
     */
    private function writeToFile(string $level, string $category, string $message, array $context, string $timestamp): void {
        $date = date('Y-m-d');

        // Determine log file based on level
        $logType = match($level) {
            self::ERROR, self::CRITICAL => 'error',
            default => 'app'
        };

        if ($this->jobId) {
            $logType = 'crawl';
        }

        $filename = "{$this->logDir}/{$logType}/{$date}.log";

        // Format log entry
        $contextStr = !empty($context) ? ' ' . json_encode($context, JSON_UNESCAPED_SLASHES) : '';
        $entry = "[{$timestamp}] [{$level}] [{$category}] {$message}{$contextStr}\n";

        // Write to file
        file_put_contents($filename, $entry, FILE_APPEND | LOCK_EX);
    }

    /**
     * Write log to database
     */
    private function writeToDb(string $level, string $category, string $message, array $context): void {
        try {
            if ($this->db === null) {
                $this->db = Database::getInstance();
            }

            $this->db->insert('nexus_logs', [
                'job_id' => $this->jobId ? $this->getJobDbId($this->jobId) : null,
                'log_level' => $level,
                'category' => $category,
                'message' => $message,
                'context' => !empty($context) ? json_encode($context) : null
            ]);
        } catch (\Exception $e) {
            // If DB logging fails, log to file only
            $this->writeToFile(self::ERROR, 'LOGGER', 'Failed to write to database: ' . $e->getMessage(), [], date('Y-m-d H:i:s'));
        }
    }

    /**
     * Get numeric job ID from UUID
     */
    private function getJobDbId(string $jobUuid): ?int {
        try {
            $result = $this->db->fetchOne(
                "SELECT id FROM nexus_jobs WHERE job_uuid = ?",
                [$jobUuid]
            );
            return $result ? (int)$result['id'] : null;
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * Log HTTP access
     */
    public function access(string $method, string $uri, int $statusCode, float $responseTime, ?string $userAgent = null): void {
        $date = date('Y-m-d');
        $timestamp = date('Y-m-d H:i:s');
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'CLI';

        $entry = sprintf(
            "[%s] %s %s %s %d %.3fms %s\n",
            $timestamp,
            $ip,
            $method,
            $uri,
            $statusCode,
            $responseTime * 1000,
            $userAgent ?? '-'
        );

        $filename = "{$this->logDir}/access/{$date}.log";
        file_put_contents($filename, $entry, FILE_APPEND | LOCK_EX);
    }

    /**
     * Get recent logs for a job
     */
    public function getJobLogs(string $jobUuid, int $limit = 100, string $minLevel = self::DEBUG): array {
        try {
            if ($this->db === null) {
                $this->db = Database::getInstance();
            }

            $jobDbId = $this->getJobDbId($jobUuid);
            if (!$jobDbId) {
                return [];
            }

            $levelNum = self::$levels[$minLevel];
            $levels = array_filter(self::$levels, fn($l) => $l >= $levelNum);
            $levelNames = array_keys($levels);
            $placeholders = implode(',', array_fill(0, count($levelNames), '?'));

            return $this->db->fetchAll(
                "SELECT * FROM nexus_logs
                 WHERE job_id = ? AND log_level IN ({$placeholders})
                 ORDER BY created_at DESC
                 LIMIT ?",
                array_merge([$jobDbId], $levelNames, [$limit])
            );
        } catch (\Exception $e) {
            return [];
        }
    }

    /**
     * Clean old logs
     */
    public function cleanup(int $daysToKeep = 14): array {
        $deleted = [
            'files' => 0,
            'db_rows' => 0
        ];

        // Clean file logs
        $cutoffDate = date('Y-m-d', strtotime("-{$daysToKeep} days"));
        $logTypes = ['app', 'crawl', 'error', 'access'];

        foreach ($logTypes as $type) {
            $dir = "{$this->logDir}/{$type}";
            if (!is_dir($dir)) continue;

            foreach (glob("{$dir}/*.log") as $file) {
                $fileDate = basename($file, '.log');
                if ($fileDate < $cutoffDate) {
                    unlink($file);
                    $deleted['files']++;
                }
            }
        }

        // Clean database logs
        try {
            if ($this->db === null) {
                $this->db = Database::getInstance();
            }

            $deleted['db_rows'] = $this->db->delete(
                'nexus_logs',
                'created_at < DATE_SUB(NOW(), INTERVAL ? DAY)',
                [$daysToKeep]
            );
        } catch (\Exception $e) {
            // Ignore DB cleanup errors
        }

        return $deleted;
    }

    /**
     * Format bytes to human readable
     */
    public static function formatBytes(int $bytes): string {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $i = 0;
        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }
        return round($bytes, 2) . ' ' . $units[$i];
    }

    /**
     * Format duration to human readable
     */
    public static function formatDuration(float $seconds): string {
        if ($seconds < 1) {
            return round($seconds * 1000) . 'ms';
        }
        if ($seconds < 60) {
            return round($seconds, 2) . 's';
        }
        if ($seconds < 3600) {
            $mins = floor($seconds / 60);
            $secs = $seconds % 60;
            return "{$mins}m " . round($secs) . 's';
        }

        $hours = floor($seconds / 3600);
        $mins = floor(($seconds % 3600) / 60);
        $secs = $seconds % 60;
        return "{$hours}h {$mins}m " . round($secs) . 's';
    }
}
