<?php

namespace App\Services;

use App\Models\ExportProFile;
use App\Models\Service;
use App\Models\ServiceAgendaItem;
use App\Models\Setting;
use App\Models\Slide;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use ProPresenter\Parser\ProFileGenerator;
use ProPresenter\Parser\ProPlaylistGenerator;
use ProPresenter\Parser\Zip64Fixer;
use ZipArchive;

class PlaylistExportService
{
    public function __construct(
        private readonly MacroResolutionService $macroResolutionService,
    ) {}

    /** @return array{path: string, filename: string, skipped: int, warnings: array<int, string>} */
    public function generatePlaylist(Service $service, bool $preview = false): array
    {
        $agendaItems = ServiceAgendaItem::where('service_id', $service->id)
            ->where('is_before_event', false)
            ->orderBy('sort_order')
            ->with([
                'slides' => fn ($q) => $q->whereNull('deleted_at')->orderBy('sort_order'),
                'serviceSong.song.arrangements.arrangementSections.section.slides',
                'serviceSong.song.arrangements.arrangementSections.section.label',
                'serviceSong.arrangement.arrangementSections.section.label',
            ])
            ->get();

        if ($agendaItems->isEmpty()) {
            return $this->generatePlaylistLegacy($service, $preview);
        }

        return $this->generatePlaylistFromAgenda($service, $agendaItems, $preview);
    }

