#!/usr/bin/env php
<?php
/**
 * Process On-Demand Log Reviews
 * Runs every minute via cron. Picks up one pending log review request,
 * collects site logs, sends them to AI for analysis, and stores results.
 *
 * Only processes 1 review per run (expensive AI calls).
 */

// Prevent concurrent runs
$lockFile = fopen('/tmp/whp-log-review.lock', 'c');
if (!$lockFile || !flock($lockFile, LOCK_EX | LOCK_NB)) {
    // Another instance is running
    exit(0);
}

// Determine the correct web directory
if (file_exists('/docker/whp/web/libs/smtp_mailer.php')) {
    $webDir = '/docker/whp/web';
} else {
    $webDir = dirname(__DIR__) . '/web-files';
}

try {
    // Load dependencies
    require_once($webDir . '/libs/smtp_mailer.php');
    require_once($webDir . '/includes/email-templates/review-complete.php');

    // Connect to database (use same extraction method as config.php)
    $password = trim(shell_exec('cat /root/.my.cnf | grep password | cut -f2- -d='));
    $db = new PDO('mysql:host=127.0.0.1;dbname=whp;charset=utf8mb4', 'root', $password);
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Find the next pending review
    $stmt = $db->query("
        SELECT r.*,
               sub.username,
               sub.notification_email,
               s.site_name,
               s.primary_domain_id,
               s.container_count,
               d.domain_name AS primary_domain
        FROM whp.ai_monitor_log_reviews r
        JOIN whp.ai_monitor_subscriptions sub ON r.subscription_id = sub.id
        JOIN whp.sites s ON r.site_id = s.id
        JOIN whp.domains d ON s.primary_domain_id = d.id
        WHERE r.status = 'pending'
        ORDER BY r.requested_at
        LIMIT 1
    ");
    $review = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$review) {
        // No pending reviews
        flock($lockFile, LOCK_UN);
        fclose($lockFile);
        exit(0);
    }

    $reviewId = $review['id'];
    echo date('Y-m-d H:i:s') . " Processing log review #{$reviewId} for {$review['primary_domain']}\n";

    // Mark as processing
    $db->prepare("UPDATE whp.ai_monitor_log_reviews SET status = 'processing' WHERE id = ?")
       ->execute([$reviewId]);

    // Collect logs for the site
    $logs = collectSiteLogs($db, $review);

    // Sanitize logs (strip internal paths, IPs)
    $sanitizedLogs = sanitizeLogs($logs);

    // Truncate to ~32000 chars to stay within token limits
    if (strlen($sanitizedLogs) > 32000) {
        $sanitizedLogs = substr($sanitizedLogs, 0, 32000) . "\n\n[... truncated for AI analysis ...]";
    }

    if (empty(trim($sanitizedLogs))) {
        $sanitizedLogs = "No log data found for this site in the last 24 hours.";
    }

    // Load AI configuration
    $aiConfig = loadAiConfig();
    if (!$aiConfig) {
        throw new Exception('AI monitor configuration not found or incomplete');
    }

    // Check if the site's main page loads (curl)
    $domain = $review['primary_domain'];
    $initialCheck = checkUrl("https://{$domain}/");
    if ($initialCheck['http_code'] === 0) {
        // HTTPS failed, try HTTP
        $initialCheck = checkUrl("http://{$domain}/");
    }
    $siteCheckData = "=== Live Site Check: {$domain} ===\n"
        . "URL: {$initialCheck['url']}\n"
        . "HTTP Status: {$initialCheck['http_code']}\n"
        . "Response Time: {$initialCheck['time_total']}s\n"
        . "Redirect URL: " . ($initialCheck['redirect_url'] ?: 'none') . "\n"
        . "Error: " . ($initialCheck['error'] ?: 'none') . "\n"
        . "Content Length: {$initialCheck['size_download']} bytes\n";

    // --- Pass 1: Send logs + initial site check to AI ---
    $prompt = buildAiPrompt($review, $sanitizedLogs, $siteCheckData);
    $aiResult = callAiApi($aiConfig, $prompt);
    $totalTokens = $aiResult['tokens'] ?? 0;
    $parsed = parseAiResponse($aiResult['content']);

    // --- Pass 2: If AI requested additional page checks, run them ---
    $checkPages = $parsed['check_pages'] ?? [];
    if (!empty($checkPages) && is_array($checkPages)) {
        $maxPages = 5;
        $pageResults = [];
        $checkedCount = 0;

        foreach ($checkPages as $pagePath) {
            if ($checkedCount >= $maxPages) break;
            // Validate: must be a relative path on the same domain
            $pagePath = '/' . ltrim(trim($pagePath), '/');
            if (!isValidPagePath($pagePath)) continue;

            $pageUrl = "https://{$domain}{$pagePath}";
            $result = checkUrl($pageUrl);
            if ($result['http_code'] === 0) {
                $pageUrl = "http://{$domain}{$pagePath}";
                $result = checkUrl($pageUrl);
            }
            $pageResults[] = [
                'path' => $pagePath,
                'http_code' => $result['http_code'],
                'time_total' => $result['time_total'],
                'error' => $result['error'],
                'size_download' => $result['size_download'],
            ];
            $checkedCount++;
        }

        if (!empty($pageResults)) {
            echo date('Y-m-d H:i:s') . " Checked {$checkedCount} additional pages for review #{$reviewId}\n";

            $followUpPrompt = buildFollowUpPrompt($review, $parsed, $pageResults);
            $aiResult2 = callAiApi($aiConfig, $followUpPrompt);
            $totalTokens += $aiResult2['tokens'] ?? 0;
            $parsed = parseAiResponse($aiResult2['content']);
            // Carry over the page check results into the final output
            $parsed['pages_checked'] = $pageResults;
        }
    }

    // Store the initial check in the results
    $parsed['initial_site_check'] = [
        'url' => $initialCheck['url'],
        'http_code' => $initialCheck['http_code'],
        'time_total' => $initialCheck['time_total'],
        'error' => $initialCheck['error'],
    ];

    // Update review with results
    $db->prepare("
        UPDATE whp.ai_monitor_log_reviews
        SET status = 'completed',
            ai_model_used = ?,
            ai_tokens_used = ?,
            result_summary = ?,
            result_details = ?,
            completed_at = NOW()
        WHERE id = ?
    ")->execute([
        $aiResult['model'] ?? $aiConfig['model'],
        $totalTokens,
        $parsed['summary'] ?? 'Analysis complete',
        json_encode($parsed),
        $reviewId,
    ]);

    echo date('Y-m-d H:i:s') . " Review #{$reviewId} completed: " . ($parsed['overall_health'] ?? 'unknown') . "\n";

    // Send completion notification if email is set
    if (!empty($review['notification_email'])) {
        queueCompletionEmail($db, $review, $parsed);
    }

} catch (Exception $e) {
    echo date('Y-m-d H:i:s') . " ERROR processing review: " . $e->getMessage() . "\n";

    // Mark review as failed if we have a review ID
    if (isset($reviewId) && isset($db)) {
        try {
            $db->prepare("
                UPDATE whp.ai_monitor_log_reviews
                SET status = 'failed',
                    error_message = ?,
                    completed_at = NOW()
                WHERE id = ?
            ")->execute([substr($e->getMessage(), 0, 1000), $reviewId]);
        } catch (Exception $innerEx) {
            echo date('Y-m-d H:i:s') . " Failed to update review status: " . $innerEx->getMessage() . "\n";
        }
    }
} finally {
    flock($lockFile, LOCK_UN);
    fclose($lockFile);
}

/**
 * Collect logs from all sources for the given site.
 */
function collectSiteLogs(PDO $db, array $review): string {
    $domain = $review['primary_domain'];
    $username = $review['username'];
    $logs = '';

    // Generate container names from domain pattern
    // Container names use the safe FQDN with dots preserved (e.g., example.com-01)
    // Wildcard domains use 'wildcard.' prefix instead of '*.'
    $containers = [];
    $safe_domain = str_replace('*.', 'wildcard.', $domain);
    $container_count = intval($review['container_count'] ?? 1);
    for ($i = 1; $i <= $container_count; $i++) {
        $containers[] = $safe_domain . '-' . str_pad($i, 2, '0', STR_PAD_LEFT);
    }

    // Collect Docker container logs (using exec with validated container names)
    foreach ($containers as $containerName) {
        // Validate container name contains only safe characters
        if (!preg_match('/^[a-zA-Z0-9._-]+$/', $containerName)) {
            continue;
        }
        $safeContainerName = escapeshellarg($containerName);
        $output = shell_exec("docker logs {$safeContainerName} --since 24h --tail 500 2>&1");
        if (!empty($output)) {
            $logs .= "=== Container: {$containerName} ===\n";
            $logs .= $output . "\n\n";
        }
    }

    // Grep Apache error log for this domain (validate domain first)
    if (preg_match('/^[a-zA-Z0-9._-]+$/', $domain)) {
        $safeDomain = escapeshellarg($domain);
        $apacheErrors = shell_exec("grep -i {$safeDomain} /var/log/httpd/error_log 2>/dev/null | tail -200");
        if (!empty($apacheErrors)) {
            $logs .= "=== Apache Error Log ===\n";
            $logs .= $apacheErrors . "\n\n";
        }

        // Check dmesg for OOM kills related to this domain
        $oomOutput = shell_exec("dmesg --time-format iso 2>/dev/null | grep -iE 'oom|killed process' | grep -i {$safeDomain} 2>/dev/null | tail -50");
        if (!empty($oomOutput)) {
            $logs .= "=== OOM / Killed Processes ===\n";
            $logs .= $oomOutput . "\n\n";
        }
    }

    // Collect application logs from site's logs directory
    $safe_domain = str_replace('*.', 'wildcard.', $domain);
    $logs_dir = "/docker/users/{$username}/{$safe_domain}/logs";

    if (is_dir($logs_dir)) {
        $log_files = glob($logs_dir . '/*.log');
        if (!empty($log_files)) {
            $logs .= "\n\n=== Application Logs ({$logs_dir}) ===\n";
            foreach ($log_files as $log_file) {
                $basename = basename($log_file);
                // Read last 200 lines of each log file (tail)
                $file_content = '';
                $lines = @file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                if ($lines !== false && count($lines) > 0) {
                    $tail_lines = array_slice($lines, -200);
                    $file_content = implode("\n", $tail_lines);
                }
                if (!empty(trim($file_content))) {
                    $logs .= "\n--- {$basename} (last " . count($tail_lines) . " lines) ---\n";
                    $logs .= $file_content . "\n";
                }
            }
        }
    }

    return $logs;
}

/**
 * Curl a URL and return status info. Does not follow redirects to other domains.
 */
function checkUrl(string $url): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 3,
        CURLOPT_NOBODY         => false,
        CURLOPT_USERAGENT      => 'WHP-AI-Monitor/1.0 (Site Health Check)',
        CURLOPT_SSL_VERIFYPEER => false, // Sites may have self-signed certs
        CURLOPT_SSL_VERIFYHOST => 0,
    ]);
    curl_exec($ch);
    $info = curl_getinfo($ch);
    $error = curl_error($ch);
    curl_close($ch);

    return [
        'url'           => $url,
        'http_code'     => (int)($info['http_code'] ?? 0),
        'time_total'    => round($info['total_time'] ?? 0, 3),
        'redirect_url'  => $info['redirect_url'] ?? '',
        'size_download' => (int)($info['size_download'] ?? 0),
        'error'         => $error ?: null,
    ];
}

