<?php

namespace App\Tmdb;

use Aimeos\Map;
use App\Enum\MediaType;
use App\ValueObject\ResultFactory;
use Carbon\Carbon;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Tmdb\Api\Find;
use Tmdb\Client;
use Tmdb\Event\BeforeRequestEvent;
use Tmdb\Event\Listener\Request\AcceptJsonRequestListener;
use Tmdb\Event\Listener\Request\ApiTokenRequestListener;
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
use Tmdb\Event\Listener\Request\UserAgentRequestListener;
use Tmdb\Event\Listener\RequestListener;
use Tmdb\Event\RequestEvent;
use Tmdb\Model\Movie;
use Tmdb\Model\Search\SearchQuery\KeywordSearchQuery;
use Tmdb\Model\Tv;
use Tmdb\Repository\MovieRepository;
use Tmdb\Repository\SearchRepository;
use Tmdb\Repository\TvEpisodeRepository;
use Tmdb\Repository\TvRepository;
use Tmdb\Repository\TvSeasonRepository;
use Tmdb\Token\Api\ApiToken;
use Tmdb\Token\Api\BearerToken;

class Tmdb
{
    protected Client $client;

    protected MovieRepository $movieRepository;

    protected TvRepository $tvRepository;

    const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";

    public function __construct(
        private readonly CacheInterface $cache,
        private readonly EventDispatcherInterface $eventDispatcher,
        #[Autowire(env: 'TMDB_API')] string $apiKey,
    ) {
        $this->client = new Client(
            [
                /** @var ApiToken|BearerToken */
                'api_token' => new BearerToken($apiKey),
                'secure' => true,
                'base_uri' => Client::TMDB_URI,
                'event_dispatcher' => [
                    'adapter' => $this->eventDispatcher,
                ],
                // We make use of PSR-17 and PSR-18 auto discovery to automatically guess these, but preferably set these explicitly.
                'http' => [
                    'client' => null,
                    'request_factory' => null,
                    'response_factory' => null,
                    'stream_factory' => null,
                    'uri_factory' => null,
                ],
                'hydration' => [
                    'event_listener_handles_hydration' => false,
                    'only_for_specified_models' => []
                ]
            ]
        );

        /**
         * Required event listeners and events to be registered with the PSR-14 Event Dispatcher.
         */
        $requestListener = new RequestListener($this->client->getHttpClient(), $this->eventDispatcher);
        $this->eventDispatcher->addListener(RequestEvent::class, $requestListener);

        $apiTokenListener = new ApiTokenRequestListener($this->client->getToken());
        $this->eventDispatcher->addListener(BeforeRequestEvent::class, $apiTokenListener);

        $acceptJsonListener = new AcceptJsonRequestListener();
        $this->eventDispatcher->addListener(BeforeRequestEvent::class, $acceptJsonListener);

        $jsonContentTypeListener = new ContentTypeJsonRequestListener();
        $this->eventDispatcher->addListener(BeforeRequestEvent::class, $jsonContentTypeListener);

        $userAgentListener = new UserAgentRequestListener();
        $this->eventDispatcher->addListener(BeforeRequestEvent::class, $userAgentListener);

        $this->movieRepository = new MovieRepository($this->client);
        $this->tvRepository = new TvRepository($this->client);
    }

    public function popularMovies(int $page = 1, ?int $limit = null)
    {
        $movies = $this->movieRepository->getPopular(['page' => $page]);

        $movies = $movies->map(function ($movie) use ($movies) {
            return $this->parseResult($movies[$movie], "movie");
        });

        $movies = Map::from($movies->toArray())->filter(function ($movie) {
            return $movie !== null
                && $movie->imdbId !== null
                && $movie->tmdbId !== null
                && $movie->title  !== null
                && $movie->poster !== null
                && $movie->description !== null
                && $movie->mediaType !== null;
        });

        $movies = array_values($movies->toArray());

        if (null !== $limit) {
            $movies = array_slice($movies, 0, $limit);
        }

        return $movies;
    }

