<?php
declare(strict_types=1);

class AI
{
    public static function isEnabled(): bool
    {
        return Helper::getSetting('ai_enabled', '0') === '1';
    }

    public static function getProvider(): string
    {
        return Helper::getSetting('ai_provider', 'ollama');
    }

    /**
     * SSRF guard for admin-configurable AI URLs. Allows http/https only and
     * blocks RFC1918 / IPv6 unique-local / link-local addresses, including the
     * cloud-metadata service at 169.254.169.254. Loopback (127.0.0.0/8 and ::1)
     * remains allowed because the default Ollama deployment listens on localhost.
     *
     * Returns the resolved host->IP map on success so callers can pin cURL to
     * the IP we just validated (defeats DNS-rebinding TOCTOU).
     */
    private static function resolveAndCheck(string $url, string $context): array|false
    {
        $parsed = parse_url($url);
        if (!$parsed
            || !in_array($parsed['scheme'] ?? '', ['http', 'https'], true)
            || empty($parsed['host'])) {
            error_log('[Plenvo AI] ' . $context . ': invalid URL scheme/host');
            return false;
        }
        $host = $parsed['host'];
        $port = $parsed['port'] ?? (($parsed['scheme'] === 'https') ? 443 : 80);

        // Resolve once; cURL will then be locked to this IP via CURLOPT_RESOLVE.
        $ips = filter_var($host, FILTER_VALIDATE_IP)
            ? [$host]
            : (@gethostbynamel($host) ?: []);
        if (empty($ips)) {
            error_log('[Plenvo AI] ' . $context . ': DNS resolution failed for ' . $host);
            return false;
        }
        foreach ($ips as $ip) {
            if (self::isPrivateIp($ip)) {
                error_log('[Plenvo AI] ' . $context . ': blocked private/link-local ' . $ip);
                return false;
            }
        }
        return ['host' => $host, 'port' => $port, 'ip' => $ips[0]];
    }

