<?php

declare(strict_types=1);

namespace App\Controller\Api\Stations\Files;

use App\Controller\Api\Traits\CanSortResults;
use App\Entity;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\RouterInterface;
use App\Http\ServerRequest;
use App\Media\MimeType;
use App\Paginator;
use App\Service\Meilisearch;
use App\Utilities;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr;
use League\Flysystem\StorageAttributes;
use Psr\Http\Message\ResponseInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

final class ListAction
{
    use CanSortResults;

    public function __construct(
        private readonly EntityManagerInterface $em,
        private readonly CacheInterface $cache,
        private readonly Meilisearch $meilisearch
    ) {
    }

    public function __invoke(
        ServerRequest $request,
        Response $response,
        string $station_id
    ): ResponseInterface {
        $router = $request->getRouter();

        $station = $request->getStation();
        $storageLocation = $station->getMediaStorageLocation();

        $fs = (new StationFilesystems($station))->getMediaFilesystem();

        $currentDir = $request->getParam('currentDirectory', '');

        $searchPhrase = trim($request->getParam('searchPhrase', ''));
        $isSearch = !empty($searchPhrase);

        $cacheKeyParts = [
            'files_list',
            $storageLocation->getIdRequired(),
            (!empty($currentDir)) ? 'dir_' . rawurlencode($currentDir) : 'root',
        ];

        if ($isSearch) {
            $cacheKeyParts[] = 'search_' . rawurlencode($searchPhrase);
        }

        $cacheKey = implode('.', $cacheKeyParts);

        $flushCache = (bool)$request->getParam('flushCache', false);

        if (!$flushCache && $this->cache->has($cacheKey)) {
            /** @var array<int, Entity\Api\FileList> $result */
            $result = $this->cache->get($cacheKey);
        } else {
            $pathLike = (empty($currentDir))
                ? '%'
                : $currentDir . '/%';

            $mediaQueryBuilder = $this->em->createQueryBuilder()
                ->select(['sm', 'spm', 'sp', 'smcf'])
                ->from(Entity\StationMedia::class, 'sm')
                ->leftJoin('sm.custom_fields', 'smcf')
                ->leftJoin('sm.playlists', 'spm')
                ->leftJoin('spm.playlist', 'sp', Expr\Join::WITH, 'sp.station = :station')
                ->where('sm.storage_location = :storageLocation')
                ->andWhere('sm.path LIKE :path')
                ->setParameter('storageLocation', $station->getMediaStorageLocation())
                ->setParameter('station', $station)
                ->setParameter('path', $pathLike);

            // Apply searching
            $foldersInDirQuery = $this->em->createQuery(
                <<<'DQL'
                    SELECT spf, sp
                    FROM App\Entity\StationPlaylistFolder spf
                    JOIN spf.playlist sp
                    WHERE spf.station = :station
                    AND spf.path LIKE :path
                DQL
            )->setParameter('station', $station)
                ->setParameter('path', $pathLike);

            $unprocessableMediaQuery = $this->em->createQuery(
                <<<'DQL'
                    SELECT upm
                    FROM App\Entity\UnprocessableMedia upm
                    WHERE upm.storage_location = :storageLocation
                    AND upm.path LIKE :path
                DQL
            )->setParameter('storageLocation', $storageLocation)
                ->setParameter('path', $pathLike);

            if ($isSearch) {
                if ('special:unprocessable' === $searchPhrase) {
                    $mediaInDirRaw = [];

                    $unprocessableMediaRaw = $unprocessableMediaQuery->toIterable(
                        [],
                        $unprocessableMediaQuery::HYDRATE_ARRAY
                    );
                } else {
                    if ('special:duplicates' === $searchPhrase) {
                        $mediaQueryBuilder->andWhere(
                            $mediaQueryBuilder->expr()->in(
                                'sm.song_id',
                                <<<'DQL'
                                    SELECT sm2.song_id FROM
                                    App\Entity\StationMedia sm2
                                    WHERE sm2.storage_location = :storageLocation
                                    GROUP BY sm2.song_id
                                    HAVING COUNT(sm2.id) > 1
                                DQL
                            )
                        );
                    } elseif ('special:unassigned' === $searchPhrase) {
                        $mediaQueryBuilder->andWhere(
                            'sm.id NOT IN (SELECT spm2.media_id FROM App\Entity\StationPlaylistMedia spm2)'
                        );
                    } else {
                        [$searchPhrase, $playlist] = $this->parseSearchQuery($station, $searchPhrase);

                        if ($this->meilisearch->isSupported()) {
                            $ids = $this->meilisearch
                                ->getIndex($storageLocation)
                                ->searchMedia($searchPhrase, $playlist);

                            $mediaQueryBuilder->andWhere(
                                'sm.id IN (:ids)'
                            )->setParameter('ids', $ids);
                        } else {
                            if (null !== $playlist) {
                                $mediaQueryBuilder->andWhere(
                                    'sm.id IN (SELECT spm2.media_id FROM App\Entity\StationPlaylistMedia spm2 '
                                    . 'WHERE spm2.playlist = :playlist)'
                                )->setParameter('playlist', $playlist);
                            }

                            if (!empty($searchPhrase)) {
                                $mediaQueryBuilder->andWhere('(sm.title LIKE :query OR sm.artist LIKE :query)')
                                    ->setParameter('query', '%' . $searchPhrase . '%');
                            }
                        }
                    }

                    $mediaQuery = $mediaQueryBuilder->getQuery();
                    $mediaInDirRaw = $mediaQuery->getArrayResult();

                    $unprocessableMediaRaw = [];
                }

                $foldersInDirRaw = [];
            } else {
                // Avoid loading subfolder media.
                $mediaQueryBuilder->andWhere('sm.path NOT LIKE :pathWithSubfolders')
                    ->setParameter('pathWithSubfolders', $pathLike . '/%');

                $mediaQuery = $mediaQueryBuilder->getQuery();
                $mediaInDirRaw = $mediaQuery->getArrayResult();

                $foldersInDirRaw = $foldersInDirQuery->getArrayResult();

                $unprocessableMediaRaw = $unprocessableMediaQuery->toIterable(
                    [],
                    $unprocessableMediaQuery::HYDRATE_ARRAY
                );
            }

            // Process all database results.
            $mediaInDir = [];
            foreach ($mediaInDirRaw as $row) {
                $media = new Entity\Api\FileListMedia();

                $media->id = (string)$row['song_id'];
                $media->title = (string)$row['title'];
                $media->artist = (string)$row['artist'];
                $media->text = $row['artist'] . ' - ' . $row['title'];
                $media->album = (string)$row['album'];
                $media->genre = (string)$row['genre'];
                $media->isrc = (string)$row['isrc'];

                $media->is_playable = ($row['length'] !== 0);
                $media->length = (int)$row['length'];
                $media->length_text = $row['length_text'];

                $media->media_id = $row['id'];
                $media->unique_id = $row['unique_id'];
                $media->art_updated_at = $row['art_updated_at'];

                foreach ((array)$row['custom_fields'] as $custom_field) {
                    $media->custom_fields[$custom_field['field_id']] = $custom_field['value'];
                }

                $playlists = [];
                foreach ($row['playlists'] as $spmRow) {
                    if (!isset($spmRow['playlist'])) {
                        continue;
                    }

                    $playlistId = $spmRow['playlist']['id'];
                    if (isset($playlists[$playlistId])) {
                        $playlists[$playlistId]['count']++;
                    } else {
                        $playlistName = $spmRow['playlist']['name'];

                        $playlists[$playlistId] = [
                            'id' => $playlistId,
                            'name' => $playlistName,
                            'short_name' => Entity\StationPlaylist::generateShortName($playlistName),
                            'count' => 1,
                        ];
                    }
                }

                $mediaInDir[$row['path']] = [
                    'media' => $media,
                    'playlists' => array_values($playlists),
                ];
            }

            $foldersInDir = [];
            foreach ($foldersInDirRaw as $folderRow) {
                if (!isset($foldersInDir[$folderRow['path']])) {
                    $foldersInDir[$folderRow['path']] = [
                        'playlists' => [],
                    ];
                }

                $foldersInDir[$folderRow['path']]['playlists'][] = [
                    'id' => $folderRow['playlist']['id'],
                    'name' => $folderRow['playlist']['name'],
                    'short_name' => Entity\StationPlaylist::generateShortName(
                        $folderRow['playlist']['name']
                    ),
                ];
            }

            $unprocessableMedia = [];
            foreach ($unprocessableMediaRaw as $unprocessableRow) {
                $unprocessableMedia[$unprocessableRow['path']] = $unprocessableRow['error'];
            }

            if ($isSearch) {
                if ('special:unprocessable' === $searchPhrase) {
                    $files = array_keys($unprocessableMedia);
                } else {
                    $files = array_keys($mediaInDir);
                }
            } else {
                $protectedPaths = [
                    Entity\StationMedia::DIR_ALBUM_ART,
                    Entity\StationMedia::DIR_WAVEFORMS,
                    Entity\StationMedia::DIR_FOLDER_COVERS,
                ];

                $files = $fs->listContents($currentDir, false)->filter(
                    fn(StorageAttributes $attributes) => !($currentDir === '' && in_array(
                        $attributes->path(),
                        $protectedPaths,
                        true
                    ))
                );
            }

            $result = [];
            foreach ($files as $file) {
                $row = new Entity\Api\FileList();

                if ($file instanceof StorageAttributes) {
                    $row->path = $file->path();
                    $row->timestamp = $file->lastModified() ?? 0;
                    $row->is_dir = $file->isDir();
                } else {
                    $row->path = $file;
                    $row->timestamp = $fs->lastModified($file);
                    $row->is_dir = false;
                }

                $row->size = ($row->is_dir) ? 0 : $fs->fileSize($row->path);

                $shortname = ($isSearch)
                    ? $row->path
                    : basename($row->path);

                $max_length = 60;
                if (mb_strlen($shortname) > $max_length) {
                    $shortname = mb_substr($shortname, 0, $max_length - 15) . '...' . mb_substr($shortname, -12);
                }
                $row->path_short = $shortname;

                $row->media = new Entity\Api\FileListMedia();

                if (isset($mediaInDir[$row->path])) {
                    $row->media = $mediaInDir[$row->path]['media'];
                    $row->text = $row->media->text;
                    $row->playlists = (array)$mediaInDir[$row->path]['playlists'];
                } elseif ($row->is_dir) {
                    $row->text = __('Directory');

                    if (isset($foldersInDir[$row->path])) {
                        $row->playlists = (array)$foldersInDir[$row->path]['playlists'];
                    }
                } elseif (isset($unprocessableMedia[$row->path])) {
                    $row->text = sprintf(
                        __('File Not Processed: %s'),
                        Utilities\Strings::truncateText($unprocessableMedia[$row->path])
                    );
                } elseif (MimeType::isPathImage($row->path)) {
                    $row->is_cover_art = true;
                    $row->text = __('Cover Art');
                } else {
                    $row->text = __('File Processing');
                }

                $result[] = $row;
            }

            $this->cache->set($cacheKey, $result, 300);
        }

        // Apply sorting
        [$sort, $sortOrder] = $this->getSortFromRequest($request);

        $propertyAccessor = self::getPropertyAccessor();

        usort(
            $result,
            static fn(Entity\Api\FileList $a, Entity\Api\FileList $b) => self::sortRows(
                $a,
                $b,
                $propertyAccessor,
                $searchPhrase,
                $sort,
                $sortOrder
            )
        );

        $paginator = Paginator::fromArray($result, $request);

        // Add processor-intensive data for just this page.
        $stationId = $station->getIdRequired();

        $paginator->setPostprocessor(
            static fn(Entity\Api\FileList $row) => self::postProcessRow($row, $router, $stationId)
        );

        return $paginator->write($response);
    }

