/*
Copyright 2022 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import { MSC3906Rendezvous, MSC3906RendezvousPayload, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous";
import { MSC3886SimpleHttpRendezvousTransport } from "matrix-js-sdk/src/rendezvous/transports";
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "matrix-js-sdk/src/rendezvous/channels";
import { logger } from "matrix-js-sdk/src/logger";
import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix";

import { _t } from "../../../languageHandler";
import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth";
import LoginWithQRFlow from "./LoginWithQRFlow";
import { Click, Mode, Phase } from "./LoginWithQR-types";

interface IProps {
    client: MatrixClient;
    mode: Mode;
    onFinished(...args: any): void;
}

interface IState {
    phase: Phase;
    rendezvous?: MSC3906Rendezvous;
    confirmationDigits?: string;
    failureReason?: FailureReason;
    mediaPermissionError?: boolean;
}

export enum LoginWithQRFailureReason {
    RateLimited = "rate_limited",
}

export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason;

/**
 * A component that allows sign in and E2EE set up with a QR code.
 *
 * It implements `login.reciprocate` capabilities and showing QR codes.
 *
 * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
 */
export default class LoginWithQR extends React.Component<IProps, IState> {
    public constructor(props: IProps) {
        super(props);

        this.state = {
            phase: Phase.Loading,
        };
    }

    public componentDidMount(): void {
        this.updateMode(this.props.mode).then(() => {});
    }

    public componentDidUpdate(prevProps: Readonly<IProps>): void {
        if (prevProps.mode !== this.props.mode) {
            this.updateMode(this.props.mode).then(() => {});
        }
    }

    private async updateMode(mode: Mode): Promise<void> {
        this.setState({ phase: Phase.Loading });
        if (this.state.rendezvous) {
            const rendezvous = this.state.rendezvous;
            rendezvous.onFailure = undefined;
            await rendezvous.cancel(RendezvousFailureReason.UserCancelled);
            this.setState({ rendezvous: undefined });
        }
        if (mode === Mode.Show) {
            await this.generateCode();
        }
    }

    public componentWillUnmount(): void {
        if (this.state.rendezvous) {
            // eslint-disable-next-line react/no-direct-mutation-state
            this.state.rendezvous.onFailure = undefined;
            // calling cancel will call close() as well to clean up the resources
            this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled).then(() => {});
        }
    }

    private approveLogin = async (): Promise<void> => {
        if (!this.state.rendezvous) {
            throw new Error("Rendezvous not found");
        }
        this.setState({ phase: Phase.Loading });

        try {
            logger.info("Requesting login token");

            const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, {
                matrixClient: this.props.client,
                title: _t("auth|qr_code_login|sign_in_new_device"),
            })();

            this.setState({ phase: Phase.WaitingForDevice });

            const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken);
            if (!newDeviceId) {
                // user denied
                return;
            }
            if (!this.props.client.getCrypto()) {
                // no E2EE to set up
                this.props.onFinished(true);
                return;
            }
            this.setState({ phase: Phase.Verifying });
            await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
            // clean up our state:
            try {
                await this.state.rendezvous.close();
            } finally {
                this.setState({ rendezvous: undefined });
            }
            this.props.onFinished(true);
        } catch (e) {
            logger.error("Error whilst approving sign in", e);
            if (e instanceof HTTPError && e.httpStatus === 429) {
                // 429: rate limit
                this.setState({ phase: Phase.Error, failureReason: LoginWithQRFailureReason.RateLimited });
                return;
            }
            this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown });
        }
    };

    private generateCode = async (): Promise<void> => {
        let rendezvous: MSC3906Rendezvous;
        try {
            const fallbackRzServer = this.props.client.getClientWellKnown()?.["io.element.rendezvous"]?.server;
            const transport = new MSC3886SimpleHttpRendezvousTransport<MSC3903ECDHPayload>({
                onFailure: this.onFailure,
                client: this.props.client,
                fallbackRzServer,
            });

            const channel = new MSC3903ECDHv2RendezvousChannel<MSC3906RendezvousPayload>(
                transport,
                undefined,
                this.onFailure,
            );

            rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure);

            await rendezvous.generateCode();
            this.setState({
                phase: Phase.ShowingQR,
                rendezvous,
                failureReason: undefined,
            });
        } catch (e) {
            logger.error("Error whilst generating QR code", e);
            this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.HomeserverLacksSupport });
            return;
        }

        try {
            const confirmationDigits = await rendezvous.startAfterShowingCode();
            this.setState({ phase: Phase.Connected, confirmationDigits });
        } catch (e) {
            logger.error("Error whilst doing QR login", e);
            // only set to error phase if it hasn't already been set by onFailure or similar
            if (this.state.phase !== Phase.Error) {
                this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown });
            }
        }
    };

    private onFailure = (reason: RendezvousFailureReason): void => {
        logger.info(`Rendezvous failed: ${reason}`);
        this.setState({ phase: Phase.Error, failureReason: reason });
    };

    public reset(): void {
        this.setState({
            rendezvous: undefined,
            confirmationDigits: undefined,
            failureReason: undefined,
        });
    }

    private onClick = async (type: Click): Promise<void> => {
        switch (type) {
            case Click.Cancel:
                await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
                this.reset();
                this.props.onFinished(false);
                break;
            case Click.Approve:
                await this.approveLogin();
                break;
            case Click.Decline:
                await this.state.rendezvous?.declineLoginOnExistingDevice();
                this.reset();
                this.props.onFinished(false);
                break;
            case Click.TryAgain:
                this.reset();
                await this.updateMode(this.props.mode);
                break;
            case Click.Back:
                await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
                this.props.onFinished(false);
                break;
        }
    };

    public render(): React.ReactNode {
        return (
            <LoginWithQRFlow
                onClick={this.onClick}
                phase={this.state.phase}
                code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined}
                confirmationDigits={this.state.phase === Phase.Connected ? this.state.confirmationDigits : undefined}
                failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined}
            />
        );
    }
}
