/*
 * Copyright 2004-present the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.oauth2.core.endpoint;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Map;

import org.jspecify.annotations.Nullable;

import org.springframework.security.oauth2.core.OAuth2DeviceCode;
import org.springframework.security.oauth2.core.OAuth2UserCode;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

/**
 * A representation of an OAuth 2.0 Device Authorization Response.
 *
 * @author Steve Riesenberg
 * @since 6.1
 * @see OAuth2DeviceCode
 * @see OAuth2UserCode
 * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8628#section-3.2">Section
 * 3.2 Device Authorization Response</a>
 */
public final class OAuth2DeviceAuthorizationResponse {

	private @Nullable OAuth2DeviceCode deviceCode;

	private @Nullable OAuth2UserCode userCode;

	private @Nullable String verificationUri;

	private @Nullable String verificationUriComplete;

	private long interval;

	private @Nullable Map<String, Object> additionalParameters;

	private OAuth2DeviceAuthorizationResponse() {
	}

	/**
	 * Returns the {@link OAuth2DeviceCode Device Code}.
	 * @return the {@link OAuth2DeviceCode}
	 */
	public OAuth2DeviceCode getDeviceCode() {
		Assert.notNull(this.deviceCode, "deviceCode cannot be null");
		return this.deviceCode;
	}

	/**
	 * Returns the {@link OAuth2UserCode User Code}.
	 * @return the {@link OAuth2UserCode}
	 */
	public OAuth2UserCode getUserCode() {
		Assert.notNull(this.userCode, "userCode cannot be null");
		return this.userCode;
	}

	/**
	 * Returns the end-user verification URI.
	 * @return the end-user verification URI
	 */
	public String getVerificationUri() {
		Assert.notNull(this.verificationUri, "verificationUri cannot be null");
		return this.verificationUri;
	}

	/**
	 * Returns the end-user verification URI that includes the user code, or {@code null}
	 * if not present.
	 * @return the end-user verification URI that includes the user code, or {@code null}
	 */
	public @Nullable String getVerificationUriComplete() {
		return this.verificationUriComplete;
	}

	/**
	 * Returns the minimum amount of time (in seconds) that the client should wait between
	 * polling requests to the token endpoint.
	 * @return the minimum amount of time between polling requests
	 */
	public long getInterval() {
		return this.interval;
	}

	/**
	 * Returns the additional parameters returned in the response.
	 * @return a {@code Map} of the additional parameters returned in the response, may be
	 * empty.
	 */
	public Map<String, Object> getAdditionalParameters() {
		Assert.notNull(this.additionalParameters, "additionalParameters cannot be null");
		return this.additionalParameters;
	}

	/**
	 * Returns a new {@link Builder}, initialized with the provided device code and user
	 * code values.
	 * @param deviceCode the value of the device code
	 * @param userCode the value of the user code
	 * @return the {@link Builder}
	 */
	public static Builder with(String deviceCode, String userCode) {
		Assert.hasText(deviceCode, "deviceCode cannot be empty");
		Assert.hasText(userCode, "userCode cannot be empty");
		return new Builder(deviceCode, userCode);
	}

	/**
	 * Returns a new {@link Builder}, initialized with the provided device code and user
	 * code.
	 * @param deviceCode the {@link OAuth2DeviceCode}
	 * @param userCode the {@link OAuth2UserCode}
	 * @return the {@link Builder}
	 */
	public static Builder with(OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) {
		Assert.notNull(deviceCode, "deviceCode cannot be null");
		Assert.notNull(userCode, "userCode cannot be null");
		return new Builder(deviceCode, userCode);
	}

	/**
	 * A builder for {@link OAuth2DeviceAuthorizationResponse}.
	 */
	public static final class Builder {

		private final String deviceCode;

		private final String userCode;

		private @Nullable String verificationUri;

		private @Nullable String verificationUriComplete;

		private long expiresIn;

		private long interval;

		private @Nullable Map<String, Object> additionalParameters;

		private Builder(OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) {
			this.deviceCode = deviceCode.getTokenValue();
			this.userCode = userCode.getTokenValue();
			this.expiresIn = ChronoUnit.SECONDS.between(deviceCode.getIssuedAt(), deviceCode.getExpiresAt());
		}

		private Builder(String deviceCode, String userCode) {
			this.deviceCode = deviceCode;
			this.userCode = userCode;
		}

		/**
		 * Sets the end-user verification URI.
		 * @param verificationUri the end-user verification URI
		 * @return the {@link Builder}
		 */
		public Builder verificationUri(String verificationUri) {
			this.verificationUri = verificationUri;
			return this;
		}

		/**
		 * Sets the end-user verification URI that includes the user code.
		 * @param verificationUriComplete the end-user verification URI that includes the
		 * user code, may be {@code null}
		 * @return the {@link Builder}
		 */
		public Builder verificationUriComplete(@Nullable String verificationUriComplete) {
			this.verificationUriComplete = verificationUriComplete;
			return this;
		}

		/**
		 * Sets the lifetime (in seconds) of the device code and user code.
		 * @param expiresIn the lifetime (in seconds) of the device code and user code
		 * @return the {@link Builder}
		 */
		public Builder expiresIn(long expiresIn) {
			this.expiresIn = expiresIn;
			return this;
		}

		/**
		 * Sets the minimum amount of time (in seconds) that the client should wait
		 * between polling requests to the token endpoint.
		 * @param interval the minimum amount of time between polling requests
		 * @return the {@link Builder}
		 */
		public Builder interval(long interval) {
			this.interval = interval;
			return this;
		}

		/**
		 * Sets the additional parameters returned in the response.
		 * @param additionalParameters the additional parameters returned in the response
		 * @return the {@link Builder}
		 */
		public Builder additionalParameters(Map<String, Object> additionalParameters) {
			this.additionalParameters = additionalParameters;
			return this;
		}

		/**
		 * Builds a new {@link OAuth2DeviceAuthorizationResponse}.
		 * @return a {@link OAuth2DeviceAuthorizationResponse}
		 */
		public OAuth2DeviceAuthorizationResponse build() {
			Assert.hasText(this.verificationUri, "verificationUri cannot be empty");
			Assert.isTrue(this.expiresIn > 0, "expiresIn must be greater than zero");

			Instant issuedAt = Instant.now();
			Instant expiresAt = issuedAt.plusSeconds(this.expiresIn);
			OAuth2DeviceCode deviceCode = new OAuth2DeviceCode(this.deviceCode, issuedAt, expiresAt);
			OAuth2UserCode userCode = new OAuth2UserCode(this.userCode, issuedAt, expiresAt);

			OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = new OAuth2DeviceAuthorizationResponse();
			deviceAuthorizationResponse.deviceCode = deviceCode;
			deviceAuthorizationResponse.userCode = userCode;
			deviceAuthorizationResponse.verificationUri = this.verificationUri;
			deviceAuthorizationResponse.verificationUriComplete = this.verificationUriComplete;
			deviceAuthorizationResponse.interval = this.interval;
			deviceAuthorizationResponse.additionalParameters = Collections
				.unmodifiableMap(CollectionUtils.isEmpty(this.additionalParameters) ? Collections.emptyMap()
						: this.additionalParameters);

			return deviceAuthorizationResponse;
		}

	}

}
