<?php

namespace App\Monitor\Action\Handler;

use Aimeos\Map;
use App\Base\Service\MediaFiles;
use App\Monitor\Action\Command\MonitorMovieCommand;
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
use App\Monitor\Action\Result\MonitorTvShowResult;
use App\Monitor\Framework\Entity\Monitor;
use App\Monitor\Framework\Repository\MonitorRepository;
use App\Tmdb\Dto\TmdbEpisodeDto;
use App\Tmdb\TmdbClient;
use App\Tmdb\TmdbResult;
use Carbon\Carbon;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use App\Base\Util\PTN;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Psr\Log\LoggerInterface;

/** @implements HandlerInterface<MonitorMovieCommand> */
readonly class MonitorTvShowHandler implements HandlerInterface
{
    public function __construct(
        private MonitorRepository $monitorRepository,
        private EntityManagerInterface $entityManager,
        private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
        private MediaFiles $mediaFiles,
        private LoggerInterface $logger,
        private TmdbClient $tmdb,
    ) {
    }

    public function handle(CommandInterface $command): ResultInterface
    {
        $this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');

        $monitor = $this->monitorRepository->find($command->monitorId);
        $showTmdbData = $this->tmdb->tvshowDetails($monitor->getImdbId());

        $this->refreshData($monitor, $showTmdbData);

        // Check current episodes
        $downloadedEpisodes = $this->getDownloadedEpisodes($monitor->getTitle(), $showTmdbData);

        $this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());

        // Compare against list from TMDB
        $episodesInShow = Map::from(
            $this->tmdb->tvshowDetails($monitor->getImdbId())->episodes
        )->flat(1)
            ->filter(fn(TmdbEpisodeDto $episode) => $episode->seasonNumber >= $monitor->getSeason())
            ->values();

        $this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle());

        $episodeMonitors = [];
        // Dispatch Episode commands for each missing Episode
        foreach ($episodesInShow as $episode) {
            /** @var TmdbEpisodeDto $episode */
            // Only monitor future episodes
            $this->logger->info('> [MonitorTvShowHandler] Evaluating "' . $monitor->getTitle() . '", season "' . $episode->seasonNumber . '" episode "' . $episode->episodeNumber . '"');
            $episodeInFuture = $this->episodeReleasedAfterMonitorCreated($monitor->getCreatedAt(), $episode);
            $this->logger->info('> [MonitorTvShowHandler] ...Released after monitor started? ' . (true === $episodeInFuture ? 'YES' : 'NO'));
            if (false === $episodeInFuture) {
                $this->logger->info('> [MonitorTvShowHandler] ...Skipping');
                continue;
            }

            // Check if the episode is already downloaded
            $episodeExists = $this->episodeExists($episode, $downloadedEpisodes);
            $this->logger->info('> [MonitorTvShowHandler] ...Episode exists? ' . (true === $episodeExists ? 'YES' : 'NO'));
            if (true === $episodeExists) {
                $this->logger->info('> [MonitorTvShowHandler] ...Skipping');
                continue;
            }

            // Check for existing monitors
            $monitorExists = $this->monitorExists($monitor, $episode);
            $this->logger->info('> [MonitorTvShowHandler] ...Monitor exists? ' . (true === $monitorExists ? 'YES' : 'NO'));
            if (true === $monitorExists) {
                $this->logger->info('> [MonitorTvShowHandler] ...Skipping');
                continue;
            }

            // Create the monitor
            $episodeMonitor = (new Monitor())
                ->setParent($monitor)
                ->setUser($monitor->getUser())
                ->setTmdbId($monitor->getTmdbId())
                ->setImdbId($monitor->getImdbId())
                ->setTitle($monitor->getTitle())
                ->setMonitorType('tvepisode')
                ->setSeason($episode->seasonNumber)
                ->setEpisode($episode->episodeNumber)
                ->setAirDate($episode->airDate !== null && $episode->airDate !== "" ? Carbon::parse($episode->airDate) : null)
                ->setCreatedAt(new DateTimeImmutable())
                ->setSearchCount(0)
                ->setStatus('New');

            $this->monitorRepository->getEntityManager()->persist($episodeMonitor);
            $this->monitorRepository->getEntityManager()->flush();

            $episodeMonitors[] = $episodeMonitor;

            // Immediately run the monitor
            $command = new MonitorTvEpisodeCommand($episodeMonitor->getId());
            $this->monitorTvEpisodeHandler->handle($command);
            $this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand');
        }

        // Set the status to Active, so it will be re-executed.
        $monitor->setStatus('Active');
        $monitor->setLastSearch(new DateTimeImmutable());
        $monitor->setSearchCount($monitor->getSearchCount() + 1);
        $this->entityManager->flush();

        return new MonitorTvShowResult(
            status: 'OK',
            result: [
                'monitor' => $monitor,
                'new_monitors' => $episodeMonitors,
            ]
        );
    }

    private function getDownloadedEpisodes(string $title, TmdbResult $showTmdbData): Map
    {
        // Episodes in folder w/o the year
        $downloadedEpisodes = $this->mediaFiles
            ->getEpisodes($title)
            ->map(fn($episode) => (object) new PTN()->parse($episode))
            ->filter(fn($episode) =>
                property_exists($episode, 'episode')
                && property_exists($episode, 'season')
                && null !== $episode->episode
                && null !== $episode->season
            );

        return $downloadedEpisodes->concat(
            // Episodes in folder w/ the year
            $this->mediaFiles
                ->getEpisodes(sprintf("%s (%s)", $title, $showTmdbData->year))
                ->map(fn($episode) => (object) new PTN()->parse($episode))
                ->filter(fn($episode) =>
                    property_exists($episode, 'episode')
                    && property_exists($episode, 'season')
                    && null !== $episode->episode
                    && null !== $episode->season
                )
        );
    }

    private function episodeReleasedAfterMonitorCreated(
        string|DateTimeImmutable $monitorStartDate,
        TmdbEpisodeDto $episodeInShow
    ): bool {
        $monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0);
        $episodeAirDate = Carbon::parse($episodeInShow->airDate);
        return $episodeAirDate >= $monitorStartDate;
    }

    private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool
    {
        return $downloadedEpisodes->filter(
                fn(object $episode) => $episode->episode === $episodeInShow->episodeNumber
                    && $episode->season === $episodeInShow->seasonNumber
            )->count() > 0;
    }

    private function monitorExists(Monitor $monitor, TmdbEpisodeDto $episode): bool
    {
        return $this->monitorRepository->findOneBy([
                'imdbId' => $monitor->getImdbId(),
                'title' => $monitor->getTitle(),
                'monitorType' => 'tvepisode',
                'season' => $episode->seasonNumber,
                'episode' => $episode->episodeNumber,
                'status' => ['New', 'Active', 'In Progress']
            ]) !== null;
    }

    private function refreshData(Monitor $monitor, TmdbResult $showTmdbData)
    {
        if (null === $monitor->getPoster()) {
            $this->logger->info('> [MonitorTvShowHandler] Refreshing poster for "' . $monitor->getTitle() . '"');
            $poster = $showTmdbData->poster;
            if (null !== $poster && "" !== $poster) {
                $monitor->setPoster($poster);
            }
        }
    }
}