    /**
     * @param  Collection<int, ServiceAgendaItem>  $agendaItems
     * @return array{path: string, filename: string, skipped: int, warnings: array<int, string>, temp_dir: string}
     */
    private function generatePlaylistFromAgenda(Service $service, Collection $agendaItems, bool $preview = false): array
    {
        $informationSlides = $this->informationSlidesFor($service);

        $announcementPatterns = Setting::get('agenda_announcement_position');
        $announcementInserted = false;

        $exportService = app(ProExportService::class);
        $tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid();
        mkdir($tempDir, 0755, true);

        $playlistItems = [];
        $embeddedFiles = [];
        $skippedUnmatched = 0;
        $warnings = [];

        $moderatorSlideData = $this->buildModeratorSlideData($service);
        $firstVisibleItemId = $agendaItems->firstWhere('is_before_event', false)?->id;

        $realContentEmitted = false;
        $keyvisualFallbackEmitted = false;

        $keywordFiles = ExportProFile::keyword()->orderBy('order')->get();

        foreach ($agendaItems as $item) {
            if ($item->id === $firstVisibleItemId && $moderatorSlideData !== null) {
                $moderatorName = trim((string) ($moderatorSlideData['text'] ?? ''));
                $moderatorPresentationName = $moderatorName !== '' ? 'Moderator - '.$moderatorName : 'Moderator';
                $this->writeProAndEmbed(
                    $moderatorPresentationName,
                    $moderatorSlideData,
                    $tempDir,
                    $playlistItems,
                    $embeddedFiles,
                );
            }

            if (! $announcementInserted && $announcementPatterns && $informationSlides->isNotEmpty()) {
                $patterns = array_map('trim', explode(',', $announcementPatterns));
                $matcher = app(AgendaMatcherService::class);
                if ($matcher->matchesAny($item->title, $patterns)) {
                    $this->addSlidesFromCollection(
                        $informationSlides,
                        'information',
                        'Informationen',
                        $tempDir,
                        $playlistItems,
                        $embeddedFiles,
                        $service,
                        'information',
                    );
                    $announcementInserted = true;
                }
            }

            if ($item->serviceSong) {
                $serviceSong = $item->serviceSong;

                if ($serviceSong->song_id && $serviceSong->song) {
                    $song = $serviceSong->song;

                    if ($this->countSongContentSlides($song) === 0) {
                        $skippedUnmatched++;
                        $warnings[] = "Lied '".$song->title."' übersprungen: keine Inhaltsfolien.";
                    } else {
                        $selectedArr = $serviceSong->arrangement
                            ?? ($song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first());

                        $proPath = $exportService->generateProFile($song, $service, $selectedArr);
                        $proFilename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro';
                        $destPath = $tempDir.'/'.$proFilename;
                        rename($proPath, $destPath);

                        $embeddedFiles[$proFilename] = file_get_contents($destPath);
                        $this->embedBackground($service, $embeddedFiles);

                        $playlistItem = [
                            'type' => 'presentation',
                            'name' => $song->title,
                            'path' => $proFilename,
                        ];
                        if ($selectedArr?->name !== null) {
                            $playlistItem['arrangement_name'] = $selectedArr->name;
                        }
                        $playlistItems[] = $playlistItem;

                        $realContentEmitted = true;
                    }
                } else {
                    $skippedUnmatched++;
                }
            } else {
                $isSermon = $this->backgroundPartTypeForAgendaItem($item) === 'sermon';

                if ($isSermon) {
                    $this->addSermonIntroPresentation($service, $tempDir, $playlistItems, $embeddedFiles);

                    if ($item->slides->isNotEmpty()) {
                        $countBefore = count($playlistItems);
                        $label = $item->title ?: 'Folien';
                        $this->addSlidesFromCollection(
                            $item->slides,
                            'agenda_'.$item->id,
                            $label,
                            $tempDir,
                            $playlistItems,
                            $embeddedFiles,
                            $service,
                            $this->backgroundPartTypeForAgendaItem($item),
                        );

                        if (count($playlistItems) > $countBefore) {
                            $realContentEmitted = true;
                        }
                    }
                } elseif ($item->slides->isNotEmpty()) {
                    $countBefore = count($playlistItems);
                    $label = $item->title ?: 'Folien';
                    $this->addSlidesFromCollection(
                        $item->slides,
                        'agenda_'.$item->id,
                        $label,
                        $tempDir,
                        $playlistItems,
                        $embeddedFiles,
                        $service,
                        $this->backgroundPartTypeForAgendaItem($item),
                    );

                    if (count($playlistItems) > $countBefore) {
                        $realContentEmitted = true;
                    }
                } elseif (! $this->isNameTagAgendaItem($item)) {
                    if ($realContentEmitted && ! $keyvisualFallbackEmitted) {
                        $this->addKeyVisualFallbackPresentation(
                            $item,
                            $service,
                            $tempDir,
                            $playlistItems,
                            $embeddedFiles,
                        );
                        $keyvisualFallbackEmitted = true;
                    } else {
                        $this->addHeadlineItem(
                            $item,
                            $playlistItems,
                        );
                    }
                }
            }

            foreach ($keywordFiles as $kf) {
                $keyword = trim((string) $kf->keyword);
                if ($keyword === '') {
                    continue;
                }

                if (str_contains(mb_strtolower((string) $item->title), mb_strtolower($keyword))) {
                    $keywordItem = $this->embedExportProFile($kf, $embeddedFiles);
                    if ($keywordItem !== null) {
                        $playlistItems[] = $keywordItem;
                    }
                }
            }
        }

        if (! $announcementInserted && $informationSlides->isNotEmpty()) {
            $prependItems = [];
            $prependFiles = [];
            $this->addSlidesFromCollection(
                $informationSlides,
                'information',
                'Informationen',
                $tempDir,
                $prependItems,
                $prependFiles,
                $service,
                'information',
            );
            $playlistItems = array_merge($prependItems, $playlistItems);
            $embeddedFiles = array_merge($prependFiles, $embeddedFiles);
        }

        if (empty($playlistItems)) {
            $this->deleteDirectory($tempDir);
            throw new \RuntimeException('Keine Songs mit Inhalt zum Exportieren gefunden.');
        }

        $this->injectExportProFiles($playlistItems, $embeddedFiles);

        $this->addAbspannPresentation($service, $tempDir, $playlistItems, $embeddedFiles);

        $dateFormatted = $service->date?->format('Y-m-d') ?? now()->format('Y-m-d');
        $safeTitle = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $service->title);

        if ($preview) {
            $exportStamp = now()->format('Y-m-d_H-i');
            $playlistName = 'Vorschau '.$dateFormatted.' '.$service->title.' ('.$exportStamp.')';
            $outputFilename = 'Vorschau '.$dateFormatted.' '.$safeTitle.' '.$exportStamp.'.proplaylist';
        } else {
            $playlistName = $dateFormatted.' '.$service->title;
            $outputFilename = $dateFormatted.' '.$safeTitle.'.proplaylist';
        }

        $outputPath = $tempDir.'/'.$outputFilename;

        $this->writePlaylistFile($outputPath, $playlistName, $playlistItems, $embeddedFiles);

