<?php

namespace Common\Files\Response;

use Carbon\Carbon;
use Common\Files\FileEntry;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\StreamedResponse;
use ZipStream\ZipStream;

class DownloadFilesResponse
{
    // basename with extension => count
    // for incrementing file names in zip for files that have duplicate name
    protected array $filesInZip = [];
    protected int $totalSize = 0;

    public function __construct(
        protected FileResponseFactory $fileResponseFactory,
    ) {
    }

    /**
     * @param Collection|FileEntry[] $entries
     * @return mixed
     */
    public function create($entries)
    {
        if ($entries->count() === 1 && $entries->first()->type !== 'folder') {
            return $this->fileResponseFactory->create(
                $entries->first(),
                'attachment',
            );
        } else {
            return $this->streamZip($entries);
        }
    }

    private function streamZip(Collection $entries): StreamedResponse
    {
        return new StreamedResponse(
            function () use ($entries) {
                $timestamp = Carbon::now()->getTimestamp();
                $zip = new ZipStream(
                    // downloading multiple files from s3 will error out without this
                    defaultEnableZeroHeader: true,
                    contentType: 'application/octet-stream',
                    sendHttpHeaders: true,
                    outputName: "download-$timestamp.zip",
                );

                $this->fillZip($zip, $entries);
                $zip->finish();
            },
            200,
            [
                'X-Accel-Buffering' => 'no',
                'Pragma' => 'public',
                'Cache-Control' => 'no-cache',
                'Content-Transfer-Encoding' => 'binary',
            ],
        );
    }

    private function fillZip(ZipStream $zip, Collection $entries): void
    {
        $entries->each(function (FileEntry $entry) use ($zip) {
            if ($entry->type === 'folder') {
                // this will load all children, nested at any level, so no need to do a recursive loop
                $entry
                    ->allChildren()
                    ->select([
                        'id',
                        'name',
                        'extension',
                        'path',
                        'type',
                        'file_name',
                        'disk_prefix',
                    ])
                    ->orderBy('path', 'asc')
                    ->chunk(300, function (Collection $chunk) use (
                        $zip,
                        $entry,
                    ) {
                        $chunk->each(function (FileEntry $childEntry) use (
                            $zip,
                            $entry,
                            $chunk,
                        ) {
                            $path = $this->transformPath(
                                $childEntry,
                                $entry,
                                $chunk,
                            );
                            if ($childEntry->type === 'folder') {
                                // add empty folder in case it has no children
                                $zip->addFile("$path/", '');
                            } else {
                                $this->addFileToZip($childEntry, $zip, $path);
                            }
                        });
                    });
            } else {
                $this->addFileToZip($entry, $zip);
            }
        });
    }

    private function addFileToZip(
        FileEntry $entry,
        ZipStream $zip,
        string|null $path = null,
    ): void {
        if (!$path) {
            $path = $entry->getNameWithExtension();
        }

        $parts = pathinfo($path);
        $basename = $parts['basename'];
        $filename = $parts['filename'];
        $extension = $parts['extension'];
        $dirname = $parts['dirname'] === '.' ? '' : $parts['dirname'];

        // add number to duplicate file names (file(1).png, file(2).png etc)
        if (isset($this->filesInZip[$basename])) {
            $newCount = $this->filesInZip[$basename] + 1;
            $this->filesInZip[$basename] = $newCount;
            $path = "$dirname/$filename($newCount).$extension";
        } else {
            $this->filesInZip[$basename] = 0;
        }

        $stream = $entry->getDisk()->readStream($entry->getStoragePath());
        if ($stream) {
            $zip->addFileFromStream($path, $stream);
            fclose($stream);
        }
    }

    /**
     * Replace entry IDs with names inside "path" property.
     */
    private function transformPath(
        FileEntry $entry,
        FileEntry $parent,
        Collection $folders,
    ): string {
        if (!$entry->path) {
            return $entry->getNameWithExtension();
        }

        // '56/55/54 => [56,55,54]
        $path = array_filter(explode('/', $entry->path));
        $path = array_map(function ($id) {
            return (int) $id;
        }, $path);

        //only generate path until specified parent and not root
        $path = array_slice($path, array_search($parent->id, $path));

        // last value will be id of the file itself, remove it
        array_pop($path);

        // map parent folder IDs to names
        $path = array_map(function ($id) use ($folders, $parent) {
            if ($id === $parent->id) {
                return $parent->name;
            }
            return $folders->find($id)->name;
        }, $path);

        return implode('/', $path) . '/' . $entry->getNameWithExtension();
    }
}
