<?php

namespace OAuth2\Storage;

use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;

/**
 * Simple Couchbase storage for all storage types
 *
 * This class should be extended or overridden as required
 *
 * NOTE: Passwords are stored in plaintext, which is never
 * a good idea.  Be sure to override this for your application
 *
 * @author Tom Park <tom@raucter.com>
 */
class CouchbaseDB implements AuthorizationCodeInterface,
	AccessTokenInterface,
	ClientCredentialsInterface,
	UserCredentialsInterface,
	RefreshTokenInterface,
	JwtBearerInterface,
	OpenIDAuthorizationCodeInterface {

	protected $db;
	protected $config;

	public function __construct( $connection, $config = array() ) {
		if ( $connection instanceof \Couchbase ) {
			$this->db = $connection;
		} else {
			if ( ! is_array( $connection ) || ! is_array( $connection['servers'] ) ) {
				throw new \InvalidArgumentException( 'First argument to OAuth2\Storage\CouchbaseDB must be an instance of Couchbase or a configuration array containing a server array' );
			}

			$this->db = new \Couchbase( $connection['servers'], ( ! isset( $connection['username'] ) ) ? '' : $connection['username'], ( ! isset( $connection['password'] ) ) ? '' : $connection['password'], $connection['bucket'], false );
		}

		$this->config = array_merge(
			array(
				'client_table'        => 'oauth_clients',
				'access_token_table'  => 'oauth_access_tokens',
				'refresh_token_table' => 'oauth_refresh_tokens',
				'code_table'          => 'oauth_authorization_codes',
				'user_table'          => 'oauth_users',
				'jwt_table'           => 'oauth_jwt',
			),
			$config
		);
	}

	// Helper function to access couchbase item by type:
	protected function getObjectByType( $name, $id ) {
		return json_decode( $this->db->get( $this->config[ $name ] . '-' . $id ), true );
	}

	// Helper function to set couchbase item by type:
	protected function setObjectByType( $name, $id, $array ) {
		$array['type'] = $name;

		return $this->db->set( $this->config[ $name ] . '-' . $id, json_encode( $array ) );
	}

	// Helper function to delete couchbase item by type, wait for persist to at least 1 node
	protected function deleteObjectByType( $name, $id ) {
		$this->db->delete( $this->config[ $name ] . '-' . $id, '', 1 );
	}

	/* ClientCredentialsInterface */
	public function checkClientCredentials( $client_id, $client_secret = null ) {
		if ( $result = $this->getObjectByType( 'client_table', $client_id ) ) {
			return $result['client_secret'] == $client_secret;
		}

		return false;
	}

	public function isPublicClient( $client_id ) {
		if ( ! $result = $this->getObjectByType( 'client_table', $client_id ) ) {
			return false;
		}

		return empty( $result['client_secret'] );
	}

	/* ClientInterface */
	public function getClientDetails( $client_id ) {
		$result = $this->getObjectByType( 'client_table', $client_id );

		return is_null( $result ) ? false : $result;
	}

	public function setClientDetails( $client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null ) {
		if ( $this->getClientDetails( $client_id ) ) {

			$this->setObjectByType(
				'client_table',
				$client_id,
				array(
					'client_id'     => $client_id,
					'client_secret' => $client_secret,
					'redirect_uri'  => $redirect_uri,
					'grant_types'   => $grant_types,
					'scope'         => $scope,
					'user_id'       => $user_id,
				)
			);
		} else {
			$this->setObjectByType(
				'client_table',
				$client_id,
				array(
					'client_id'     => $client_id,
					'client_secret' => $client_secret,
					'redirect_uri'  => $redirect_uri,
					'grant_types'   => $grant_types,
					'scope'         => $scope,
					'user_id'       => $user_id,
				)
			);
		}

		return true;
	}

	public function checkRestrictedGrantType( $client_id, $grant_type ) {
		$details = $this->getClientDetails( $client_id );
		if ( isset( $details['grant_types'] ) ) {
			$grant_types = explode( ' ', $details['grant_types'] );

			return in_array( $grant_type, $grant_types );
		}

		// if grant_types are not defined, then none are restricted
		return true;
	}

	/* AccessTokenInterface */
	public function getAccessToken( $access_token ) {
		$token = $this->getObjectByType( 'access_token_table', $access_token );

		return is_null( $token ) ? false : $token;
	}

	public function setAccessToken( $access_token, $client_id, $user_id, $expires, $scope = null ) {
		// if it exists, update it.
		if ( $this->getAccessToken( $access_token ) ) {
			$this->setObjectByType(
				'access_token_table',
				$access_token,
				array(
					'access_token' => $access_token,
					'client_id'    => $client_id,
					'expires'      => $expires,
					'user_id'      => $user_id,
					'scope'        => $scope,
				)
			);
		} else {
			$this->setObjectByType(
				'access_token_table',
				$access_token,
				array(
					'access_token' => $access_token,
					'client_id'    => $client_id,
					'expires'      => $expires,
					'user_id'      => $user_id,
					'scope'        => $scope,
				)
			);
		}

		return true;
	}

	/* AuthorizationCodeInterface */
	public function getAuthorizationCode( $code ) {
		$code = $this->getObjectByType( 'code_table', $code );

		return is_null( $code ) ? false : $code;
	}

	public function setAuthorizationCode( $code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null ) {
		// if it exists, update it.
		if ( $this->getAuthorizationCode( $code ) ) {
			$this->setObjectByType(
				'code_table',
				$code,
				array(
					'authorization_code' => $code,
					'client_id'          => $client_id,
					'user_id'            => $user_id,
					'redirect_uri'       => $redirect_uri,
					'expires'            => $expires,
					'scope'              => $scope,
					'id_token'           => $id_token,
				)
			);
		} else {
			$this->setObjectByType(
				'code_table',
				$code,
				array(
					'authorization_code' => $code,
					'client_id'          => $client_id,
					'user_id'            => $user_id,
					'redirect_uri'       => $redirect_uri,
					'expires'            => $expires,
					'scope'              => $scope,
					'id_token'           => $id_token,
				)
			);
		}

		return true;
	}

	public function expireAuthorizationCode( $code ) {
		$this->deleteObjectByType( 'code_table', $code );

		return true;
	}

	/* UserCredentialsInterface */
	public function checkUserCredentials( $username, $password ) {
		if ( $user = $this->getUser( $username ) ) {
			return $this->checkPassword( $user, $password );
		}

		return false;
	}

	public function getUserDetails( $username ) {
		if ( $user = $this->getUser( $username ) ) {
			$user['user_id'] = $user['username'];
		}

		return $user;
	}

	/* RefreshTokenInterface */
	public function getRefreshToken( $refresh_token ) {
		$token = $this->getObjectByType( 'refresh_token_table', $refresh_token );

		return is_null( $token ) ? false : $token;
	}

	public function setRefreshToken( $refresh_token, $client_id, $user_id, $expires, $scope = null ) {
		$this->setObjectByType(
			'refresh_token_table',
			$refresh_token,
			array(
				'refresh_token' => $refresh_token,
				'client_id'     => $client_id,
				'user_id'       => $user_id,
				'expires'       => $expires,
				'scope'         => $scope,
			)
		);

		return true;
	}

	public function unsetRefreshToken( $refresh_token ) {
		$this->deleteObjectByType( 'refresh_token_table', $refresh_token );

		return true;
	}

	// plaintext passwords are bad!  Override this for your application
	protected function checkPassword( $user, $password ) {
		return $user['password'] == $password;
	}

	public function getUser( $username ) {
		$result = $this->getObjectByType( 'user_table', $username );

		return is_null( $result ) ? false : $result;
	}

	public function setUser( $username, $password, $firstName = null, $lastName = null ) {
		if ( $this->getUser( $username ) ) {
			$this->setObjectByType(
				'user_table',
				$username,
				array(
					'username'   => $username,
					'password'   => $password,
					'first_name' => $firstName,
					'last_name'  => $lastName,
				)
			);

		} else {
			$this->setObjectByType(
				'user_table',
				$username,
				array(
					'username'   => $username,
					'password'   => $password,
					'first_name' => $firstName,
					'last_name'  => $lastName,
				)
			);

		}

		return true;
	}

	public function getClientKey( $client_id, $subject ) {
		if ( ! $jwt = $this->getObjectByType( 'jwt_table', $client_id ) ) {
			return false;
		}

		if ( isset( $jwt['subject'] ) && $jwt['subject'] == $subject ) {
			return $jwt['key'];
		}

		return false;
	}

	public function getClientScope( $client_id ) {
		if ( ! $clientDetails = $this->getClientDetails( $client_id ) ) {
			return false;
		}

		if ( isset( $clientDetails['scope'] ) ) {
			return $clientDetails['scope'];
		}

		return null;
	}

	public function getJti( $client_id, $subject, $audience, $expiration, $jti ) {
		// TODO: Needs couchbase implementation.
		throw new \Exception( 'getJti() for the Couchbase driver is currently unimplemented.' );
	}

	public function setJti( $client_id, $subject, $audience, $expiration, $jti ) {
		// TODO: Needs couchbase implementation.
		throw new \Exception( 'setJti() for the Couchbase driver is currently unimplemented.' );
	}
}