        return [
            'path' => $outputPath,
            'filename' => $outputFilename,
            'skipped' => $skippedUnmatched,
            'warnings' => $warnings,
            'temp_dir' => $tempDir,
        ];
    }

    /**
     * Legacy export for services without agenda items.
     * Hardcoded block order: info → songs → moderation → sermon.
     *
     * @return array{path: string, filename: string, skipped: int, warnings: array<int, string>, temp_dir: string}
     */
    private function generatePlaylistLegacy(Service $service, bool $preview = false): array
    {
        $service->loadMissing('serviceSongs.song.arrangements.arrangementSections.section.slides', 'serviceSongs.song.arrangements.arrangementSections.section.label');

        $matchedSongs = $service->serviceSongs()
            ->whereNotNull('song_id')
            ->orderBy('order')
            ->with(
                'song.arrangements.arrangementSections.section.slides',
                'song.arrangements.arrangementSections.section.label',
                'arrangement.arrangementSections.section.slides',
                'arrangement.arrangementSections.section.label',
            )
            ->get();

        $skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count();
        $skippedEmpty = 0;
        $warnings = [];

        $exportService = app(ProExportService::class);
        $tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid();
        mkdir($tempDir, 0755, true);

        $playlistItems = [];
        $embeddedFiles = [];

        $this->addSlidePresentation(
            'information',
            'Informationen',
            $service,
            $tempDir,
            $playlistItems,
            $embeddedFiles,
        );

        foreach ($matchedSongs as $serviceSong) {
            $song = $serviceSong->song;

            if (! $song || $this->countSongContentSlides($song) === 0) {
                $skippedEmpty++;
                if ($song !== null) {
                    $warnings[] = "Lied '".$song->title."' übersprungen: keine Inhaltsfolien.";
                }

                continue;
            }

            $selectedArr = $serviceSong->arrangement
                ?? ($song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first());

            $proPath = $exportService->generateProFile($song, $service, $selectedArr);
            $proFilename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro';
            $destPath = $tempDir.'/'.$proFilename;
            rename($proPath, $destPath);

            $embeddedFiles[$proFilename] = file_get_contents($destPath);
            $this->embedBackground($service, $embeddedFiles);

            $playlistItem = [
                'type' => 'presentation',
                'name' => $song->title,
                'path' => $proFilename,
            ];
            if ($selectedArr?->name !== null) {
                $playlistItem['arrangement_name'] = $selectedArr->name;
            }
            $playlistItems[] = $playlistItem;
        }

        $this->addSlidePresentation(
            'moderation',
            'Moderation',
            $service,
            $tempDir,
            $playlistItems,
            $embeddedFiles,
        );

        $this->addSlidePresentation(
            'sermon',
            'Predigt',
            $service,
            $tempDir,
            $playlistItems,
            $embeddedFiles,
        );

        if (empty($playlistItems)) {
            $this->deleteDirectory($tempDir);
            throw new \RuntimeException('Keine Songs mit Inhalt zum Exportieren gefunden.');
        }

        $this->injectExportProFiles($playlistItems, $embeddedFiles);

        $this->addAbspannPresentation($service, $tempDir, $playlistItems, $embeddedFiles);

        $dateFormatted = $service->date?->format('Y-m-d') ?? now()->format('Y-m-d');
        $safeTitle = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $service->title);

        if ($preview) {
            $exportStamp = now()->format('Y-m-d_H-i');
            $playlistName = 'Vorschau '.$dateFormatted.' '.$service->title.' ('.$exportStamp.')';
            $outputFilename = 'Vorschau '.$dateFormatted.' '.$safeTitle.' '.$exportStamp.'.proplaylist';
        } else {
            $playlistName = $dateFormatted.' '.$service->title;
            $outputFilename = $dateFormatted.' '.$safeTitle.'.proplaylist';
        }

        $outputPath = $tempDir.'/'.$outputFilename;

        $this->writePlaylistFile($outputPath, $playlistName, $playlistItems, $embeddedFiles);

        return [
            'path' => $outputPath,
            'filename' => $outputFilename,
            'skipped' => $skippedUnmatched + $skippedEmpty,
            'warnings' => $warnings,
            'temp_dir' => $tempDir,
        ];
    }

    private function addSlidesFromCollection(
        Collection $slides,
        string $prefix,
        string $label,
        string $tempDir,
        array &$playlistItems,
        array &$embeddedFiles,
        ?Service $service = null,
        ?string $backgroundPartType = null,
    ): void {
        $slideDataList = [];
        $imageFiles = [];
        $background = $this->backgroundData($service);
        $backgroundAttached = false;

        foreach ($slides->values() as $index => $slide) {
            $storedPath = Storage::disk('public')->path($slide->stored_filename);

            if (! file_exists($storedPath)) {
                continue;
            }

            $imageFilename = $prefix.'_'.($index + 1).'_'.basename($slide->stored_filename);
            $destPath = $tempDir.'/'.$imageFilename;
            copy($storedPath, $destPath);

            $imageFiles[$imageFilename] = file_get_contents($destPath);

            $singleSlideData = [
                'media' => $imageFilename,
                'format' => 'JPG',
                'label' => $slide->original_filename,
                'bundleRelative' => true,
            ];

            if ($this->shouldAttachBackground($slide, $backgroundPartType, $background)) {
                $singleSlideData['background'] = $background;
                $backgroundAttached = true;
            }

            $slideDataList[] = $singleSlideData;
        }

        if ($backgroundAttached) {
            $this->embedBackground($service, $embeddedFiles);
        }

        if (empty($slideDataList)) {
            return;
        }

        $groups = [
            [
                'name' => $label,
                'color' => [0.5, 0.5, 0.5, 1.0],
                'slides' => $slideDataList,
            ],
        ];

        $arrangements = [
            [
                'name' => 'Standard',
                'groupNames' => [$label],
            ],
        ];

        $safeLabel = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $label);
        $proFilename = $safeLabel.'.pro';
        $proPath = $tempDir.'/'.$proFilename;

        $this->writeProFile($proPath, $label, $groups, $arrangements);

        foreach ($imageFiles as $filename => $contents) {
            $embeddedFiles[$filename] = $contents;
        }

        $embeddedFiles[$proFilename] = file_get_contents($proPath);

        $playlistItems[] = [
            'type' => 'presentation',
            'name' => $label,
            'path' => $proFilename,
        ];
    }

    private function addSlidePresentation(
        string $type,
        string $label,
        Service $service,
        string $tempDir,
        array &$playlistItems,
        array &$embeddedFiles,
    ): void {
        $query = Slide::where('type', $type)
            ->whereNull('deleted_at');

        if ($type === 'information') {
            $query->where(function ($q) use ($service) {
                $q->whereNull('service_id')
                    ->orWhere('service_id', $service->id);
            });

            if ($service->date) {
                $query->where(function ($q) use ($service) {
                    $q->whereNull('expire_date')
                        ->orWhereDate('expire_date', '>=', $service->date);
                })->whereDate('uploaded_at', '<=', $service->date);
            }
        } else {
            $query->where('service_id', $service->id);
        }

        $slides = $query->orderBy('sort_order')->orderByDesc('uploaded_at')->get();

        if ($slides->isEmpty()) {
            return;
        }

        $this->addSlidesFromCollection(
            $slides,
            $type,
            $label,
            $tempDir,
            $playlistItems,
            $embeddedFiles,
            $service,
            $type,
        );
    }

    private function addKeyVisualFallbackPresentation(
        ServiceAgendaItem $item,
        Service $service,
        string $tempDir,
        array &$playlistItems,
        array &$embeddedFiles,
    ): void {
        $background = $this->keyVisualData($service);

        if ($background === null) {
            return;
        }

        $this->embedKeyVisual($service, $embeddedFiles);

        $label = $item->title ?: 'Keyvisual';
        $groups = [
            [
                'name' => 'Keyvisual',
                'color' => [0, 0, 0, 1],
                'slides' => [
                    [
                        'imageOnly' => true,
                        'background' => $background,
                    ],
                ],
            ],
        ];
        $arrangements = [
            [
                'name' => 'normal',
                'groupNames' => ['Keyvisual'],
            ],
        ];

        $safeLabel = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $label);
        $proFilename = $safeLabel.'.pro';
        $proPath = $tempDir.'/'.$proFilename;

        $this->writeProFile($proPath, $label, $groups, $arrangements);

        $embeddedFiles[$proFilename] = file_get_contents($proPath);

        $playlistItems[] = [
            'type' => 'presentation',
            'name' => $label,
            'path' => $proFilename,
        ];
    }

    private function addHeadlineItem(
        ServiceAgendaItem $item,
        array &$playlistItems,
    ): void {
        $playlistItems[] = [
            'type' => 'header',
            'name' => $item->title ?: 'Ablaufpunkt',
            'color' => [0.5, 0.5, 0.5, 1.0],
        ];
    }

    private function injectExportProFiles(array &$playlistItems, array &$embeddedFiles): void
    {
        $prefixItems = $this->buildExportProItems(
            ExportProFile::prefix()->orderBy('order')->get(),
            $embeddedFiles,
        );

        $postfixItems = $this->buildExportProItems(
            ExportProFile::postfix()->orderBy('order')->get(),
            $embeddedFiles,
        );

        $playlistItems = array_merge($prefixItems, $playlistItems, $postfixItems);
    }

    /** @param \Illuminate\Support\Collection<int, ExportProFile> $files */
    private function buildExportProItems(\Illuminate\Support\Collection $files, array &$embedded): array
    {
        $items = [];

        foreach ($files as $file) {
            $item = $this->embedExportProFile($file, $embedded);
            if ($item !== null) {
                $items[] = $item;
            }
        }

        return $items;
    }

    /**
     * Read a single export .pro/.probundle file from the 'local' disk, embed its
     * bytes (and any .probundle media) into $embeddedFiles, and return the matching
     * playlist presentation item. Returns null when the file is missing or a
     * .probundle has no inner .pro entry.
     *
     * The embedded .pro name uses the uppercased type as marker
     * (PREFIX/POSTFIX/KEYWORD) so prefix/postfix naming stays identical.
     *
     * @return array{type: string, name: string, path: string}|null
     */
    private function embedExportProFile(ExportProFile $file, array &$embeddedFiles): ?array
    {
        if (! Storage::disk('local')->exists($file->stored_path)) {
            return null;
        }

        $ext = strtolower(pathinfo($file->stored_path, PATHINFO_EXTENSION));
        $safeBase = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', pathinfo($file->original_name, PATHINFO_FILENAME));
        $embeddedProName = strtoupper($file->type).'_'.$file->order.'_'.$safeBase.'.pro';
        $displayName = pathinfo($file->original_name, PATHINFO_FILENAME);

        if ($ext === 'probundle') {
            $storedAbsPath = Storage::disk('local')->path($file->stored_path);

            $extracted = $this->extractProBundle($storedAbsPath);
            if ($extracted === null) {
                return null;
            }

            [$proBytes, $mediaFiles] = $extracted;

            foreach ($mediaFiles as $entryName => $entryBytes) {
                $mediaName = basename((string) $entryName);
                if (! isset($embeddedFiles[$mediaName])) {
                    $embeddedFiles[$mediaName] = $entryBytes;
                }
            }

            $embeddedFiles[$embeddedProName] = $proBytes;
        } else {
            $bytes = Storage::disk('local')->get($file->stored_path);
            if ($bytes === null) {
                return null;
            }

            $embeddedFiles[$embeddedProName] = $bytes;
        }

        return [
            'type' => 'presentation',
            'name' => $displayName,
            'path' => $embeddedProName,
        ];
    }

    /**
     * Extract the inner .pro bytes (verbatim) and media files from a .probundle archive.
     *
     * Uses raw ZipArchive extraction so the original .pro bytes are preserved exactly
     * (no protobuf round-trip). ProPresenter-authored bundles use Zip64; Zip64Fixer is
     * applied first so those archives open correctly. Normal (non-Zip64) archives pass
     * through Zip64Fixer unchanged. Returns null when the bundle has no .pro entry.
     *
     * @return array{0: string, 1: array<string, string>}|null [proBytes, mediaFiles]
     */
    private function extractProBundle(string $absPath): ?array
    {
        $rawBytes = @file_get_contents($absPath);
        if ($rawBytes === false || $rawBytes === '') {
            return null;
        }

        try {
            $fixedBytes = Zip64Fixer::fix($rawBytes);
        } catch (\Throwable) {
            // Not Zip64-fixable: fall back to the original bytes.
            $fixedBytes = $rawBytes;
        }

        $tempPath = tempnam(sys_get_temp_dir(), 'export-probundle-');
        if ($tempPath === false) {
            return null;
        }

        $zip = new ZipArchive;
        $isOpen = false;

        try {
            if (@file_put_contents($tempPath, $fixedBytes) === false) {
                return null;
            }

            if ($zip->open($tempPath) !== true) {
                return null;
            }
            $isOpen = true;

            $proFilename = null;
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $name = $zip->getNameIndex($i);
                if ($name !== false && str_ends_with(strtolower($name), '.pro')) {
                    $proFilename = $name;
                    break;
                }
            }

            if ($proFilename === null) {
                return null;
            }

            $proBytes = $zip->getFromName($proFilename);
            if ($proBytes === false || $proBytes === '') {
                return null;
            }

            $mediaFiles = [];
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $name = $zip->getNameIndex($i);
                if ($name === false || $name === $proFilename) {
                    continue;
                }

                $contents = $zip->getFromIndex($i);
                if ($contents === false) {
                    continue;
                }

                $mediaFiles[$name] = $contents;
            }

            return [$proBytes, $mediaFiles];
        } finally {
            if ($isOpen) {
                $zip->close();
            }
            @unlink($tempPath);
        }
    }

    protected function writeProFile(string $path, string $name, array $groups, array $arrangements, array $options = []): void
    {
        ProFileGenerator::generateAndWrite($path, $name, $groups, $arrangements, [], $options);
    }

    private function buildModeratorSlideData(Service $service): ?array
    {
        $name = app(NameTagResolver::class)->moderatorFor($service);
        if ($name === null) {
            return null;
        }

        return app(NameTagSlideBuilder::class)->buildModeratorSlide($name);
    }

    private function buildPreacherSlideData(Service $service): ?array
    {
        $name = app(NameTagResolver::class)->preacherFor($service);
        if ($name === null) {
            return null;
        }

        return app(NameTagSlideBuilder::class)->buildPreacherSlide($name);
    }

    /**
     * Emit the sermon intro as ONE presentation with up to two cues:
     *   slide 1 = key-visual (image only)
     *   slide 2 = key-visual WITH the preacher name tag rendered on top
     *
     * The presentation is named after the preacher ("Prediger - <Name>") when a
     * name tag is configured, otherwise "Keyvisual-Predigt". Emits nothing when
     * neither a key-visual nor a name tag is available.
     */
    private function addSermonIntroPresentation(Service $service, string $tempDir, array &$playlistItems, array &$embeddedFiles): void
    {
        $kvData = $this->keyVisualData($service);
        $nameTagData = $this->buildPreacherSlideData($service);

        if ($kvData === null && $nameTagData === null) {
            return;
        }

        $slides = [];

        if ($kvData !== null) {
            $this->embedKeyVisual($service, $embeddedFiles);
            $slides[] = ['imageOnly' => true, 'background' => $kvData];
        }

        if ($nameTagData !== null) {
            if ($kvData !== null) {
                $nameTagData['background'] = $kvData;
            }
            $slides[] = $nameTagData;

            $preacher = trim((string) ($nameTagData['text'] ?? ''));
            $name = $preacher !== '' ? 'Prediger - '.$preacher : 'Prediger';
        } else {
            $name = 'Keyvisual-Predigt';
        }

        $groups = [['name' => $name, 'color' => [0, 0, 0, 1], 'slides' => $slides]];
        $arrangements = [['name' => 'normal', 'groupNames' => [$name]]];
        $filename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $name).'-'.uniqid().'.pro';
        $path = $tempDir.'/'.$filename;
        $this->writeProFile($path, $name, $groups, $arrangements);
        $embeddedFiles[$filename] = file_get_contents($path);
        $playlistItems[] = ['type' => 'presentation', 'name' => $name, 'path' => $filename];
    }

    /**
     * Information slides visible for the given service: type=information, not soft-deleted,
     * global or this-service, unexpired at (and uploaded on/before) the service date.
     *
     * @return Collection<int, Slide>
     */
    private function informationSlidesFor(Service $service): Collection
    {
        $query = Slide::where('type', 'information')
            ->where(fn ($q) => $q->whereNull('expire_date')->orWhereDate('expire_date', '>=', $service->date))
            ->where(fn ($q) => $q->whereNull('service_id')->orWhere('service_id', $service->id))
            ->whereNull('deleted_at');

        if ($service->date) {
            $query->whereDate('uploaded_at', '<=', $service->date);
        }

        return $query->orderBy('sort_order')->orderByDesc('uploaded_at')->get();
    }

    /**
     * Append the closing "Abspann" presentation as the very last playlist item.
     *
     * Slides: key-visual (first) followed by ALL information slides. Every cue
     * auto-advances after 10s; the last cue loops back to the first ("auto repeat
     * all"). The presentation uses a Dissolve transition and each cue carries any
     * configured 'abspann' macro. Emits nothing when there is neither a key-visual
     * nor an information slide.
     */
    private function addAbspannPresentation(Service $service, string $tempDir, array &$playlistItems, array &$embeddedFiles): void
    {
        $informationSlides = $this->informationSlidesFor($service);
        $kvData = $this->keyVisualData($service);

        if ($kvData === null && $informationSlides->isEmpty()) {
            return;
        }

        $slides = [];

        if ($kvData !== null) {
            $this->embedKeyVisual($service, $embeddedFiles);
            $slides[] = [
                'imageOnly' => true,
                'background' => $kvData,
                'completion' => ['time' => 10.0, 'target' => 'next'],
            ];
        }

        foreach ($informationSlides->values() as $index => $slide) {
            $storedPath = Storage::disk('public')->path($slide->stored_filename);

            if (! file_exists($storedPath)) {
                continue;
            }

            $imageFilename = 'abspann_'.($index + 1).'_'.basename($slide->stored_filename);
            $destPath = $tempDir.'/'.$imageFilename;
            copy($storedPath, $destPath);

            $embeddedFiles[$imageFilename] = file_get_contents($destPath);

            $slides[] = [
                'media' => $imageFilename,
                'format' => 'JPG',
                'label' => $slide->original_filename,
                'bundleRelative' => true,
                'completion' => ['time' => 10.0, 'target' => 'next'],
            ];
        }

        if (empty($slides)) {
            return;
        }

        $total = count($slides);
        foreach ($slides as $i => &$sd) {
            $macros = $this->macroResolutionService->macrosForSlide($service, 'abspann', [
                'index' => $i,
                'total' => $total,
                'label_id' => null,
            ]);
            if (! empty($macros)) {
                $sd['macro'] = $macros[0];
            }
        }
        unset($sd);

        $slides[$total - 1]['completion']['target'] = 'first';

        $name = 'Abspann';
        $groups = [['name' => $name, 'color' => [0, 0, 0, 1], 'slides' => $slides]];
        $arrangements = [['name' => 'normal', 'groupNames' => [$name]]];

        $filename = 'Abspann-'.uniqid().'.pro';
        $path = $tempDir.'/'.$filename;
        $this->writeProFile($path, $name, $groups, $arrangements, ['transition' => 'dissolve', 'transitionDuration' => 1.0]);
        $embeddedFiles[$filename] = file_get_contents($path);
        $playlistItems[] = ['type' => 'presentation', 'name' => $name, 'path' => $filename];
    }

    private function writeProAndEmbed(string $name, array $slideData, string $tempDir, array &$playlistItems, array &$embeddedFiles): void
    {
        $groups = [['name' => $name, 'color' => [0, 0, 0, 1], 'slides' => [$slideData]]];
        $arrangements = [['name' => 'normal', 'groupNames' => [$name]]];
        $filename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $name).'-'.uniqid().'.pro';
        $path = $tempDir.'/'.$filename;
        $this->writeProFile($path, $name, $groups, $arrangements);
        $embeddedFiles[$filename] = file_get_contents($path);
        $playlistItems[] = ['type' => 'presentation', 'name' => $name, 'path' => $filename];
    }

    /**
     * Number of content slides in the song's default arrangement.
     * Content = SongSlide rows belonging to NON-locked sections (locked false/null)
     * that have a label. Locked prefix/postfix sections are ignored even if non-empty.
     */
    private function countSongContentSlides(\App\Models\Song $song): int
    {
        $song->loadMissing('arrangements.arrangementSections.section.slides', 'arrangements.arrangementSections.section.label');

        $defaultArr = $song->arrangements->firstWhere('is_default', true)
            ?? $song->arrangements->first();

        if ($defaultArr === null) {
            return 0;
        }

        $count = 0;
        $seenSectionIds = [];

        foreach ($defaultArr->arrangementSections->sortBy('order') as $arrangementSection) {
            $section = $arrangementSection->section;

            if ($section === null || $section->label === null) {
                continue;
            }

            if ($section->locked === true) {
                continue;
            }

            if (in_array($section->id, $seenSectionIds, true)) {
                continue;
            }
            $seenSectionIds[] = $section->id;

            $count += $section->slides->count();
        }

        return $count;
    }

    private function backgroundData(?Service $service): ?array
    {
        if ($service === null) {
            return null;
        }

        if ($this->backgroundSourcePath($service) === null) {
            return null;
        }

        return [
            'path' => ServiceImageResolver::BACKGROUND_EXPORT_NAME,
            'format' => 'JPG',
            'width' => 1920,
            'height' => 1080,
            'bundleRelative' => true,
        ];
    }

    private function keyVisualData(Service $service): ?array
    {
        if ($this->keyVisualSourcePath($service) === null) {
            return null;
        }

        return [
            'path' => ServiceImageResolver::KEY_VISUAL_EXPORT_NAME,
            'format' => 'JPG',
            'width' => 1920,
            'height' => 1080,
            'bundleRelative' => true,
        ];
    }

    /** Absolute filesystem path of the resolved background image, or null. */
    private function backgroundSourcePath(?Service $service): ?string
    {
        if ($service === null) {
            return null;
        }

        $background = app(ServiceImageResolver::class)->backgroundFor($service);

        if ($background === null || ! Storage::disk('public')->exists($background)) {
            return null;
        }

        return Storage::disk('public')->path($background);
    }

    /** Absolute filesystem path of the resolved key-visual image, or null. */
    private function keyVisualSourcePath(Service $service): ?string
    {
        $keyVisual = app(ServiceImageResolver::class)->keyVisualFor($service);

        if ($keyVisual === null || ! Storage::disk('public')->exists($keyVisual)) {
            return null;
        }

        return Storage::disk('public')->path($keyVisual);
    }

    /** Embed the resolved background image bytes into the archive under the fixed export name. */
    private function embedBackground(?Service $service, array &$embeddedFiles): void
    {
        $sourcePath = $this->backgroundSourcePath($service);

        if ($sourcePath === null || isset($embeddedFiles[ServiceImageResolver::BACKGROUND_EXPORT_NAME])) {
            return;
        }

        $contents = @file_get_contents($sourcePath);
        if ($contents !== false) {
            $embeddedFiles[ServiceImageResolver::BACKGROUND_EXPORT_NAME] = $contents;
        }
    }

    /** Embed the resolved key-visual image bytes into the archive under the fixed export name. */
    private function embedKeyVisual(Service $service, array &$embeddedFiles): void
    {
        $sourcePath = $this->keyVisualSourcePath($service);

        if ($sourcePath === null || isset($embeddedFiles[ServiceImageResolver::KEY_VISUAL_EXPORT_NAME])) {
            return;
        }

        $contents = @file_get_contents($sourcePath);
        if ($contents !== false) {
            $embeddedFiles[ServiceImageResolver::KEY_VISUAL_EXPORT_NAME] = $contents;
        }
    }

    private function shouldAttachBackground(Slide $slide, ?string $backgroundPartType, ?array $background): bool
    {
        if ($background === null) {
            return false;
        }

        if ($backgroundPartType !== 'sermon' && $slide->type !== 'sermon') {
            return false;
        }

        return $slide->cover_mode !== true;
    }

    private function backgroundPartTypeForAgendaItem(ServiceAgendaItem $item): string
    {
        if ($item->slides->contains(fn (Slide $slide) => $slide->type === 'sermon')) {
            return 'sermon';
        }

        $sermonPatterns = Setting::get('agenda_sermon_matching');
        if ($sermonPatterns === null) {
            return 'agenda_item';
        }

        $patterns = array_map('trim', explode(',', $sermonPatterns));

        return app(AgendaMatcherService::class)->matchesAny($item->title, $patterns)
            ? 'sermon'
            : 'agenda_item';
    }

    private function isNameTagAgendaItem(ServiceAgendaItem $item): bool
    {
        $title = mb_strtolower($item->title ?? '');
        $type = mb_strtolower($item->type ?? '');

        return str_contains($title, 'nametag')
            || str_contains($title, 'namenseinblender')
            || str_contains($type, 'nametag')
            || str_contains($type, 'namenseinblender');
    }

    protected function writePlaylistFile(string $path, string $name, array $items, array $embeddedFiles): void
    {
        ProPlaylistGenerator::generateAndWrite($path, $name, $items, $embeddedFiles);
    }

    private function deleteDirectory(string $dir): void
    {
        if (! is_dir($dir)) {
            return;
        }

        $items = scandir($dir);
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            $path = $dir.'/'.$item;
            is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
        }
        rmdir($dir);
    }
}
