import { Auth } from "@padloc/core/src/auth";
import { Config, ConfigParam } from "@padloc/core/src/config";
import { AuthRequest, AuthServer, AuthType, Authenticator } from "@padloc/core/src/auth";
import { request } from "../transport/http";
import { Err, ErrorCode } from "@padloc/core/src/error";
import { base64ToString, bytesToBase64, stringToBytes } from "@padloc/core/src/encoding";
import { getCryptoProvider } from "@padloc/core/src/platform";
import { HashParams } from "@padloc/core/src/crypto";

export class OpenIdConfig extends Config {
    @ConfigParam()
    clientId!: string;

    @ConfigParam("string", true)
    clientSecret!: string;

    @ConfigParam()
    authorizationEndpoint!: string;

    @ConfigParam()
    tokenEndpoint!: string;

    @ConfigParam()
    redirectUri!: string;
}

export class OpenIDServer implements AuthServer {
    constructor(public config: OpenIdConfig) {}

    supportsType(type: AuthType): boolean {
        return type === AuthType.OpenID;
    }

    async initAuthenticator(
        authenticator: Authenticator<any>,
        auth: Auth,
        { email = auth.email }: { email?: string } = {}
    ): Promise<any> {
        authenticator.state = {
            email,
            activationParams: await this._generateAuthUrl(email),
        };
        return { authUrl: authenticator.state.activationParams.authUrl };
    }

    async activateAuthenticator(authenticator: Authenticator<any>, { code }: { code: string }): Promise<any> {
        const token = await this._getToken({ code, codeVerifier: authenticator.state.activationParams.codeVerifier });
        if (token.email !== authenticator.state?.email) {
            throw new Err(ErrorCode.AUTHENTICATION_FAILED, "Email returned from authenticator does not match.");
        }
        authenticator.state.id_token = token;
    }

    async initAuthRequest(authenticator: Authenticator<any>, request: AuthRequest<any>, _params?: any): Promise<any> {
        request.state = await this._generateAuthUrl(authenticator.state.email);
        return { authUrl: request.state.authUrl };
    }

    async verifyAuthRequest(
        authenticator: Authenticator<any>,
        request: AuthRequest<any>,
        { code }: { code: string }
    ): Promise<void> {
        const token = await this._getToken({ code, codeVerifier: request.state.codeVerifier });
        if (token.email !== authenticator.state?.email) {
            throw new Err(ErrorCode.AUTHENTICATION_FAILED, "Email returned from authenticator does not match.");
        }
        authenticator.state.id_token = token;
    }

    private async _generateAuthUrl(email: string) {
        const { clientId, authorizationEndpoint, redirectUri } = this.config;
        const crypto = getCryptoProvider();
        const state = bytesToBase64(await crypto.randomBytes(8));
        const nonce = bytesToBase64(await crypto.randomBytes(8));
        const codeVerifier = bytesToBase64(await crypto.randomBytes(32));
        const codeChallenge = bytesToBase64(
            await crypto.hash(
                stringToBytes(codeVerifier),
                new HashParams({
                    algorithm: "SHA-256",
                })
            )
        );

        const params = new URLSearchParams();
        params.set("client_id", clientId);
        params.set("response_type", "code");
        params.set("response_mode", "query");
        params.set("scope", "openid email profile");
        params.set("state", state);
        params.set("nonce", nonce);
        params.set("redirect_uri", redirectUri);
        params.set("code_challenge", codeChallenge);
        params.set("code_challenge_method", "S256");
        params.set("login_hint", email);

        return {
            authUrl: `${authorizationEndpoint}?${params.toString()}`,
            state,
            nonce,
            codeVerifier,
        };
    }

    private async _parseToken(token: string) {
        const [_header, payload, _signature] = token.split(".");
        return JSON.parse(base64ToString(payload));
    }

    private async _getToken({ code, codeVerifier }: { code: string; codeVerifier: string }) {
        const body = new URLSearchParams({
            client_id: this.config.clientId,
            client_secret: this.config.clientSecret,
            grant_type: "authorization_code",
            code,
            code_verifier: codeVerifier,
            scope: "openid profile email",
            redirect_uri: this.config.redirectUri,
        }).toString();

        try {
            const tokenRes = await request(this.config.tokenEndpoint, "POST", body, {
                "Content-Type": "application/x-www-form-urlencoded",
                Accept: "application/json",
                "Content-Length": body.length.toString(),
            });

            const { id_token } = JSON.parse(tokenRes);

            if (!id_token) {
                throw new Err(ErrorCode.AUTHENTICATION_FAILED, "No id token was returned.");
            }

            try {
                return this._parseToken(id_token);
            } catch (e) {
                throw new Err(ErrorCode.AUTHENTICATION_FAILED, "Failed to parse ID token.");
            }
        } catch (e) {
            throw new Err(ErrorCode.AUTHENTICATION_FAILED, "Failed to retrieve auth token.");
        }
    }
}
