<?php

namespace MediaWiki\Extension\SSOLink;

use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\Html\Html;
use MediaWiki\MediaWikiServices;

class SpecialLinkSSO extends SpecialPage {

	public function __construct() {
		parent::__construct( 'LinkSSO' );
	}

	public function execute( $subPage ) {
		$this->setHeaders();
		$user = $this->getUser();
		$request = $this->getRequest();
		$out = $this->getOutput();

		if ( $subPage === 'callback' ) {
			$this->handleCallback();
			return;
		}

		if ( !$user->isRegistered() ) {
			$out->addWikiMsg( 'ssolink-error-not-logged-in' );
			return;
		}

		$action = $request->getVal( 'action' );

		if ( $request->wasPosted() ) {
			if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
				$out->addWikiMsg( 'ssolink-error-csrf' );
				return;
			}
			if ( $action === 'link' ) {
				$this->initiateLink();
				return;
			}
			if ( $action === 'unlink' ) {
				$this->unlinkAccount();
				return;
			}
		}

		$this->showStatus();
	}

	private function showStatus() {
		$user = $this->getUser();
		$out = $this->getOutput();
		$dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();

		$row = $dbr->newSelectQueryBuilder()
			->select( [ 'oidc_subject' ] )
			->from( 'openid_connect' )
			->where( [ 'oidc_user' => $user->getId() ] )
			->caller( __METHOD__ )
			->fetchRow();

		if ( $row ) {
			$out->addHTML( Html::successBox(
				$this->msg( 'ssolink-status-linked', $row->oidc_subject )->parse()
			) );
			$out->addHTML( $this->buildUnlinkForm() );
		} else {
			$out->addHTML( Html::warningBox(
				$this->msg( 'ssolink-status-unlinked' )->parse()
			) );
			$out->addHTML( $this->buildLinkForm() );
		}
	}

	private function buildLinkForm(): string {
		$user = $this->getUser();
		return Html::openElement( 'form', [
				'method' => 'post',
				'action' => $this->getPageTitle()->getLocalURL(),
			] )
			. Html::hidden( 'action', 'link' )
			. Html::hidden( 'wpEditToken', $user->getEditToken() )
			. Html::submitButton(
				$this->msg( 'ssolink-link-button' )->text(),
				[ 'class' => 'mw-ui-button mw-ui-progressive' ]
			)
			. Html::closeElement( 'form' );
	}

	private function buildUnlinkForm(): string {
		$user = $this->getUser();
		return Html::openElement( 'form', [
				'method' => 'post',
				'action' => $this->getPageTitle()->getLocalURL(),
			] )
			. Html::hidden( 'action', 'unlink' )
			. Html::hidden( 'wpEditToken', $user->getEditToken() )
			. Html::submitButton(
				$this->msg( 'ssolink-unlink-button' )->text(),
				[ 'class' => 'mw-ui-button mw-ui-destructive' ]
			)
			. Html::closeElement( 'form' );
	}

	private function initiateLink() {
		$config = $this->getConfig();
		$providerURL = $config->get( 'SSOLinkProviderURL' );
		$clientID = $config->get( 'SSOLinkClientID' );

		$discoveryURL = rtrim( $providerURL, '/' ) . '/.well-known/openid-configuration';
		$httpFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
		$discoveryResponse = $httpFactory->get( $discoveryURL, [ 'timeout' => 10 ] );

		if ( !$discoveryResponse ) {
			$this->getOutput()->addWikiMsg( 'ssolink-error-discovery' );
			return;
		}

		$discovery = json_decode( $discoveryResponse, true );
		$authEndpoint = $discovery['authorization_endpoint'] ?? null;

		if ( !$authEndpoint ) {
			$this->getOutput()->addWikiMsg( 'ssolink-error-no-auth-endpoint' );
			return;
		}

		$state = bin2hex( random_bytes( 16 ) );
		$session = $this->getRequest()->getSession();
		$session->set( 'ssolink_state', $state );
		$session->set( 'ssolink_user_id', $this->getUser()->getId() );
		$session->save();

		$callbackURL = $this->getPageTitle( 'callback' )->getFullURL();

		$params = [
			'response_type' => 'code',
			'client_id' => $clientID,
			'redirect_uri' => $callbackURL,
			'scope' => 'openid',
			'state' => $state,
		];

		$this->getOutput()->redirect( $authEndpoint . '?' . http_build_query( $params ) );
	}

	private function handleCallback() {
		$request = $this->getRequest();
		$out = $this->getOutput();
		$session = $request->getSession();
		$config = $this->getConfig();

		$code = $request->getVal( 'code' );
		if ( !$code ) {
			$out->addWikiMsg( 'ssolink-error-no-code' );
			return;
		}

		$state = $request->getVal( 'state' );
		$expectedState = $session->get( 'ssolink_state' );
		if ( !$state || $state !== $expectedState ) {
			$out->addWikiMsg( 'ssolink-error-state-mismatch' );
			return;
		}

		$userId = $session->get( 'ssolink_user_id' );
		$session->remove( 'ssolink_state' );
		$session->remove( 'ssolink_user_id' );
		$session->save();

		if ( !$userId ) {
			$out->addWikiMsg( 'ssolink-error-not-logged-in' );
			return;
		}

		$currentUser = $this->getUser();
		if ( !$currentUser->isRegistered() || $currentUser->getId() !== $userId ) {
			$out->addWikiMsg( 'ssolink-error-not-logged-in' );
			return;
		}

		$providerURL = $config->get( 'SSOLinkProviderURL' );
		$clientID = $config->get( 'SSOLinkClientID' );
		$clientSecret = $config->get( 'SSOLinkClientSecret' );

		$discoveryURL = rtrim( $providerURL, '/' ) . '/.well-known/openid-configuration';
		$httpFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
		$discoveryResponse = $httpFactory->get( $discoveryURL, [ 'timeout' => 10 ] );

		if ( !$discoveryResponse ) {
			$out->addWikiMsg( 'ssolink-error-token-exchange' );
			return;
		}

		$discovery = json_decode( $discoveryResponse, true );
		$tokenEndpoint = $discovery['token_endpoint'] ?? null;
		$userinfoEndpoint = $discovery['userinfo_endpoint'] ?? null;

		if ( !$tokenEndpoint || !$userinfoEndpoint ) {
			$out->addWikiMsg( 'ssolink-error-token-exchange' );
			return;
		}

		$callbackURL = $this->getPageTitle( 'callback' )->getFullURL();

		$tokenRequest = $httpFactory->create( $tokenEndpoint, [
			'method' => 'POST',
			'timeout' => 10,
			'postData' => http_build_query( [
				'grant_type' => 'authorization_code',
				'code' => $code,
				'redirect_uri' => $callbackURL,
				'client_id' => $clientID,
				'client_secret' => $clientSecret,
			] ),
		] );
		$tokenRequest->setHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
		$tokenStatus = $tokenRequest->execute();

		if ( !$tokenStatus->isOK() ) {
			$out->addWikiMsg( 'ssolink-error-token-exchange' );
			return;
		}

		$tokenData = json_decode( $tokenRequest->getContent(), true );
		$accessToken = $tokenData['access_token'] ?? null;

		if ( !$accessToken ) {
			$out->addWikiMsg( 'ssolink-error-token-exchange' );
			return;
		}

		$userinfoRequest = $httpFactory->create( $userinfoEndpoint, [
			'method' => 'GET',
			'timeout' => 10,
		] );
		$userinfoRequest->setHeader( 'Authorization', 'Bearer ' . $accessToken );
		$userinfoStatus = $userinfoRequest->execute();

		if ( !$userinfoStatus->isOK() ) {
			$out->addWikiMsg( 'ssolink-error-userinfo' );
			return;
		}

		$userinfo = json_decode( $userinfoRequest->getContent(), true );
		$subject = $userinfo['sub'] ?? null;

		if ( !$subject ) {
			$out->addWikiMsg( 'ssolink-error-userinfo' );
			return;
		}

		// Store the issuer exactly as the OpenIDConnect extension does:
		// the raw providerURL from config, preserving any trailing slash.
		$issuer = $providerURL;

		$dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();

		$existing = $dbw->newSelectQueryBuilder()
			->select( [ 'oidc_user' ] )
			->from( 'openid_connect' )
			->where( [ 'oidc_subject' => $subject, 'oidc_issuer' => $issuer ] )
			->caller( __METHOD__ )
			->fetchRow();

		if ( $existing && (int)$existing->oidc_user !== $userId ) {
			$out->addWikiMsg( 'ssolink-error-already-linked' );
			return;
		}

		$dbw->startAtomic( __METHOD__ );
		$dbw->newDeleteQueryBuilder()
			->deleteFrom( 'openid_connect' )
			->where( [ 'oidc_user' => $userId ] )
			->caller( __METHOD__ )
			->execute();

		$dbw->newInsertQueryBuilder()
			->insertInto( 'openid_connect' )
			->row( [
				'oidc_user' => $userId,
				'oidc_subject' => $subject,
				'oidc_issuer' => $issuer,
			] )
			->caller( __METHOD__ )
			->execute();
		$dbw->endAtomic( __METHOD__ );

		$out->addHTML( Html::successBox(
			$this->msg( 'ssolink-success-linked' )->parse()
		) );
	}

	private function unlinkAccount() {
		$user = $this->getUser();
		$out = $this->getOutput();
		$dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();

		$dbw->newDeleteQueryBuilder()
			->deleteFrom( 'openid_connect' )
			->where( [ 'oidc_user' => $user->getId() ] )
			->caller( __METHOD__ )
			->execute();

		$out->addHTML( Html::successBox(
			$this->msg( 'ssolink-success-unlinked' )->parse()
		) );
	}

	public function doesWrites() {
		return true;
	}

	public function getGroupName() {
		return 'users';
	}
}
