<?php
namespace BooklyOutlookCalendar\Lib\Microsoft;

use Bookly\Lib as BooklyLib;
use Bookly\Lib\Entities\Staff;
use BooklyPro\Lib\Plugin;
use BooklyOutlookCalendar\Lib\Config;

/**
 * Class Client
 * @package BooklyOutlookCalendar\Lib\Microsoft
 */
class Client
{
    /** @var Vendor\Graph */
    protected $graph;

    /** @var Calendar */
    protected $calendar;

    /** @var Data\Holder */
    protected $data;

    /** @var Staff */
    protected $staff;

    /** @var array */
    protected $errors = array();

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->graph = new Vendor\Graph();
    }

    /**
     * Authenticate Microsoft Client with given staff.
     *
     * @param Staff $staff
     * @param bool  $force
     * @return bool
     */
    public function auth( Staff $staff, $force = false )
    {
        if ( $force || $staff->getVisibility() != 'archive' ) {
            $json = $staff->getOutlookData();
            if ( $json != '' ) {
                try {
                    $data = Data\Holder::fromJson( $json );
                    if ( $this->isAccessTokenExpired( $data->token ) ) {
                        // Try to refresh access token.
                        $token = $this->refreshAccessToken( $data->token );
                        if ( ! $token ) {
                            return false;
                        }
                        $data->token = $token;
                        $staff
                            ->setOutlookData( $data->toJson() )
                            ->save()
                        ;
                    }

                    $this->graph->setAccessToken( $data->token->access_token );

                    $this->calendar = new Calendar( $this );
                    $this->staff    = $staff;
                    $this->data     = $data;

                    return true;

                } catch ( \Exception $e ) {
                    $this->addError( $e->getMessage() );
                }
            }
        }

        return false;
    }

    /**
     * Authenticate Google Client with given staff ID.
     *
     * @param int $staff_id
     * @return bool
     */
    public function authWithStaffId( $staff_id )
    {
        return $this->auth( Staff::find( $staff_id ) );
    }

    /**
     * Get list of Outlook Calendars.
     *
     * @return array|false
     */
    public function getCalendarList()
    {
        try {
            $result = array();

            // Fetch calendars.
            $calendars = $this->graph
                ->createCollectionRequest( 'GET', '/me/calendars' )
                ->setReturnType( 'BooklyOutlookCalendar\Lib\Microsoft\Vendor\Model\Calendar' )
                ->getPage()
            ;

            /** @var Vendor\Model\Calendar $calendar */
            foreach ( $calendars as $calendar ) {
                if ( $calendar->getCanEdit() ) {
                    $result[ $calendar->getId() ] = $calendar->getName();
                }
            }

            return $result;

        } catch ( \Exception $e ) {
            $this->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Check whether given calendar ID belongs to Outlook Calendar associated with staff.
     *
     * @param string $calendar_id
     * @return bool
     */
    public function validateCalendarId( $calendar_id )
    {
        try {
            $this->graph
                ->createRequest( 'GET', '/me/calendars/' . $calendar_id )
                ->execute()
            ;

            return true;

        } catch ( \Exception $e ) {
            $this->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Construct authorization request URI for given staff ID.
     *
     * @param int $staff_id
     * @return string
     */
    public function createAuthUrl( $staff_id )
    {
        return add_query_arg( urlencode_deep( array(
            'client_id'     => Config::getAppId(),
            'response_type' => 'code',
            'redirect_uri'  => self::generateRedirectURI(),
            'scope'         => 'offline_access calendars.readwrite',
            'response_mode' => 'query',
            'state'         => 'bookly-outlook-calendar-auth-' . $staff_id,
        ) ), 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize' );
    }

    /**
     * Try to extract staff ID from given state parameter of authorization response.
     *
     * @param string $state
     * @return int|false
     */
    public function extractStaffIdFromState( $state )
    {
        if ( preg_match( '/^bookly-outlook-calendar-auth-(\d+)$/', $state, $match ) ) {
            return (int) $match[1];
        }

        return false;
    }

    /**
     * Attempt to exchange given code for a valid oAuth2 access token.
     *
     * @param string $code
     * @return Data\Token|false
     */
    public function exchangeCodeForAccessToken( $code )
    {
        $response = wp_remote_post( 'https://login.microsoftonline.com/common/oauth2/v2.0/token', array(
            'timeout' => 25,
            'body'    => array(
                'client_id'     => Config::getAppId(),
                'client_secret' => Config::getAppSecret(),
                'grant_type'    => 'authorization_code',
                'code'          => $code,
                'redirect_uri'  => self::generateRedirectURI(),
                'scope'         => 'offline_access calendars.readwrite',
            )
        ) );

        if ( $response instanceof \WP_Error ) {
            $this->addError( $response->get_error_message() );

            return false;
        }

        if ( $response['response']['code'] != 200 ) {
            $error = 'Error fetching OAuth2 access token';
            $body  = json_decode( $response['body'], true );
            if ( $body != null && $body['error'] ) {
                $error = $body['error'];
                if ( isset ( $body['error_description'] ) ) {
                    $error .= ': ' . $body['error_description'];
                }
            }
            $this->addError( $error );

            return false;
        }

        $token = Data\Token::fromJson( $response['body'] );
        $token->created = time();

        return $token;
    }

    /**
     * Check if the access token is expired.
     *
     * @param Data\Token $token
     * @return bool
     */
    public function isAccessTokenExpired( Data\Token $token )
    {
        if ( ! isset ( $token->created ) ) {
            return true;
        }

        // If the token is set to expire in the next 30 seconds.
        $expired = ( $token->created + ( $token->expires_in - 30 ) ) < time();

        return $expired;
    }

    /**
     * Refresh access token.
     *
     * @param Data\Token $token
     * @return Data\Token|false
     */
    public function refreshAccessToken( Data\Token $token )
    {
        $response = wp_remote_post( 'https://login.microsoftonline.com/common/oauth2/v2.0/token', array(
            'timeout' => 25,
            'body'    => array(
                'client_id'     => Config::getAppId(),
                'client_secret' => Config::getAppSecret(),
                'grant_type'    => 'refresh_token',
                'refresh_token' => $token->refresh_token,
                'redirect_uri'  => self::generateRedirectURI(),
                'scope'         => 'offline_access calendars.readwrite',
            )
        ) );

        if ( $response instanceof \WP_Error ) {
            $this->addError( $response->get_error_message() );

            return false;
        }

        if ( $response['response']['code'] != 200 ) {
            $error = 'Error refreshing OAuth2 access token';
            $body  = json_decode( $response['body'], true );
            if ( $body != null && $body['error'] ) {
                $error = $body['error'];
                if ( isset ( $body['error_description'] ) ) {
                    $error .= ': ' . $body['error_description'];
                }
            }
            $this->addError( $error );

            return false;
        }

        $token = Data\Token::fromJson( $response['body'] );
        $token->created = time();

        return $token;
    }

    /**
     * Get staff.
     *
     * @return Staff
     */
    public function staff()
    {
        return $this->staff;
    }

    /**
     * Get graph.
     *
     * @return Vendor\Graph
     */
    public function graph()
    {
        return $this->graph;
    }

    /**
     * Get calendar.
     *
     * @return Calendar
     */
    public function calendar()
    {
        return $this->calendar;
    }

    /**
     * Get data.
     *
     * @return Data\Holder
     */
    public function data()
    {
        return $this->data;
    }

    /**
     * Add error.
     *
     * @param string $error
     */
    public function addError( $error )
    {
        $this->errors[] = $error;
    }

    /**
     * Get errors.
     *
     * @return array
     */
    public function getErrors()
    {
        return $this->errors;
    }

    /**
     * Generate Redirect URI.
     *
     * @return string
     */
    public static function generateRedirectURI()
    {
        return admin_url( '', 'https' );
    }
}