/**
 * Validate a page path is safe to curl (no off-domain requests, no protocol tricks).
 */
function isValidPagePath(string $path): bool {
    // Must start with /
    if ($path[0] !== '/') return false;
    // No protocol injection
    if (preg_match('#^//#', $path)) return false;
    // No path traversal
    if (strpos($path, '..') !== false) return false;
    // Only allow reasonable URL characters
    if (!preg_match('#^/[a-zA-Z0-9_./%+~@!&=,;:()-]*(\?[a-zA-Z0-9_./%+~@!&=,;:()-]*)?$#', $path)) return false;
    // Max length
    if (strlen($path) > 500) return false;
    return true;
}

/**
 * Remove internal paths, IPs, and sensitive info from log output.
 */
function sanitizeLogs(string $logs): string {
    // Strip internal filesystem paths
    $logs = str_replace('/docker/whp/', '[internal]/', $logs);
    $logs = str_replace('/etc/httpd/', '[config]/', $logs);
    $logs = str_replace('/root/', '[root]/', $logs);

    // Strip internal/private IPs (10.x, 172.16-31.x, 192.168.x)
    $logs = preg_replace('/\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/', '[internal-ip]', $logs);
    $logs = preg_replace('/\b172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}\b/', '[internal-ip]', $logs);
    $logs = preg_replace('/\b192\.168\.\d{1,3}\.\d{1,3}\b/', '[internal-ip]', $logs);

    return $logs;
}