    public function popularTvShows(int $page = 1, ?int $limit = null)
    {
        $movies = $this->tvRepository->getPopular(['page' => $page]);

        $movies = $movies->map(function ($movie) use ($movies) {
            return $this->parseResult($movies[$movie], "movie");
        });

        $movies = Map::from($movies->toArray())->filter(function ($movie) {
            return $movie !== null
                && $movie->imdbId !== null
                && $movie->tmdbId !== null
                && $movie->title  !== null
                && $movie->poster !== null
                && $movie->description !== null
                && $movie->mediaType !== null;
        });

        $movies = array_values($movies->toArray());

        if (null !== $limit) {
            $movies = array_slice($movies, 0, $limit);
        }

        return $movies;
    }

    public function search(string $term, int $page = 1)
    {
        $searchRepository = new SearchRepository($this->client);
        $searchResults = $searchRepository->searchMulti($term, new KeywordSearchQuery(['page' => $page]));

        $results = [];
        foreach ($searchResults as $result) {
            if (!$result instanceof Movie && !$result instanceof Tv) {
                continue;
            }

            $results[] = $this->parseResult($result);
        }

        $results = array_filter($results, fn ($result) => null !== $result->imdbId);

        return $results;
    }

    public function find(string $id)
    {
        $finder = new Find($this->client);
        $result = $finder->findBy($id, ['external_source' => 'imdb_id']);

        if (count($result['movie_results']) > 0) {
            return $result['movie_results'][0]['id'];
        } elseif (count($result['tv_results']) > 0) {
            return $result['tv_results'][0]['id'];
        } elseif (count($result['tv_episode_results']) > 0) {
            return $result['tv_episode_results'][0]['show_id'];
        }

        throw new \Exception("No results found for $id");
    }

    public function movieDetails(string $id)
    {
        $client = new MovieRepository($this->client);
        $details = $client->getApi()->getMovie($id, ['append_to_response' => 'external_ids']);
        return $this->parseResult($details, "movie");
    }

    public function tvDetails(string $id)
    {
        $client = new TvRepository($this->client);
        $details = $client->getApi()->getTvshow($id, ['append_to_response' => 'external_ids,seasons']);
        $details = $this->getEpisodesFromSeries($details);
        return $this->parseResult($details, "tvshow");
    }

    public function episodeDetails(string $id, string $season, string $episode)
    {
        $client = new TvEpisodeRepository($this->client);
        $result = $client->getApi()->getEpisode($id, $season, $episode, ['append_to_response' => 'external_ids']);
        return $this->parseResult($result, "episode");
    }

    public function getEpisodesFromSeries(array $series)
    {
        $client = new TvSeasonRepository($this->client);
        foreach ($series['seasons'] as $season) {
            if ($season['episode_count'] <= 0 || $season['name'] === 'Specials') {
                continue;
            }

            $series['episodes'][$season['season_number']] = Map::from(
                $client->getApi()->getSeason($series['id'], $season['season_number'])['episodes']
            )->map(function ($data) {
                $data['poster'] = (null !== $data['still_path']) ? self::POSTER_IMG_PATH . $data['still_path'] : null;
                return $data;
            })->toArray();
        }
        return $series;
    }

    public function mediaDetails(string $id, string $type)
    {
        $id = $this->find($id);

        if ($type === "movies") {
            return $this->movieDetails($id);
        } else {
            return $this->tvDetails($id);
        }
    }

    private function parseResult($result, $mediaType = null)
    {
        if (is_array($result)) {
            return $this->parseFromArray($result, $mediaType);
        } else {
            return $this->parseFromObject($result);
        }
    }

    private function parseFromArray($data, $mediaType)
    {
        if (null === $mediaType) {
            throw new \Exception("A media type must be set when parsing from an array.");
        }

        if ($mediaType === 'movie') {
            $result = $this->parseMovie($data, self::POSTER_IMG_PATH);
        } elseif ($mediaType === 'tvshow') {
            $result = $this->parseTvShow($data, self::POSTER_IMG_PATH);
        } elseif ($mediaType === 'episode') {
            $result = $this->parseEpisode($data, self::POSTER_IMG_PATH);
        }

        return $result;
    }

