content: import 'dart:async';
  import 'dart:convert';
  
  import 'package:flutter/material.dart';
  import 'package:flutter_hbb/common/hbbs/hbbs.dart';
  import 'package:flutter_hbb/models/platform_model.dart';
  import 'package:flutter_hbb/models/user_model.dart';
  import 'package:get/get.dart';
  import 'package:flutter_svg/flutter_svg.dart';
  import 'package:url_launcher/url_launcher.dart';
  
  import '../../common.dart';
  import './dialog.dart';
  
  const kOpSvgList = [
    'github',
    'gitlab',
    'google',
    'apple',
    'okta',
    'facebook',
    'azure',
    'auth0'
  ];
  
  class _IconOP extends StatelessWidget {
    final String op;
    final String? icon;
    final EdgeInsets margin;
    const _IconOP(
        {Key? key,
        required this.op,
        required this.icon,
        this.margin = const EdgeInsets.symmetric(horizontal: 4.0)})
        : super(key: key);
  
    @override
    Widget build(BuildContext context) {
      final svgFile =
          kOpSvgList.contains(op.toLowerCase()) ? op.toLowerCase() : 'default';
      return Container(
        margin: margin,
        child: icon == null
            ? SvgPicture.asset(
                'assets/auth-$svgFile.svg',
                width: 20,
              )
            : SvgPicture.string(
                icon!,
                width: 20,
              ),
      );
    }
  }
  
  class ButtonOP extends StatelessWidget {
    final String op;
    final RxString curOP;
    final String? icon;
    final Color primaryColor;
    final double height;
    final Function() onTap;
  
    const ButtonOP({
      Key? key,
      required this.op,
      required this.curOP,
      required this.icon,
      required this.primaryColor,
      required this.height,
      required this.onTap,
    }) : super(key: key);
  
    @override
    Widget build(BuildContext context) {
      final opLabel = {
            'github': 'GitHub',
            'gitlab': 'GitLab'
          }[op.toLowerCase()] ??
          toCapitalized(op);
      return Row(children: [
        Container(
          height: height,
          width: 200,
          child: Obx(() => ElevatedButton(
              style: ElevatedButton.styleFrom(
                backgroundColor: curOP.value.isEmpty || curOP.value == op
                    ? primaryColor
                    : Colors.grey,
              ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
              onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null,
              child: Row(
                children: [
                  SizedBox(
                    width: 30,
                    child: _IconOP(
                      op: op,
                      icon: icon,
                      margin: EdgeInsets.only(right: 5),
                    ),
                  ),
                  Expanded(
                    child: FittedBox(
                      fit: BoxFit.scaleDown,
                      child: Center(
                          child: Text('${translate("Continue with")} $opLabel')),
                    ),
                  ),
                ],
              ))),
        ),
      ]);
    }
  }
  
  class ConfigOP {
    final String op;
    final String? icon;
    ConfigOP({required this.op, required this.icon});
  }
  
  class WidgetOP extends StatefulWidget {
    final ConfigOP config;
    final RxString curOP;
    final Function(Map<String, dynamic>) cbLogin;
    const WidgetOP({
      Key? key,
      required this.config,
      required this.curOP,
      required this.cbLogin,
    }) : super(key: key);
  
    @override
    State<StatefulWidget> createState() {
      return _WidgetOPState();
    }
  }
  
  class _WidgetOPState extends State<WidgetOP> {
    Timer? _updateTimer;
    String _stateMsg = '';
    String _failedMsg = '';
    String _url = '';
  
    @override
    void dispose() {
      super.dispose();
      _updateTimer?.cancel();
    }
  
    _beginQueryState() {
      _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
        _updateState();
      });
    }
  
    _updateState() {
      bind.mainAccountAuthResult().then((result) {
        if (result.isEmpty) {
          return;
        }
        final resultMap = jsonDecode(result);
        if (resultMap == null) {
          return;
        }
        final String stateMsg = resultMap['state_msg'];
        String failedMsg = resultMap['failed_msg'];
        final String? url = resultMap['url'];
        final bool urlLaunched = (resultMap['url_launched'] as bool?) ?? false;
        final authBody = resultMap['auth_body'];
        if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
          if (_url.isEmpty && url != null && url.isNotEmpty) {
            if (!urlLaunched) {
              launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
            }
            _url = url;
          }
          if (authBody != null) {
            _updateTimer?.cancel();
            widget.curOP.value = '';
            widget.cbLogin(authBody as Map<String, dynamic>);
          }
  
          setState(() {
            _stateMsg = stateMsg;
            _failedMsg = failedMsg;
            if (failedMsg.isNotEmpty) {
              widget.curOP.value = '';
              _updateTimer?.cancel();
            }
          });
        }
      });
    }
  
    _resetState() {
      _stateMsg = '';
      _failedMsg = '';
      _url = '';
    }
  
    @override
    Widget build(BuildContext context) {
      return Column(
        children: [
          ButtonOP(
            op: widget.config.op,
            curOP: widget.curOP,
            icon: widget.config.icon,
            primaryColor: str2color(widget.config.op, 0x7f),
            height: 36,
            onTap: () async {
              _resetState();
              widget.curOP.value = widget.config.op;
              await bind.mainAccountAuth(op: widget.config.op, rememberMe: true);
              _beginQueryState();
            },
          ),
          Obx(() {
            if (widget.curOP.isNotEmpty &&
                widget.curOP.value != widget.config.op) {
              _failedMsg = '';
            }
            return Offstage(
              offstage:
                  _failedMsg.isEmpty && widget.curOP.value != widget.config.op,
              child: RichText(
                text: TextSpan(
                  text: '$_stateMsg  ',
                  style:
                      DefaultTextStyle.of(context).style.copyWith(fontSize: 12),
                  children: <TextSpan>[
                    TextSpan(
                      text: _failedMsg,
                      style: DefaultTextStyle.of(context).style.copyWith(
                            fontSize: 14,
                            color: Colors.red,
                          ),
                    ),
                  ],
                ),
              ),
            );
          }),
          Obx(
            () => Offstage(
              offstage: widget.curOP.value != widget.config.op,
              child: const SizedBox(
                height: 5.0,
              ),
            ),
          ),
          Obx(
            () => Offstage(
              offstage: widget.curOP.value != widget.config.op,
              child: ConstrainedBox(
                constraints: BoxConstraints(maxHeight: 20),
                child: ElevatedButton(
                  onPressed: () {
                    widget.curOP.value = '';
                    _updateTimer?.cancel();
                    _resetState();
                    bind.mainAccountAuthCancel();
                  },
                  child: Text(
                    translate('Cancel'),
                    style: TextStyle(fontSize: 15),
                  ),
                ),
              ),
            ),
          ),
        ],
      );
    }
  }
  
  class LoginWidgetOP extends StatelessWidget {
    final List<ConfigOP> ops;
    final RxString curOP;
    final Function(Map<String, dynamic>) cbLogin;
  
    LoginWidgetOP({
      Key? key,
      required this.ops,
      required this.curOP,
      required this.cbLogin,
    }) : super(key: key);
  
    @override
    Widget build(BuildContext context) {
      var children = ops
          .map((op) => [
                WidgetOP(
                  config: op,
                  curOP: curOP,
                  cbLogin: cbLogin,
                ),
                const Divider(
                  indent: 5,
                  endIndent: 5,
                )
              ])
          .expand((i) => i)
          .toList();
      if (children.isNotEmpty) {
        children.removeLast();
      }
      return SingleChildScrollView(
          child: Container(
              width: 200,
              child: Column(
                mainAxisSize: MainAxisSize.min,
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: children,
              )));
    }
  }
  
  class LoginWidgetUserPass extends StatelessWidget {
    final TextEditingController username;
    final TextEditingController pass;
    final String? usernameMsg;
    final String? passMsg;
    final bool isInProgress;
    final RxString curOP;
    final Function() onLogin;
    final FocusNode? userFocusNode;
    const LoginWidgetUserPass({
      Key? key,
      this.userFocusNode,
      required this.username,
      required this.pass,
      required this.usernameMsg,
      required this.passMsg,
      required this.isInProgress,
      required this.curOP,
      required this.onLogin,
    }) : super(key: key);
  
    @override
    Widget build(BuildContext context) {
      return Padding(
          padding: EdgeInsets.all(0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              const SizedBox(height: 8.0),
              DialogTextField(
                  title: translate(DialogTextField.kUsernameTitle),
                  controller: username,
                  focusNode: userFocusNode,
                  prefixIcon: DialogTextField.kUsernameIcon,
                  errorText: usernameMsg),
              PasswordWidget(
                controller: pass,
                autoFocus: false,
                reRequestFocus: true,
                errorText: passMsg,
              ),
              // NOT use Offstage to wrap LinearProgressIndicator
              if (isInProgress) const LinearProgressIndicator(),
              const SizedBox(height: 12.0),
              FittedBox(
                  child:
                      Row(mainAxisAlignment: MainAxisAlignment.center, children: [
                Container(
                  height: 38,
                  width: 200,
                  child: Obx(() => ElevatedButton(
                        child: Text(
                          translate('Login'),
                          style: TextStyle(fontSize: 16),
                        ),
                        onPressed:
                            curOP.value.isEmpty || curOP.value == 'rustdesk'
                                ? () {
                                    onLogin();
                                  }
                                : null,
                      )),
                ),
              ])),
            ],
          ));
    }
  }
  
  const kAuthReqTypeOidc = 'oidc/';
  
  // call this directly
  Future<bool?> loginDialog() async {
    var username =
        TextEditingController(text: UserModel.getLocalUserInfo()?['name'] ?? '');
    var password = TextEditingController();
    final userFocusNode = FocusNode()..requestFocus();
    Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus());
  
    String? usernameMsg;
    String? passwordMsg;
    var isInProgress = false;
    final RxString curOP = ''.obs;
  
    final loginOptions = [].obs;
    Future.delayed(Duration.zero, () async {
      loginOptions.value = await UserModel.queryOidcLoginOptions();
    });
  
    final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
      username.addListener(() {
        if (usernameMsg != null) {
          setState(() => usernameMsg = null);
        }
      });
  
      password.addListener(() {
        if (passwordMsg != null) {
          setState(() => passwordMsg = null);
        }
      });
  
      onDialogCancel() {
        isInProgress = false;
        close(false);
      }
  
      handleLoginResponse(LoginResponse resp, bool storeIfAccessToken,
          void Function([dynamic])? close) async {
        switch (resp.type) {
          case HttpType.kAuthResTypeToken:
            if (resp.access_token != null) {
              if (storeIfAccessToken) {
                await bind.mainSetLocalOption(
                    key: 'access_token', value: resp.access_token!);
                await bind.mainSetLocalOption(
                    key: 'user_info', value: jsonEncode(resp.user ?? {}));
              }
              if (close != null) {
                close(true);
              }
              return;
            }
            break;
          case HttpType.kAuthResTypeEmailCheck:
            bool? isEmailVerification;
            if (resp.tfa_type == null ||
                resp.tfa_type == HttpType.kAuthResTypeEmailCheck) {
              isEmailVerification = true;
            } else if (resp.tfa_type == HttpType.kAuthResTypeTfaCheck) {
              isEmailVerification = false;
            } else {
              passwordMsg = "Failed, bad tfa type from server";
            }
            if (isEmailVerification != null) {
              if (isMobile) {
                if (close != null) close(null);
                verificationCodeDialog(
                    resp.user, resp.secret, isEmailVerification);
              } else {
                setState(() => isInProgress = false);
                // Workaround for web, close the dialog first, then show the verification code dialog.
                // Otherwise, the text field will keep selecting the text and we can't input the code.
                // Not sure why this happens.
                if (isWeb && close != null) close(null);
                final res = await verificationCodeDialog(
                    resp.user, resp.secret, isEmailVerification);
                if (res == true) {
                  if (!isWeb && close != null) close(false);
                  return;
                }
              }
            }
            break;
          default:
            passwordMsg = "Failed, bad response from server";
            break;
        }
      }
  
      onLogin() async {
        // validate
        if (username.text.isEmpty) {
          setState(() => usernameMsg = translate('Username missed'));
          return;
        }
        if (password.text.isEmpty) {
          setState(() => passwordMsg = translate('Password missed'));
          return;
        }
        curOP.value = 'rustdesk';
        setState(() => isInProgress = true);
        try {
          final resp = await gFFI.userModel.login(LoginRequest(
              username: username.text,
              password: password.text,
              id: await bind.mainGetMyId(),
              uuid: await bind.mainGetUuid(),
              autoLogin: true,
              type: HttpType.kAuthReqTypeAccount));
          await handleLoginResponse(resp, true, close);
        } on RequestException catch (err) {
          passwordMsg = translate(err.cause);
        } catch (err) {
          passwordMsg = "Unknown Error: $err";
        }
        curOP.value = '';
        setState(() => isInProgress = false);
      }
  
      thirdAuthWidget() => Obx(() {
            return Offstage(
              offstage: loginOptions.isEmpty,
              child: Column(
                children: [
                  const SizedBox(
                    height: 8.0,
                  ),
                  Center(
                      child: Text(
                    translate('or'),
                    style: TextStyle(fontSize: 16),
                  )),
                  const SizedBox(
                    height: 8.0,
                  ),
                  LoginWidgetOP(
                    ops: loginOptions
                        .map((e) => ConfigOP(op: e['name'], icon: e['icon']))
                        .toList(),
                    curOP: curOP,
                    cbLogin: (Map<String, dynamic> authBody) async {
                      LoginResponse? resp;
                      try {
                        // access_token is already stored in the rust side.
                        resp =
                            gFFI.userModel.getLoginResponseFromAuthBody(authBody);
                      } catch (e) {
                        debugPrint(
                            'Failed to parse oidc login body: "$authBody"');
                      }
                      close(true);
  
                      if (resp != null) {
                        handleLoginResponse(resp, false, null);
                      }
                    },
                  ),
                ],
              ),
            );
          });
  
      final title = Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            translate('Login'),
          ).marginOnly(top: MyTheme.dialogPadding),
          InkWell(
            child: Icon(
              Icons.close,
              size: 25,
              // No need to handle the branch of null.
              // Because we can ensure the color is not null when debug.
              color: Theme.of(context)
                  .textTheme
                  .titleLarge
                  ?.color
                  ?.withOpacity(0.55),
            ),
            onTap: onDialogCancel,
            hoverColor: Colors.red,
            borderRadius: BorderRadius.circular(5),
          ).marginOnly(top: 10, right: 15),
        ],
      );
      final titlePadding = EdgeInsets.fromLTRB(MyTheme.dialogPadding, 0, 0, 0);
  
      return CustomAlertDialog(
        title: title,
        titlePadding: titlePadding,
        contentBoxConstraints: BoxConstraints(minWidth: 400),
        content: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            const SizedBox(
              height: 8.0,
            ),
            LoginWidgetUserPass(
              username: username,
              pass: password,
              usernameMsg: usernameMsg,
              passMsg: passwordMsg,
              isInProgress: isInProgress,
              curOP: curOP,
              onLogin: onLogin,
              userFocusNode: userFocusNode,
            ),
            thirdAuthWidget(),
          ],
        ),
        onCancel: onDialogCancel,
        onSubmit: onLogin,
      );
    });
  
    if (res != null) {
      await UserModel.updateOtherModels();
    }
  
    return res;
  }
  
  Future<bool?> verificationCodeDialog(
      UserPayload? user, String? secret, bool isEmailVerification) async {
    var autoLogin = true;
    var isInProgress = false;
    String? errorText;
  
    final code = TextEditingController();
  
    final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
      void onVerify() async {
        setState(() => isInProgress = true);
  
        try {
          final resp = await gFFI.userModel.login(LoginRequest(
              verificationCode: code.text,
              tfaCode: isEmailVerification ? null : code.text,
              secret: secret,
              username: user?.name,
              id: await bind.mainGetMyId(),
              uuid: await bind.mainGetUuid(),
              autoLogin: autoLogin,
              type: HttpType.kAuthReqTypeEmailCode));
  
          switch (resp.type) {
            case HttpType.kAuthResTypeToken:
              if (resp.access_token != null) {
                await bind.mainSetLocalOption(
                    key: 'access_token', value: resp.access_token!);
                close(true);
                return;
              }
              break;
            default:
              errorText = "Failed, bad response from server";
              break;
          }
        } on RequestException catch (err) {
          errorText = translate(err.cause);
        } catch (err) {
          errorText = "Unknown Error: $err";
        }
  
        setState(() => isInProgress = false);
      }
  
      final codeField = isEmailVerification
          ? DialogEmailCodeField(
              controller: code,
              errorText: errorText,
              readyCallback: onVerify,
              onChanged: () => errorText = null,
            )
          : Dialog2FaField(
              controller: code,
              errorText: errorText,
              readyCallback: onVerify,
              onChanged: () => errorText = null,
            );
  
      getOnSubmit() => codeField.isReady ? onVerify : null;
  
      return CustomAlertDialog(
          title: Text(translate("Verification code")),
          contentBoxConstraints: BoxConstraints(maxWidth: 300),
          content: Column(
            children: [
              Offstage(
                  offstage: !isEmailVerification || user?.email == null,
                  child: TextField(
                    decoration: InputDecoration(
                        labelText: "Email", prefixIcon: Icon(Icons.email)),
                    readOnly: true,
                    controller: TextEditingController(text: user?.email),
                  ).workaroundFreezeLinuxMint()),
              isEmailVerification ? const SizedBox(height: 8) : const Offstage(),
              codeField,
              /*
              CheckboxListTile(
                contentPadding: const EdgeInsets.all(0),
                dense: true,
                controlAffinity: ListTileControlAffinity.leading,
                title: Row(children: [
                  Expanded(child: Text(translate("Trust this device")))
                ]),
                value: trustThisDevice,
                onChanged: (v) {
                  if (v == null) return;
                  setState(() => trustThisDevice = !trustThisDevice);
                },
              ),
              */
              // NOT use Offstage to wrap LinearProgressIndicator
              if (isInProgress) const LinearProgressIndicator(),
            ],
          ),
          onCancel: close,
          onSubmit: getOnSubmit(),
          actions: [
            dialogButton("Cancel", onPressed: close, isOutline: true),
            dialogButton("Verify", onPressed: getOnSubmit()),
          ]);
    });
    // For verification code, desktop update other models in login dialog, mobile need to close login dialog first,
    // otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog.
    if (isMobile && res == true) {
      await UserModel.updateOtherModels();
    }
  
    return res;
  }
  
  void logOutConfirmDialog() {
    gFFI.dialogManager.show((setState, close, context) {
      submit() {
        close();
        gFFI.userModel.logOut();
      }
  
      return CustomAlertDialog(
        content: Text(translate("logout_tip")),
        actions: [
          dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
          dialogButton(translate("OK"), onPressed: submit),
        ],
        onSubmit: submit,
        onCancel: close,
      );
    });
  }
  