import 'dart:convert';
import 'dart:io';
import 'dart:ui';

import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart';

// for ed2219 key generation
import 'package:cryptography/cryptography.dart';
import 'package:flutter/services.dart';

// secret storage
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:oauth2/oauth2.dart' as oauth2;

import 'package:url_launcher/url_launcher.dart';

import 'package:provider/provider.dart';

import 'authentik_api.dart' as authentik;

import 'package:app_links/app_links.dart';

void main() {
  final errorNotifier = ValueNotifier<String?>(null);

  // Fehler-Handler registrieren
  PlatformDispatcher.instance.onError = (error, stack) {
    final timeStr = DateTime.now().toIso8601String();
    final errorMessage = 'Time: $timeStr\nError: $error\nStack: $stack';

    if (errorNotifier.value == null) {
      errorNotifier.value = errorMessage;
    } else {
      errorNotifier.value = errorNotifier.value! + '\n\n' + errorMessage;
    }
    return true;
  };

  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (context) => AuthentikUserSettingsChangeDialogState(),
        ), // Do something (navigation, ...)
      ],
      child: MyApp(errorNotifier: errorNotifier),
    ),
  );
}

Future<OpenSSHEd25519KeyPair> generateKeyPair() async {
  final algorithm = Ed25519();

  // Generate a key pair
  final cryptographyKeyPair = await algorithm.newKeyPair();

  final privateKey = await cryptographyKeyPair.extractPrivateKeyBytes();
  final publicKey = (await cryptographyKeyPair.extractPublicKey()).bytes;

  final dartsshKeyPair = OpenSSHEd25519KeyPair(
    Uint8List.fromList(publicKey),
    Uint8List.fromList(privateKey + publicKey),
    "",
  );

  return dartsshKeyPair;
}

String encodePublicKey(OpenSSHEd25519KeyPair keyPair, {String comment = ''}) {
  final publicKeyBytes = keyPair.toPublicKey().encode();
  return "ssh-ed25519 ${base64.encode(publicKeyBytes)} $comment";
}

class MyApp extends StatelessWidget {
  final ValueNotifier<String?> errorNotifier;