    /**
     * True for RFC1918, link-local, IPv6 unique-local and IPv6 link-local.
     * Loopback (127/8 and ::1) is intentionally NOT considered private here so
     * the default Ollama-on-localhost setup keeps working.
     */
    private static function isPrivateIp(string $ip): bool
    {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            if (str_starts_with($ip, '10.'))      return true;
            if (str_starts_with($ip, '192.168.')) return true;
            if (str_starts_with($ip, '169.254.')) return true; // link-local + cloud metadata
            if (preg_match('/^172\.(1[6-9]|2\d|3[0-1])\./', $ip)) return true;
            return false;
        }
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            $low = strtolower($ip);
            if (str_starts_with($low, 'fe80:'))      return true; // link-local
            if (str_starts_with($low, 'fc') || str_starts_with($low, 'fd')) return true; // unique-local
            return false;
        }
        return false;
    }

    /**
     * Apply restrictive cURL options to mitigate SSRF: HTTP(S) only, no
     * redirects, short connect timeout, and pin to a pre-resolved IP so a
     * DNS rebinder cannot swap targets between our check and curl_exec.
     */
    private static function hardenCurl(\CurlHandle $ch, ?array $resolved = null): void
    {
        // PHP 8.2+ exposes CURLOPT_PROTOCOLS_STR; older builds use the bitmask.
        if (defined('CURLOPT_PROTOCOLS_STR')) {
            curl_setopt($ch, CURLOPT_PROTOCOLS_STR, 'http,https');
            curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS_STR, 'http,https');
        } elseif (defined('CURLPROTO_HTTP') && defined('CURLPROTO_HTTPS')) {
            curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
            curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
        }
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        if ($resolved !== null) {
            // Pin the host:port to the IP we already validated. cURL will skip its
            // own DNS resolution for this entry, defeating rebinding attacks.
            curl_setopt($ch, CURLOPT_RESOLVE, [
                $resolved['host'] . ':' . $resolved['port'] . ':' . $resolved['ip'],
            ]);
        }
    }

    /**
     * Send a chat completion request.
     * @param array $messages [['role'=>'user'|'assistant','content'=>'...'],...]
     */
    public static function chat(string $systemPrompt, array $messages): string|false
    {
        return match (self::getProvider()) {
            'claude'  => self::chatClaude($systemPrompt, $messages),
            'openai'  => self::chatOpenAI($systemPrompt, $messages),
            'copilot' => self::chatCopilot($systemPrompt, $messages),
            default   => self::chatOllama($systemPrompt, $messages),
        };
    }

    // ── Ollama (lokal oder remote) ────────────────────────────────────────────────

    private static function chatOllama(string $systemPrompt, array $messages): string|false
    {
        $url      = rtrim(Helper::getSetting('ai_ollama_url', 'http://localhost:11434'), '/');
        $model    = Helper::getSetting('ai_ollama_model', 'llama3');
        $authType = Helper::getSetting('ai_ollama_auth_type', 'none');

        $fullUrl  = $url . '/api/chat';
        $resolved = self::resolveAndCheck($fullUrl, 'Ollama');
        if ($resolved === false) return false;

        $headers = ['Content-Type: application/json'];
        if ($authType === 'bearer') {
            $token = Helper::getSetting('ai_ollama_bearer_token', '');
            if ($token) $headers[] = 'Authorization: Bearer ' . $token;
        } elseif ($authType === 'basic') {
            $user = Helper::getSetting('ai_ollama_basic_user', '');
            $pass = Helper::getSetting('ai_ollama_basic_pass', '');
            if ($user) $headers[] = 'Authorization: Basic ' . base64_encode($user . ':' . $pass);
        }

        $all = [];
        if ($systemPrompt) $all[] = ['role' => 'system', 'content' => $systemPrompt];
        foreach ($messages as $m) $all[] = ['role' => $m['role'], 'content' => $m['content']];

        $ch = curl_init($fullUrl);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode(['model' => $model, 'messages' => $all, 'stream' => false]),
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_TIMEOUT        => 120,
        ]);
        self::hardenCurl($ch, $resolved);
        $resp = curl_exec($ch);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($err || !$resp) { error_log('[Plenvo AI] Ollama error: ' . $err); return false; }
        $data = json_decode($resp, true);
        return $data['message']['content'] ?? false;
    }

    // ── Claude (Anthropic) ────────────────────────────────────────────────────────

    private static function chatClaude(string $systemPrompt, array $messages): string|false
    {
        $apiKey = Helper::getSetting('ai_claude_key', '');
        $model  = Helper::getSetting('ai_claude_model', 'claude-opus-4-7');
        if (!$apiKey) return false;

        $body = ['model' => $model, 'max_tokens' => 4096, 'messages' => $messages];
        if ($systemPrompt) $body['system'] = $systemPrompt;

        $ch = curl_init('https://api.anthropic.com/v1/messages');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($body),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'x-api-key: ' . $apiKey,
                'anthropic-version: 2023-06-01',
            ],
            CURLOPT_TIMEOUT => 120,
        ]);
        self::hardenCurl($ch);
        $resp = curl_exec($ch);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($err || !$resp) { error_log('[Plenvo AI] Claude error: ' . $err); return false; }
        $data = json_decode($resp, true);
        if (isset($data['error'])) {
            error_log('[Plenvo AI] Claude API error: ' . ($data['error']['message'] ?? json_encode($data['error'])));
            return false;
        }
        return $data['content'][0]['text'] ?? false;
    }

    // ── OpenAI (ChatGPT) ──────────────────────────────────────────────────────────

    private static function chatOpenAI(string $systemPrompt, array $messages): string|false
    {
        $apiKey = Helper::getSetting('ai_openai_key', '');
        $model  = Helper::getSetting('ai_openai_model', 'gpt-4o');
        if (!$apiKey) return false;

        $all = [];
        if ($systemPrompt) $all[] = ['role' => 'system', 'content' => $systemPrompt];
        foreach ($messages as $m) $all[] = ['role' => $m['role'], 'content' => $m['content']];

        $ch = curl_init('https://api.openai.com/v1/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode(['model' => $model, 'messages' => $all, 'max_tokens' => 4096]),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $apiKey,
            ],
            CURLOPT_TIMEOUT => 120,
        ]);
        self::hardenCurl($ch);
        $resp = curl_exec($ch);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($err || !$resp) { error_log('[Plenvo AI] OpenAI error: ' . $err); return false; }
        $data = json_decode($resp, true);
        if (isset($data['error'])) {
            error_log('[Plenvo AI] OpenAI API error: ' . ($data['error']['message'] ?? json_encode($data['error'])));
            return false;
        }
        return $data['choices'][0]['message']['content'] ?? false;
    }

    // ── Microsoft Copilot (Azure OpenAI) ─────────────────────────────────────────

    private static function chatCopilot(string $systemPrompt, array $messages): string|false
    {
        $endpoint   = rtrim(Helper::getSetting('ai_copilot_endpoint', ''), '/');
        $deployment = Helper::getSetting('ai_copilot_deployment', '');
        $apiKey     = Helper::getSetting('ai_copilot_key', '');
        $apiVersion = Helper::getSetting('ai_copilot_api_version', '2024-02-01');

        if (!$endpoint || !$deployment || !$apiKey) return false;

        $url      = $endpoint . '/openai/deployments/' . $deployment . '/chat/completions?api-version=' . $apiVersion;
        $resolved = self::resolveAndCheck($url, 'Copilot');
        if ($resolved === false) return false;

        $all = [];
        if ($systemPrompt) $all[] = ['role' => 'system', 'content' => $systemPrompt];
        foreach ($messages as $m) $all[] = ['role' => $m['role'], 'content' => $m['content']];

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode(['messages' => $all, 'max_tokens' => 4096]),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'api-key: ' . $apiKey,
            ],
            CURLOPT_TIMEOUT => 120,
        ]);
        self::hardenCurl($ch, $resolved);
        $resp = curl_exec($ch);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($err || !$resp) { error_log('[Plenvo AI] Copilot error: ' . $err); return false; }
        $data = json_decode($resp, true);
        if (isset($data['error'])) {
            error_log('[Plenvo AI] Copilot API error: ' . ($data['error']['message'] ?? json_encode($data['error'])));
            return false;
        }
        return $data['choices'][0]['message']['content'] ?? false;
    }

    private static function validateDate(?string $d): ?string
    {
        if (!$d) return null;
        $dt = \DateTime::createFromFormat('Y-m-d', $d);
        return ($dt && $dt->format('Y-m-d') === $d) ? $d : null;
    }

    // ── Helpers ───────────────────────────────────────────────────────────────────

    public static function buildSystemPrompt(array $project): string
    {
        $name  = $project['name'];
        $today = date('Y-m-d');
        $descLine = !empty(trim($project['description'] ?? ''))
            ? "\nProjektbeschreibung: " . trim($project['description'])
            : '';
        return <<<PROMPT
Du bist ein KI-Projektassistent für das Projekt "{$name}".{$descLine}
Helfe dem Team bei der Planung, Strukturierung und Verwaltung von Aufgaben.

Wenn du Aufgaben generieren sollst, füge am Ende deiner Antwort genau diesen JSON-Block ein:

```json
{
  "tasks": [
    {"title": "Aufgabentitel", "description": "Optionale Beschreibung", "priority": "medium", "start_date": "{$today}", "due_date": "YYYY-MM-DD", "predecessors": []},
    {"title": "Zweite Aufgabe", "description": null, "priority": "high", "start_date": "YYYY-MM-DD", "due_date": "YYYY-MM-DD", "predecessors": [1]}
  ]
}
```

Regeln für den JSON-Block:
- priority: low | medium | high | urgent
- start_date: Realistisches Startdatum (YYYY-MM-DD). Das Projekt startet heute ({$today}). Aufgaben ohne Vorgänger beginnen am {$today}. Aufgaben mit Vorgängern beginnen frühestens am Tag nach dem due_date des letzten Vorgängers.
- due_date: Realistisches Enddatum (YYYY-MM-DD) basierend auf dem geschätzten Aufwand. Immer angeben – niemals null.
- predecessors: Array mit 1-basierten Indizes der Vorgänger-Aufgaben (leer = keine). Beispiel: [1, 2] bedeutet, Aufgabe 1 und 2 müssen zuerst abgeschlossen sein.
- Schätze Aufwand und Dauer realistisch: einfache Aufgaben 1–3 Tage, mittlere 3–7 Tage, komplexe 1–3 Wochen.
- Füge den JSON-Block NUR ein, wenn der Nutzer explizit Aufgaben generieren/erstellen möchte.
- Antworte auf Deutsch, außer der Nutzer schreibt in einer anderen Sprache.
PROMPT;
    }

    public static function parseTasks(string $response): array
    {
        if (!preg_match('/```json\s*(\{.*?"tasks".*?\})\s*```/s', $response, $m)) {
            return [];
        }
        $data = json_decode($m[1], true);
        if (!isset($data['tasks']) || !is_array($data['tasks'])) return [];
        $today = date('Y-m-d');
        $tasks = array_values(array_filter($data['tasks'], fn($t) => !empty(trim($t['title'] ?? ''))));
        foreach ($tasks as &$t) {
            $preds = $t['predecessors'] ?? [];
            $t['predecessors'] = is_array($preds)
                ? array_values(array_filter(array_map('intval', $preds), fn($i) => $i > 0))
                : [];
            // Validate / fallback dates
            $t['start_date'] = self::validateDate($t['start_date'] ?? null) ?? $today;
            $t['due_date']   = self::validateDate($t['due_date'] ?? null)
                ?? date('Y-m-d', strtotime($t['start_date'] . ' +3 days'));
        }
        return $tasks;
    }
}
