/**
 * Client
 *
 * Create a micropub client app — allows users to log in and authorize this app to make requests on their behalf to,
 * e.g. a micropub endpoint, authenticates requests based on remember-me cookie.
 * $dataToCookie and $dataFromCookie map between the array of information about the current user and the string value
 * stored in the remember-me cookie
 *
 * Adds routes:
 * /login (indieauth.login) — POST to this URL with required “me” and optional “next” parameters to start the login process
 * /authorize (indieauth.authorize) — the URL redirected to by the user’s authorization server after successful authorization. Checks details and sets remember-me cookie
 * /logout (indieauth.logout) — POST to this URL (with optional “next” parameter) whilst logged in to log out, i.e. remove the remmeber-me cookies.
 *
 * Adds ->before() handler which attaches data about the current user, which scopes they’ve granted this app (if any)
 * their URL, access token and micropub endpoint to $request. Example usage within a controller:
 *
 *     $token = $request->attributes->get('indieauth.client.token', null);
 *     if ($token !== null) {
 *       // User is logged in as $token['me']
 *       if (!empty($token['access_token'] and !empty($token['micropub_endpoint'])) {
 *         // The user has granted this app privileges detailed in $token['scope'], which can be carried out by sending
 *         // requests to $token['micropub_endpoint'] with $token['access_token']
 *         // Now you might check that the “post” scope is granted, and create some new content on their site (pseudocode):
 *         if (in_array('post', explode($scope, ' '))) {
 *           micrpub_post($token['micropub_endpoint'], $token['access_token'], $postDetails);
 *         }
 *       } else {
 *         // The user has logged in using the basic indieauth flow — we know that they’re authenticated as $token['me'],
 *         // but they haven’t granted us any permissions.
 *       }
 *     }
 *
 * @param \Silex\Application $app
 * @param callable|null $dataToCookie
 * @param callable|null $dataFromCookie
 * @return \Symfony\Component\Routing\RouteCollection
 */
