<?php

/*
 * This file is part of the Fidry\Console package.
 *
 * (c) Théo FIDRY <theo.fidry@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Fidry\Console\Internal;

use Fidry\Console\Input\InvalidInputValueType;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException as AssertInvalidArgumentException;
use function array_is_list;
use function get_debug_type;
use function is_array;
use function is_bool;
use function is_string;
use function sprintf;
use function var_export;

/**
 * @private
 * @psalm-type ArgumentInput = null|string|list<string>
 * @psalm-type OptionInput = null|bool|string|list<string>
 */
final class InputAssert
{
    private function __construct()
    {
    }

    /**
     * @psalm-assert ArgumentInput $argument
     */
    public static function assertIsValidArgumentType(mixed $argument, string $name): void
    {
        if (null === $argument || is_string($argument)) {
            return;
        }

        if (!is_array($argument) || !array_is_list($argument)) {
            throw new InvalidInputValueType(
                sprintf(
                    'Expected an argument value type to be "null|string|list<string>". Got "%s" for the argument "%s".',
                    get_debug_type($argument),
                    $name,
                ),
            );
        }

        foreach ($argument as $item) {
            self::assertIsValidArgumentType($item, $name);
        }
    }

    /**
     * @psalm-assert OptionInput $option
     */
    public static function assertIsValidOptionType(mixed $option, string $name): void
    {
        if (null === $option || is_bool($option) || is_string($option)) {
            return;
        }

        if (!is_array($option) || !array_is_list($option)) {
            throw new InvalidInputValueType(
                sprintf(
                    'Expected an option value type to be "null|bool|string|list<string>". Got "%s" for the option "%s".',
                    get_debug_type($option),
                    $name,
                ),
            );
        }

        foreach ($option as $item) {
            self::assertIsValidOptionType($item, $name);
        }
    }

    /**
     * @param ArgumentInput|OptionInput $value
     * @param non-empty-string          $label
     *
     * @psalm-assert scalar|null $value
     */
    public static function assertIsScalar(array|bool|string|null $value, string $label): void
    {
        self::castThrowException(
            static function () use ($value): void {
                if (null === $value) {
                    return;
                }

                Assert::scalar(
                    $value,
                    sprintf(
                        'Expected a null or scalar value. Got the value: "%s"',
                        self::castType($value),
                    ),
                );
            },
            $label,
        );
    }

    /**
     * @param ArgumentInput|OptionInput $value
     * @param non-empty-string          $label
     *
     * @psalm-assert list<string> $value
     */
    public static function assertIsList(array|bool|string|null $value, string $label): void
    {
        self::castThrowException(
            static function () use ($value): void {
                Assert::isArray(
                    $value,
                    sprintf(
                        'Cannot cast a non-array input argument into an array. Got "%s"',
                        self::castType($value),
                    ),
                );
                /** @psalm-suppress RedundantConditionGivenDocblockType */
                Assert::isList(
                    $value,
                    sprintf(
                        'Expected array to be a list. Got "%s"',
                        self::castType($value),
                    ),
                );
            },
            $label,
        );
    }

    /**
     * @param ArgumentInput|OptionInput $value
     * @param non-empty-string          $label
     *
     * @psalm-assert numeric $value
     */
    public static function numericString(array|bool|string|null $value, string $label): void
    {
        self::castThrowException(
            static function () use ($value, $label): void {
                self::assertIsScalar($value, $label);
                Assert::string(
                    $value,
                    sprintf(
                        'Expected a numeric string. Got "%s"',
                        self::castType($value),
                    ),
                );
                Assert::numeric(
                    $value,
                    sprintf(
                        'Expected a numeric string. Got "%s"',
                        self::castType($value),
                    ),
                );
            },
            $label,
        );
    }

    /**
     * @param ArgumentInput|OptionInput $value
     * @param non-empty-string          $label
     *
     * @psalm-assert string $value
     */
    public static function integerString(array|bool|string|null $value, string $label): void
    {
        self::castThrowException(
            static function () use ($value, $label): void {
                self::assertIsScalar($value, $label);
                Assert::string(
                    $value,
                    sprintf(
                        'Expected an integer string. Got "%s"',
                        self::castType($value),
                    ),
                );
                Assert::digits(
                    $value,
                    sprintf(
                        'Expected an integer string. Got "%s"',
                        self::castType($value),
                    ),
                );
            },
            $label,
        );
    }

    /**
     * @param ArgumentInput|OptionInput $value
     * @param non-empty-string          $label
     *
     * @psalm-assert string $value
     */
    public static function string(array|bool|string|null $value, string $label): void
    {
        self::castThrowException(
            static function () use ($value, $label): void {
                self::assertIsScalar($value, $label);
                Assert::string(
                    $value,
                    sprintf(
                        'Expected a string. Got "%s"',
                        self::castType($value),
                    ),
                );
            },
            $label,
        );
    }

    /**
     * @param callable():void  $callable
     * @param non-empty-string $label
     */
    public static function castThrowException(callable $callable, string $label): void
    {
        try {
            $callable();
        } catch (AssertInvalidArgumentException $exception) {
            throw InvalidInputValueType::fromAssert($exception, $label);
        }
    }

    /**
     * @param ArgumentInput|OptionInput $value
     */
    public static function castType(array|bool|string|null $value): string
    {
        return var_export($value, true);
    }
}
