<?php

namespace App\Jobs;

use App\Models\Document;
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\Log;
use Illuminate\Support\Facades\Storage;

// Note: we intentionally use exec() with careful quoting and polling because on Windows soffice may spawn a child process.

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

    /**
     * Max seconds this job may run before the worker kills it.
     * Does not speed up the job, but protects the queue and surfaces timeouts.
     */
    public $timeout = 180; // 3 minutes

    public function __construct(public int $documentId) {}

    public function handle(): void
    {
        $doc = Document::find($this->documentId);
        if (! $doc) {
            return;
        }

        $disk = $doc->disk ?: 'public';
        // Normalize path to support legacy entries with a leading 'public/'
        $relPath = ltrim($doc->path ?? '', '/\\');
        if (str_starts_with($relPath, 'public/')) {
            $relPath = substr($relPath, 7);
        }
        $sourceBytes = null;
        if (Storage::disk($disk)->exists($relPath)) {
            $sourceBytes = Storage::disk($disk)->get($relPath);
        } else {
            // Fallback to public/ filesystem in case of legacy placement
            $publicFull = public_path($relPath);
            $real = @realpath($publicFull);
            $publicRoot = @realpath(public_path());
            $realN = $real ? str_replace('\\', '/', $real) : null;
            $rootN = $publicRoot ? str_replace('\\', '/', $publicRoot) : null;
            if ($realN && $rootN && str_starts_with($realN, $rootN) && is_file($real)) {
                \Log::info('Preview job: using public path fallback for source file', [
                    'document_id' => $doc->id,
                    'path' => $realN,
                ]);
                $sourceBytes = @file_get_contents($real);
            } else {
                \Log::warning('Preview job: source file missing on disk and public fallback failed', [
                    'document_id' => $doc->id,
                    'disk' => $disk,
                    'path' => $doc->path,
                    'normalized' => $relPath,
                    'public_candidate' => $publicFull,
                ]);

                return;
            }
        }

        $ext = strtolower(pathinfo($doc->original_name ?: $doc->file_name, PATHINFO_EXTENSION));

        // If a preview was already generated after the document was last updated, skip re-generation
        if ($doc->preview_generated_at && $doc->updated_at && $doc->preview_path) {
            $previewDisk = config('files.preview_disk', 'public');
            if (Storage::disk($previewDisk)->exists($doc->preview_path)) {
                if ($doc->updated_at->lte($doc->preview_generated_at)) {
                    Log::info('Skipping preview generation (already up to date)', [
                        'document_id' => $doc->id,
                        'preview_path' => $doc->preview_path,
                        'updated_at' => (string) $doc->updated_at,
                        'preview_generated_at' => (string) $doc->preview_generated_at,
                    ]);

                    return;
                }
            }
        }
        if (! in_array($ext, ['doc', 'docx'])) {
            return;
        } // only convert office docs here

        // Prepare temp files - keep original extension so LibreOffice can detect filter
        $tmpBase = sys_get_temp_dir().DIRECTORY_SEPARATOR.'doc_in_'.uniqid();
        $tmpIn = $tmpBase.'.'.$ext; // e.g., .doc or .docx
        file_put_contents($tmpIn, $sourceBytes);

        $outDir = sys_get_temp_dir();
        // Run soffice headless to convert to PDF
        $binCfg = config('files.libreoffice_bin');
        $bin = $binCfg ? (string) $binCfg : 'soffice';
        // If an absolute path is configured, ensure it exists to avoid long PATH resolution delays
        if ($binCfg && preg_match('/^[a-zA-Z]:\\\\|^\//', $bin) && ! file_exists($bin)) {
            Log::warning('Configured LibreOffice binary not found; falling back to PATH', [
                'document_id' => $doc->id,
                'bin' => $bin,
            ]);
            $bin = 'soffice';
        }
        // Use a temporary user profile to avoid permissions/profile lock issues
        $loProfileDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'lo_profile_'.$doc->id;
        if (! is_dir($loProfileDir)) {
            @mkdir($loProfileDir, 0700, true);
        }
        $loProfileUri = 'file:///'.ltrim(str_replace('\\', '/', $loProfileDir), '/');

        // Build command string for exec()
        $isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
        $binPart = $isWin ? '"'.$bin.'"' : escapeshellcmd($bin);
        $outDirPart = $isWin ? '"'.$outDir.'"' : escapeshellarg($outDir);
        $inPart = $isWin ? '"'.$tmpIn.'"' : escapeshellarg($tmpIn);
        $profilePart = $isWin ? '"'.$loProfileUri.'"' : escapeshellarg($loProfileUri);
        $cmd = sprintf(
            '%s --headless --norestore --nolockcheck --nologo --nodefault --nofirststartwizard -env:UserInstallation=%s --convert-to pdf --outdir %s %s',
            $binPart,
            $profilePart,
            $outDirPart,
            $inPart
        );
        $t0 = microtime(true);
        Log::info('Starting LibreOffice preview conversion', [
            'document_id' => $doc->id,
            'cmd' => $cmd,
            'is_windows' => $isWin,
        ]);
        $out = [];
        $ret = 0;
        @exec($cmd.' 2>&1', $out, $ret);
        // Some Windows installs may return before file is fully written; we'll poll for the output file below anyway.
        if ($ret !== 0) {
            Log::warning('Preview generation failed', [
                'document_id' => $doc->id,
                'exit_code' => $ret,
                'output' => implode("\n", $out),
            ]);
            @unlink($tmpIn);

            return;
        }

        $pdfPathLocal = $tmpIn.'.pdf';
        // LibreOffice writes output with source filename base; derive path
        $base = pathinfo($tmpIn, PATHINFO_FILENAME);
        $pdfPathLocal = $outDir.DIRECTORY_SEPARATOR.$base.'.pdf';
        // Poll for up to 10s for the PDF to appear (handles async write on Windows)
        $waitUntil = microtime(true) + 10.0;
        while (! file_exists($pdfPathLocal) && microtime(true) < $waitUntil) {
            usleep(200 * 1000); // 200ms
        }
        if (! file_exists($pdfPathLocal)) {
            // fallback: try with original name base
            $origBase = pathinfo($doc->original_name ?: $doc->file_name, PATHINFO_FILENAME);
            $try = $outDir.DIRECTORY_SEPARATOR.$origBase.'.pdf';
            // brief poll for fallback name as well
            $waitUntil2 = microtime(true) + 5.0;
            while (! file_exists($try) && microtime(true) < $waitUntil2) {
                usleep(200 * 1000);
            }
            if (file_exists($try)) {
                $pdfPathLocal = $try;
            }
        }
        if (! file_exists($pdfPathLocal)) {
            Log::warning('Preview generation did not produce expected PDF output', [
                'document_id' => $doc->id,
                'out_dir' => $outDir,
                'tmp_base' => $base,
                'command' => $cmd,
                'stdout' => implode("\n", $out),
            ]);
            @unlink($tmpIn);

            return;
        }

        // Compute elapsed time once output exists
        $elapsed = (int) round((microtime(true) - $t0) * 1000);

        // Store preview PDF to configured disk inside configured previews base path
        $previewDisk = config('files.preview_disk', 'public');
        $base = trim(config('files.preview_base', 'previews/cases'), '/');
        $previewDir = $base.'/'.($doc->documentable?->uuid ?? 'unknown');
        $stored = Storage::disk($previewDisk)->put($previewDir.'/'.($doc->uuid).'.pdf', file_get_contents($pdfPathLocal));
        if ($stored) {
            $doc->preview_path = $previewDir.'/'.$doc->uuid.'.pdf';
            $doc->preview_mime = 'application/pdf';
            $doc->preview_generated_at = now();
            $doc->save();
            Log::info('Preview generated and stored', [
                'document_id' => $doc->id,
                'preview_path' => $doc->preview_path,
                'elapsed_ms' => $elapsed,
            ]);
        } else {
            Log::warning('Preview generated but storing to disk failed', [
                'document_id' => $doc->id,
                'preview_disk' => $previewDisk,
                'target' => $previewDir.'/'.$doc->uuid.'.pdf',
            ]);
        }

        @unlink($tmpIn);
        @unlink($pdfPathLocal);
        // Clean up temporary LO profile directory
        try {
            if (is_dir($loProfileDir)) {
                @rmdir($loProfileDir);
            }
        } catch (\Throwable $e) {
            // ignore
        }
    }
}