function client($app, $dataToCookie = null, $dataFromCookie = null)
{
    $auth = $app['controllers_factory'];
    // If cookie mapping functions aren’t defined, use the simplest approach of encrypting the data.
    if ($dataToCookie === null) {
        $dataToCookie = function ($data) use($app) {
            return $app['encryption']->encrypt($data);
        };
    }
    if ($dataFromCookie === null) {
        $dataFromCookie = function ($token) use($app) {
            return $app['encryption']->decrypt($token);
        };
    }
    $app['indieauth'] = new IndieauthHelpers($app);
    // If no cookie lifetime is set, default to 60 days.
    $cookieLifetime = !empty($app['indieauth.cookielifetime']) ? $app['indieauth.cookielifetime'] : 60 * 60 * 24 * 60;
    $cookieName = !empty($app['indieauth.cookiename']) ? $app['indieauth.cookiename'] : 'indieauth_token';
    $secureCookies = isset($app['indieauth.securecookies']) ? $app['indieauth.securecookies'] : true;
    $clientIdForRequest = function (Http\Request $request) use($app) {
        // If no explicit client ID is set, use the domain name of this site.
        return !empty($app['indieauth.clientid']) ? $app['indieauth.clientid'] : $request->getHttpHost();
    };
    $redirectUrlForRequest = function (Http\Request $request) use($app) {
        // If no default login redirect URL is set (it can be reset on a request-by-request basis by setting the “next” parameter), default to the homepage
        $defaultLoginRedirectUrl = !empty($app['indieauth.loginredirecturl']) ? $app['indieauth.loginredirecturl'] : "{$request->getScheme()}://{$request->getHttpHost()}";
        return $request->request->get('next', $defaultLoginRedirectUrl);
    };
    $auth->post('/login/', function (Http\Request $request) use($app, $cookieName, $redirectUrlForRequest, $clientIdForRequest, $secureCookies) {
        $me = $request->request->get('me');
        $next = $redirectUrlForRequest($request);
        if ($me === null) {
            // TODO: better error handling, although in practical cases this will never happen.
            return $app->redirect($next);
        }
        $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint(ensureUrlHasHttp($me));
        if ($authorizationEndpoint === false) {
            // If the current user has no authorization endpoint set, they are using the basic indieauth flow.
            $authorizationEndpoint = rtrim($app['indieauth.url'], '/') . '/auth';
            return $app->redirect("{$authorizationEndpoint}?me={$me}&redirect_uri={$next}");
        }
        // As more scopes become defined, this will need to be expanded + probably made configurable.
        $micropubEndpoint = IndieAuth\Client::discoverMicropubEndpoint(ensureUrlHasHttp($me));
        $scope = !empty($micropubEndpoint) ? 'post' : '';
        $random = mt_rand(1000000, pow(2, 31));
        $redirectEndpoint = $app['url_generator']->generate('indieauth.authorize', [], true);
        $authorizationUrl = IndieAuth\Client::buildAuthorizationUrl($authorizationEndpoint, $me, $redirectEndpoint, $clientIdForRequest($request), $random, $scope);
        $response = $app->redirect($authorizationUrl);
        // Retain random state for five minutes in secure, HTTP-only cookie.
        $cookie = new Http\Cookie("{$cookieName}_random", $app['encryption']->encrypt($random), time() + 60 * 5, null, null, $secureCookies, true);
        $response->headers->setCookie($cookie);
        return $response;
    })->bind('indieauth.login');
    $auth->get('/authorize/', function (Http\Request $request) use($app, $dataToCookie, $cookieName, $cookieLifetime, $redirectUrlForRequest, $clientIdForRequest, $secureCookies) {
        $random = $app['encryption']->decrypt($request->cookies->get("{$cookieName}_random"));
        $me = $request->query->get('me');
        $state = $request->query->get('state');
        $code = $request->query->get('code');
        if ($state != $random) {
            $app['logger']->info('Authentication failed as state didn’t match random in cookie', ['state' => $state, 'cookie.random' => $random]);
            return $app->redirect('/');
        }
        $tokenEndpoint = IndieAuth\Client::discoverTokenEndpoint($me);
        $redirectUrl = $app['url_generator']->generate('indieauth.authorize', [], true);
        $token = IndieAuth\Client::getAccessToken($tokenEndpoint, $code, $me, $redirectUrl, $clientIdForRequest($request), $state);
        $token['micropub_endpoint'] = IndieAuth\Client::discoverMicropubEndpoint(ensureUrlHasHttp($me));
        $app['logger']->info("Indieauth: Got token, discovered micropub endpoint", ['token' => $token]);
        $response = $app->redirect($redirectUrlForRequest($request));
        // Store token data in secure, HTTP-only session cookie.
        $tokenCookie = new Http\Cookie($cookieName, $dataToCookie($token), time() + $cookieLifetime, null, null, $secureCookies, true);
        $response->headers->setCookie($tokenCookie);
        return $response;
    })->bind('indieauth.authorize');
    $auth->post('/logout/', function (Http\Request $request) use($app, $redirectUrlForRequest, $cookieName) {
        // In the bizarre case that a request to /logout/ also had a basic-flow indieauth token, prevent the ->after() handler
        // from setting remember-me cookies.
        $request->attributes->set('indieauth.islogoutrequest', true);
        $response = $app->redirect($redirectUrlForRequest($request));
        $app['indieauth']->logoutResponse($response);
        return $response;
    })->bind('logout');
    $app->before(function (Http\Request $request) use($app, $dataFromCookie, $cookieName) {
        // If the user has full indieauth credentials, make their token information (scope,
        // access key, micropub endpoint) available to controllers.
        if ($request->cookies->has($cookieName)) {
            try {
                /**
                 * indieauth.client.token is an array potentially containing the following properties:
                 * * me: URL of the current user (guaranteed to exist + be a valid URL)
                 * * scope: space-separated list of scopes the user has granted this app
                 * * access_token: the user’s access token
                 * * micropub_endpoint: the user’s micropub endpoint
                 *
                 * If only “me” exists then the user is logged in using the basic indieauth flow — their URL is confirmed but they
                 * haven’t granted us any permissions, and maybe don’t even have a micropub endpoint.
                 */
                $token = $dataFromCookie($request->cookies->get($cookieName));
                $loggableToken = $token;
                // Don’t log the sensitive access key, only the length, so devs can see if there *was* an access token or not.
                $loggableToken['access_token'] = 'Unlogged string of length ' . strlen($token['access_token']);
                $request->attributes->set('indieauth.client.token', $token);
                $app['logger']->info('Request has indieauth token', ['token' => $loggableToken]);
            } catch (Exception $e) {
                $app['logger']->warning("Caught an unhandled exception whilst running \$dataFromCookie on the current user’s indieauth token — consider handling this exception appropriately", ['exception class' => get_class($e), 'message' => $e->getMessage()]);
            }
        } elseif ($request->query->has('token')) {
            // The user is logging in using the basic indieauth flow, so all we know about them is their URL.
            // A remember-me cookie will be set for them later.
            $client = new Guzzle\Http\Client($app['indieauth.url']);
            try {
                $response = $client->get('session?token=' . $request->query->get('token'))->send();
                $basicToken = json_decode($response->getBody(true), true);
                $request->attributes->set('indieauth.client.token', $basicToken);
            } catch (Guzzle\Common\Exception\GuzzleException $e) {
                $app['logger']->warning('Authenticating user with indieauth.com failed: ' . $e->getMessage());
            }
        }
    });
    $app->after(function (Http\Request $request, Http\Response $response) use($app, $dataToCookie, $cookieName, $cookieLifetime, $secureCookies) {
        // If the request is a basic-flow indieauth login request, set a remember-me cookie.
        if ($request->query->has('token') and $request->attributes->has('indieauth.client.token') and !$request->attributes->get('indieauth.islogoutrequest', false)) {
            $tokenCookie = new Http\Cookie($cookieName, $dataToCookie($request->attributes->get('indieauth.client.token')), time() + $cookieLifetime, null, null, $secureCookies, true);
            $response->headers->setCookie($tokenCookie);
        }
    });
    return $auth;
}