    private function parseSearchQuery(
        Entity\Station $station,
        string $query
    ): array {
        $playlist = null;

        if (str_contains($query, 'playlist:')) {
            preg_match('/playlist:(\S*)/', $query, $matches, PREG_UNMATCHED_AS_NULL);

            if ($matches[1]) {
                $playlistId = $matches[1];

                if (!is_numeric($playlistId)) {
                    $playlistNameLookupRaw = $this->em->createQuery(
                        <<<'DQL'
                        SELECT sp.id, sp.name
                        FROM App\Entity\StationPlaylist sp
                        WHERE sp.station = :station
                        DQL
                    )->setParameter('station', $station)
                        ->getArrayResult();

                    foreach ($playlistNameLookupRaw as $playlistRow) {
                        $shortName = Entity\StationPlaylist::generateShortName($playlistRow['name']);
                        if ($shortName === $playlistId) {
                            $playlistId = $playlistRow['id'];
                            break;
                        }
                    }
                }

                $playlist = $this->em->getRepository(Entity\StationPlaylist::class)
                    ->findOneBy(
                        [
                            'station' => $station,
                            'id' => $playlistId,
                        ]
                    );
            }

            $query = trim(str_replace($matches[0] ?? '', '', $query));
        }

        if (in_array($query, ['*', '%'], true)) {
            $query = '';
        }

        return [$query, $playlist];
    }

