<?php

namespace App\Services\Documents;

use App\Models\Document;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;

class DocumentStreamService
{
    /**
     * Stream a document either inline or as attachment with all Windows/public fallbacks.
     */
    public function stream(Document $document, bool $inline = true): StreamedResponse|Response
    {
        $disk = $document->disk ?: 'public';
        $relPath = $this->normalizePath($document->path ?? '');

        // Handle DOC/DOCX previews for inline viewing
        if ($inline) {
            $previewResponse = $this->tryPreview($document);
            if ($previewResponse) {
                return $previewResponse;
            }
        }

        // Try to find the file using multiple path candidates
        $found = $this->findFile($disk, $relPath);
        
        if (! $found) {
            // Try public/ fallback
            $found = $this->tryPublicFallback($relPath);
            if (! $found) {
                abort(404, 'Document file not found');
            }
        }

        $headers = $this->buildHeaders($document, $inline);

        // Try streaming first
        $stream = Storage::disk($disk)->readStream($found);
        if ($stream !== false) {
            return response()->stream(function () use ($stream) {
                fpassthru($stream);
            }, 200, $headers);
        }

        // Fallbacks on readStream failure
        return $this->fallbackStream($disk, $found, $document, $relPath, $headers);
    }

    /**
     * Normalize path for Windows and legacy prefixes.
     */
    protected function normalizePath(string $path): string
    {
        $path = str_replace('\\', '/', $path);
        $path = ltrim($path, '/');
        if (str_starts_with($path, 'public/')) {
            $path = substr($path, 7);
        }

        return $path;
    }

    /**
     * Build path candidates to try.
     */
    protected function buildPathCandidates(string $relPath, ?string $documentPath): array
    {
        $candidates = [$relPath];
        $raw = $documentPath ? ltrim(str_replace('\\', '/', $documentPath), '/') : null;
        
        if ($raw && $raw !== $relPath) {
            $candidates[] = $raw;
        }
        if (str_starts_with($relPath, 'storage/')) {
            $candidates[] = substr($relPath, 8);
        }
        if ($raw && str_starts_with($raw, 'storage/')) {
            $candidates[] = substr($raw, 8);
        }

        return array_unique($candidates);
    }

    /**
     * Try to find file using path candidates.
     */
    protected function findFile(string $disk, string $relPath, ?string $documentPath = null): ?string
    {
        $candidates = $this->buildPathCandidates($relPath, $documentPath);
        
        foreach ($candidates as $cand) {
            if (Storage::disk($disk)->exists($cand)) {
                return $cand;
            }
        }

        return null;
    }

    /**
     * Try public/ fallback path.
     */
    protected function tryPublicFallback(string $relPath): ?string
    {
        $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)) {
            return $real;
        }

        return null;
    }

    /**
     * Try to stream preview for DOC/DOCX files.
     */
    protected function tryPreview(Document $document): StreamedResponse|Response|null
    {
        $ext = strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION));
        if (! in_array($ext, ['doc', 'docx'])) {
            return null;
        }

        $previewDisk = config('files.preview_disk', 'public');
        if ($document->preview_path && Storage::disk($previewDisk)->exists($document->preview_path)) {
            $stream = Storage::disk($previewDisk)->readStream($document->preview_path);
            if ($stream !== false) {
                $previewNameBase = $document->name ?: pathinfo($document->original_name ?: $document->file_name, PATHINFO_FILENAME);

                return response()->stream(function () use ($stream) {
                    fpassthru($stream);
                }, 200, [
                    'Content-Type' => $document->preview_mime ?: 'application/pdf',
                    'Content-Disposition' => 'inline; filename="'.addslashes($previewNameBase.'.pdf').'"',
                    'Cache-Control' => 'private, max-age=0, no-cache',
                    'Pragma' => 'no-cache',
                ]);
            }
        }

        // Queue preview generation if not available
        \App\Jobs\GenerateDocumentPreview::dispatch($document->id);

        return response('Preview is being generated. Please try again shortly.', 202);
    }

    /**
     * Build response headers.
     */
    protected function buildHeaders(Document $document, bool $inline): array
    {
        $nameBase = $document->name ?: pathinfo($document->original_name ?: $document->file_name, PATHINFO_FILENAME);
        $ext = strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION));
        $name = $ext ? ($nameBase.'.'.$ext) : $nameBase;

        return [
            'Content-Type' => $document->mime_type ?: 'application/octet-stream',
            'Content-Disposition' => ($inline ? 'inline' : 'attachment').'; filename="'.addslashes($name).'"',
            'Cache-Control' => 'private, max-age=0, no-cache',
            'Pragma' => 'no-cache',
        ];
    }

    /**
     * Fallback streaming methods when readStream fails.
     */
    protected function fallbackStream(string $disk, string $found, Document $document, string $relPath, array $headers): StreamedResponse|Response
    {
        // Fallback 1: get() the bytes directly
        try {
            $bytes = Storage::disk($disk)->get($found);
            if (! is_null($bytes) && $bytes !== false) {
                return response($bytes, 200, $headers);
            }
        } catch (\Throwable $e) {
            // Continue to next fallback
        }

        // Fallback 2: open via absolute storage path
        $abs = null;
        try {
            if (method_exists(Storage::disk($disk), 'path')) {
                $abs = Storage::disk($disk)->path($found);
            }
        } catch (\Throwable $e) {
            $abs = null;
        }
        
        if ($abs && is_file($abs)) {
            $fp = @fopen($abs, 'rb');
            if ($fp !== false) {
                return response()->stream(function () use ($fp) {
                    fpassthru($fp);
                }, 200, $headers);
            }
        }

        // Fallback 3: serve from public path if available
        $publicFull = public_path($found);
        $real = @realpath($publicFull);
        if ($real && is_file($real)) {
            $fp = @fopen($real, 'rb');
            if ($fp !== false) {
                return response()->stream(function () use ($fp) {
                    fpassthru($fp);
                }, 200, $headers);
            }
        }

        abort(404, 'Document file could not be streamed');
    }
}