  const MyApp({super.key, required this.errorNotifier});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LeineLab e.V. Key App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.yellow),
      ),
      home: MyHomePage(
        title: 'LeineLab e.V. Key App',
        errorNotifier: errorNotifier,
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    super.key,
    required this.title,
    required this.errorNotifier,
  });

  final String title;
  final ValueNotifier<String?> errorNotifier;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int navIndex = 0;
  OpenSSHEd25519KeyPair? keyPair;
  String? _output;
  SSHClient? client;
  String key = '';

  Future<void> doSSH(String user) async {
    SSHSocket? socket;

    if (client != null) {
      // If we already have a client, close it
      client!.close();
      client = null;
    }

    if (key == '') {
      // make sure, the key is loaded
      await obtainKeyPair();
    }

    setState(() {
      _output = null;
    });

    try {
      socket = await SSHSocket.connect(
        '192.168.0.15',
        22,
        timeout: const Duration(seconds: 0, milliseconds: 3000),
      );

      client = SSHClient(
        socket,
        username: user,
        identities: List.of(keyPair != null ? [keyPair!] : []),
      );

      setState(() {
        _output = 'Connecting to SSH server...\n\n';
      });

      if (client == null) {
        return;
      }

      final shell = await client!.shell();

      shell.stdout.listen((data) {
        setState(() {
          if (_output == null) {
            return;
          }
          _output = _output! + utf8.decode(data);
        });
      });

      if (client == null) {
        return;
      }

      await shell.done;

      if (client == null) {
        return;
      }
      client!.close();
      setState(() {
        _output = null;
      });
    } on SSHAuthError catch (_) {
      setState(() {
        _output = null;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            "Error! Server rejected your key. Maybe you need to register it first? Or you do not have access to this lock?",
          ),
          duration: const Duration(seconds: 3),
        ),
      );
      return;
    } on SocketException catch (_) {
      setState(() {
        _output = null;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            "Error! Could not connect to SSH server. Maybe you are not in the right wifi network?",
          ),
          duration: const Duration(seconds: 3),
        ),
      );
      return;
    }
  }

  Future<void> obtainKeyPair({bool regenerate = false}) async {
    // Generate a key pair

    final storage = const FlutterSecureStorage();
    final storedKey = await storage.read(key: 'ssh_key_pair');

    // Restore the key if we have one stored and regeneration is not requested
    if (storedKey != null && !regenerate) {
      // If we have a stored key, use it
      final keyPairsFromStorage = SSHKeyPair.fromPem(storedKey);
      assert(keyPairsFromStorage.length == 1);
      assert(keyPairsFromStorage[0] is OpenSSHEd25519KeyPair);

      final keyPairFromStorage =
          keyPairsFromStorage[0] as OpenSSHEd25519KeyPair;

      setState(() {
        keyPair = keyPairFromStorage;
        key = encodePublicKey(keyPairFromStorage, comment: "leinelab-app-key");
      });
      return;
    }

    final generatedKeyPair =
        await generateKeyPair(); // local variable avoids downcast to ? ptr

    await storage.write(key: 'ssh_key_pair', value: generatedKeyPair.toPem());

    setState(() {
      keyPair = generatedKeyPair;
      key = encodePublicKey(generatedKeyPair, comment: "leinelab-app-key");
    });

    await SystemChannels.platform.invokeMethod<void>(
      'Clipboard.setData',
      <String, dynamic>{'text': key},
    );
  }

  @override
  void initState() {
    super.initState();

    final appLinks = AppLinks(); // AppLinks is singleton

    // Subscribe to all events (initial link and further)
    final sub = appLinks.uriLinkStream.listen((uri) {
      final allowedActions = [
        'innentuer-open',
        'aussentuer-open',
        'lab-close',
        'werkstatt-open',
        'werkstatt-close',
      ];

      final action = uri.host;

      if (!allowedActions.contains(action)) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text("Unknown action: $action"),
            duration: const Duration(seconds: 3),
          ),
        );
        return;
      }

      doSSH(action);
    });

    // we do not await generateKey() here, as this is the intended way
    obtainKeyPair();
  }

  @override
  Widget build(BuildContext context) {
    final authentikApiState = context
        .watch<AuthentikUserSettingsChangeDialogState>();

    //final userSettingsDialog =

    final bodyComponentMain = Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.all(20),
            child: Card(
              child: Column(
                children: [
                  Padding(
                    padding: EdgeInsets.all(20),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Padding(
                          padding: EdgeInsets.only(right: 15),
                          child: Icon(Icons.diversity_1, size: 25),
                        ),
                        Text(
                          "Hauptraum (oben)",
                          style: TextStyle(fontSize: 25),
                        ),
                      ],
                    ),
                  ),
                  Padding(
                    padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
                    child: Wrap(
                      spacing:
                          8.0, // horizontaler Abstand zwischen den Elementen
                      runSpacing: 4.0, // vertikaler Abstand zwischen den Zeilen
                      children: [
                        TextButton(
                          onPressed: () => doSSH("innentuer-open"),
                          child: const Text('Open Inside Door'),
                        ),
                        TextButton(
                          onPressed: () => doSSH("aussentuer-open"),
                          child: const Text('Open Outdoor'),
                        ),
                        TextButton(
                          onPressed: () => doSSH("lab-close"),
                          child: const Text('Close All Doors'),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
          Padding(
            padding: EdgeInsets.all(20),
            child: Card(
              child: Column(
                children: [
                  Padding(
                    padding: EdgeInsets.all(20),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Padding(
                          padding: EdgeInsets.only(right: 15),
                          child: Icon(Icons.construction, size: 25),
                        ),
                        Text(
                          "Werkstatt (oben)",
                          style: TextStyle(fontSize: 25),
                        ),
                      ],
                    ),
                  ),
                  Padding(
                    padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
                    child: Wrap(
                      spacing:
                          8.0, // horizontaler Abstand zwischen den Elementen
                      runSpacing: 4.0, // vertikaler Abstand zwischen den Zeilen
                      children: [
                        TextButton(
                          onPressed: () => doSSH("werkstatt-open"),
                          child: const Text('Open'),
                        ),
                        TextButton(
                          onPressed: () => doSSH("werkstatt-close"),
                          child: const Text('Close'),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );

    final bodyComponentInfo = Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.all(20),
            child: Card(
              child: Column(
                children: [
                  Padding(
                    padding: EdgeInsets.all(20),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Padding(
                          padding: EdgeInsets.only(right: 15),
                          child: Icon(Icons.key, size: 25),
                        ),
                        Text(
                          "Your Current Key",
                          style: TextStyle(fontSize: 25),
                        ),
                      ],
                    ),
                  ),
                  Padding(
                    padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
                    child: Text(key, style: TextStyle(fontSize: 15)),
                  ),
                  Padding(
                    padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
                    child: TextButton(
                      onPressed: () => authentikApiState.start(key),
                      child: Text("Manage Keys Registered To Locks"),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );

    final bodyComponentLogs = Center(
      child: SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: EdgeInsets.all(20),
              child: Card(
                child: Padding(
                  padding: EdgeInsets.all(20),
                  child: ValueListenableBuilder<String?>(
                    valueListenable: widget.errorNotifier,
                    builder: (context, error, child) {
                      if (error != null) {
                        return Text(
                          "Error: $error",
                          style: TextStyle(color: Colors.red, fontSize: 15),
                        );
                      }
                      return Text("No exceptions captured yet.");
                    },
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );

    final actionButtonLogs = FloatingActionButton(
    final actionButtonInfo = FloatingActionButton(
      onPressed: () => obtainKeyPair(regenerate: true),
      tooltip: 'Regenerate Key',
      child: const Icon(Icons.refresh),
    );

    var bodyComponent = bodyComponentMain;
    Widget? actionButton;

    if (navIndex == 0) {
      // Main page
      bodyComponent = bodyComponentMain;
      actionButton = null;
    } else if (navIndex == 1) {
      // Info page
      bodyComponent = bodyComponentInfo;
      actionButton = actionButtonInfo;
    } else if (navIndex == 2) {
      // Logs page
      bodyComponent = bodyComponentLogs;
      actionButton = null;
    } else {
      throw Exception('Unknown navIndex: $navIndex');
    }

    final mainPage = Scaffold(
      appBar: AppBar(backgroundColor: Colors.amber, title: Text(widget.title)),
      body: bodyComponent,
      floatingActionButton: actionButton,
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: <Widget>[
            DrawerHeader(
              decoration: BoxDecoration(color: Colors.amber),
              child: Text(
                widget.title,
                style: TextStyle(color: Colors.black, fontSize: 24),
              ),
            ),
            ListTile(
              title: const Text('Actions'),
              onTap: () {
                setState(() {
                  navIndex = 0;
                });
                Navigator.pop(context);
              },
            ),
            ListTile(
              title: const Text('Key Management'),
              onTap: () {
                setState(() {
                  navIndex = 1;
                });
                Navigator.pop(context);
              },
            ),
            ListTile(
              title: const Text('Logs'),
              onTap: () {
                setState(() {
                  navIndex = 2;
                });
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
    );

    Widget? title;
    var children = <Widget>[];
    var actions = [
      TextButton(onPressed: authentikApiState.exit, child: Text("Cancel")),
    ];

    switch (authentikApiState.status) {
      case AuthentikUserSettingsChangeDialogStatus.closed:
        break;
      case AuthentikUserSettingsChangeDialogStatus.waitingForOAuth:
        title = Text("Waiting for OAuth");
        children = [Text("Please complete the OAuth flow in your browser.")];
      case AuthentikUserSettingsChangeDialogStatus.loadingUserSettings:
        title = Text("Loading User Settings");
        children = [Text("Loading...")];
      case AuthentikUserSettingsChangeDialogStatus.userSettingsObtained:
        title = Text("User Settings Obtained");
        children = [Text("You can now edit your user settings.")];

        children.add(KeepOrDeleteKey(sshKey: key, isOurKey: true));

        for (var k in authentikApiState.allKeys) {
          if (k == key) {
            // Skip our key, since the widget is on top already
            continue;
          }
          children.add(KeepOrDeleteKey(sshKey: k));
        }

        actions.add(
          TextButton(
            onPressed: () {
              // Close the dialog and return to the main page
              authentikApiState.save();
            },
            child: Text("Save and Close"),
          ),
        );
      case AuthentikUserSettingsChangeDialogStatus.savingUserSettings:
        title = Text("Saving User Settings");
        children = [Text("Saving...")];
    }

    if (_output != null) {
      title = Text("SSH Output");
      children.add(Text(_output!, style: TextStyle(fontSize: 12)));
      // TODO: fix action.
      actions = [
        TextButton(
          onPressed: () {
            setState(() {
              _output = null;
              if (client != null) {
                client!.close();
                client = null;
              }
            });
          },
          child: Text("Cancel"),
        ),
      ];
    }

    if (title == null) {
      // If we have no title, just return the main page
      return mainPage;
    }

    final dialog = AlertDialog(
      title: title,
      content: SingleChildScrollView(child: Column(children: children)),
      actions: actions,
    );

    return Stack(
      children: [
        mainPage,
        ModalBarrier(dismissible: false, color: Colors.black54),
        dialog,
      ],
    );
  }
}

enum AuthentikUserSettingsChangeDialogStatus {
  closed,
  waitingForOAuth,
  loadingUserSettings,
  userSettingsObtained,
  savingUserSettings,
}

class AuthentikUserSettingsChangeDialogState extends ChangeNotifier {
  AuthentikUserSettingsChangeDialogStatus _status =
      AuthentikUserSettingsChangeDialogStatus.closed;

  oauth2.Client? oauthClient;
  String? sessionCookie;
  Map<String, dynamic>? userSettings;
  HttpServer? server;

  AuthentikUserSettingsChangeDialogStatus get status => _status;

  List<String> _allKeys = [];
  List<String> _keysToKeep = [];

  List<String> get allKeys => _allKeys;
  List<String> get keysToKeep => _keysToKeep;

  Future<oauth2.Client?> getOAuth2Client() async {
    final authorizationEndpoint = Uri.parse(
      'https://auth.leinelab.org/application/o/authorize/',
    );

    final tokenEndpoint = Uri.parse(
      'https://auth.leinelab.org/application/o/token/',
    );

    final identifier = 'UwSMm8gTwBTUURSaxp5uPpuwX1OkGO4FRHeO9v3i';
    final secret = null;

    server = await HttpServer.bind("127.0.0.1", 0);

    final port = server!.port;
    final redirectUrl = Uri.parse('http://localhost:$port/');

    var grant = oauth2.AuthorizationCodeGrant(
      identifier,
      authorizationEndpoint,
      tokenEndpoint,
      secret: secret,
    );

    var authorizationUrl = grant.getAuthorizationUrl(
      redirectUrl,
      scopes: ["profile", "email", "goauthentik.io/api", "openid"],
    );

    if (!await launchUrl(
      authorizationUrl,
      mode: LaunchMode.externalApplication,
    )) {
      throw Exception("Could not launch the authorization URL.");
    }

    if (server == null) {
      // exit() might have been called before we arrived here.
      return null;
    }

    // When the http server receives a request, we store the query parameters,
    // so the oauth2 client can handle the response.
    Map<String, String>? queryParameters;

    await server!.forEach((HttpRequest request) {
      request.response.write(
        'Success! You can close this window now and go back to the app.',
      );
      queryParameters = request.uri.queryParameters;
      request.response.close();
      server!.close();
    });

    if (queryParameters == null) {
      // If we did not receive any query parameters, exit
      return null;
    }

    return await grant.handleAuthorizationResponse(queryParameters!);
  }

  Future<void> start(String currentKey) async {
    // Reset the state to be sure
    oauthClient = null;
    sessionCookie = null;
    userSettings = null;

    _status = AuthentikUserSettingsChangeDialogStatus.waitingForOAuth;
    notifyListeners();

    oauthClient = await getOAuth2Client();
    if (isClosed()) {
      // If the dialog was closed before we got the client, exit
      return;
    }

    sessionCookie = await authentik.getSessionCookie(oauthClient!);
    if (isClosed()) {
      // If the dialog was closed before we got the session cookie, exit
      return;
    }

    if (sessionCookie == null) {
      throw Exception("No session cookie obtained.");
    }

    _status = AuthentikUserSettingsChangeDialogStatus.loadingUserSettings;
    notifyListeners();

    userSettings = await authentik.getUserSettings(
      oauthClient!,
      sessionCookie!,
    );

    if (isClosed()) {
      // If the dialog was closed before we got the user settings, exit
      return;
    }

    _status = AuthentikUserSettingsChangeDialogStatus.userSettingsObtained;

    if (userSettings != null &&
        userSettings!['attributes.sshPublicKeys'] != null) {
      // If we have SSH keys, add them to the list of all keys
      final sshKeysString = userSettings!['attributes.sshPublicKeys'] as String;
      _allKeys = sshKeysString
          .split('\n')
          .where((key) => key.isNotEmpty)
          .toList();

      _keysToKeep = _allKeys.toList();
      if (!_keysToKeep.contains(currentKey)) {
        // If the current key is not in the list of keys to keep, add it
        _keysToKeep.add(currentKey);
      }
    } else {
      throw Exception(
        "Expected 'attributes.sshPublicKeys' in user settings, but got: $userSettings",
      );
    }

    notifyListeners();
  }

  void exit() {
    _status = AuthentikUserSettingsChangeDialogStatus.closed;
    oauthClient = null;
    sessionCookie = null;
    userSettings = null;
    if (server != null) {
      server!.close();
      server = null;
    }
    notifyListeners();
  }

  bool isClosed() {
    return _status == AuthentikUserSettingsChangeDialogStatus.closed;
  }

  Future<void> save() async {
    if (isClosed()) {
      return;
    }

    if (_status !=
        AuthentikUserSettingsChangeDialogStatus.userSettingsObtained) {
      throw Exception("Cannot save, not started or no settings obtained.");
    }

    userSettings!['attributes.sshPublicKeys'] = _keysToKeep.join('\n');

    _status = AuthentikUserSettingsChangeDialogStatus.savingUserSettings;
    notifyListeners();

    await authentik.setUserSettings(
      oauthClient!,
      sessionCookie!,
      userSettings!,
    );
    exit();
  }

  void keepKey(String key) {
    if (_keysToKeep.contains(key)) {
      // If the key is already in the list, do nothing
      return;
    }
    _keysToKeep.add(key);
    notifyListeners();
  }

  void selectKeyForDeletion(String key) {
    _keysToKeep.remove(key);
    notifyListeners();
  }

  bool isKeySelectedToKeep(String key) {
    return _keysToKeep.contains(key);
  }

  void clearKeys() {
    _keysToKeep.clear();
    notifyListeners();
  }
}

class KeepOrDeleteKey extends StatelessWidget {
  final String sshKey;
  final bool isOurKey;

  const KeepOrDeleteKey({
    super.key,
    required this.sshKey,
    this.isOurKey = false,
  });

  @override
  Widget build(BuildContext context) {
    final key = sshKey;
    final isKeyPreviouslyExisting = context
        .select<AuthentikUserSettingsChangeDialogState, bool>(
          (keylist) => keylist.allKeys.contains(key),
        );
    final isKeySelectedToKeep = context
        .select<AuthentikUserSettingsChangeDialogState, bool>(
          (keylist) => keylist.isKeySelectedToKeep(key),
        );

    final buttonStyleKeepActive = ButtonStyle(
      backgroundColor: WidgetStateProperty.all(Colors.blue),
      foregroundColor: WidgetStateProperty.all(Colors.white),
    );

    final buttonStyleDeleteActive = ButtonStyle(
      backgroundColor: WidgetStateProperty.all(Colors.red),
      foregroundColor: WidgetStateProperty.all(Colors.white),
    );

    ButtonStyle? buttonStyleKeep;
    ButtonStyle? buttonStyleDelete = buttonStyleDeleteActive;

    if (isKeySelectedToKeep) {
      buttonStyleKeep = buttonStyleKeepActive;
      buttonStyleDelete = null;
    }

    var title = "Unnamed Key";
    final sshKeySplit = sshKey.split(' ');
    if (sshKeySplit.length >= 3) {
      final potentialTitle = sshKeySplit.sublist(2).join(' ');
      if (potentialTitle != "") {
        title = potentialTitle;
      }
    }

    var textPush = "Keep Key";
    var textDoNotPush = "Select for Deletion";

    if (!isKeyPreviouslyExisting) {
      textPush = "Push Key";
      textDoNotPush = "Do Not Push Key";
    }

    Widget titleWidget = Text(title, style: TextStyle(fontSize: 25));

    if (isOurKey) {
      titleWidget = Wrap(
        spacing: 8.0, // horizontaler Abstand zwischen den Elementen
        runSpacing: 4.0, // vertikaler Abstand zwischen den Zeilen
        children: [
          titleWidget,
          Container(
            decoration: BoxDecoration(
              color: Colors.green,
              borderRadius: BorderRadius.circular(5),
            ),
            padding: EdgeInsets.all(5),
            child: Text(
              "This Device",
              style: TextStyle(fontSize: 15, color: Colors.white),
            ),
          ),
        ],
      );
    }

    return Card(
      margin: EdgeInsets.fromLTRB(20, 20, 20, 0),
      child: Column(
        children: [
          Padding(padding: EdgeInsets.all(20), child: titleWidget),
          Padding(
            padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
            child: Text(sshKey, style: TextStyle(fontSize: 15)),
          ),
          Padding(
            padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
            child: Wrap(
              spacing: 8.0, // horizontaler Abstand zwischen den Elementen
              runSpacing: 4.0, // vertikaler Abstand zwischen den Zeilen
              children: [
                TextButton(
                  onPressed: () {
                    Provider.of<AuthentikUserSettingsChangeDialogState>(
                      context,
                      listen: false,
                    ).keepKey(sshKey);
                  },
                  style: buttonStyleKeep,
                  child: Text(textPush),
                ),
                TextButton(
                  onPressed: () {
                    Provider.of<AuthentikUserSettingsChangeDialogState>(
                      context,
                      listen: false,
                    ).selectKeyForDeletion(sshKey);
                  },
                  style: buttonStyleDelete,
                  child: Text(textDoNotPush),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