    private static function sortRows(
        Entity\Api\FileList $a,
        Entity\Api\FileList $b,
        PropertyAccessorInterface $propertyAccessor,
        ?string $searchPhrase = null,
        ?string $sort = null,
        string $sortOrder = Criteria::ASC
    ): int {
        if ('special:duplicates' === $searchPhrase) {
            return $a->media->id <=> $b->media->id;
        }

        $isDirComp = $b->is_dir <=> $a->is_dir;
        if (0 !== $isDirComp) {
            return $isDirComp;
        }

        if (!$sort) {
            $aVal = $a->path;
            $bVal = $b->path;
            return (Criteria::ASC === $sortOrder) ? $aVal <=> $bVal : $bVal <=> $aVal;
        }

        return self::sortByDotNotation($a, $b, $propertyAccessor, $sort, $sortOrder);
    }

    private static function postProcessRow(
        Entity\Api\FileList $row,
        RouterInterface $router,
        int $stationId
    ): Entity\Api\FileList {
        if (null !== $row->media->media_id) {
            $artMediaId = $row->media->unique_id;
            if (0 !== $row->media->art_updated_at) {
                $artMediaId .= '-' . $row->media->art_updated_at;
            }

            $row->media->art = $router->named(
                'api:stations:media:art',
                [
                    'station_id' => $stationId,
                    'media_id' => $artMediaId,
                ]
            );

            $row->media->links = [
                'play' => $router->named(
                    'api:stations:files:play',
                    ['station_id' => $stationId, 'id' => $row->media->media_id],
                    [],
                    true
                ),
                'edit' => $router->named(
                    'api:stations:file',
                    ['station_id' => $stationId, 'id' => $row->media->media_id],
                ),
                'art' => $router->named(
                    'api:stations:media:art-internal',
                    ['station_id' => $stationId, 'media_id' => $row->media->media_id]
                ),
                'waveform' => $router->named(
                    'api:stations:media:waveform',
                    [
                        'station_id' => $stationId,
                        'media_id' => $row->media->unique_id . '-' . $row->media->art_updated_at,
                    ]
                ),
            ];
        }

        $row->links = [
            'download' => $router->named(
                'api:stations:files:download',
                ['station_id' => $stationId],
                ['file' => $row->path]
            ),
            'rename' => $router->named(
                'api:stations:files:rename',
                ['station_id' => $stationId],
                ['file' => $row->path]
            ),
        ];

        return $row;
    }
}
