<?php

declare(strict_types=1);

namespace Kreait\Firebase\JWT\Value;

use DateInterval;
use DateTimeImmutable;
use InvalidArgumentException;
use Stringable;
use Throwable;

use function assert;
use function is_int;

/**
 * Adapted duration class from gamez/duration.
 *
 * @see https://github.com/jeromegamez/duration-php
 */
final class Duration implements Stringable
{
    public const NONE = 'PT0S';

    private function __construct(private readonly DateInterval $value)
    {
    }

    public function __toString(): string
    {
        return $this->toString();
    }

    public static function make(self|DateInterval|int|string $value): self
    {
        if ($value instanceof self) {
            return $value;
        }

        if ($value instanceof DateInterval) {
            return self::fromDateInterval($value);
        }

        if (is_int($value)) {
            return self::inSeconds($value);
        }

        if (mb_strpos($value, 'P') === 0) {
            return self::fromDateIntervalSpec($value);
        }

        try {
            $interval = DateInterval::createFromDateString($value);
            assert($interval instanceof DateInterval);
        } catch (Throwable) {
            throw new InvalidArgumentException("Unable to determine a duration from '{$value}'");
        }

        $duration = self::fromDateInterval($interval);
        // If the string doesn't contain a zero, but the result equals to zero
        // the value must be invalid.
        if (mb_strpos($value, '0') !== false) {
            return $duration;
        }

        if (!$duration->equals(self::none())) {
            return $duration;
        }

        throw new InvalidArgumentException("Unable to determine a duration from '{$value}'");
    }

    /**
     * @throws InvalidArgumentException
     */
    public static function inSeconds(int $seconds): self
    {
        if ($seconds < 0) {
            throw new InvalidArgumentException('A duration can not be negative');
        }

        return self::fromDateIntervalSpec('PT'.$seconds.'S');
    }

    /**
     * @throws InvalidArgumentException
     */
    public static function fromDateIntervalSpec(string $spec): self
    {
        try {
            $interval = new DateInterval($spec);
        } catch (Throwable) {
            throw new InvalidArgumentException("'{$spec}' is not a valid DateInterval specification");
        }

        return self::fromDateInterval($interval);
    }

    public static function fromDateInterval(DateInterval $interval): self
    {
        $now = new DateTimeImmutable();
        $then = $now->add($interval);

        if ($then < $now) {
            throw new InvalidArgumentException('A duration can not be negative');
        }

        return new self($interval);
    }

    public static function none(): self
    {
        return self::fromDateIntervalSpec(self::NONE);
    }

    public function value(): DateInterval
    {
        return $this->value;
    }

    public function isLargerThan(self|DateInterval|int|string $other): bool
    {
        return 1 === $this->compareTo($other);
    }

    public function equals(self|DateInterval|int|string $other): bool
    {
        return 0 === $this->compareTo($other);
    }

    public function isSmallerThan(self|DateInterval|int|string $other): bool
    {
        return -1 === $this->compareTo($other);
    }

    public function compareTo(self|DateInterval|int|string $other): int
    {
        $other = self::make($other);

        $now = self::now();

        return $now->add($this->value) <=> $now->add($other->value);
    }

    public function toString(): string
    {
        return self::toDateIntervalSpec(self::normalizeInterval($this->value));
    }

    private static function now(): DateTimeImmutable
    {
        return new DateTimeImmutable('@'.time());
    }

    private static function normalizeInterval(DateInterval $value): DateInterval
    {
        $now = self::now();
        $then = $now->add($value);

        return $now->diff($then);
    }

    private static function toDateIntervalSpec(DateInterval $value): string
    {
        $spec = 'P';
        $spec .= 0 !== $value->y ? $value->y.'Y' : '';
        $spec .= 0 !== $value->m ? $value->m.'M' : '';
        $spec .= 0 !== $value->d ? $value->d.'D' : '';

        $spec .= 'T';
        $spec .= 0 !== $value->h ? $value->h.'H' : '';
        $spec .= 0 !== $value->i ? $value->i.'M' : '';
        $spec .= 0 !== $value->s ? $value->s.'S' : '';

        if ('T' === mb_substr($spec, -1)) {
            $spec = mb_substr($spec, 0, -1);
        }

        if ('P' === $spec) {
            return self::NONE;
        }

        return $spec;
    }
}