/**
 * Load AI monitor configuration from /etc/whp/ai-monitor.conf.
 */
function loadAiConfig(): ?array {
    $configFile = '/etc/whp/ai-monitor.conf';
    if (!file_exists($configFile)) {
        return null;
    }

    $config = parse_ini_file($configFile, true);
    if (!$config || empty($config['ai'])) {
        return null;
    }

    $ai = $config['ai'];

    $baseUrl = $ai['base_url'] ?? '';
    $apiKeyFile = $ai['api_key_file'] ?? '';
    $model = $ai['basic_model'] ?? '';

    if (empty($baseUrl) || empty($apiKeyFile) || empty($model)) {
        return null;
    }

    // Read API key from file
    if (!file_exists($apiKeyFile)) {
        return null;
    }
    $apiKey = trim(file_get_contents($apiKeyFile));
    if (empty($apiKey)) {
        return null;
    }

    return [
        'base_url' => rtrim($baseUrl, '/'),
        'api_key'  => $apiKey,
        'model'    => $model,
    ];
}

/**
 * Build the AI analysis prompt (pass 1: logs + initial site check).
 */
function buildAiPrompt(array $review, string $sanitizedLogs, string $siteCheckData): string {
    $requestType = $review['request_type'] ?? 'general';

    return <<<PROMPT
You are a web hosting support engineer analyzing logs for a customer's website.
Site: {$review['site_name']}
Domain: {$review['primary_domain']}
Review type: {$requestType}

The logs below may include Docker container logs, Apache error logs, OOM/killed process events, and application logs (e.g. Node.js, PM2, or other runtime logs from the site's logs directory).

We also performed a live HTTP check of the site's main page — those results are included below.

IGNORE THE FOLLOWING — these are normal and expected during container startup:
- "No such file or directory" errors for removing default config files (e.g. /etc/php-fpm.d/www.conf, /etc/httpd/conf.d/*.conf). The platform removes default configs to apply its own.
- "user already exists" or "useradd: user already exists" warnings. The platform runs idempotent startup scripts that ensure the user exists.
- "group already exists" or similar account-creation warnings.
- Any errors from entrypoint/init scripts related to removing files, creating users/groups, or setting permissions — these are part of the normal container bootstrap process.
- WordPress sodium_compat / ParagonIE_Sodium_* class loading errors. WordPress bundles a sodium polyfill for servers without the native PHP sodium extension. On PHP 8.x the native extension is already loaded, so autoloader probes for the polyfill classes fail harmlessly. These are NOT real fatal errors.
- WordPress autoloader "class not found" or "file not found" messages for sodium_compat, Requests, PHPMailer, or other bundled libraries — these are normal autoloader behavior, not missing dependencies.
Do NOT report these as findings.

ANALYSIS INSTRUCTIONS:
1. Use the live site check results to determine if the site is LOADING PROPERLY. An HTTP 200 with reasonable response time and content length means the site loads. A 5xx, timeout, or connection refused means it does not.

2. For each issue found, assess its REAL IMPACT on site visitors:
   - "site_down": The site cannot load at all (container crash, fatal startup error, port not listening)
   - "partial_outage": Major functionality is broken (database connection failures, auth system down)
   - "degraded": Specific features or pages are affected (certain API routes failing, intermittent timeouts)
   - "cosmetic": Minor issues that don't affect core functionality (deprecation warnings, non-critical log noise)

3. Write recommendations as if speaking to a website owner, not a system administrator. Be specific and actionable.

4. If the logs mention errors on SPECIFIC PAGES or URL paths (e.g. 404s on /api/something, 500 on /checkout, PHP errors in a specific file), you may request that we check up to 5 of those pages. Include a "check_pages" array with the URL paths you want tested. These must be relative paths on the same domain (e.g. "/contact", "/api/health"). Only request pages that are likely public-facing (not admin panels, login pages, or authenticated areas). If you don't need any page checks, omit "check_pages" or set it to an empty array.

Respond with JSON in this exact structure:
{
  "site_status": {
    "is_loading": true/false,
    "confidence": "high|medium|low",
    "explanation": "Brief explanation based on the live site check and log analysis"
  },
  "summary": "2-3 sentence plain-English overview of the site's health and any action needed",
  "overall_health": "healthy|degraded|critical",
  "findings": [
    {
      "severity": "critical|warning|info",
      "impact": "site_down|partial_outage|degraded|cosmetic",
      "title": "Short, clear title",
      "description": "What is happening, written for a site owner to understand",
      "recommendation": "Specific steps the site owner can take to resolve this",
      "affects_loading": true/false
    }
  ],
  "check_pages": ["/path/to/check", "/another/path"]
}

Sort findings by severity (critical first), then by impact. If there are no issues, return an empty findings array with overall_health "healthy".

{$siteCheckData}

Logs:
{$sanitizedLogs}
PROMPT;
}

/**
 * Build follow-up prompt with page check results (pass 2).
 */
function buildFollowUpPrompt(array $review, array $firstPassResults, array $pageResults): string {
    $pageCheckText = "=== Additional Page Check Results ===\n";
    foreach ($pageResults as $pr) {
        $status = $pr['http_code'] ?: 'connection failed';
        $pageCheckText .= "  {$pr['path']}: HTTP {$status}, {$pr['time_total']}s, {$pr['size_download']} bytes";
        if ($pr['error']) $pageCheckText .= " (error: {$pr['error']})";
        $pageCheckText .= "\n";
    }

    $firstPassJson = json_encode($firstPassResults, JSON_PRETTY_PRINT);

    return <<<PROMPT
You are continuing a site health review for {$review['site_name']} ({$review['primary_domain']}).

In the first pass, you analyzed the logs and live site check and produced this preliminary report:
{$firstPassJson}

You requested checks on specific pages. Here are the results:
{$pageCheckText}

Based on these additional page check results, provide your FINAL updated report. Update your findings, site_status, summary, and overall_health to reflect what you now know from the actual page responses. If a page you were concerned about returns 200 OK, downgrade or remove that finding. If a page returns 5xx or fails, confirm or escalate the finding.

Respond with the same JSON structure (without "check_pages" this time):
{
  "site_status": {
    "is_loading": true/false,
    "confidence": "high|medium|low",
    "explanation": "Updated explanation with page check evidence"
  },
  "summary": "Updated 2-3 sentence overview",
  "overall_health": "healthy|degraded|critical",
  "findings": [
    {
      "severity": "critical|warning|info",
      "impact": "site_down|partial_outage|degraded|cosmetic",
      "title": "Short, clear title",
      "description": "Updated description reflecting page check results",
      "recommendation": "Specific steps the site owner can take",
      "affects_loading": true/false
    }
  ]
}
PROMPT;
}

/**
 * Call the AI API (OpenAI-compatible via LiteLLM proxy).
 */
function callAiApi(array $aiConfig, string $prompt): array {
    $url = $aiConfig['base_url'] . '/chat/completions';

    $payload = json_encode([
        'model'       => $aiConfig['model'],
        'messages'    => [
            ['role' => 'user', 'content' => $prompt],
        ],
        'temperature' => 0.3,
    ]);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 120,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Authorization: Bearer ' . $aiConfig['api_key'],
        ],
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlError = curl_error($ch);
    curl_close($ch);

    if ($response === false) {
        throw new Exception("AI API request failed: {$curlError}");
    }

    if ($httpCode !== 200) {
        throw new Exception("AI API returned HTTP {$httpCode}: " . substr($response, 0, 500));
    }

    $data = json_decode($response, true);
    if (!$data || empty($data['choices'][0]['message']['content'])) {
        throw new Exception('AI API returned invalid response');
    }

    return [
        'content' => $data['choices'][0]['message']['content'],
        'model'   => $data['model'] ?? $aiConfig['model'],
        'tokens'  => $data['usage']['total_tokens'] ?? 0,
    ];
}

/**
 * Parse the AI response content (expected JSON).
 */
function parseAiResponse(string $content): array {
    $jsonContent = $content;
    if (preg_match('/```(?:json)?\s*\n?(.*?)\n?```/s', $content, $matches)) {
        $jsonContent = $matches[1];
    }

    $parsed = json_decode(trim($jsonContent), true);

    if (!$parsed) {
        return [
            'site_status'    => ['is_loading' => null, 'confidence' => 'low', 'explanation' => 'Could not parse AI response'],
            'summary'        => substr($content, 0, 500),
            'findings'       => [],
            'overall_health' => 'unknown',
        ];
    }

    $result = [
        'site_status'    => $parsed['site_status'] ?? ['is_loading' => null, 'confidence' => 'low', 'explanation' => 'Not evaluated'],
        'summary'        => $parsed['summary'] ?? 'Analysis complete',
        'findings'       => $parsed['findings'] ?? [],
        'overall_health' => $parsed['overall_health'] ?? 'unknown',
    ];

    // Carry over check_pages from pass 1 (used by the main flow to trigger pass 2)
    if (!empty($parsed['check_pages'])) {
        $result['check_pages'] = $parsed['check_pages'];
    }

    return $result;
}

/**
 * Queue a completion notification email for the review.
 */
function queueCompletionEmail(PDO $db, array $review, array $parsed): void {
    // Load settings for dashboard URL
    $settingsFile = '/docker/whp/settings.json';
    $dashboardUrl = '#';
    if (file_exists($settingsFile)) {
        $settings = json_decode(file_get_contents($settingsFile), true);
        $panelDomain = $settings['panel_domain'] ?? '';
        if ($panelDomain) {
            $dashboardUrl = "https://{$panelDomain}/?page=ai-monitor-customer";
        }
    }

    $siteName = $review['site_name'] ?? $review['primary_domain'];

    $bodyHtml = render_review_complete([
        'site_name'      => $siteName,
        'result_summary' => $parsed['summary'] ?? 'Analysis complete',
        'findings'       => $parsed['findings'] ?? [],
        'dashboard_url'  => $dashboardUrl,
    ]);

    $findingsCount = count($parsed['findings'] ?? []);
    $bodyText = "Log Review Complete for {$siteName}\n\n"
        . ($parsed['summary'] ?? 'Analysis complete') . "\n\n"
        . "{$findingsCount} finding(s). View the full report in your dashboard.\n";

    $subject = "Log Review Complete: {$siteName}";

    $db->prepare("
        INSERT INTO whp.ai_monitor_email_queue
            (subscription_id, recipient_email, subject, body_html, body_text, status, created_at, max_attempts)
        VALUES
            (?, ?, ?, ?, ?, 'queued', NOW(), 3)
    ")->execute([
        $review['subscription_id'],
        $review['notification_email'],
        $subject,
        $bodyHtml,
        $bodyText,
    ]);

    echo date('Y-m-d H:i:s') . " Queued completion email for review #{$review['id']}\n";
}
