<?php

/**
 * This program is copyrighted by Franz Geffke (Softmax Co., Ltd.)
 */

namespace ones\oidc;

use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use Exception;
use UnexpectedValueException;
use GuzzleHttp;

/**
 * Make CIBA request
 * 
 * @param string $clientId The client ID
 * @param array $providerMetadata The OpenID provider metadata
 * @param string $privateKey The private key (PEM)
 * @param string $loginHint The login hint
 * @param string $loginHintKind The login hint kind
 * @param string $scope The scope
 * @param string $bindingMessage The binding message
 * @param string $deviceJWT The device JWT
 * @param string $resource The resource
 */
function make_ciba_request($clientId, $providerMetadata, $privateKey, $loginHint, $loginHintKind, $scope, $bindingMessage, $deviceJWT, $resource = null, $qr_session_id=null) {
    $now = time();
    $cibaPayload = [
        'iss' => $clientId,
        'sub' => $clientId,
        'aud' => $providerMetadata['issuer'],
        'jti' => bin2hex(random_bytes(16)),
        'iat' => $now,
        'exp' => $now + 300,
        'nbf' => $now,
        'scope' => $scope,
        'binding_message' => $bindingMessage,
        'resource' => $resource
    ];

    // Set either login_hint_token or login_hint based on loginHintKind
    if ($loginHintKind === 'login_hint_token') {
        $cibaPayload['login_hint_token'] = $loginHint;
    } else {
        $cibaPayload['login_hint'] = $loginHint;
    }

    if ($qr_session_id) {
        $cibaPayload['qr_session_id'] = $qr_session_id;
    }

    $signedRequest = JWT::encode($cibaPayload, $privateKey, 'RS256');

    $client = new GuzzleHttp\Client();
    $response = $client->post($providerMetadata['backchannel_authentication_endpoint'], [
        'form_params' => [
            'client_id' => $clientId,
            'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
            'client_assertion' => $deviceJWT,
            'request' => $signedRequest
        ],
        'headers' => [
            'Content-Type' => 'application/x-www-form-urlencoded'
        ],
        'timeout' => 3
    ]);

    return json_decode($response->getBody(), true);
}

/**
 * Get the 'kid' from a JWT
 * 
 * @param string $jwt The JWT
 */
function get_jwt_kid($jwt) {
    $tks = explode('.', $jwt);
    if (count($tks) != 3) {
        throw new UnexpectedValueException('Wrong number of segments');
    }
    $headb64 = $tks[0];
    $header = json_decode(JWT::urlsafeB64Decode($headb64), true);
    if (null === $header) {
        throw new UnexpectedValueException('Invalid header encoding');
    }
    return $header['kid'] ?? null;
}

/**
 * Decode a JWT and verify the signature
 * 
 * @param string $jwt The JWT
 * @param array $jwks The JWKS
 */
function decode_jwt($jwt, $jwks) {
    try {
        $kid = get_jwt_kid($jwt);
        if (!$kid) {
            throw new Exception("No 'kid' found in JWT header");
        }

        $keys = JWK::parseKeySet($jwks);
        
        if (!isset($keys[$kid])) {
            throw new Exception("No matching 'kid' found in JWKS");
        }

        $key = $keys[$kid];
        $decoded = JWT::decode($jwt, $key);
        return (array) $decoded;
    } catch (Exception $e) {
        echo 'Caught exception: ',  $e->getMessage(), "\n";
        return null;
    }
}

/**
 * Decode the CIBA status response to validate the JWTs
 * 
 * @param string $jwks The JWKS
 * @param array $body The response body
 */
function decode_ciba_status_response($jwks, $body) {
    $accessToken = $body['access_token'];
    $idToken = $body['id_token'] ?? null;

    $accessTokenContent = decode_jwt($accessToken, $jwks);
    $idTokenContent = null;
    if ($idToken) {
        $idTokenContent = decode_jwt($idToken, $jwks);
    }

    return [
        'status_code' => 200,
        'body' => [
            'access_token' => $accessToken,
            'access_token_content' => $accessTokenContent,
            'id_token' => $idToken,
            'id_token_content' => $idTokenContent,
            'refresh_token' => $body['refresh_token'] ?? null,
            'expires_in' => $body['expires_in'],
            'scope' => $body['scope'] ?? null
        ]
    ];
}

/**
 * Helper: Check CIBA status
 * 
 * @param array $providerMetadata The OpenID provider metadata
 * @param string $authRequestId The auth request ID
 * @param string $deviceJWT The device JWT
 * @param string $clientId The client ID
 */
function _check_ciba_status($providerMetadata, $authRequestId, $deviceJWT, $clientId) {
    $client = new GuzzleHttp\Client();
    
    try {
        $response = $client->post($providerMetadata['token_endpoint'], [
            'form_params' => [
                'grant_type' => 'urn:openid:params:grant-type:ciba',
                'auth_req_id' => $authRequestId,
                'client_id' => $clientId,
                'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
                'client_assertion' => $deviceJWT
            ],
            'headers' => [
                'Content-Type' => 'application/x-www-form-urlencoded'
            ],
            'timeout' => 3
        ]);

        return [
            'body' => json_decode($response->getBody(), true),
            'status_code' => $response->getStatusCode()
        ];

    } catch (GuzzleHttp\Exception\ClientException $e) {
        $response = $e->getResponse();
        $responseBody = json_decode($response->getBody(), true);

        if ($response->getStatusCode() == 400 && $responseBody['error'] == 'authorization_pending') {
            throw new Exception('Authorization pending');
        } else {
            throw new Exception('CIBA status check failed: ' . $responseBody['error_description']);
        }
    } catch (Exception $e) {
        throw new Exception('CIBA status check failed: ' . $e->getMessage());
    }
}

/**
 * Check CIBA status
 * 
 * @param array $providerMetadata The OpenID provider metadata
 * @param array $providerJwks The OpenID provider JWKS
 * @param string $authRequestId The auth request ID
 * @param string $deviceJWT The device JWT
 * @param string $clientId The client ID
 * 
 * This function is a wrapper around _check_ciba_status that decodes the response
 * JWT responses are decoded and verified using the JWKS
 */
function check_ciba_status($providerMetadata, $providerJwks, $authRequestId, $deviceJWT, $clientId) {
    $response = _check_ciba_status($providerMetadata, $authRequestId, $deviceJWT, $clientId);

    if (isset($response['status_code']) && $response['status_code'] === 200) {
        return decode_ciba_status_response($providerJwks, $response['body']);
    } else {
        return $response;
    }
}

?>