    private function parseTvShow(array $data, string $posterBasePath): TmdbResult
    {
        return new TmdbResult(
            imdbId: $data['external_ids']['imdb_id'],
            tmdbId: $data['id'],
            title: $data['name'],
            poster: (null !== $data['poster_path']) ? $posterBasePath . $data['poster_path'] : null,
            description: $data['overview'],
            year: (new \DateTime($data['first_air_date']))->format('Y'),
            mediaType: "tvshows",
            episodes: $data['episodes'],
        );
    }

    private function parseEpisode(array $data, string $posterBasePath): TmdbResult
    {
        return new TmdbResult(
            imdbId: $data['external_ids']['imdb_id'],
            tmdbId: $data['id'],
            title: $data['name'],
            poster: (null !== $data['still_path']) ? $posterBasePath . $data['still_path'] : null,
            description: $data['overview'],
            year: (new \DateTime($data['air_date']))->format('Y'),
            mediaType: "tvshows",
            episodes: null,
            episodeAirDate: (new \DateTime($data['air_date']))->format('m/d/Y'),
        );
    }

    private function parseMovie(array $data, string $posterBasePath): TmdbResult
    {
        return new TmdbResult(
            imdbId: $data['external_ids']['imdb_id'],
            tmdbId: $data['id'],
            title: $data['title'],
            poster: (null !== $data['poster_path']) ? $posterBasePath . $data['poster_path'] : null,
            description: $data['overview'],
            year: (new \DateTime($data['release_date']))->format('Y'),
            mediaType: "movies",
        );
    }


    private function parseFromObject($result): TmdbResult
    {
            $mediaType = $result instanceof Movie ? MediaType::Movie->value : MediaType::TvShow->value;
            $tmdbResult = new TmdbResult();
            $tmdbResult->mediaType = $mediaType;
            $tmdbResult->tmdbId = $result->getId();
            $tmdbResult->imdbId = $this->getImdbId($result->getId(), $mediaType);
            $tmdbResult->title = $this->getTitle($result, $mediaType);
            $tmdbResult->poster = self::POSTER_IMG_PATH . $result->getPosterImage();
            $tmdbResult->year = $this->getReleaseDate($result, $mediaType);
            $tmdbResult->description = $result->getOverview();
            return $tmdbResult;
    }

    public function getImdbId(string $tmdbId, $mediaType)
    {
        $externalIds = $this->cache->get("tmdb.externalIds.{$tmdbId}",
            function (ItemInterface $item) use ($tmdbId, $mediaType) {
                switch (MediaType::tryFrom($mediaType)->value) {
                    case MediaType::Movie->value:
                        return $this->movieRepository->getExternalIds($tmdbId);
                    case MediaType::TvShow->value:
                        return $this->tvRepository->getExternalIds($tmdbId);
                    default:
                        return null;
                }
        });

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

        return $externalIds->getImdbId() !== "" ? $externalIds->getImdbId() : "null";
    }

    public function getImages($tmdbId, $mediaType)
    {
        return $this->cache->get("tmdb.images.{$tmdbId}",
            function (ItemInterface $item) use ($tmdbId, $mediaType) {
                switch (MediaType::tryFrom($mediaType)->value) {
                    case MediaType::Movie->value:
                        return $this->movieRepository->getImages($tmdbId);
                    case MediaType::TvShow->value:
                        return $this->tvRepository->getImages($tmdbId);
                    default:
                        return null;
                }
        });
    }

    private function getReleaseDate($result, $mediaType): string
    {
        switch (MediaType::tryFrom($mediaType)->value) {
            case MediaType::Movie->value:
                return ($result->getReleaseDate() instanceof \DateTime)
                    ? $result->getReleaseDate()->format('Y')
                    : $result->getReleaseDate();
            case MediaType::TvShow->value:
                return ($result->getFirstAirDate() instanceof \DateTime)
                    ? $result->getFirstAirDate()->format('Y')
                    : $result->getFirstAirDate();
            default:
                return "";
        }
    }

    private function getTitle($result, $mediaType): string
    {
        switch (MediaType::tryFrom($mediaType)->value) {
            case MediaType::Movie->value:
                return $result->getTitle();
            case MediaType::TvShow->value:
                return $result->getName();
            default:
                return "";
        }
    }
}
