<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\CLI\Command\ShortUrl;

use Shlinkio\Shlink\CLI\Input\EndDateOption;
use Shlinkio\Shlink\CLI\Input\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtils;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithDeps;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

use function array_keys;
use function array_pad;
use function explode;
use function implode;
use function Shlinkio\Shlink\Core\ArrayUtils\map;
use function sprintf;

class ListShortUrlsCommand extends Command
{
    public const string NAME = 'short-url:list';

    private readonly StartDateOption $startDateOption;
    private readonly EndDateOption $endDateOption;

    public function __construct(
        private readonly ShortUrlListServiceInterface $shortUrlService,
        private readonly ShortUrlDataTransformerInterface $transformer,
    ) {
        parent::__construct();
        $this->startDateOption = new StartDateOption($this, 'short URLs');
        $this->endDateOption = new EndDateOption($this, 'short URLs');
    }

    protected function configure(): void
    {
        $this
            ->setName(self::NAME)
            ->setDescription('List all short URLs')
            ->addOption(
                'page',
                'p',
                InputOption::VALUE_REQUIRED,
                'The first page to list (10 items per page unless "--all" is provided).',
                '1',
            )
            ->addOption(
                'search-term',
                'st',
                InputOption::VALUE_REQUIRED,
                'A query used to filter results by searching for it on the longUrl and shortCode fields.',
            )
            ->addOption(
                'domain',
                'd',
                InputOption::VALUE_REQUIRED,
                'Used to filter results by domain. Use DEFAULT keyword to filter by default domain',
            )
            ->addOption(
                'tags',
                't',
                InputOption::VALUE_REQUIRED,
                'A comma-separated list of tags to filter results.',
            )
            ->addOption(
                'including-all-tags',
                'i',
                InputOption::VALUE_NONE,
                'If tags is provided, returns only short URLs having ALL tags.',
            )
            ->addOption(
                'exclude-max-visits-reached',
                null,
                InputOption::VALUE_NONE,
                'Excludes short URLs which reached their max amount of visits.',
            )
            ->addOption(
                'exclude-past-valid-until',
                null,
                InputOption::VALUE_NONE,
                'Excludes short URLs which have a "validUntil" date in the past.',
            )
            ->addOption(
                'order-by',
                'o',
                InputOption::VALUE_REQUIRED,
                'The field from which you want to order by. '
                    . 'Define ordering dir by passing ASC or DESC after "-" or ",".',
            )
            ->addOption(
                'show-tags',
                null,
                InputOption::VALUE_NONE,
                'Whether to display the tags or not.',
            )
            ->addOption(
                'show-domain',
                null,
                InputOption::VALUE_NONE,
                'Whether to display the domain or not. Those belonging to default domain will have value "DEFAULT".',
            )
            ->addOption(
                'show-api-key',
                'k',
                InputOption::VALUE_NONE,
                'Whether to display the API key name from which the URL was generated or not.',
            )
            ->addOption('show-api-key-name', 'm', InputOption::VALUE_NONE, '[DEPRECATED] Use show-api-key')
            ->addOption(
                'all',
                'a',
                InputOption::VALUE_NONE,
                'Disables pagination and just displays all existing URLs. Caution! If the amount of short URLs is big,'
                . ' this may end up failing due to memory usage.',
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $page = (int) $input->getOption('page');
        $searchTerm = $input->getOption('search-term');
        $domain = $input->getOption('domain');
        $tags = $input->getOption('tags');
        $tagsMode = $input->getOption('including-all-tags') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
        $tags = ! empty($tags) ? explode(',', $tags) : [];
        $all = $input->getOption('all');
        $startDate = $this->startDateOption->get($input, $output);
        $endDate = $this->endDateOption->get($input, $output);
        $orderBy = $this->processOrderBy($input);
        $columnsMap = $this->resolveColumnsMap($input);

        $data = [
            ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
            ShortUrlsParamsInputFilter::DOMAIN => $domain,
            ShortUrlsParamsInputFilter::TAGS => $tags,
            ShortUrlsParamsInputFilter::TAGS_MODE => $tagsMode,
            ShortUrlsParamsInputFilter::ORDER_BY => $orderBy,
            ShortUrlsParamsInputFilter::START_DATE => $startDate?->toAtomString(),
            ShortUrlsParamsInputFilter::END_DATE => $endDate?->toAtomString(),
            ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED => $input->getOption('exclude-max-visits-reached'),
            ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL => $input->getOption('exclude-past-valid-until'),
        ];

        if ($all) {
            $data[ShortUrlsParamsInputFilter::ITEMS_PER_PAGE] = Paginator::ALL_ITEMS;
        }

        do {
            $data[ShortUrlsParamsInputFilter::PAGE] = $page;
            $result = $this->renderPage($output, $columnsMap, ShortUrlsParams::fromRawData($data), $all);
            $page++;

            $continue = $result->hasNextPage() && $io->confirm(
                sprintf('Continue with page <options=bold>%s</>?', $page),
                false,
            );
        } while ($continue);

        $io->newLine();
        $io->success('Short URLs properly listed');

        return self::SUCCESS;
    }

    /**
     * @param array<string, callable(array $serializedShortUrl, ShortUrl $shortUrl): ?string> $columnsMap
     * @return Paginator<ShortUrlWithDeps>
     */
    private function renderPage(
        OutputInterface $output,
        array $columnsMap,
        ShortUrlsParams $params,
        bool $all,
    ): Paginator {
        $shortUrls = $this->shortUrlService->listShortUrls($params);

        $rows = map([...$shortUrls], function (ShortUrlWithDeps $shortUrl) use ($columnsMap) {
            $serializedShortUrl = $this->transformer->transform($shortUrl);
            return map($columnsMap, fn (callable $call) => $call($serializedShortUrl, $shortUrl->shortUrl));
        });

        ShlinkTable::default($output)->render(
            array_keys($columnsMap),
            $rows,
            $all ? null : PagerfantaUtils::formatCurrentPageMessage($shortUrls, 'Page %s of %s'),
        );

        return $shortUrls;
    }

    private function processOrderBy(InputInterface $input): string|null
    {
        $orderBy = $input->getOption('order-by');
        if (empty($orderBy)) {
            return null;
        }

        [$field, $dir] = array_pad(explode(',', $orderBy), 2, null);
        return $dir === null ? $field : sprintf('%s-%s', $field, $dir);
    }

    /**
     * @return array<string, callable(array $serializedShortUrl, ShortUrl $shortUrl): ?string>
     */
    private function resolveColumnsMap(InputInterface $input): array
    {
        $pickProp = static fn (string $prop): callable => static fn (array $shortUrl) => $shortUrl[$prop];
        $columnsMap = [
            'Short Code' => $pickProp('shortCode'),
            'Title' => $pickProp('title'),
            'Short URL' => $pickProp('shortUrl'),
            'Long URL' => $pickProp('longUrl'),
            'Date created' => $pickProp('dateCreated'),
            'Visits count' => static fn (array $shortUrl) => $shortUrl['visitsSummary']->total,
        ];
        if ($input->getOption('show-tags')) {
            $columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);
        }
        if ($input->getOption('show-domain')) {
            $columnsMap['Domain'] = static fn (array $_, ShortUrl $shortUrl): string =>
                $shortUrl->getDomain()->authority ?? Domain::DEFAULT_AUTHORITY;
        }
        if ($input->getOption('show-api-key') || $input->getOption('show-api-key-name')) {
            $columnsMap['API Key Name'] = static fn (array $_, ShortUrl $shortUrl): string|null =>
                $shortUrl->authorApiKey?->name;
        }

        return $columnsMap;
    }
}
