<?php

namespace App\Jobs;

use App\Models\AiHealingLog;
use App\Services\AI\AiService;
use App\Services\SystemConfig\SystemConfigService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;

class AiHealerJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $logId;
    public $faultyFile;
    public $lineNum;

    public function __construct($logId, $faultyFile = null, $lineNum = null)
    {
        $this->logId      = $logId;
        $this->faultyFile = $faultyFile;
        $this->lineNum    = $lineNum;
    }

    public function handle(AiService $aiService, SystemConfigService $cfg)
    {
        $healingLog = AiHealingLog::find($this->logId);
        if (!$healingLog) {
            return;
        }

        $healingLog->update(['status' => 'diagnosing']);

        // ── Capability gates ────────────────────────────────────────────────
        // These honour the toggles on the AI Self-Healing config panel.
        $allowCache       = (bool) $cfg->get('ai_healing_allow_cache', true);
        $allowQueue       = (bool) $cfg->get('ai_healing_allow_queue', true);
        $allowMaintenance = (bool) $cfg->get('ai_healing_allow_maintenance', false);
        $allowDb          = (bool) $cfg->get('ai_healing_allow_db', false);

        $commandCapabilityMap = [
            'cache:clear'    => $allowCache,
            'view:clear'     => $allowCache,
            'config:clear'   => $allowCache,
            'route:clear'    => $allowCache,
            'optimize:clear' => $allowCache,
            'queue:restart'  => $allowQueue,
            'down'           => $allowMaintenance,
            'up'             => $allowMaintenance,
            'migrate'        => $allowDb,
            'migrate:fresh'  => false, // never permitted — destructive
        ];

        $stackTrace = $healingLog->stack_trace;
        $faultyFile = $this->faultyFile;
        $lineNum    = (int) $this->lineNum;

        if (!$faultyFile || str_contains($faultyFile, 'vendor/')) {
            if (preg_match('/(\/var\/www\/html\/(?!vendor\/)[^\s:]+\.php):(\d+)/', $stackTrace, $matches)) {
                $faultyFile = $matches[1];
                $lineNum    = (int) $matches[2];
            }
        }

        if ($faultyFile && str_contains($faultyFile, 'storage/framework/views')) {
            $compiledContent = @file_get_contents($faultyFile);
            if ($compiledContent && preg_match('/PATH (.*?) ENDPATH/', $compiledContent, $m)) {
                $faultyFile = trim($m[1]);
            }
        }

        $fileContext = '';
        if ($faultyFile && file_exists($faultyFile)) {
            $lines       = file($faultyFile);
            $start       = max(0, $lineNum - 25);
            $end         = min(count($lines), $lineNum + 25);
            $fileContext = implode('', array_slice($lines, $start, $end - $start));
        }

        $enabledCommands = array_keys(array_filter($commandCapabilityMap, fn ($v) => $v === true));
        $enabledList = $enabledCommands ? implode(', ', $enabledCommands) : '(none — only code edits are permitted)';

        $isBlade = $faultyFile && str_ends_with($faultyFile, '.blade.php');
        $isView  = $isBlade
            || str_contains($healingLog->error_type, 'View')
            || str_contains($healingLog->error_message, 'storage/framework/views');

        $prompt  = "You are an autonomous Laravel 11 self-healing agent. The user has explicitly authorised you to apply fixes WITHOUT human confirmation when the error is unambiguous. A timestamped .bak backup is written before every code edit, so the user has a rollback path.\n\n";
        $prompt .= "EXCEPTION DETAILS:\n";
        $prompt .= 'Error Type: ' . $healingLog->error_type . "\n";
        $prompt .= 'Error Message: ' . $healingLog->error_message . "\n";

        if ($fileContext) {
            $prompt .= "The error originated in file: {$faultyFile}\n";
            $prompt .= "Here is the code snippet around the error (±25 lines):\n```php\n{$fileContext}\n```\n";
        } else {
            $prompt .= 'Stack Trace (first 1200 chars): ' . substr($stackTrace, 0, 1200) . "\n\n";
        }

        if ($isView) {
            $prompt .= "\nBLADE-SPECIFIC HINTS:\n";
            $prompt .= "- Literal `@directive` in HTML output (e.g. documentation that mentions @can, @if, @foreach) MUST be escaped as `@@directive` so Blade outputs it as text. Example: change `<code>@can('view users')</code>` to `<code>@@can('view users')</code>`.\n";
            $prompt .= "- Literal `{{ }}` in HTML must be escaped as `@{{ }}`. Literal `{!! !!}` as `@{!! !!}`.\n";
            $prompt .= "- 'unexpected end of file, expecting elseif/else/endif' usually means an unescaped `@can` / `@if` / `@auth` directive in plain text — Blade thinks it must close it. Find the directive that has no matching `@endX` and either escape it (`@@X`) or add the missing `@endX`.\n";
            $prompt .= "- Unmatched `@foreach`/`@endforeach`, `@if`/`@endif`, `@can`/`@endcan` pairs cause the same EOF error.\n";
            $prompt .= "- When the error's file is in `storage/framework/views/` (compiled cache), the real bug is in the source `.blade.php` shown above.\n\n";
        }

        $prompt .= "===== DECISION POLICY =====\n";
        $prompt .= "Classify this error into one of three tiers and act accordingly:\n\n";

        $prompt .= "TIER 1 — AUTO-FIX MANDATORY (confidence ≥ 0.8). You MUST return a code_edit or action_command. Examples:\n";
        $prompt .= "  • Typo in variable / method / class / route name visible in the snippet\n";
        $prompt .= "  • Missing or wrong `use` import statement\n";
        $prompt .= "  • Unescaped Blade directive in plain text (apply @@ escape)\n";
        $prompt .= "  • Unmatched `@if`/`@endif`, `@foreach`/`@endforeach`, `@can`/`@endcan` (add the missing closer)\n";
        $prompt .= "  • `Undefined array key` where a safe default is obvious → use `?? default`\n";
        $prompt .= "  • `Trying to access property of null` → apply `?->` null-safe operator\n";
        $prompt .= "  • `Class \"X\" not found` after a clear rename / typo → fix the FQCN\n";
        $prompt .= "  • Stale Blade / route / config cache → return action_command (view:clear, route:clear, optimize:clear)\n";
        $prompt .= "  • Failed worker after code deploy → return action_command queue:restart (if allowed)\n";
        $prompt .= "  • Wrong helper signature with obvious right form (Cache::set → Cache::put)\n";

        $prompt .= "\nTIER 2 — BEST-EFFORT FIX (confidence 0.5–0.79). Attempt the most likely fix if you can identify a concrete change. Examples:\n";
        $prompt .= "  • Type mismatch where a cast solves it (e.g. `(int) \$id`)\n";
        $prompt .= "  • SQL function not available in this driver — replace with portable PHP equivalent (e.g. Postgres lacks DATE_FORMAT; use Carbon bucketing in PHP)\n";
        $prompt .= "  • Missing model attribute that has a safe fallback\n";

        $prompt .= "\nTIER 3 — DO NOT AUTO-FIX (confidence < 0.5). Set both code_edit and action_command to null, write a precise diagnosis. Examples:\n";
        $prompt .= "  • Business logic / data semantics (you cannot infer intent)\n";
        $prompt .= "  • Multi-file refactor (you can only edit one file per fix)\n";
        $prompt .= "  • Race conditions / concurrency issues\n";
        $prompt .= "  • Anything in vendor/ (never touch)\n";
        $prompt .= "  • The fix would require deleting / re-creating significant code blocks\n";
        $prompt .= "  • The exception is itself coming from the healing pipeline (avoid loops)\n";

        $prompt .= "\nGENERAL RULES:\n";
        $prompt .= "- Bias toward decisive ACTION for Tier 1 and Tier 2. The user has authorised auto-fix and there is a backup.\n";
        $prompt .= "- For code_edit, `target_content` MUST be an EXACT substring that exists in the file (preserve all whitespace and indentation).\n";
        $prompt .= "- `replacement_content` must be the minimal change that fixes the bug. Do NOT refactor surrounding code.\n";
        $prompt .= "- Only use commands from the allow-list: {$enabledList}\n";
        $prompt .= "- If you return code_edit, action_command MUST be null (apply exactly one remedy).\n";
        $prompt .= "- Diagnosis should be ≤ 2 sentences, plain language, explaining what was wrong AND what you changed.\n";

        $prompt .= "\nRESPOND ONLY WITH VALID JSON (no markdown fences, no prose around it):\n";
        $prompt .= "{\n";
        $prompt .= "  \"confidence\": 0.0-1.0,\n";
        $prompt .= "  \"tier\": 1|2|3,\n";
        $prompt .= "  \"diagnosis\": \"What was wrong + what you changed (or why you can't fix it).\",\n";
        $prompt .= "  \"action_command\": \"cache:clear\" | null,\n";
        $prompt .= "  \"code_edit\": {\"file_path\":\"...\",\"target_content\":\"...\",\"replacement_content\":\"...\"} | null\n";
        $prompt .= "}";

        try {
            $aiResult = $aiService->provider()->generate($prompt);

            if (!$aiResult['success']) {
                throw new \Exception($aiResult['error'] ?? 'Unknown AI Provider error');
            }

            $response = $aiResult['response'];
            $jsonStr  = trim(preg_replace('/```json|```/', '', $response));
            $result   = json_decode($jsonStr, true);

            if (json_last_error() !== JSON_ERROR_NONE) {
                $healingLog->update([
                    'status'       => 'failed',
                    'ai_diagnosis' => 'AI response was not valid JSON: ' . $response,
                    'action_taken' => 'Invalid JSON from provider',
                ]);
                return;
            }

            $diagnosis  = $result['diagnosis']      ?? 'AI could not provide a diagnosis.';
            $action     = $result['action_command'] ?? null;
            $codeEdit   = $result['code_edit']      ?? null;
            $confidence = isset($result['confidence']) ? (float) $result['confidence'] : null;
            $tier       = isset($result['tier']) ? (int) $result['tier'] : null;

            if ($confidence !== null || $tier !== null) {
                $tag = '';
                if ($confidence !== null) {
                    $tag .= '[conf=' . number_format($confidence, 2) . ']';
                }
                if ($tier !== null) {
                    $tag .= '[tier=' . $tier . ']';
                }
                $diagnosis = trim($tag . ' ' . $diagnosis);
            }

            // ── Try code edit first ─────────────────────────────────────────
            if (is_array($codeEdit) && isset($codeEdit['file_path'], $codeEdit['target_content'], $codeEdit['replacement_content'])) {
                $applied = $this->applyCodeEdit($healingLog, $codeEdit, $diagnosis);
                if ($applied !== null) {
                    return; // resolved or hard-failed
                }
                // If $applied is null, the edit couldn't be applied (e.g., file missing) — fall through to command attempt.
            }

            // ── Try Artisan command ────────────────────────────────────────
            if (is_string($action) && $action !== '') {
                if (!array_key_exists($action, $commandCapabilityMap)) {
                    $healingLog->update([
                        'status'       => 'failed',
                        'action_taken' => "Refused: command '{$action}' is not in security policy",
                        'ai_diagnosis' => $diagnosis,
                    ]);
                    return;
                }

                if (!$commandCapabilityMap[$action]) {
                    $healingLog->update([
                        'status'       => 'failed',
                        'action_taken' => "Refused: command '{$action}' is currently disabled by capability toggle",
                        'ai_diagnosis' => $diagnosis,
                    ]);
                    return;
                }

                try {
                    Artisan::call($action);
                    $healingLog->update([
                        'status'       => 'resolved',
                        'action_taken' => 'Executed: ' . $action,
                        'ai_diagnosis' => $diagnosis,
                    ]);
                    Log::info("AI Healer executed Artisan command: {$action}");
                } catch (\Throwable $e) {
                    $healingLog->update([
                        'status'       => 'failed',
                        'action_taken' => "Artisan '{$action}' threw: " . $e->getMessage(),
                        'ai_diagnosis' => $diagnosis,
                    ]);
                }
                return;
            }

            // ── Nothing actionable ─────────────────────────────────────────
            $healingLog->update([
                'status'       => 'failed',
                'ai_diagnosis' => $diagnosis,
                'action_taken' => 'No fix applied (AI provided no actionable remedy)',
            ]);
        } catch (\Throwable $e) {
            Log::error('AI Healer failed: ' . $e->getMessage());
            $healingLog->update([
                'status'       => 'failed',
                'ai_diagnosis' => 'Internal Error: ' . $e->getMessage(),
                'action_taken' => 'Healer pipeline crashed',
            ]);
        }
    }

    /**
     * Apply a code edit suggested by the AI.
     * Returns true on resolved, false on hard fail, null if it should fall through to command attempt.
     */
    private function applyCodeEdit(AiHealingLog $log, array $edit, string $diagnosis): ?bool
    {
        $filePath = $edit['file_path'];

        if (!file_exists($filePath) || !is_writable($filePath)) {
            // Fall through — let the action_command branch try instead.
            return null;
        }

        $content = file_get_contents($filePath);

        if (!str_contains($content, $edit['target_content'])) {
            $log->update([
                'status'       => 'failed',
                'action_taken' => 'Code edit failed: target snippet not found in ' . basename($filePath),
                'ai_diagnosis' => $diagnosis,
            ]);
            return false;
        }

        $newContent = str_replace($edit['target_content'], $edit['replacement_content'], $content);

        if (trim($newContent) === '') {
            $log->update([
                'status'       => 'failed',
                'action_taken' => 'Code edit aborted: replacement would empty the file',
                'ai_diagnosis' => $diagnosis,
            ]);
            return false;
        }

        // Write a .bak before editing — gives the user a rollback path.
        $backupPath = $filePath . '.bak.' . date('YmdHis');
        @file_put_contents($backupPath, $content);
        file_put_contents($filePath, $newContent);

        Artisan::call('optimize:clear');

        $log->update([
            'status'        => 'resolved',
            'action_taken'  => 'CODE_EDIT|' . $filePath . '|' . $backupPath,
            'ai_diagnosis'  => $diagnosis,
            'original_code' => $content,
            'fixed_code'    => $newContent,
        ]);
        return true;
    }
}
